Files
tibi-svelte-starter/.agents/skills/nova-pagebuilder-modeling/SKILL.md
T
apairon 491f495c66 feat: enhance project setup and architecture documentation
- Updated `tibi-project-setup` skill to clarify project initialization goals and steps.
- Improved `tibi-ssr-caching` skill to detail SSR architecture, responsibilities, and caching mechanisms.
- Introduced `website-solution-architecture` skill for translating website requirements into coherent solutions.
- Refined `AGENTS.md` to provide a structured roadmap for project development phases.
- Added `ADMIN_ASSET_VERSION` to `api/config.yml.env` for asset versioning.
- Updated SSR request flow and cache invalidation logic in `api/hooks/ssr/AGENTS.md`.
- Removed obsolete `esbuild.config.admin.js` and integrated asset versioning into the main `esbuild.config.js`.
- Adjusted `api/collections/content.yml` to utilize asset versioning for admin scripts.
2026-05-12 20:01:22 +00:00

15 KiB

name, description
name description
nova-pagebuilder-modeling Model editor-friendly block systems for tibi-admin-nova. Covers pagebuilder structure, block schemas, preview, drillDown, dependsOn, containerProps.layout, and the required alignment between admin config, frontend block registry, and SSR.

nova-pagebuilder-modeling

When to use this skill

Use this skill when:

  • Building a flexible pagebuilder for pages, landing pages, reusable sections, or site settings
  • Designing nested object[] schemas for blocks in Nova
  • Deciding how editors should create, scan, reorder, and edit blocks
  • Translating website requirements into maintainable block types
  • Refactoring a block system that is technically valid but editor-hostile

Goal

The goal is not just to make blocks storable. The goal is to model a block system that is:

  • understandable for editors
  • safe to extend over time
  • easy to preview in Nova
  • aligned with frontend rendering and SSR
  • structured enough that an LLM can add new blocks without inventing ad-hoc patterns

Source of truth

Use these sources when designing or reviewing the schema:

  • tibi-admin-nova/types/admin.d.ts
  • tibi-admin-nova/docs/collection-config.md
  • api/collections/content.yml
  • frontend/src/blocks/
  • frontend/src/blocks/BlockRenderer.svelte
  • types/global.d.ts

Do not model pagebuilder structures from memory when current Nova types are available.

Core mental model

In this starter family, a pagebuilder is usually an object[] field where each array item represents one block. Each block needs three layers to stay coherent:

  1. Data model in collection YAML
  2. Render component in frontend/src/blocks/
  3. Type and registry alignment in TypeScript and BlockRenderer.svelte

If one of these layers is missing, the system is incomplete.

Design rules

1. Prefer a small block vocabulary with strong reuse

Do not create a new block type for every tiny content variation.

Prefer:

  • hero
  • richText
  • imageText
  • cta
  • featureGrid
  • faq
  • logos
  • testimonials

Avoid block libraries that mirror every page one-to-one. That produces brittle schemas and weak editor UX.

2. Every block must be recognizable in lists

Editors should understand an entry without opening each block.

Use current Nova preview capabilities on block objects:

meta:
    preview:
        label: headline
        secondary: type
        badge: variant

If a block has no single identifying field, use a preview eval that combines multiple fields.

3. Large blocks should open in drill-down editing

If a block contains many fields, nested objects, or repeated items, prefer drill-down editing instead of forcing everything into one long inline form.

Use drillDown when the inline view becomes noisy or error-prone.

4. Use dependsOn to keep block forms focused

Conditional fields are essential in block schemas.

Use dependsOn when:

  • a field is only relevant for one variant
  • a CTA only appears when showCta is true
  • media settings depend on layout choice
  • a nested group only matters for one block subtype

Do not dump every optional field into the same visible form.

5. Use containerProps.layout to model editor flow

Block editing should reflect visual and editorial grouping, not raw storage order.

Use containerProps.layout to:

  • put related fields side by side
  • separate content from appearance controls
  • reduce scroll depth
  • keep critical fields in the first viewport

6. Keep the block model SSR-safe

If a block is page-critical, it must render correctly in SSR too.

That means:

  • the block data must come through the same content-loading path as the page
  • the Svelte block component must be importable by the SSR bundle
  • the renderer must not rely on browser-only APIs during initial render

7. Model for migrations, not just first delivery

Blocks evolve. Design schemas so fields can be added without breaking every existing entry.

Prefer additive changes and explicit defaults over brittle implicit assumptions.

Step 1: Start from editorial jobs, not component names

Define what editors need to do:

  • create a page hero
  • add structured intro content
  • place testimonials
  • create CTA sections
  • insert FAQs
  • reuse site-wide sections

Then derive block types from these jobs.

Step 2: Decide which data belongs at page level and which belongs inside blocks

Keep page-level fields for concerns that apply to the whole page, such as:

  • path
  • language
  • SEO
  • publication
  • translation linking

Keep block-level fields for modular content slices.

Step 3: Define the block array schema

Typical pagebuilder field:

- name: blocks
  type: object[]
  meta:
      label: { de: "Blöcke", en: "Blocks" }
      widget: pagebuilder
      pagebuilder:
          blockTypeField: type
      preview:
          label: headline
          secondary: type
          badge: variant
      drillDown: true
  subFields:
      - name: type
        type: string
        meta:
            widget: select
            choices:
                - value: hero
                  label: Hero
                - value: richText
                  label: Rich text
                - value: featureGrid
                  label: Feature grid
      - name: headline
        type: string
      - name: variant
        type: string
        meta:
            dependsOn:
                eval: "$parent.type === 'hero'"

The exact shape can vary, but the pattern stays the same: block type first, then a previewable and conditionally focused schema.

Step 3a: Build and wire the block registry

For this starter, the pagebuilder registry is not implicit. Nova loads it from the admin bundle via meta.pagebuilder.blockRegistry.file.

The concrete chain is:

  1. define the registry in frontend/src/admin.ts
  2. export it as blockRegistry
  3. build the admin bundle with yarn build
  4. point the collection field or collection meta to the built module

The current starter already does this in frontend/src/admin.ts and api/collections/content.yml.

Typical starter pattern:

const blockRegistry = {
    hero: {
        label: "Hero",
        render(container, row, context) {
            return {
                update(nextRow, nextContext) {},
                destroy() {},
            }
        },
    },
}

export { blockRegistry }

And the collection wiring:

meta:
    widget: pagebuilder
    pagebuilder:
        blockTypeField: type
        blockRegistry:
            file: /_/assets/dist/admin.mjs?v=${ADMIN_ASSET_VERSION}

Important constraints for this starter:

  • the registry module must be part of the admin bundle, not a random standalone file outside the build pipeline
  • the exported registry keys must match the block type values stored in the collection
  • after registry changes, run yarn build so frontend/dist/admin.mjs is regenerated
  • if the registry file path in YAML and the built admin asset diverge, Nova can still render the schema but the pagebuilder preview/picker loses its real block definitions
  • in Nova pagebuilder preview, file fields are already normalized by the admin backend to absolute http(s)://... URLs when appropriate; preview code must not prepend apiBase, projectBase, or other frontend URL helpers when the value is already absolute
  • Nova may also pass preview rows with hydrated _lookup data for FK-like fields; the registry/block preview should consume that data directly instead of trying to re-fetch or manually hydrate references inside the admin preview

Use collection-level meta.pagebuilder.blockRegistry.file when several pagebuilder fields share the same registry. Override at field level only when one field genuinely needs a different registry.

Step 4: Map each block type to a frontend component

Every allowed type value in the schema must be handled in BlockRenderer.svelte.

Do not leave “temporary” admin-only block types without a renderer unless they are truly non-public and intentionally excluded.

Frontend preparation requirements

For this starter, pagebuilder work is only half done when the collection schema exists. The frontend must be prepared explicitly so block-based rendering stays maintainable.

1. Keep one clear renderer boundary

frontend/src/blocks/BlockRenderer.svelte should remain the central registry that maps block.type to concrete Svelte components.

That means:

  • every public block type in the schema gets one renderer branch
  • unknown block handling stays explicit
  • block selection logic stays centralized instead of being scattered across many unrelated files

Do not distribute block-type branching across the app shell, page components, and nested helpers at the same time.

2. Use a stable component contract

Each block component should receive the block object in a consistent way.

In this starter, the default contract is:

<script lang="ts">
  let { block }: { block: ContentBlockEntry } = $props()
</script>

This matters because the block system becomes much easier to extend when every component follows the same top-level prop contract.

If a block needs additional derived data, derive it inside the component or in a small helper, but do not invent a different top-level prop API for every block.

3. Keep ContentBlockEntry aligned with real frontend usage

The frontend preparation is incomplete until types/global.d.ts can express the fields the block components actually read.

Whenever a new block type or field is added, verify alignment between:

  • collection YAML subfields
  • ContentBlockEntry
  • the block component implementation
  • BlockRenderer.svelte

If a component reads fields that are only implicit or typed as vague leftovers, the pagebuilder is not ready for reliable future extension.

4. Plan lookup data together with the block model

If blocks reference media or foreign entities, the frontend must be prepared to receive the resolved lookup data through the page-loading path.

For this starter, that usually means checking the lookup strings used when loading content in App.svelte.

For Nova admin previews, treat the incoming row differently from a raw frontend API payload:

  • _lookup may already be hydrated by the admin preview pipeline
  • file/image values may already be absolute URLs

Do not add preview logic that blindly rewrites file URLs or assumes it still has to hydrate foreign references before rendering the admin pagebuilder preview.

Do not add a block that depends on:

  • media lookups
  • referenced collections
  • nested foreign references

without also updating the content-loading layer so the renderer receives the required _lookup data.

5. Treat SSR compatibility as part of frontend preparation

A pagebuilder block is not frontend-ready if it only works after hydration.

Every public block should render safely during SSR:

  • no unconditional window/document usage at module top level
  • browser-only behavior guarded inside typeof window !== "undefined"
  • meaningful initial markup without waiting for client-only effects

If a block absolutely requires browser APIs, keep the browser-only part small and ensure the surrounding block still renders a stable SSR shell.

6. Unknown block handling should help development without hiding errors

BlockRenderer.svelte should make unknown block types visible enough during development that schema/frontend drift is caught early.

For this starter, the current renderer already has a development-side unknown-block fallback. Keep a mechanism like that in place when the demo renderer is refactored.

Do not silently swallow unknown block types in a way that makes editor-created content disappear with no signal.

7. Keep block components presentation-focused

Pagebuilder block components should mostly render data, not own cross-page application logic.

Prefer:

  • block-local formatting and small derived values
  • presentational composition
  • small helper components inside frontend/src/blocks/

Avoid pushing these concerns into block components unless there is a strong reason:

  • route loading
  • global app state orchestration
  • unrelated API fetching
  • page-level navigation concerns

8. Prepare for styling consistency across blocks

A block system works better when blocks share a few stable layout conventions.

Examples:

  • container width choices
  • vertical spacing conventions
  • anchor/id behavior
  • CTA shape and link handling
  • media aspect ratio conventions

Do not let every new block invent its own spacing, width, and link semantics from scratch unless the design system really requires it.

When a block is actually ready in the frontend

A new pagebuilder block should only be considered integrated when all of these are true:

  1. The schema contains the block type and required subfields.
  2. ContentBlockEntry expresses the fields used by the block.
  3. A dedicated Svelte block component exists in frontend/src/blocks/.
  4. BlockRenderer.svelte routes the block type to that component.
  5. Any required lookup data is loaded by the app content-loading path.
  6. The block renders acceptably in SSR and browser navigation.
  7. Unknown or stale block types remain debuggable.

Step 5: Keep types aligned

Update project types when the block model changes.

In this starter family, block schemas usually affect:

  • types/global.d.ts
  • Svelte component props
  • block renderer branching

If TypeScript cannot express the new block shape, the schema work is incomplete.

Practical block design patterns

Hero block

Use for top-of-page messaging. Keep the editor form short and obvious.

Typical fields:

  • eyebrow
  • headline
  • subline
  • image
  • cta
  • variant

Use dependsOn for variant-specific media and CTA settings.

Rich text block

Use for long-form body content. Avoid mixing it with too many presentational toggles.

Typical fields:

  • headline
  • body
  • maxWidth

Feature grid block

Use nested repeatable objects for feature items, but make the parent block previewable.

Typical fields:

  • headline
  • items[]
  • columns
  • variant

For items[], add its own preview so editors can scan the nested list.

Reusable section reference

If the same content must appear on many pages, consider a dedicated collection plus foreign reference instead of copy-pasting large pagebuilder blocks.

Use foreign previews so editors understand the referenced entity before opening it.

Anti-patterns

  • One block type per page template fragment with no reuse
  • Giant catch-all block with dozens of unrelated optional fields
  • No preview on nested objects
  • No drill-down for large objects
  • Using array order as the only meaning without labels or previews
  • Frontend blocks that exist without matching collection schema
  • Collection schema values that have no renderer

Verification checklist

After adding or changing pagebuilder blocks, verify all of these:

  1. Editors can identify blocks quickly in Nova.
  2. The block form hides irrelevant fields.
  3. Reordering works without losing meaning.
  4. BlockRenderer.svelte handles every public block type.
  5. SSR renders the affected page correctly.
  6. yarn validate stays clean.

What an LLM should inspect first

When asked to extend a pagebuilder on this starter, inspect in this order:

  1. api/collections/content.yml
  2. frontend/src/blocks/BlockRenderer.svelte
  3. existing files in frontend/src/blocks/
  4. types/global.d.ts
  5. tibi-admin-nova/types/admin.d.ts

This order prevents schema-only or frontend-only changes.