Steeren/Getting a site on Nukipa: build, migrate, or integratelive from the platform← site
Get a site live

Getting a site on Nukipa: build, migrate, or integrate

Nukipa sits behind a website. Posts are authored in the platform CMS, the public API serves them, lead-capture forms and page-view + CTA analytics flow back to the platform — but a visitor still hits an actual website, and that website has to exist somewhere. There are three ways to get one onto Nukipa, depending on what you're starting with:

  • Build — you have nothing yet (or close to it), and want a site generated from scratch.
  • Migrate — you have a live site somewhere (WordPress, Webflow, Squarespace, hand-rolled), and want to move it onto Nukipa without losing content or SEO.
  • Integrate — you already have a Next.js app of your own, and want to add the Nukipa-managed pieces (blog, lead capture, analytics) to it rather than replace it.

The first two are driven by skills that orchestrate the whole thing (nukipa-site-from-scratch and nukipa-site-migrate). The third is a manual wiring job against the same public contract those skills target. This article is about choosing between them and knowing what each one actually does.

[!NOTE] "Build" and "migrate" both produce the same kind of output: a fresh Next.js 15 (App Router) project, pushed to a GitHub repo in the nukipa-labs org, with the repo URL recorded on your tenant via nukipa_connect_github_repo. From there the platform's deployer takes over. "Integrate" keeps your existing app and codebase — you only add the Nukipa wiring.


What all three routes share

No matter which route you take, the site talks to the platform through one public contract. Understanding it makes the trade-offs concrete.

  • One SDK wrapper. A src/lib/nukipa.ts file creates a @nukipa/site-sdk client (getNukipaClient() for server components, getMiddlewareClient() for middleware). It resolves which tenant a request belongs to from the host header, falling back to NUKIPA_TENANT_HOST for build-time/edge contexts.
  • Blog routes render, they don't author. /blog (index) and /blog/[slug] (detail) call client.listPosts(...) / client.getPostBySlug(...) and render the body through <PostBody> from @nukipa/post-renderer-react. Posts are written in the CMS; the site is a renderer.
  • Lead capture and analytics flow back to the platform. Page-view pings go through middleware; CTA clicks and inline forms are interactive islands that <PostBody> hydrates client-side and wires back through the SDK.
  • Two required env vars: NUKIPA_GATEWAY_URL (server, the API base, e.g. https://api.nukipa.com) and NEXT_PUBLIC_NUKIPA_GATEWAY_URL (browser, for the gate form, which runs client-side where server-only vars aren't readable). The starter's .env.example ships both un-commented. NUKIPA_TENANT_HOST is optional — it's the build-time/edge fallback host, commented out by default.

[!WARNING] <PostBody> takes individual propsbody, components, sources, postId, lang — not a post object. Passing post={post} silently renders a blank body. And you must not render post.body with your own markdown parser (marked, etc.): posts can contain interactive islands (auto-tracked CTA buttons, inline forms) that only hydrate through <PostBody>. A hand-rolled renderer collapses them to dead static markup — no clicks tracked, no submissions accepted.

Gated posts return a deliberately incomplete payload

Any post can be content-gated in the CMS (lead magnets, paywalls). When an anonymous visitor hits a gated post, getPostBySlug(slug) returns a truncated body (the first gate_after_paragraph paragraphs), emptied components and sources, and a form payload (is_gated: true, gated_form_slug, gated_form_fields, …) to render below the cut. The public read sets Cache-Control: private, no-store on gated payloads so a CDN can't serve an unlocked body to a locked visitor; unlock identity is matched by an email cookie set on form submit. The starter handles this — src/components/GateForm.tsx plus the gating branch in src/app/blog/[slug]/page.tsx are platform contract. Don't delete or restyle them out of recognition. (The same NEXT_PUBLIC_NUKIPA_GATEWAY_URL is what GateForm POSTs to.)

The point: the platform contract is the same in all three cases. What differs is where the rest of the site comes from — generated, imported, or already yours.


Route 1: Build from scratch (greenfield)

Use this when the customer "has just signed up and has almost nothing in the platform yet" — empty CMS, blank brand wiki, no USPs or ICPs. Driven by the nukipa-site-from-scratch skill.

What it does

It's heavier on research than the other build path. The skill:

  1. Deep-researches the business. Scrapes every reachable page of the company URL (cap ~30 pages, depth 2, same-origin), discovers 5–8 competitors via web search, infers the ICP from testimonials/case studies/logos, and extracts up to three USPs — each defensible from public evidence. Output is a BRIEF.md. Then it asks you to correct anything it got wrong.

  2. Seeds the workspace with what it learned. This is the distinguishing step. It writes the research back into the platform via MCP, so the blog generator, campaigns, social, and briefings all inherit the same context the site was built from. The seed calls are named per field:

    Field Tool
    Brand theme context_set_brand_theme
    Language context_set_language
    USPs (one call each) add_company_usp
    ICPs (one call each) add_company_icp
    Competitors context_discover_competitorscontext_confirm_competitors
    Company profile update_company_profile
    Image style context_set_image_style

    Every seed call is idempotent, so the seed phase is safe to re-run. After each batch the skill verifies with the matching get_* / list_* tool, so a silently-failed insert surfaces here rather than at site-build time. If no theme/colors were extractable from the scrape, the skill asks you for one or two primary colors before continuing rather than guessing.

  3. Optionally pre-generates 3–5 starter blog posts with cms_generate_post (left as drafts for you to review) so the blog doesn't ship as an empty grid.

  4. Generates the site — the same parallel content/design agents, the same starter project, the same blog wiring as every other route. Once context is seeded, this part is identical to the one-shot flow.

When it fits

  • A brand-new customer with no existing website, or a thin one not worth migrating.
  • You want the platform's other surfaces (campaigns, social, briefings) to have brand/USP/ICP context from day one, not just the website.

[!NOTE] The skill does not provision the tenant. Signing up is a one-line action you do yourself in the dashboard first. The skill expects the tenant slug + ID + NUKIPA_TOKEN to already exist (it reads them from BRIEF.preload.md if present).

Trade-offs

Cost ~15–25 min wall time; most of it research + seeding. Site generation itself is ~5 min.
Content Generated. Starter posts are AI-drafted from research — realistic, but you review/edit/publish each one. Capped at 5 (more is a moderation burden, not a head start).
Honesty caveat Research is "fast but lossy." The user-correction step after Phase 1 is not optional — it catches what the scrape missed. No fake testimonials or invented stats get written; gaps are marked // TODO.

Route 2: Migrate an existing site

Use this when "the customer's content already exists somewhere on the public internet" and the job is to pull it in and rebuild on Nukipa — not generate from scratch. Driven by the nukipa-site-migrate skill.

What it does

  1. Crawls the source. Tries sitemap.xml first; otherwise walks depth-2 same-origin links from the homepage (cap 200). Produces a crawl-manifest.json with each page's HTML + normalized markdown.
  2. Classifies every page as post, marketing, about, case_study, legal, contact, or skip — then shows you the tally and lets you recategorize before anything is imported.
  3. Imports blog posts into the CMS via cms_import_post_body — preserving the source URL's slug by default (better for SEO) or regenerating clean ones. It records each (source_url, new_post_id, new_slug) triple in import-map.json.
  4. Extracts brand (logo, colors, font) into tenant settings so the rebuilt site looks recognizable, not reinvented. Media stays referenced by source-CDN URL; rehosting into Nukipa's bucket is an opt-in extra pass.
  5. Generates the new site on the same starter — the blog merger automatically picks up the imported posts through the SDK, since the CMS reads the same regardless of where a post came from.
  6. Writes a redirect map (vercel.json / netlify.toml) so every old URL 308-redirects to its new home. After deploy, it spot-checks the top source URLs against the live site to confirm the redirects fire.

[!WARNING] cms_import_post_body (like cms_create_post) always leaves the post in draft status, and the public API returns nothing for drafts — so the migrated blog shows "No articles" until you publish. The skill calls cms_publish_post immediately after each successful import (not batched at the end) so a partial run is still recoverable.

[!NOTE] Import is batched and individually fault-tolerant. cms_import_post_body enqueues a snapshot worker per post, so the skill runs at most 10 imports in parallel (Promise.all in batches of 10) to avoid saturating the CMS. If a single import returns a 4xx it's logged and skipped, not aborted — you can re-run the failed slugs from the dashboard or via MCP afterwards.

Two things the build routes don't do — and why

These are the whole reason migrate exists as its own route rather than "build, but point it at the old site":

  • Imported posts are not regenerated. They're the customer's own words. The writer agent only fills gaps (missing legal pages, polishing imported titles). Running an 80-post backlog through a rewrite would lose the voice and the SEO history — explicitly disallowed.
  • The redirect map is the highest-leverage step in the migration. Skip it and inbound links to /blog/old-slug 404 on the new site; the customer can lose a chunk of organic traffic on cutover day. Permanent (308) redirects tell crawlers to update the index while old bookmarks still resolve.

When it fits

  • A customer with real existing content and SEO equity they can't afford to drop.
  • A platform switch, not a redesign — the brand must carry over exactly.

Trade-offs

Cost ~30–60 min wall time; most of it crawling + importing, not generating.
Content Imported verbatim. Polishing (sanitize, link-fixing) is fine; rewriting is not.
Scope cap Default 200 posts. A "migrate everything" run on a 5,000-post blog burns hours and money — lift the cap explicitly only when you truly need to.
Known rough edges Media isn't rehosted unless you opt in, so some images point at the old CDN. One-off source layouts can produce wonky markdown. Imported legal pages must pass the legal-polish check — they often reference trackers the new site doesn't use and miss the flows it does (Nukipa's visit ping, gate forms).

[!TIP] Three quick scope answers shape the whole run, and the skill asks them up front: import all posts or cap at N (default all up to 200); preserve source slugs or regenerate clean ones (default preserve); any URL patterns to skip (e.g. dead campaign landing pages). Decide these before you start.


Route 3: Integrate into your own Next.js app

The first two routes hand you a whole site. Integrate is for when you already have a Next.js app — your own codebase, your own design system, possibly in your own repo — and you only want to add the Nukipa-managed surfaces to it.

There is no dedicated skill for this. It's a manual wiring job against the same public contract the build/migrate skills target. What you're adding is, concretely:

  1. The SDK wrapper. Create src/lib/nukipa.ts exactly as in the blog-integration contract — getNukipaClient() (server components) and getMiddlewareClient() (middleware). It throws at module load if NUKIPA_GATEWAY_URL is blank (that's intentional — a missing gateway fails loud at startup instead of as a generic 500 on every page). Set NUKIPA_GATEWAY_URL in .env.local, and bake it into next.config.mjs under env for production (because .env.local is gitignored and never reaches the deployed build):

    // next.config.mjs
    const nextConfig = {
      env: {
        NUKIPA_GATEWAY_URL:             'https://api.nukipa.com',
        NUKIPA_TENANT_HOST:             '<tenant-slug>.sites.nukipa.io',
        NEXT_PUBLIC_NUKIPA_GATEWAY_URL: 'https://api.nukipa.com',
      },
    };
    
  2. The two blog routes. src/app/blog/page.tsx (lists via client.listPosts({ limit })) and src/app/blog/[slug]/page.tsx (renders via <PostBody> + generateMetadata). Set export const revalidate = 60 on each. This isn't blog-specific — revalidate = 60 is load-bearing on every route that reads through the SDK (folder routes too); Next's fetch cache respects it. Style the routes however you want; keep the data flow and <PostBody> rendering identical to the contract.

  3. The GSC verification meta tag. Your root src/app/layout.tsx must export an async generateMetadata() (not just a static metadata) that fetches the tenant and injects the google-site-verification token. Without it, search-console verification stays stuck at pending and the dashboard's submit-sitemap action stays disabled.

  4. Connect the repo. Once it's pushed, record it on the tenant:

    nukipa_connect_github_repo({
      workspace_id: "<tenant uuid>",
      repo_url:     "https://github.com/<owner>/<repo>"
    })
    

[!WARNING] Host-resolution order is load-bearing. The wrapper must prefer the visitor host (x-forwarded-hosthost) and use NUKIPA_TENANT_HOST only as a fallback. If the env host wins unconditionally, every request to a customer's attached custom domain (e.g. acme.com) gets resolved against the staging host instead — the gateway then can't return the per-domain google_verification_token, and GSC verification stalls at pending forever. Visitor host first, env host last.

What you get vs. what you give up

Integrate keeps your app — but you're now responsible for keeping it on the contract. The build/migrate skills ship a pre-wired starter and a mandatory npm run sanitize pre-push gate that strips smart quotes, stray dashes, and LLM citation artefacts before the repo is pushed. On the integrate path you maintain those touchpoints yourself: any refactor that collapses the async generateMetadata() back to a static metadata, swaps <PostBody> for a markdown renderer, drops revalidate, or reorders host resolution silently breaks a platform feature.

[!NOTE] The starter also ships a floating feedback widget (public/nukipa-widget.js, mounted from layout.tsx) for the design-review loop — but it's not part of the platform contract. It stores comments in localStorage and offers a "Copy all" button that produces a markdown summary you paste back into chat; there is no backend round-trip. It does no platform or gateway work, so it doesn't verify any wiring. If you integrate by hand you can skip it entirely.

When it fits

  • You have an existing Next.js app you're committed to and don't want regenerated.
  • You want Nukipa's blog/lead-capture/analytics inside your design and routing, not a fresh template.
  • You're comfortable owning the wiring rather than leaning on the starter + agents.

When it doesn't

  • Your app isn't Next.js App Router. The contract (server components, generateMetadata, revalidate, headers()-based host resolution, @nukipa/post-renderer-react) assumes Next.js 15 App Router. Other frameworks would mean re-implementing the SDK glue and the <PostBody> hydration by hand — well off the supported path. Treat that as unsupported for now.
  • You don't actually have a site worth keeping — then build or migrate is less work, not more.

Choosing

Question Build Migrate Integrate
Do you have an existing site? No / negligible Yes, live on the public web Yes — your own Next.js app
Where does content come from? Generated (+ optional starter posts) Imported verbatim from the source Wherever it already lives + the CMS
Output New repo from the starter New repo from the starter Your existing repo, wired
SEO continuity matters? N/A (no old URLs) Yes — redirect map is the point You own redirects/URLs
Who maintains the platform wiring? Starter + agents Starter + agents You
Driven by nukipa-site-from-scratch nukipa-site-migrate Manual (contract docs)
Rough wall time ~15–25 min ~30–60 min Depends on your app

A quick decision path:

  1. Already have a Next.js app you want to keep? → Integrate.
  2. Have a live site (any platform) with content/SEO to preserve? → Migrate.
  3. Starting cold, or your existing site isn't worth keeping? → Build from scratch.

[!NOTE] A fourth skill, nukipa-site-one-shot, is the base the build and migrate skills are forked from. It's for a tenant that already has mature CMS content and seeded context in the platform — it skips the research/seeding (build) and the crawl/import (migrate), and goes straight to generating the site. If your workspace is already populated, that's the shorter path; this article covers the three routes for getting there in the first place.


FAQ

Can I switch routes partway through? The build and migrate skills are separate orchestrations with different phases (research+seed vs. crawl+import+redirect), so you pick one at the start. But they converge on the same starter and the same platform contract — so a site produced by either can later be edited by hand exactly like an integrated one. Nothing locks you in.

Do build and migrate overwrite my repo or my files on the platform? No platform-side file overlay. The output is a GitHub repo in the nukipa-labs org with its URL recorded on the tenant. The deployer reads from the repo; there's no per-version daemon mutating your files. And the seed/import MCP calls are idempotent on the platform side, so re-running a build/migrate is safe — it doesn't duplicate context rows.

I migrated but the blog still says "No articles." Why? Imported posts land as draft, and the public API returns nothing for drafts. Each must be published with cms_publish_post. The migrate skill does this per-post right after import; if you imported manually, publish them yourself.

Why is my Search Console verification stuck at pending? Two common causes, both in the contract above: the root layout isn't injecting the google-site-verification meta tag via an async generateMetadata(), or the SDK wrapper is resolving the env host ahead of the visitor host so the gateway never returns the per-domain token. Check both.

Which renderer version does the canonical starter pin? The starter's package.json pins @nukipa/post-renderer-react at ^0.1.0. Note this trails the live Nukipa website, which has since bumped to ^0.2.4 — if you're integrating by hand, match the version your own app already uses rather than copying the starter's pin blindly.

Can I integrate into something other than Next.js? Not on the supported path today. The contract assumes Next.js 15 App Router (server components, revalidate, generateMetadata, <PostBody> hydration). Other frameworks would mean re-implementing the SDK and renderer glue by hand. Basic for now — Next.js is the one wired route.

Served live from the platform · /docs/choose-your-path