✨ feat: add admin-ui-config, content-authoring, and frontend-architecture skills documentation
- Introduced `admin-ui-config` skill for configuring admin UI for collections. - Added `content-authoring` skill detailing page and block creation in the CMS. - Included `frontend-architecture` skill explaining custom SPA routing and state management. - Updated `AGENTS.md` to reference new skills and provide infrastructure prerequisites. - Enhanced `frontend/AGENTS.md` with routing details and SPA navigation information.
This commit is contained in:
325
.agents/skills/content-authoring/SKILL.md
Normal file
325
.agents/skills/content-authoring/SKILL.md
Normal file
@@ -0,0 +1,325 @@
|
||||
---
|
||||
name: content-authoring
|
||||
description: Add new pages, content blocks, and collections to a tibi project. Covers the content-based routing model, block registration in BlockRenderer, collection YAML authoring, and TypeScript type definitions. Use when creating new pages, block types, or collections.
|
||||
---
|
||||
|
||||
# content-authoring
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Adding a new page to the website
|
||||
- Creating a new content block type (e.g. testimonials, pricing table, gallery)
|
||||
- Adding a new collection to the CMS (e.g. products, events, team members)
|
||||
- Understanding how content is structured and rendered
|
||||
|
||||
## Key concept: content-based routing
|
||||
|
||||
This project does **NOT** use file-based routing (no SvelteKit router). Instead:
|
||||
|
||||
1. Pages are **CMS entries** in the `content` collection with a `path` field.
|
||||
2. `App.svelte` reacts to URL changes → calls `getCachedEntries("content", { lang, path, active: true })`.
|
||||
3. The matching `ContentEntry.blocks[]` array is passed to `BlockRenderer.svelte`.
|
||||
4. Each block has a `type` field that maps to a Svelte component.
|
||||
|
||||
**Implication:** To add a new page, you create a content entry (via Admin UI or API) — no new Svelte file or route config is needed.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new page
|
||||
|
||||
### Option A: Via Admin UI (preferred for content editors)
|
||||
|
||||
1. Open the tibi-admin at `CODING_URL/_/admin/`.
|
||||
2. Navigate to **Inhalte** (Content) collection.
|
||||
3. Click **New** and fill in:
|
||||
- `name`: Display name (e.g. "Über uns")
|
||||
- `path`: URL path without language prefix (e.g. `/ueber-uns`)
|
||||
- `lang`: Language code (e.g. `de`)
|
||||
- `active`: `true`
|
||||
- `translationKey`: Shared key for cross-language linking (e.g. `about`)
|
||||
- `blocks`: Add content blocks (see below)
|
||||
- `meta.title` / `meta.description`: SEO metadata
|
||||
4. Save. The page is immediately available at `/{lang}{path}`.
|
||||
|
||||
### Option B: Via API
|
||||
|
||||
```sh
|
||||
curl -X POST "$CODING_URL/api/content" \
|
||||
-H "Token: $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"active": true,
|
||||
"lang": "de",
|
||||
"name": "Über uns",
|
||||
"path": "/ueber-uns",
|
||||
"translationKey": "about",
|
||||
"blocks": [
|
||||
{ "type": "hero", "headline": "Über uns", "subline": "Unser Team" }
|
||||
],
|
||||
"meta": { "title": "Über uns", "description": "Erfahre mehr über unser Team." }
|
||||
}'
|
||||
```
|
||||
|
||||
### Option C: Via mock data (for MOCK=1 mode)
|
||||
|
||||
Add the entry to `frontend/mocking/content.json` — the mock engine supports MongoDB-style filtering.
|
||||
|
||||
### Adding to navigation
|
||||
|
||||
To make the page appear in the header/footer menu, edit the corresponding `navigation` entry:
|
||||
|
||||
```sh
|
||||
# Get existing header nav
|
||||
curl "$CODING_URL/api/navigation?filter[type]=header&filter[language]=de" -H "Token: $ADMIN_TOKEN"
|
||||
|
||||
# PUT to update elements array (add your page)
|
||||
curl -X PUT "$CODING_URL/api/navigation/<id>" \
|
||||
-H "Token: $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "elements": [ ...existing, { "name": "Über uns", "page": "/ueber-uns" } ] }'
|
||||
```
|
||||
|
||||
### Multi-language pages
|
||||
|
||||
- Create one `ContentEntry` per language with the **same `translationKey`** but different `lang` and `path`.
|
||||
- The language switcher in `App.svelte` uses `currentContentEntry.translationKey` to find the equivalent page.
|
||||
- Add localized route slugs to `ROUTE_TRANSLATIONS` in `frontend/src/lib/i18n.ts` if URLs should differ per language (e.g. `/ueber-uns` vs `/about`).
|
||||
|
||||
---
|
||||
|
||||
## Adding a new content block type
|
||||
|
||||
### Step 1: Create the Svelte component
|
||||
|
||||
Create `frontend/src/blocks/MyNewBlock.svelte`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
let { block }: { block: ContentBlockEntry } = $props()
|
||||
</script>
|
||||
|
||||
<section class="py-16 sm:py-24" id={block.anchorId || undefined}>
|
||||
<div class="max-w-6xl mx-auto px-6">
|
||||
{#if block.headline}
|
||||
<h2 class="text-3xl font-bold mb-6">{block.headline}</h2>
|
||||
{/if}
|
||||
<!-- Block-specific content here -->
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Conventions:**
|
||||
|
||||
- Accept `block: ContentBlockEntry` as the single prop.
|
||||
- Use `block.anchorId` for scroll anchoring.
|
||||
- Respect `block.containerWidth` (`""` = default, `"wide"`, `"full"`).
|
||||
- Guard browser-only code with `typeof window !== "undefined"` (SSR safety).
|
||||
|
||||
### Step 2: Register in BlockRenderer
|
||||
|
||||
Edit `frontend/src/blocks/BlockRenderer.svelte`:
|
||||
|
||||
```svelte
|
||||
<!-- Add import at the top -->
|
||||
import MyNewBlock from "./MyNewBlock.svelte"
|
||||
|
||||
<!-- Add case in the {#each} block -->
|
||||
{:else if block.type === "my-new-block"}
|
||||
<MyNewBlock {block} />
|
||||
```
|
||||
|
||||
### Step 3: Extend TypeScript types (if new fields are needed)
|
||||
|
||||
Edit `types/global.d.ts` — add fields to `ContentBlockEntry`:
|
||||
|
||||
```typescript
|
||||
interface ContentBlockEntry {
|
||||
// ... existing fields ...
|
||||
// my-new-block fields
|
||||
myCustomField?: string
|
||||
myItems?: { title: string; description: string }[]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Extend collection YAML (if new fields need admin editing)
|
||||
|
||||
Edit `api/collections/content.yml` — add subFields under `blocks`:
|
||||
|
||||
```yaml
|
||||
- name: blocks
|
||||
type: object[]
|
||||
subFields:
|
||||
# ... existing subFields ...
|
||||
- name: myCustomField
|
||||
type: string
|
||||
- name: myItems
|
||||
type: object[]
|
||||
subFields:
|
||||
- name: title
|
||||
type: string
|
||||
- name: description
|
||||
type: string
|
||||
```
|
||||
|
||||
### Step 5: Update mock data (if using MOCK=1)
|
||||
|
||||
Add a block with your new type to `frontend/mocking/content.json`.
|
||||
|
||||
### Step 6: Verify
|
||||
|
||||
```sh
|
||||
yarn validate # TypeScript check — must be warning-free
|
||||
```
|
||||
|
||||
### Existing block types for reference
|
||||
|
||||
| Type | Component | Purpose |
|
||||
| -------------- | ------------------------- | ----------------------------------------- |
|
||||
| `hero` | `HeroBlock.svelte` | Full-width hero with image, headline, CTA |
|
||||
| `features` | `FeaturesBlock.svelte` | Feature grid with icons |
|
||||
| `richtext` | `RichtextBlock.svelte` | Rich text with optional image |
|
||||
| `accordion` | `AccordionBlock.svelte` | Expandable FAQ/accordion items |
|
||||
| `contact-form` | `ContactFormBlock.svelte` | Contact form |
|
||||
|
||||
---
|
||||
|
||||
## Adding a new collection
|
||||
|
||||
### Step 1: Create collection YAML
|
||||
|
||||
Create `api/collections/mycollection.yml`. Use `content.yml` or `navigation.yml` as a template:
|
||||
|
||||
```yaml
|
||||
########################################################################
|
||||
# MyCollection — description of what this collection stores
|
||||
########################################################################
|
||||
|
||||
name: mycollection
|
||||
meta:
|
||||
label: { de: "Meine Sammlung", en: "My Collection" }
|
||||
muiIcon: category # Material UI icon name
|
||||
rowIdentTpl: { twig: "{{ name }}" } # Row display in admin list
|
||||
|
||||
views:
|
||||
- type: simpleList
|
||||
mediaQuery: "(max-width: 600px)"
|
||||
primaryText: name
|
||||
- type: table
|
||||
columns:
|
||||
- name
|
||||
- source: active
|
||||
filter: true
|
||||
|
||||
permissions:
|
||||
public:
|
||||
methods:
|
||||
get: true # Public read access
|
||||
user:
|
||||
methods:
|
||||
get: true
|
||||
post: true
|
||||
put: true
|
||||
delete: true
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
label: { de: "Aktiv", en: "Active" }
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "Name", en: "Name" }
|
||||
# Add more fields as needed
|
||||
```
|
||||
|
||||
**Field types:** `string`, `number`, `boolean`, `object`, `object[]`, `string[]`, `file`, `file[]`.
|
||||
|
||||
For the full schema reference: `tibi-types/schemas/api-config/collection.json`.
|
||||
|
||||
### Step 2: Include in config.yml
|
||||
|
||||
Edit `api/config.yml`:
|
||||
|
||||
```yaml
|
||||
collections:
|
||||
- !include collections/content.yml
|
||||
- !include collections/navigation.yml
|
||||
- !include collections/ssr.yml
|
||||
- !include collections/mycollection.yml # ← add this line
|
||||
```
|
||||
|
||||
### Step 3: Add TypeScript types
|
||||
|
||||
Edit `types/global.d.ts`:
|
||||
|
||||
```typescript
|
||||
interface MyCollectionEntry {
|
||||
id?: string
|
||||
_id?: string
|
||||
active?: boolean
|
||||
name?: string
|
||||
// ... fields matching your YAML
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Configure API layer (optional)
|
||||
|
||||
If you need typed helpers, extend the `EntryTypeSwitch` in `frontend/src/lib/api.ts`:
|
||||
|
||||
```typescript
|
||||
type CollectionNameT = "medialib" | "content" | "mycollection" | string
|
||||
|
||||
type EntryTypeSwitch<T extends string> = T extends "medialib"
|
||||
? MedialibEntry
|
||||
: T extends "content"
|
||||
? ContentEntry
|
||||
: T extends "mycollection"
|
||||
? MyCollectionEntry
|
||||
: Record<string, unknown>
|
||||
```
|
||||
|
||||
### Step 5: Add hooks (optional)
|
||||
|
||||
Common hook patterns:
|
||||
|
||||
- **Public filter** — reuse `filter_public.js` to enforce `active: true` for unauthenticated users.
|
||||
- **Before-save validation** — create `api/hooks/mycollection_validate.js`.
|
||||
- **Cache invalidation** — add your collection to `api/hooks/clear_cache.js` if it affects rendered pages.
|
||||
|
||||
Reference hook in YAML:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
beforeRead: |
|
||||
!include hooks/filter_public.js
|
||||
afterWrite: |
|
||||
!include hooks/clear_cache.js
|
||||
```
|
||||
|
||||
### Step 6: Add mock data (if using MOCK=1)
|
||||
|
||||
Create `frontend/mocking/mycollection.json`:
|
||||
|
||||
```json
|
||||
[{ "_id": "1", "active": true, "name": "Example Entry" }]
|
||||
```
|
||||
|
||||
### Step 7: Verify
|
||||
|
||||
```sh
|
||||
yarn validate # TypeScript check
|
||||
# If Docker is running, the tibi-server auto-reloads the collection config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Path format**: Content paths do NOT include the language prefix. The path `/ueber-uns` becomes `/{lang}/ueber-uns` via the i18n layer.
|
||||
- **Active flag**: Pages with `active: false` are filtered out by `filter_public.js` for public users. The admin can still see them.
|
||||
- **Block `hide` field**: Blocks with `hide: true` are skipped by `BlockRenderer.svelte` — useful for draft blocks.
|
||||
- **Collection YAML indentation**: YAML uses 2-space indentation. Sub-fields under `object[]` require a `subFields` key.
|
||||
- **After adding a collection**: The tibi-server auto-reloads hooks on file change, but a new collection in `config.yml` may require `make docker-restart-frontend` or a full `make docker-up`.
|
||||
Reference in New Issue
Block a user