Templates

A template is a GitHub repository that the dashboard can provision as the frontend for a new space. Pick a built-in template when creating a site, or connect a repo of your own to reuse it across every space in your organization.

This guide covers the three things worth knowing up front:

  1. What the dashboard actually does when you provision from a template.
  2. The preflight checklist your repo must pass — use this when authoring a custom starter.
  3. How content import works, and why the wrong JSON shape silently produces an empty site.

Built-in vs. custom templates

Every organization sees the same three built-in templates in the Full Stack card on the new-space wizard:

  • Decoupled Components (Next.js) — the reference starter, content-aware, ships with sample landing-page paragraphs.
  • Decoupled Components (Astro) — same content model, Astro flavored.
  • Decoupled Minimal — no sample content, bring-your-own everything.

Custom templates live under Templates in the sidebar. They're scoped to your organization, appear in the same Full Stack starter picker, and only your users see them.

How provisioning works

When you create a space from a template, the dashboard runs in this order:

  1. Drupal tenant provisions on Fly.io — fresh DB, install profile (dc_core), baseline content types.
  2. Netlify site is created and linked to the template's GitHub repo. Environment variables are set to placeholders.
  3. Once Drupal is ready, the connect flow runs:
    • Pushes template info into Drupal state so /dc-config can surface the right Vercel "deploy your own copy" button and content-import URL.
    • Fetches OAuth credentials from Drupal and pushes them into Netlify env vars.
    • Content import (optional) — fetches the template's content JSON from GitHub and posts it to Drupal's /api/dc-import endpoint.
    • Preview iframe — Drupal auto-enables iframe preview for every node bundle that exists on the tenant. No hardcoded bundle list — if your template adds a homepage node type, preview wires up automatically.
    • Puck editor — enabled when the template declares features.supports_puck.
    • Netlify rebuild triggers so the new env vars take effect.

The user visits /dc-config on the tenant to confirm everything is hooked up. This page reads the pushed template info and surfaces a one-click link to clone the repo to the user's own Vercel, plus a content-import fallback if the automatic import didn't run.

Private repos

The dashboard supports private repositories via a GitHub OAuth connection scoped to your organization:

  1. On the Templates page, click Connect GitHub.
  2. Authorize the OAuth app — you'll return to the dashboard with an active connection.
  3. Save a template pointing at a private repo — the dashboard uses the connection's access token to read package.json, fetch content, and provision Netlify with a per-site deploy key it adds to your repo automatically.

Tokens are AES-256-GCM encrypted at rest, derived from NEXTAUTH_SECRET. We never request write scope.

Netlify handles its own private-repo access separately — if you see the first Netlify deploy fail on private repos, install the Netlify GitHub App on the repo.

Preflight checklist

If you're authoring a repo to use as a custom template, these are the gotchas worth getting right up front. The dashboard's built-in Verify button on each template card will check most of these for you.

Repo structure

Your repo must have:

  • A package.json at the root with a buildable build script.

  • A framework declaration the dashboard recognizes — next, astro, or other.

  • For Next.js templates deploying to Netlify: a netlify.toml and @netlify/plugin-nextjs in devDependencies. Without the plugin, Netlify treats .next/ as flat static files — dynamic routes like app/[...slug]/page.tsx return 404, and catch-all paths never render.

    Minimal netlify.toml:

    [build]
      command = "npm run build"
      publish = ".next"
    
    [[plugins]]
      package = "@netlify/plugin-nextjs"
    
  • A catch-all route (app/[...slug]/page.tsx in Next.js app router, src/pages/[...slug].astro in Astro) that resolves Drupal paths. The built-in starters are a good reference.

Environment variables

The dashboard sets these automatically on Netlify at provision time — your app should read them without any manual setup:

  • NEXT_PUBLIC_DRUPAL_BASE_URL (and DRUPAL_BASE_URL / PUBLIC_DRUPAL_BASE_URL for Astro)
  • DRUPAL_CLIENT_ID
  • DRUPAL_CLIENT_SECRET
  • NEXT_PUBLIC_DEMO_MODE=false (and PUBLIC_DEMO_MODE for Astro)

You can declare additional env vars in the template's env_manifest if you need other values surfaced to users at provision time.

Content import payload

This is the most common surprise. Your content JSON file (default data/components-content.json, configurable on the template save form) must use the content-entity shape that /api/dc-import expects — not a content-type-definition shape.

The import endpoint expects:

{
  "nodes": [
    {
      "type": "landing_page",
      "title": "Welcome",
      "field_paragraphs": [ ... ]
    }
  ],
  "paragraphs": [ ... ]
}

A file that instead describes content types themselves — like:

{
  "model": [
    {
      "bundle": "homepage",
      "fields": [ { "id": "hero_title", "type": "string" } ]
    }
  ]
}

— is a model definition, not content. The dashboard will fetch it, post it, Drupal will accept the request, and your site will end up with zero content entities. If that's all your repo ships, uncheck "Import content" when saving the template, or point content_payload_path at a different file that contains actual entries.

See Content for the full shape reference.

Preview-iframe compatibility

The Drupal tenant auto-enables iframe preview for every node bundle that exists. Your frontend's catch-all route needs to render those bundle paths correctly when loaded inside an iframe — don't redirect() unknown paths to /, handle them the same way the route would during a normal page visit.

Verify preflight

On each template card there's a Verify button that runs a subset of the above checks against the repo without having to create a space:

  • GitHub connection is active (for private repos).
  • Repo + branch are reachable.
  • package.json exists and is parseable.
  • For Next.js targets — netlify.toml + @netlify/plugin-nextjs are present.
  • Content payload exists at the configured path, parses as JSON, and looks like content (not a model definition).

Run Verify before creating spaces from a newly-saved template — it catches most of the "deployed fine, but the site is empty or 404s everywhere" surprises.