Status: Stable ✅
The Pages feature provides a CMS-like page management system with page types, block-based editing, tree organization, and custom domain support.
Add the pages and editor layers to your nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'@fyit/crouton',
'@fyit/crouton-editor' // Required for rich text blocks
]
})
/[team]/[locale]/[...slug] for public pages| Route | Purpose |
|---|---|
/[team]/[...slug] | Public page display (no locale) |
/[team]/[locale]/[...slug] | Public page display (with locale) |
/[team]/ | Homepage (empty slug) |
/admin/[team]/pages | Admin page management |
Apps can register custom page types that appear in the page type selector.
core:regular - Standard rich text page// app.config.ts
export default defineAppConfig({
croutonApps: {
bookings: {
id: 'bookings',
name: 'Bookings',
pageTypes: [
{
id: 'calendar',
name: 'Booking Calendar',
component: 'CroutonBookingsCalendar',
icon: 'i-lucide-calendar',
category: 'customer',
description: 'Interactive calendar for bookings'
}
]
}
}
})
const {
pageTypes, // All aggregated page types (apps + publishable collections)
getPageType // Get by fullId (e.g., 'bookings:calendar')
} = usePageTypes()
The block-based editor uses TipTap and Nuxt UI page components.
| Block | Component | Purpose |
|---|---|---|
heroBlock | UPageHero | Title, description, CTA, image |
sectionBlock | UPageSection | Feature grid with icons |
ctaBlock | UPageCTA | Call-to-action banner |
cardGridBlock | UPageGrid + UPageCard | Grid of cards |
separatorBlock | USeparator | Visual divider |
richTextBlock | prose div | Standard text content |
faqBlock | FAQ | Frequently asked questions accordion |
twoColumnBlock | TwoColumn | Two-column layout |
imageBlock | Image | Image with caption |
videoBlock | Video | Embedded video |
fileBlock | File | File download/attachment |
embedBlock | Embed | External embed (iframe) |
buttonRowBlock | ButtonRow | Row of action buttons |
statsBlock | Stats | Statistics/metrics display |
galleryBlock | Gallery | Image gallery grid |
logoBlock | Logo | Logo cloud/display |
collectionBlock | Collection | Dynamic collection listing |
addonBlock | Addon | Addon/extension content |
<template>
<CroutonPagesBlockContent
v-model="content"
placeholder="Type / to insert a block..."
/>
</template>
Content auto-detects format:
type: 'doc' - Renders as blocksinterface PageBlockContent {
type: 'doc'
content: PageBlock[]
}
interface PageBlock {
type: 'heroBlock' | 'sectionBlock' | 'ctaBlock' | 'cardGridBlock' | 'separatorBlock' | 'richTextBlock' | 'faqBlock' | 'twoColumnBlock' | 'imageBlock' | 'videoBlock' | 'fileBlock' | 'embedBlock' | 'buttonRowBlock' | 'statsBlock' | 'galleryBlock' | 'logoBlock' | 'collectionBlock' | 'addonBlock'
attrs: Record<string, any>
}
interface PageRecord {
id: string
teamId: string
title: string
slug: string
pageType: string // 'core:regular' or 'appId:pageTypeId'
content?: string // For regular pages (JSON blocks or HTML)
config?: object // For app pages (type-specific settings)
status: 'draft' | 'published' | 'archived'
visibility: 'public' | 'members' | 'hidden'
showInNavigation: boolean
parentId?: string // For hierarchy
order: number // For sorting
path?: string // Materialized path
depth?: number // Nesting level
}
The pages package supports custom domain resolution.
Custom Domain Request: booking.acme.com/about
│
▼
Domain resolver middleware
│ looks up 'booking.acme.com' in domain table
│ finds: organizationId → org with slug 'acme'
▼
URL Rewrite: /about → /acme/about
│
▼
Normal routing: [team]/[...slug].vue
const {
isCustomDomain, // Whether request is from custom domain
resolvedDomain, // The custom domain hostname
resolvedTeamId, // Team ID from domain lookup
hideTeamInUrl, // true on custom domains
hostname, // Current hostname
isAppDomain // Whether hostname is known app domain
} = useDomainContext()
runtimeConfig: {
public: {
croutonPages: {
// Domains to skip (not custom domains)
appDomains: ['myapp.com', 'staging.myapp.com'],
debug: false
}
}
}
Build navigation from published pages:
const {
navigation, // Hierarchical navigation tree
flatNavigation, // Flat list of all nav items
isLoading, // Loading state
currentPage, // Current active page
isActive, // Check if nav item is active
refresh, // Refresh navigation data
team // Current team slug
} = useNavigation()
| Component | Purpose |
|---|---|
CroutonPagesRenderer | Renders page based on type |
CroutonPagesRegularContent | Rich text content display |
CroutonPagesBlockContent | Block-based content display and editor |
CroutonPagesForm | Page creation/editing form (registered via manifest, not shipped as a standalone file) |
<template>
<CroutonCollection
collection="pagesPages"
layout="tree"
:columns="['title', 'slug', 'pageType', 'status']"
/>
</template>
<template>
<CroutonPagesRenderer :page="pageData" />
</template>
| Endpoint | Method | Purpose |
|---|---|---|
/api/teams/[id]/pages | GET | List published pages (for navigation) |
/api/teams/[id]/pages/[...slug] | GET | Get single page by slug (catch-all) |