Skip to content

Wire an app you already have

Goal: join an app you already have to Canvas Headless, reaching the same state the quickstart’s scaffold gives you: the app serving the Canvas contract, ready to register.

  • The Canvas Headless SDK and your framework’s adapter installed and wired in your app.
  • The app answering the adapter’s 401 at the component metadata route, ready for the quickstart’s registration step and everything after it.
  • An existing Next.js, Astro, Nuxt, or TanStack Start app you can add dependencies to.
  • Everything from the quickstart’s prerequisites: a Source CMS site you can administer, the two Canvas Headless permissions, Node.js 22 or later, and a Chromium-based browser while you develop over plain HTTP.

The quickstart’s first two steps scaffold a new project. An existing app reaches the same place by installing the adapter and wiring it itself, and the quickstart’s remaining steps (registration, preview, components) then run unchanged. Do not hand-write the Canvas contract: the adapter ships the draft session exchange, the component metadata endpoint, the component registry, and the frame headers. Canvas checks for the adapter’s exact responses when it decides whether your app is connected.

  1. Two files are the same whatever the framework. canvas.config.json at the project root tells component discovery where your components live:

    {
    "componentDir": "src/components",
    "aliasBaseDir": ".",
    "globalCssPath": "src/styles/global.css"
    }

    componentDir is the only key that has to match your project; the value above is the default. And .env holds the whole of the app’s Canvas configuration:

    Terminal window
    CANVAS_SITE_URL=https://your-site.example.com
  2. Terminal window
    npm install @drupal-canvas/headless @drupal-canvas/headless-react @drupal-canvas/headless-next

    Wrap the Next.js config. withCanvas() transpiles the SDK packages, generates the component registry and keeps it current in development, writes the component manifest at build time, and sends the Content-Security-Policy: frame-ancestors header that keeps the app un-embeddable until a live preview session names the editor’s origin:

    next.config.ts
    import { withCanvas } from '@drupal-canvas/headless-next/config';
    export default withCanvas();

    Import it from /config rather than the package root. next.config.ts is loaded outside any request scope, and the root entry reaches for request-scoped Next.js APIs.

    Next.js is one of the two frameworks where you mount the Canvas routes yourself (TanStack Start is the other). Four files, each one line of behavior. The paths of the first and the last are fixed by the integration contract, so put them exactly here:

    app/api/draft/route.ts
    import { createDraftRouteHandlers } from '@drupal-canvas/headless-next';
    export const GET = createDraftRouteHandlers().draft.GET;
    app/api/draft/renew/route.ts
    import { createDraftRouteHandlers } from '@drupal-canvas/headless-next';
    export const POST = createDraftRouteHandlers().draftRenew.POST;
    app/api/disable-draft/route.ts
    import { createDraftRouteHandlers } from '@drupal-canvas/headless-next';
    export const POST = createDraftRouteHandlers().disableDraft.POST;
    app/api/canvas/components/route.ts
    import { createComponentMetadataHandler } from '@drupal-canvas/headless-next';
    export const runtime = 'nodejs';
    export const dynamic = 'force-dynamic';
    export const { GET, OPTIONS } = createComponentMetadataHandler();

    Leaving draft mode is a POST on purpose: a GET reachable by a link would be eligible for prefetching, and a prefetch would end the editor’s session silently.

    Last, the catch-all that renders any path your site routes:

    app/[...slug]/page.tsx
    import { fetchPage } from '@drupal-canvas/headless-next';
    import CanvasComponentTree from '@drupal-canvas/headless-next/CanvasComponentTree';
    import { notFound } from 'next/navigation';
    export const dynamic = 'force-dynamic';
    export default async function CanvasPage({
    params,
    }: {
    params: Promise<{ slug: string[] }>;
    }) {
    const { slug } = await params;
    const path = `/${slug.map(encodeURIComponent).join('/')}`;
    const page = await fetchPage(path);
    if (!page) notFound();
    return <CanvasComponentTree tree={page.content} />;
    }

    If the app already owns /, or already has its own catch-all, keep them: fetchPage() is a per-route call, not a takeover. Only the paths you hand to it are rendered by Canvas.

  3. Start the dev server and run the curl check from the quickstart’s start-the-app step. A 401 carrying WWW-Authenticate: Bearer means the adapter is live and the app is ready to register.

Components come next, and you may already have some. npx canvas pull brings components that exist on your site into the app’s codebase, in the format the quickstart’s component scaffold step writes; the Canvas CLI reference covers it. Existing app components become Canvas components by gaining a component.yml beside them.

The registered row shows Setup needed after wiring

a server answered at that URL, but not with the adapter’s 401. The usual cause here is an adapter that never reached the build: withCanvas() missing from next.config.ts, canvas() missing from astro.config.mjs’s integrations, @drupal-canvas/headless-nuxt missing from nuxt.config.ts’s modules, or canvas() missing from vite.config.ts’s plugins for TanStack Start. Re-run the curl check after fixing the wiring; the row re-probes on its own.

Was this page helpful?