Page Kit
Build, preview, and deploy multiple campaign funnels from a single repo.
Next Campaign Page Kit is the tooling that turns a directory of static HTML into a fully isolated, multi-campaign workspace — with hot-reload dev, deterministic builds, and a static output you can host anywhere.
Why Page Kit
Most static site generators are designed around a single site. When you need to manage multiple campaign funnels in one repository, you quickly hit problems: shared layouts bleed across campaigns, asset paths collide, and a change to one campaign silently breaks another.
Page Kit treats each campaign as a fully isolated unit within a single repo. Every campaign lives in its own subdirectory with its own layouts, assets, and configuration — but they're all built, versioned, and deployed together.
The CLI tools (setup, dev, clone, config, compress) and template filters (campaign_asset, campaign_link, campaign_include) enforce this isolation at every step, so you can work on one campaign without fear of affecting another.
Quick Start
1. Create a project directory
mkdir my-campaigns && cd my-campaigns2. Initialize and install
npm init -y
npm install next-campaign-page-kit3. Run the setup script
npx campaign-initcampaign-init walks you through everything in one flow:
- Adds CLI scripts (
dev,build,clone,config,compress,migrate, …) to yourpackage.json - Creates an empty
_data/campaigns.jsonregistry - Fetches the list of available starter templates and shows a picker
- Asks for your Campaign name (display name) and Campaign slug (directory + URL path)
- Downloads only the chosen template's
src/<slug>/files into your project - Merges the template's registry data into your local
_data/campaigns.json - Optionally prompts for your Campaign API key and writes it to
assets/config.js - Optionally installs an AI context doc for your editor or agent
Get your Campaign API key from the Campaigns App in your store. You can skip this step during init and run npm run config later.
4. Start the dev server
npm run devThis will:
- Show a list of available campaigns
- Let you pick which campaign to preview
- Start the dev server
- Open your browser to the selected campaign
By default the dev server starts on port 3000 and prompts you to pick a campaign.
| Flag | Purpose |
|---|---|
--campaign <slug>, -c <slug> | Skip the picker and start this campaign (must exist in _data/campaigns.json) |
--port <n>, -p <n> | Port to listen on, 1–65535 (defaults to 3000) |
Both flags accept =-syntax (--campaign=my-camp, --port=8080). The first bare positional argument is also accepted as a shortcut: numeric → port, non-numeric → campaign slug. The PORT env var sets the port when no flag is given.
npm run dev # interactive picker, port 3000
npm run dev my-campaign # specific campaign, default port
npm run dev -c my-campaign -p 8080 # specific campaign and portNon-interactive setup (agents, CI)
campaign-init can run with no prompts. Pass every value as a flag and add --non-interactive:
npx campaign-init --non-interactive \
--template olympus \
--slug grounding-mat-v2 \
--name "Grounding Mat V2" \
--api-key "$CAMPAIGN_API_KEY" \
--ai-context claudeAdd --json for agent-friendly automation — a single structured object on stdout, all human UI suppressed:
npx campaign-init --json \
--template olympus --slug grounding-mat-v2 --name "Grounding Mat V2" \
--api-key "$CAMPAIGN_API_KEY" \
--ai-context claude| Flag | Purpose |
|---|---|
--template <slug> | Starter template slug (must exist upstream) |
--slug <name> | Local campaign slug (folder under src/, also URL path) |
--name <"display"> | Display name (defaults to upstream template name) |
--api-key <key> | Campaign API key, written to assets/config.js |
--non-interactive | Never prompt; missing required input exits with code 5 |
--json | Machine-readable stdout; suppresses all human UI |
--dry-run | Resolve plan; no downloads, no writes |
--overwrite | Replace existing src/<slug>/ and registry entry |
--ai-context <tool> | Write AI context doc for claude, codex, cursor, copilot, or none |
--keep-ai-context | Preserve an existing AI context file |
--help, -h | Show full help |
Exit codes: 0 ok · 2 template not found · 3 target conflict (use --overwrite) · 4 upstream fetch failed · 5 missing required input · 6 invalid input · 7 partial write rolled back · 8 rollback failed.
AI context
--ai-context writes the upstream context doc verbatim (with a sentinel header) to wherever your tool auto-loads it:
| Tool | Path |
|---|---|
| Claude Code | CLAUDE.md at the project root |
| OpenAI Codex | AGENTS.md at the project root |
| Cursor | .cursor/rules/campaign-page-kit.mdc (with alwaysApply: true) |
| GitHub Copilot | .github/copilot-instructions.md |
If the tool's file already exists, you're asked whether to update it. The written file always carries a sentinel header noting it was generated by campaign-init and will be overwritten on re-run unless you pass --keep-ai-context.
Commands
| Command | Description |
|---|---|
npm start | Interactive menu: dev server, compress, clone, configure |
npm run setup | Bootstrap a project, install a starter template, set the API key (alias for campaign-init) |
npm run dev | Start dev server with interactive campaign picker |
npm run build | Build all campaigns to _site/ |
npm run clone | Clone an existing local campaign to a new slug |
npm run config | Set the API key for an existing local campaign |
npm run compress | Compress all images in a campaign directory |
npm run compress:preview | Preview compression savings without modifying files |
npm run migrate | Migrate campaigns.json from old array format to key-based format |
Project structure
your-project/
├── _data/
│ └── campaigns.json # Campaign registry (all campaigns)
├── src/
│ └── [campaign-slug]/ # Individual campaign directory
│ ├── _layouts/
│ │ └── base.html # Base layout template
│ ├── _includes/ # Reusable campaign components
│ ├── assets/
│ │ ├── css/
│ │ ├── images/
│ │ ├── js/
│ │ └── config.js # SDK configuration
│ ├── presell.html
│ ├── checkout.html
│ ├── upsell.html
│ ├── receipt.html
│ └── *.html # Any other page
└── package.jsonKey files:
_data/campaigns.json— registers every campaign and its configuration data. Uses a key-based format where each key is the campaign slug. Older projects on the array format can convert withnpm run migrate.src/[campaign]/_layouts/base.html— campaign's base layoutsrc/[campaign]/assets/config.js— Campaign Cart SDK configuration
Page frontmatter
Each campaign page uses YAML frontmatter to configure how it renders.
| Field | Type | Required | Description |
|---|---|---|---|
page_layout | string | No | Layout file in _layouts/. Defaults to base.html |
title | string | Yes | Page title for <title> tag |
page_type | string | Yes | product, checkout, upsell, or receipt |
permalink | string | No | Custom URL path (e.g., /starter/) |
next_url | string | No | Next page in the funnel — the universal forward pointer. Layouts map it to next-success-url on checkout pages and next-upsell-accept-url on upsell pages. |
decline_url | string | No | Override for upsell decline. Defaults to next_url. Maps to next-upsell-decline-url. |
styles | array | No | Page-specific CSS (relative paths or external URLs) |
scripts | array | No | Page-specific JS (relative paths or external URLs) |
footer | boolean | No | Show footer on this page |
Example:
---
page_layout: base.html
title: Checkout
page_type: checkout
next_url: upsell.html
styles:
- https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css
- css/offer.css
scripts:
- https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js
- js/offer.js
footer: true
---Layout resolution
Layouts resolve to the current campaign's _layouts/ directory:
page_layout: base.html→<slug>/_layouts/base.htmlpage_layout: custom.html→<slug>/_layouts/custom.html
No layout specified? Defaults to base.html.
Campaign context
Every page automatically has access to its campaign's data from _data/campaigns.json via the campaign object — so you can drive copy, links, and config from the registry instead of hardcoding it into templates.
<h1>{{ campaign.name }}</h1>
<p>Contact: {{ campaign.support_email }}</p>Add any keys you want to your campaign's entry, and they become available immediately:
{
"starter": {
"name": "Starter Campaign",
"entry_url": "presell.html",
"support_email": "[email protected]",
"custom_headline": "Welcome to our Store!"
}
}<h2>{{ campaign.custom_headline }}</h2>Reserved fields
These keys have built-in CLI behavior:
| Field | Type | Description |
|---|---|---|
entry_url | string | Page that npm run dev opens in the browser. Defaults to the campaign root (/<slug>/). Accepts a page name like presell or landing.html — the path is normalized to /<slug>/<page>/. A warning is shown if the page doesn't exist under src/<slug>/. |
environment variable
Every template also has access to an environment variable indicating the current build mode — useful for conditionally including analytics, debug tools, or other environment-specific content.
| Command | Default |
|---|---|
npm run dev | development |
npm run build | production |
{% unless environment == "development" %}
<!-- Google Tag Manager -->
<script>...</script>
{% endunless %}Override the default by setting CPK_ENV — useful for build pipelines like Netlify or GitHub Pages where you want a custom value such as staging:
CPK_ENV=staging npm run buildTemplate filters
Templates use Liquid syntax. Page Kit adds three custom filters and tags for campaign-relative includes, assets, and links.
Always use these filters instead of hardcoding paths — they're what makes cloning a campaign to a new slug "just work."
campaign_asset
Resolves an asset path to the current campaign.
<script src="{{ 'config.js' | campaign_asset }}"></script>
<!-- Output: /starter/config.js -->
<link href="{{ 'css/custom.css' | campaign_asset }}" rel="stylesheet">
<!-- Output: /starter/css/custom.css -->
<img src="{{ 'images/logo.png' | campaign_asset }}" alt="Logo">
<!-- Output: /starter/images/logo.png -->Use for: CSS, JS, images, config.js, any campaign asset.
campaign_link
Generates a clean URL for inter-page navigation within a campaign.
<a href="{{ 'checkout.html' | campaign_link }}">Checkout</a>
<!-- Output: /starter/checkout/ -->
<meta name="next-success-url" content="{{ next_url | campaign_link }}">
<!-- Output: /starter/upsell/ -->
<button data-next-url="{{ 'upsell.html' | campaign_link }}">Continue</button>
<!-- Output: /starter/upsell/ -->The filter strips .html, adds a trailing slash, prepends the campaign slug, and passes anchor links (#section) and absolute URLs through untouched.
Use for: page links, navigation URLs, redirect URLs, SDK meta tags.
campaign_include
Includes a file from the current campaign's _includes/ directory.
{% campaign_include 'slider.html' images=slider_images %}
{% campaign_include 'slider.html' images=slider_images show_package_image=true %}Use for: reusable components within a campaign (sliders, testimonials, badges).
Building from scratch (no template)
Most users should start with campaign-init and pick a starter template. If you'd rather start empty, run campaign-init and cancel the template picker (Ctrl+C). The bootstrap step still runs — you'll have the CLI scripts and an empty _data/campaigns.json.
Then add an entry keyed by slug:
{
"my-campaign": {
"name": "My Campaign",
"description": "My first campaign",
"sdk_version": "0.4.18"
}
}When in doubt, copy the current sdk_version from the starter template registry you are matching.
…and create the matching directory tree under src/:
src/
└── my-campaign/
├── _layouts/
│ └── base.html
├── assets/
│ └── config.js
└── presell.htmlThen run npm run config to set the API key.
Compressing images
Compress every image in a campaign directory in-place. Supports JPEG, PNG, WebP, and GIF. The file is only overwritten if the compressed output is actually smaller.
npm run compressThis will:
- Show a list of available campaigns
- Let you pick which one to compress
- Compress all images anywhere under
src/<campaign>/ - Print a before/after table with file sizes and total savings
Preview mode — see what would be saved without modifying any files:
npm run compress:previewAlready-optimized images are skipped and reported in a debug line above the summary.
Building for production
npm run buildOutput is written to _site/ — fully static, no server, no runtime. From there, push to any static host. See Hosting in Getting Started for provider-specific setup (Netlify, Cloudflare Pages, Render, Vercel, etc.).
Make sure the host's domain is listed under your campaign's authorized domains in the Campaigns App — otherwise the SDK calls will be rejected from the deployed origin.