The page builder

How a Sanity type becomes a Vue component, and why adding one requires three files.

The idea

Most CMS integrations model a page as a document with fields: a hero image, headline, three feature slots. That buys you a fast first page and an unmaintainable twentieth, because every new layout is a new document type and every layout tweak is a deploy.

Nuxion models a page as an ordered array of blocks. A renderer walks the array, checks each block's _type value in a registry, and mounts the matching component. Editors compose; the system supplies the layout primitives. Reordering a page is content editing.

Blocks are imported with defineAsyncComponent, so a page with four blocks ships four blocks of JavaScript — not the whole library.

Adding a block

  1. A Sanity schema

    studio/schemas/web/custom/custom.hero.ts — the fields an editor sees, plus the shared section options every block includes, so layout controls stay consistent.

    TypeScript
    npm run cssvars:generate
  2. A Vue component

    `app/components/custom/CustomHero.vue` — props are typed from the generated Sanity types, so the schema and the component cannot drift.

    Vue
    npm run fonts:generate

  3. One line of registration

    app/components/custom/index.ts — The key is the Sanity _type, exactly.

    TypeScript
    'custom.hero': {
        element: defineAsyncComponent(() => import('./CustomHero.vue')),
        props: { ...data },
    },

Naming

One convention, applied everywhere: nuxion.heroNuxionHero.vuenuxion.hero.ts. Every dot- or hyphen-separated segment is capitalised and joined, so custom.logo-stripCustomLogoStrip.vue. The namespace is yours to pick; custom. is a suggestion, not a rule — and it's the same rule Sanity's TypeGen uses, so the generated TypeScript type matches too. Once you know a block's Sanity type, you know both filenames without looking.

The scaffolder

You rarely write those three files by hand.

Shell
npm run scaffold:component -- custom.pricing-table

That generates the schema, the component, and every registration, then regenerates the TypeScript types. --kind navigation and --kind footer produce navigation bars and footer blocks instead, wired into the right slots — those used to be hand-threaded across three files.

Who owns width

Blocks provide vertical rhythm and content. Horizontal width and gutter belong to the section wrapper, which offers three named intents rather than a padding input:

  • Contained — a max width with a gutter. The default.
  • Full — full-width band, content still inset.
  • Bleed — edge-to-edge, for media, carousels and maps.


Three named choices, not a padding input. It is why blocks from differentsources line up on the left edge without anyone coordinating.