- Implemented `resolveApiAssetUrl` function to normalize asset URLs based on API base. - Updated `MedialibImage` component to utilize new asset URL resolution and added support for alt text and class properties. - Enhanced image loading behavior with improved width measurement and focal point handling. - Added placeholder image handling and improved accessibility with alt text. - Introduced new test script for auditing broken links in skill documentation. - Expanded seeded test content to include medialib entries and updated related tests for pagebuilder previews. - Improved global setup and teardown logging for clarity on seeded content management.
23 KiB
name, description
| name | description |
|---|---|
| admin-ui-config | Configure the admin UI for collections and project-level Nova behavior — meta labels, preview/viewHint, sidebar layout, collectionGroups, i18n, field widgets, foreign references, and image handling. Use when setting up or customizing admin views. |
admin-ui-config
When to use this skill
Use this skill when:
- Configuring how a collection appears in the tibi-admin UI
- Configuring collection preview and default list presentation
- Configuring field widgets (dropdowns, media pickers, richtext, etc.)
- Organizing fields into sidebar groups or sections
- Setting up foreign key references between collections
- Customizing the admin module (
frontend/src/admin.ts)
Reference source
The canonical type definitions are in tibi-admin-nova/types/admin.d.ts. Always consult this file for the full API. This skill provides a practical summary.
Treat this skill as Nova-first. Use current Nova concepts such as preview, singleton: { enabled }, drillDown, dependsOn, containerProps.layout, pagebuilder, viewHint, subNavigation, and AI media assist.
Project-level admin contracts
Not every important Nova contract lives in a collection YAML file. Some of the most important admin behaviors are configured at project level in api/config.yml under meta:.
Current starter example:
meta:
imageUrl:
eval: "$projectBase + '_/assets/img/admin-pic.svg'"
i18n:
defaultLanguage: de
languages:
- code: de
label: Deutsch
- code: en
label: English
collectionGroups:
- name: content
label: { de: "Inhalte", en: "Content" }
icon: article
- name: media
label: { de: "Medien", en: "Media" }
icon: image_multiple
Treat these as part of the admin design, not as optional polish:
meta.imageUrl— project card/preview imagery in the adminmeta.i18n— project-wide language model for field-level and entry-level translation workflowsmeta.collectionGroups— ordered collection groups for the sidebar
Important rule:
- collection-level
meta.groupmust reference one of the project-levelmeta.collectionGroups[].namevalues if the collection should appear inside an explicit group
If project-level meta.i18n is missing or inconsistent, even well-modeled collections can become confusing in Nova.
Collection meta configuration
The meta key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI.
name: mycollection
meta:
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
muiIcon: shopping_cart # Material UI icon name
group: shop # Group in admin sidebar
singleton:
enabled: false
hide: false # Set to true to hide the collection for non-admin users
preview:
label: name
secondary: price
Preview
Use meta.preview as the universal entry representation for Nova lists, breadcrumbs, foreign-key widgets, and search result previews:
preview: name
preview:
label: name
secondary: slug
badge: status
preview:
eval: "`${$this.firstName} ${$this.lastName}`"
List presentation
For current Nova, use meta.viewHint plus meta.preview for collection/list presentation.
meta:
viewHint: table
preview:
label: name
secondary: slug
badge: status
table:
- name
- source: status
label: Status
- source: author.name
label: Author
select:
- author.name
meta.viewHintcontrols the preferred collection presentation (table,cards,media, ornavigationobject where supported).preview.tabledefines explicit list columns for Nova.preview.selectcan reduce lookup work for preview table columns.meta.subNavigationdefines filtered entry tabs in the sidebar.
Sub-navigation tabs
Use meta.subNavigation when one collection needs multiple curated views in the admin without splitting into multiple collections.
meta:
subNavigation:
- name: pages
label: { de: "Seiten", en: "Pages" }
muiIcon: article
filter:
type: page
defaultSort:
field: insertTime
order: DESC
setDefault:
field: type
value: page
- name: news
label: { de: "News", en: "News" }
muiIcon: feed
filter:
type: news
setDefault:
field: type
value: news
Use sub-navigation when:
- one collection has several stable editorial slices
- the underlying schema is still shared enough to stay one collection
- authors benefit from filtered entry views and sensible defaults
Do not use sub-navigation to hide a bad collection model. If the workflows truly diverge, split the collection instead.
Field configuration
Each field in the fields array can have a meta section controlling its admin UI behavior.
Basic field with meta
fields:
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
helperText: { de: "Anzeigename", en: "Display name" }
position: main # "main" (default) or "sidebar"
Field types
YAML type |
Admin widget (default) | Notes |
|---|---|---|
string |
Text input | Use inputProps.multiline: true for textarea |
number |
Number input | |
number[] |
Number chip array | Multiple numeric values |
boolean |
Toggle/checkbox | |
date |
Date picker | |
object |
Nested field group | Requires subFields |
object[] |
Repeatable group | Requires subFields, drag-to-reorder |
string[] |
Tag input | |
file |
File upload | |
file[] |
Multi-file upload | |
any |
JSON editor | For mixed/arbitrary data |
inputProps — widget customization
inputProps passes props directly to the field widget:
# Multiline text (textarea)
- name: description
type: string
meta:
label: { de: "Beschreibung", en: "Description" }
inputProps:
multiline: true
rows: 5
# Number with min/max
- name: price
type: number
meta:
inputProps:
min: 0
max: 99999
step: 0.01
# Placeholder text
- name: email
type: string
meta:
inputProps:
placeholder: "name@example.com"
Widget override
Override the default widget with meta.widget:
- name: content
type: string
meta:
widget: richtext # Rich text editor (HTML)
- name: heroImage
type: file
meta:
widget: image # Image-focused file widget
- name: relatedPages
type: string[]
meta:
widget: foreignKeyChipArray
Common widget types: text, checkbox, select, chipArray, checkboxArray, date, datetime, file, image, richtext, json, foreignKey, foreignKeyChipArray, pagebuilder, containerLessObject, containerLessObjectArray.
Important current widgets/features to consider when designing a real website backoffice:
pagebuilderfor CMS-driven block/page authoringforeignKeyChipArrayfor many-reference editingimageplusimageEditor/downscalefor image-heavy workflowsdrillDownediting for complex nested arrays
Choices — dropdowns/selects
Static choices:
- name: type
type: string
meta:
label: { de: "Typ", en: "Type" }
choices:
- id: page
name: { de: "Seite", en: "Page" }
- id: blog
name: { de: "Blog", en: "Blog" }
- id: product
name: { de: "Produkt", en: "Product" }
Dynamic choices from API:
- name: category
type: string
meta:
choices:
endpoint: categories # Collection name
mapping:
id: id
name: name
Foreign references
Link to entries in another collection:
- name: author
type: string
meta:
label: { de: "Autor", en: "Author" }
foreign:
collection: users
id: id
sort: name
projection: name,email
render:
label: name
secondary: email
createDefaults:
role: author
Use foreign.id: id for the outward FK field identity. Only Mongo-style filters/query conditions use _id. Use foreign.render or target-collection meta.preview so references stay readable. Bare IDs are not acceptable authoring UX for a serious website project.
Image fields
- name: image
type: file
meta:
widget: image
downscale: # Auto-resize on upload
maxWidth: 1920
maxHeight: 1080
quality: 0.85
imageEditor: true # Enable crop/rotate editor
This field config controls the editor widget, not the filesystem target. Configure file storage once at collection level via top-level uploadPath (for this starter typically ../media/<collection>), not on the individual file field.
Layout: position, sections, sidebar
Sidebar placement
- name: active
type: boolean
meta:
position: sidebar # Moves field to sidebar
- name: publishDate
type: date
meta:
position: "sidebar:publishing" # Sidebar with group key
Sidebar groups (ordered)
Define sidebar group order in collection meta:
meta:
sidebar:
- group: publishing
label: { de: "Veröffentlichung", en: "Publishing" }
- group: seo
label: { de: "SEO", en: "SEO" }
- group: settings
label: { de: "Einstellungen", en: "Settings" }
Sections in main area
- name: seoTitle
type: string
meta:
section: SEO # Groups fields under a section header
- name: seoDescription
type: string
meta:
section: SEO
Grid layout (columns)
Use containerProps for multi-column layout:
- name: firstName
type: string
meta:
containerProps:
layout:
size: col-6 # Half width (12-column grid)
- name: lastName
type: string
meta:
containerProps:
layout:
size: col-6
containerProps.layout is one of the most important Nova ergonomics features. Use it aggressively to avoid long, single-column forms.
Recommended pattern for real projects:
- sidebar for publication, SEO, flags, relations, admin-only metadata
- main area for editorial content
- 2-column or 3-column layout for short related fields
- section headings for repeated conceptual groups
Nested objects and arrays
Object (nested group)
- name: address
type: object
meta:
label: { de: "Adresse", en: "Address" }
subFields:
- name: street
type: string
- name: city
type: string
- name: zip
type: string
Object array (repeatable blocks)
- name: blocks
type: object[]
meta:
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
widget: pagebuilder
preview: { eval: "`${$this.type}: ${$this.headline || ''}`" }
drillDown: true
subFields:
- name: type
type: string
meta:
choices:
- id: hero
name: Hero
- id: richtext
name: Richtext
- name: headline
type: string
- name: hide
type: boolean
The preview eval determines what's shown in the collapsed state of each array item.
Drill-down arrays
For complex object[] data, prefer drillDown: true over dense inline editing. This is especially important for:
- nested content blocks
- FAQs / accordions
- team members with nested metadata
- pricing tables / feature matrices
Pagebuilder fields
Nova supports pagebuilder configuration at both collection and field level.
Typical pattern:
meta:
pagebuilder:
blockTypeField: type
defaultViewport: desktop
blockRegistry:
file: /_/assets/dist/admin.mjs
fields:
- name: blocks
type: object[]
meta:
widget: pagebuilder
pagebuilder:
blockTypeField: type
Use pagebuilder when editors work with heterogeneous content blocks. Use plain object[] only when the structure is uniform and simple.
dependsOn
Use dependsOn to show only fields relevant to the selected block or mode:
- name: image
type: file
meta:
dependsOn:
eval: $parent.type == 'hero'
This is critical for keeping pagebuilder schemas usable.
AI-aware media and admin features
Current Nova types support AI-related admin capabilities, especially around media workflows. When appropriate for a project:
- use AI-assisted alt/caption generation for image-heavy collections
- prefer explicit target fields for generated metadata
- keep AI assist opt-in and editorially reviewable
Use AI only where it improves authoring quality; do not force it into every collection.
Field-level permissions and authoring safety
Current tibi-server supports readonlyFields, hiddenFields, and eval-based field visibility/readonly rules.
Reflect these server rules in admin design:
- do not put critical computed fields front-and-center if editors may not be allowed to modify them
- use
dependsOn,hidden, and readonly semantics deliberately - remember that server-side permissions are authoritative even if the UI looks editable
Drill-down
For complex nested objects, use drillDown to render them as a sub-page:
- name: variants
type: object[]
meta:
drillDown: true # Opens as sub-page instead of inline
Admin module (frontend/src/admin.ts)
The admin.ts file exports the pagebuilder block registry and optional custom Svelte components for the tibi-admin UI. This is how the admin preview renders your Svelte blocks.
Pagebuilder block registry
The current starter uses createContentBlockDefinition() to register each block type. This mounts real Svelte block components into Shadow DOM for admin previews:
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
import BlockRenderer from "./blocks/BlockRenderer.svelte"
// Creates a block definition that renders the same Svelte component
// used in the public frontend. The block is mounted inside Shadow DOM
// for style isolation.
function createContentBlockDefinition(presentation: { label: string; icon: string; color: string }) {
return {
css: [previewCssUrl], // CSS files to inject into Shadow DOM
label: presentation.label,
icon: presentation.icon,
color: presentation.color,
previewStyles: {
"background-color": "white",
},
render(container, row, context) {
// Mount the Svelte component inside the admin preview
const target = document.createElement("div")
container.appendChild(target)
let mountedComponent = mount(BlockRenderer as Component<any>, {
target,
props: { blocks: [row], isAdminPreview: true },
})
return {
update(nextRow) {
unmount(mountedComponent)
target.innerHTML = ""
mountedComponent = mount(BlockRenderer as Component<any>, {
target,
props: { blocks: [nextRow], isAdminPreview: true },
})
},
destroy() {
unmount(mountedComponent)
target.remove()
},
}
},
}
}
const blockRegistry = {
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
richtext: createContentBlockDefinition({ label: "Richtext", icon: "article", color: "#7c3aed" }),
// ... add new blocks here
}
export { blockRegistry }
Key points:
- Each registry entry wraps the Svelte
BlockRendererto render the block in the admin preview. - The
rowobject is the block data (same shape asContentBlockEntry). - Preview data may contain hydrated
_lookup.<fieldPath>foreign key data and absolute file URLs — do not prependapiBaseor attempt re-fetching. - The
previewCssUrlloads the project'sindex.cssinto Shadow DOM so block styles apply. - After adding blocks to the registry, run
yarn buildsofrontend/dist/admin.mjsis regenerated.
Custom Svelte components (advanced)
For custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI, use getRenderedElement():
import type { SvelteComponent } from "svelte"
function getRenderedElement(
component: typeof SvelteComponent,
options?: { props: { [key: string]: any }; addCss?: string[] },
nestedElements?: { tagName: string; className?: string }[]
) {
// Creates a Shadow DOM container, mounts the Svelte component inside
}
export { getRenderedElement }
Build
Run yarn build. The admin module (frontend/src/admin.ts) is compiled into frontend/dist/admin.mjs as part of the esbuild build pipeline (the same build produces both index.mjs for the SPA and admin.mjs for the admin module). tibi-admin-nova loads this module from the project's asset path (/_/assets/dist/admin.mjs). The ADMIN_ASSET_VERSION from config.yml.env is appended as a query parameter for cache busting: admin.mjs?v=${ADMIN_ASSET_VERSION}.
Complete collection example
name: products
meta:
label: { de: "Produkte", en: "Products" }
muiIcon: inventory_2
group: shop
viewHint: table
defaultSort:
field: insertTime
order: DESC
preview:
label: name
secondary: sku
badge: active
table:
- name
- sku
- source: price
label: { de: "Preis", en: "Price" }
- source: category
label: { de: "Kategorie", en: "Category" }
sidebar:
- group: publishing
label: { de: "Veröffentlichung", en: "Publishing" }
- group: seo
label: { de: "SEO", en: "SEO" }
permissions:
public:
methods:
get: true
user:
methods:
get: true
post: true
put: true
delete: false # usually false for real editorial workflows
fields:
- name: active
type: boolean
meta:
label: { de: "Aktiv", en: "Active" }
position: "sidebar:publishing"
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
- name: sku
type: string
meta:
label: { de: "Artikelnummer", en: "SKU" }
containerProps:
layout:
size: col-6
- name: price
type: number
meta:
label: { de: "Preis", en: "Price" }
inputProps:
min: 0
step: 0.01
containerProps:
layout:
size: col-6
- name: category
type: string
meta:
label: { de: "Kategorie", en: "Category" }
choices:
- id: electronics
name: { de: "Elektronik", en: "Electronics" }
- id: clothing
name: { de: "Kleidung", en: "Clothing" }
- name: description
type: string
meta:
label: { de: "Beschreibung", en: "Description" }
inputProps:
multiline: true
rows: 4
- name: image
type: file
meta:
label: { de: "Produktbild", en: "Product Image" }
widget: image
downscale:
maxWidth: 1200
quality: 0.85
- name: seoTitle
type: string
meta:
label: { de: "SEO Titel", en: "SEO Title" }
position: "sidebar:seo"
- name: seoDescription
type: string
meta:
label: { de: "SEO Beschreibung", en: "SEO Description" }
position: "sidebar:seo"
inputProps:
multiline: true
rows: 3
Indexes and search
For production collections with many entries, consider adding indexes in the YAML:
name: products
indexes:
- name: price_sort
key: [price]
- name: category_active
key: [category, -active] # -prefix for descending
- name: slug_unique
key: [slug]
unique: true
Search configurations can be added for advanced text/vector search:
search:
- name: default
mode: text
fields: [name, description]
See tibi-server/docs/04-collections.md (sections on indexes and search config) for full reference.
Checklist-facing verification
For a real project, do not stop after writing the YAML. Validate the authoring contract explicitly.
Minimum review points:
- project-level
meta.i18nandmeta.collectionGroupsare coherent - each collection has a readable
meta.preview - list views show meaningful columns instead of raw IDs or empty rows
- foreign references render with readable previews
- sidebars and
containerProps.layoutproduce usable forms - pagebuilder collections expose both a working chooser and working preview path
Committed admin Playwright coverage is preferred for stable contracts that should not regress.
Common pitfalls
meta.labelsupports both strings and i18n objects — Use i18n objects only when the collection or field label must be localized.- Project-level admin config is easy to forget —
collectionGroupsand project-levelmeta.i18nlive inapi/config.yml, not in individual collection files. meta.groupwithout a matching project group — The collection still exists, but the sidebar grouping model becomes inconsistent.choices.idmust match stored value — Theidin choices is what gets saved to the database.inputPropsdepends on widget — Not all props work with all widgets. Check tibi-admin-nova source if unsure.position: sidebarwithout group — Fields go to an ungrouped area. Useposition: "sidebar:GroupName"for grouping.type: object[]needssubFields— ForgettingsubFieldsrenders an empty repeater.