Installing P1 on an existing Next.js site
This guide helps you connect P1 to an existing Next.js application deployed on Pantheon. You need access to the Pantheon Next.js site/repo and a developer for this process.
Goal
The goal here is to install P1 on an existing site to be able to use P1 content management and rendering capabilities for existing and new Content on that site, be it the totality of the site or a part of it
What you get, and who does what
By the end, your site has:
- A Visual Editor at /p1, signed in with your Pantheon P1 account.
- Published P1 pages served at their real URLs (like /about), right next to your existing pages.
- Your existing hand-coded pages untouched β served exactly as before.
- Content your team edits in P1 and reviews in a Workstream before it ever reaches your live site.
Every step below is tagged so you can see how close we are to a self-serve experience:
Tag | Meaning |
π’ Anyone | No code β done in a dashboard or the Visual Editor. A content creator can do this today. |
π§ Developer | Needs the terminal, source changes, or a deploy on the Pantheon hosting platform. Engineering today. |
The Road to self-serve section at the end tracks exactly which π§ steps stand between today and a fully no-code onboarding.
One guiding principle: use the default SDK and starter kit β no custom connectors, no forks. It keeps you on the upgrade path and means your site behaves like every other P1 site.
How P1 works alongside your site
Three ideas clear up almost every question before it comes up.
Code and content are separate. Your Next.js app β routes, components, the P1 SDK β is code, and it ships through git, one environment at a time. The pages your team edits are content, and they live in P1's collaborative content repository (the "CCR" backend), keyed by a P1 site ID and branch. Content is never in your repo.
Content is shared across your Pantheon next.js environments. Dev, Test, and Live all read the same P1 site and branch, so editing a page updates everywhere at once. That's extremely helpful when you want to stage content changes before they go live; you do it in a Workstream (more on that later), not by juggling environments.
P1 doesn't automatically adopt your existing pages. Installing the editor and moving a specific page into it are two different jobs. The editor works as soon as it's installed; bringing an existing page under it is a deliberate, one-at-a-time step we cover near the end.
One detail drives a couple of steps: the P1 editor registers your components. The set of blocks P1 lets editors place on a page is populated the first time someone opens /p1βthe editor pushes your code's components to the repository using their login. Keep that in mind; it's why a newly added block "appears" in P1 only after someone opens the editor.
Before you begin
You'll need π§
- Terminus β₯ 4.2.0, signed in (terminus auth:login picks up your saved token automatically).
- Check out the Terminus installation guide to set up and log in.
- A Pantheon Front-End Site connected to your GitHub repo.
Gather these values. Create your P1 site in the dashboard first, then collect the rest. Notice how many a non-technical teammate can grab on their own:
You'll need β set an env var | Where to get it | Who |
P1 site ID β NEXT_PUBLIC_CSS_SITE_ID | Create the site at content.pantheon.io; the ID is in the dashboard URL. | π’ Anyone |
API token β CSS_API_KEY | Dashboard β generate a token, scope "All content including drafts." | π’ Anyone |
Base URL β NEXT_PUBLIC_CSS_BASE_URL | Always https://ccr.p1.pantheon.io for production. | π’ Anyone |
Main branch ID β NEXT_PUBLIC_CSS_BRANCH_ID | GET {BASE_URL}/api/sites/{siteId}/content-pages with header Authorization: Bearer <token> β read branchId. | π§ Developer |
Site machine name | terminus site:list | π§ Developer |
Pin the branch ID. That content token is read-only β great for serving pages, but it can't list branches. If you leave NEXT_PUBLIC_CSS_BRANCH_ID empty, /p1 returns a 500. Always set it.
Choosing your version (an important heads-up)
The P1 SDK is distributed as a coordinated suite of packages that remain synchronized β css-client, puck-css, p1-next-sdk, alongside pds-toolkit-react. Install every package on a matching release. Right now, 0.14.0 is current, 0.15 sits in canary, and minor updates arrive regularly β target a unified release line rather than relying on a static version tag.
npm add @pantheon-systems/css-client@latest \
β β β β@pantheon-systems/puck-css@latest \
β β β β@pantheon-systems/p1-next-sdk@latest Installing and wiring up P1 π§
1. Install the SDK
Install the coordinated set at your chosen version:
npm add @pantheon-systems/css-client@0.8.0 \
β β β β@pantheon-systems/puck-css@0.8.0 \
β β β β@pantheon-systems/p1-next-sdk@0.8.0 2. Bring in the starter routes
Copy these from pantheon-systems/puck-css-integration β apps/p1-starter (the main branch), adapting the paths to your repo (for example, under src/). Keep the starter's folder structure and ensure its relative imports resolve cleanly.
- app/p1/api/[...p1]/route.ts β the API handler (createP1Handler)
- app/p1/auth/[...action]/route.ts β the auth handler (keep prompt: "select_account", not "login", which would ask people to sign in on every visit)
- app/p1/(editor)/[[...p1]]/β¦ β the editor itself
- app/[...puckPath]/{page.tsx,client.tsx} β the catch-all that renders published P1 pages at real URLs. Heads-up: this changes what happens on unknown paths β instead of a hard 404, they show P1's "create this page" screen (a 200). Make sure that's what you want.
- puck.config.tsx, the starter blocks in components/puck/*, and any datasource or SEO helpers those routes import.
Leave your own layout.tsx, page.tsx, and existing pages alone β don't let the starter's versions overwrite them.
3. Run the editor-layout migration
A small codemod moves the editor into a persistent layout so it doesn't fully reload as editors move between pages. It needs a clean git tree:
npx p1-migrate --dry-run β # checks: clean tree, nothing missing, not already migrated
npx p1-migrate β β β β β β # makes the change4. Turn on managed redirects
Add a middleware.ts that runs createP1Middleware. It checks each incoming path against P1 and applies redirects your team configures in the dashboard β no new secrets, it reuses what you already set. (On Next 16.3+ this file is being renamed to proxy; the current name still works.)
5. Adjust next.config
- Add the three SDK packages to transpilePackages.
- Add experimental.staleTimes.dynamic: 0 so client cache doesn't hold back fresh content.
- Add a webpack alias that forces a single copy of yjs (yjs: require.resolve("yjs")). (This is what keeps the 0.8 build healthy β and, as noted above, what doesn't carry to a turbopack build.)
- Keep your site's nav and footer off /p1. If your root layout wraps everything in global chrome, gate it behind a small client component that checks usePathname() and renders nothing on /p1*, so the editor opens on a clean surface.
6. Point at P1 locally and check
Create .env.local (git-ignored):
NEXT_PUBLIC_CSS_BASE_URL=https://ccr.p1.pantheon.io
NEXT_PUBLIC_CSS_SITE_ID=<site id>
NEXT_PUBLIC_CSS_BRANCH_ID=<main branch id> β # pinned β see the note above
CSS_API_KEY=<token>Then confirm:
- npm run build succeeds.
- /p1 shows the sign-in screen, and signing in takes you into the editor.
- POST /p1/auth/login returns 200 with a loginUrl. (A 500 means the base URL or token isn't reaching the runtime.)
- Your existing pages still work and static assets load.
Going live π§
1. Set your Pantheon secrets
Set the build-time values site-wide (these get baked into the build, and per-environment overrides aren't read at build time):
This requires a Pantheon Dashboard SSH key - follow the steps here to create one if you donβt already have an SSH key.
terminus secret:site:set <site> NEXT_PUBLIC_CSS_BASE_URL "https://ccr.p1.pantheon.io" --type=env --scope=web -n
terminus secret:site:set <site> NEXT_PUBLIC_CSS_SITE_ID β "<id>" β β--type=env --scope=web -n
terminus secret:site:set <site> NEXT_PUBLIC_CSS_BRANCH_ID "<uuid>" β--type=env --scope=web -n
terminus secret:site:set <site> CSS_API_KEY β β β β β β β "<token>" --type=env --scope=web -n
P1_SITE_URL keeps the sign-in redirect pointed at the right place behind Pantheon's proxy. It's per-environment β but an environment override needs a site-wide default to exist first:
terminus secret:site:set <site> β β βP1_SITE_URL "https://dev-<site>.pantheonsite.io" β--type=env --scope=web -n β# default
terminus secret:site:set <site>.live P1_SITE_URL "https://live-<site>.pantheonsite.io" --type=env --scope=web -n β# per-env2. Deploy by pushing and tagging
This site type deploys by git reference (opening a PR only runs security scans):
- Push main β builds and deploys Dev.
- Test and Live go out as tags: pantheon_test_N / pantheon_live_N (incrementing numbers). The first tag also creates that environment.
git tag pantheon_live_1 -a -m "Deploy to Live" && git push origin pantheon_live_1- Need a multidev? Create it with terminus multidev:create <site>.dev <name> β not by pushing a branch.
3. Clear the cache, then check β every time
After every deploy, clear the environment's cache. Pantheon's CDN can cache a missing-asset response from a brief restart window and keep serving it, which leaves the site looking unstyled with no interactivity:
terminus env:clear-cache <site>.<env>Then check the deployed URL: pages return 200, /p1/auth/login returns 200, a stylesheet under /_next/static/ returns 200, and /p1 opens the editor. Build logs live at terminus node:logs:build:list <site>.<env>.
Editing your pages in P1
This is where your team lives day to day β and where P1 gets closest to fully self-serve. Bringing an existing page into P1 has two parts: making its building blocks available (a developer, once) and creating and reviewing the content (anyone, forever after).
Make your components available (developer, once per component) π§
- Register the page's blocks in puck.config.tsx and deploy that change.
- Have someone open /p1 and sign in β loading the editor syncs those blocks into P1 so editors can use them.
Create and review the page β the everyday flow π’
- In /p1, open a Workstream so your changes stay separate from your live site.
- Build or edit the page with your registered blocks, and preview it right there using the Workstream switcher.
- When it's ready, merge the Workstream into main to publish β think of it as hitting "publish," with a real review step in front of it.
Serve it at the real URL (developer, once per page) π§ For a page you'd previously hand-coded, remove or repoint that route so the catch-all serves the P1 version instead. Because content is shared across environments but routes ship per-environment, you can switch Dev to the P1 page while Live keeps the old one until you're ready.
Seeding content in bulk (advanced): P1's automation tools can create and publish pages programmatically with your login's permissions β useful for moving a page's existing content over in one shot. The blocks still have to be registered first (open the editor once, as above).
When something's not right
What you see | What's going on, and the fix |
/p1 returns 500 | NEXT_PUBLIC_CSS_BRANCH_ID isn't set (the read-only token can't list branches). Pin the branch ID. |
POST /p1/auth/login returns 500 | The base URL or CSS_API_KEY isn't reaching the runtime β check your secrets. (A 500 right after a deploy is often just the server restarting; retry.) |
Site looks unstyled after a deploy | The CDN cached a missing-asset response. Run terminus env:clear-cache <site>.<env>. (If you have output: 'standalone' in your config, remove it β it causes the same symptom.) |
Editor won't load documents; log shows "Yjs was already imported" | yjs got bundled twice. The webpack alias fixes it on the 0.8 build; it's the open blocker on the 0.10 turbopack build (see Choosing your version). |
npm ci fails on @fortawesome/pro-* | Only on the older 0.5.x line (fixed in 0.8). Add a package.json override aliasing the pro-* icons to the public free-* ones. |
The editor rejects a block: "unknown component type" | The block isn't registered yet. Deploy the puck.config change and open /p1 once to sync it. |
A "CORS" error in the editor console | Usually a backend 500 (like a brief "pool timeout") returned without CORS headers, which the browser reports as CORS. It's the backend, not your code β retry, and flag persistent cases to the P1 team. |
Clean CI build fails on a package that works locally | Declare every package you import directly in package.json (a "phantom" dependency resolves locally but fails a clean npm ci). Verify with rm -rf node_modules && npm ci && next build. |
Reference
Environment variables
Variable | Baked in at build? | What it's for |
NEXT_PUBLIC_CSS_BASE_URL | Yes | Production content backend: https://ccr.p1.pantheon.io |
NEXT_PUBLIC_CSS_SITE_ID | Yes | Your P1 site ID |
NEXT_PUBLIC_CSS_BRANCH_ID | Yes | Main branch ID (pin it) |
CSS_API_KEY | No (runtime) | Read-only token that serves content |
P1_SITE_URL | No (runtime, per-env) | Keeps the sign-in redirect correct behind the proxy |
NEXT_PUBLIC_LD_CLIENT_ID | Yes (optional) | Turns on the 0.10 AI chatbot; unset = hidden |
Deploy at a glance
To⦠| Run |
Deploy Dev | git push origin main |
Deploy Test / Live | tag pantheon_test_N / pantheon_live_N, then push the tag |
Create a multidev | terminus multidev:create <site>.dev <name> |
Clear cache | terminus env:clear-cache <site>.<env> |
Read build logs | terminus node:logs:build:list <site>.<env> |
Road to self-serve
We want a content creator to add P1 with only light help from engineering. These are the π§ steps still in the way, roughly in the order worth solving them:
- The build itself β installing the SDK, bringing in the starter routes, adjusting next.config, running the migration. This is the biggest barrier and the best candidate for a one-command scaffolding tool or a dashboard "Add P1" action.
- Secrets and deploy β a dashboard button that sets the secrets and triggers a build would remove the terminal entirely.
- The branch-ID lookup β surfacing it in the dashboard turns the whole "gather these values" table green.
Everything under Editing your pages in P1 after that one-time block registration is already green β the editor and Workstream flow is exactly where a marketer will feel at home.