✨ 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.
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
---
|
||||
name: nova-pagebuilder-modeling
|
||||
description: 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:
|
||||
|
||||
```yaml
|
||||
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.
|
||||
|
||||
## Recommended modeling workflow
|
||||
|
||||
### 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:
|
||||
|
||||
```yaml
|
||||
- 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:
|
||||
|
||||
```ts
|
||||
const blockRegistry = {
|
||||
hero: {
|
||||
label: "Hero",
|
||||
render(container, row, context) {
|
||||
return {
|
||||
update(nextRow, nextContext) {},
|
||||
destroy() {},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export { blockRegistry }
|
||||
```
|
||||
|
||||
And the collection wiring:
|
||||
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```svelte
|
||||
<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.
|
||||
Reference in New Issue
Block a user