Content-types og blocks

4. Content-types og blocks

Tilføjer du nye content-types og blocks ofte — det er kerne-loopet for at ekstendere CMSen. Denne side viser eksempler du kan kopiere.

En typisk content-type

src/sitetypes/artist-portfolio/types/artwork.ts:

import { registerContentType, type ContentTypeDefinition } from "@/engine";

export const artworkTypeDefinition: ContentTypeDefinition = {
  name: "Artwork",
  label: { en: "Artwork", da: "Kunstværk" },
  layer: "sitetype",
  category: "art",
  extends: "BaseContentItem",
  defaultView: "default",
  views: [{ name: "default", description: "Single-artwork page" }],
  fields: [
    { type: "Text", name: "title", label: { en: "Title", da: "Titel" }, required: true },
    { type: "Number", name: "year", label: { en: "Year", da: "År" } },
    { type: "Money", name: "price", label: { en: "Price", da: "Pris" } },
    { type: "Image", name: "image", label: { en: "Image", da: "Billede" }, required: true },
    { type: "Boolean", name: "forSale", label: { en: "For sale", da: "Til salg" } },
  ],
};

let registered = false;
export function registerArtworkType(): void {
  if (registered) return;
  registerContentType(artworkTypeDefinition);
  registered = true;
}

Nøgle-points:

  • name — unique på tværs af engine. Bruges som type-key i storage og blocks
  • label — LocalizedString (en + da). Vises i admin-UI
  • layer"engine" (basic) eller "sitetype" (specifikt)
  • extends — base-type (typisk "BaseContentItem" eller "Page"). Field-inheritance sker automatisk
  • fields — array af FieldDefinitions. Hver field har type + name + label + valgfri required
  • registered flag — idempotent registration

En typisk block

src/sitetypes/docs/blocks/docs-hero.tsx:

import { registerBlock } from "@/engine/blocks";

interface DocsHeroConfig {
  title?: string;
  subtitle?: string;
  intro?: string;
  ctaLabel?: string;
  ctaHref?: string;
}

function DocsHeroRender({ config }: { config: Record<string, unknown> }) {
  const c = config as DocsHeroConfig;
  if (!c.title?.trim()) return null;
  return (
    <section className="docs-hero">
      <h1>{c.title}</h1>
      {c.subtitle && <p>{c.subtitle}</p>}
    </section>
  );
}

let registered = false;
export function registerDocsHeroBlock(): void {
  if (registered) return;
  registerBlock({
    definition: {
      name: "DocsHero",
      label: { en: "Docs Hero", da: "Docs Hero" },
      layer: "sitetype",
      composable: true,
      fields: [
        { type: "Text", name: "title", label: { en: "Title", da: "Titel" }, required: true },
        { type: "Text", name: "subtitle", label: { en: "Subtitle", da: "Undertitel" } },
      ],
      defaultConfig: { title: "", subtitle: "" },
    },
    Render: DocsHeroRender as never,
  });
  registered = true;
}

Nøgle-points:

  • Block har definition (schema for admin-editor) + Render (komponent der render'es public-side)
  • composable: true — kan tilføjes til BlockArrays
  • Fields er samme shape som content-types
  • Default-config sætter tomme strings så form-editor render'er rene felter
  • Render skal returnere null hvis content er tom (undgår blank-block-fejl)

Tilføj til Frontpage's allowedBlocks

En block skal være i allowedBlocks på en content-type for at blive vist i palette'n:

// sitetypes/artist-portfolio/types/frontpage.ts
allowedBlocks: ["Hero", "DocsHero", "AboutRollup", "Oprulninger", "ContactSection", "Columns", "SocialFeed", "SocialCTA"],

Vigtig: rendering er ikke begrænset af allowedBlocks — kun palette-visning. Storage kan teknisk indeholde blocks der ikke er allowed; de render'es stadig korrekt hvis blocken er registreret.

Registrér din nye type/block

For content-type: tilføj registerArtworkType() til sitetypens bootstrap*()-funktion.

For block: tilføj registerDocsHeroBlock() til sitetypens bootstrap.

Bootstrap-funktionerne kaldes fra engine/bootstrap.ts (engine-side) eller sitetypes/index.ts (sitetype-side).

Storage shape

Når en content-type record gemmes:

PartitionKey: <tenant-slug>:Artwork
RowKey: artwork_123       (slug eller UUID)
Fields: title="Solnedgang", year=2025, price=12500, image="<storage-path>", forSale=true
Metadata: sequentialId=42, status="published", createdAt, updatedAt

Felter på top-niveau er primitiver (string/number/bool). Complex shapes (arrays, objects) gemmes som JSON-strings og parse'es ved read.

BlockArray-felter (typisk på Pages): hvert block-instance er én JSON-objekt i array'et med shape { name, config, anchor? }.

Test din nye type/block

  1. Sæt typecheck (npx tsc --noEmit) — fanger field-name typos
  2. Restart dev-server (engine-bootstrap kører kun ved start)
  3. Gå til /admin/<modul>/ny og prøv at oprette en record
  4. Brug Storage Explorer til at se den faktiske data-row
  5. Gå til en page hvor blocken bruges og verificér public-rendering

Common pitfall: glemt at registrere blocken før Block-array field registreres — sker hvis du tilføjer blocken efter registerBlockArrayField() i bootstrap.ts. Fix: flyt din registerXxxBlock() op før registerBlockArrayField().