Directus restaurant catalog example
This is a generic pattern for a restaurant or catalog CMS. It uses fake collection names and local URLs only. Route scans still need a running frontend; backend-only projects can start with config and agent context.
Collection shape
| Collection | Use | Route | Fields to check |
|---|---|---|---|
branches | Physical locations | /branches/:slug | name, slug, city, is_active |
menu_categories | Menu grouping | /categories/:slug | name, slug |
menu_items | Dishes or products | /menu/:branch/:slug when route data exists | name, slug, description, base_price, category_id |
item_branch_pricing | Per-branch pricing and availability | not directly routable | menu_item_id, branch_id, price, is_available |
inventory | Stock per item and branch | not directly routable | menu_item_id, branch_id, current_stock, low_stock_threshold |
Config
Keep the Directus collections explicit. Only map routes when the frontend can actually render that document shape. If a branch-specific menu route needs a branch slug, make that slug available in the fetched item data or use a denormalized route field.
A starter version of this shape is available from cms-lab init --cms directus --router pages.
import { defineConfig, readCmsDataPath } from "@cms-lab/core";
export default defineConfig({
site: { url: "http://localhost:3000" },
framework: { type: "next", router: "app" },
cms: {
provider: "directus",
url: "http://localhost:8055",
token: process.env.DIRECTUS_TOKEN,
collections: [
{ type: "branch", collection: "branches", uidField: "slug" },
{ type: "category", collection: "menu_categories", uidField: "slug" },
{ type: "menu_item", collection: "menu_items", uidField: "slug", urlField: "routing.url" },
{ type: "pricing", collection: "item_branch_pricing", uidField: "id", routable: false },
{ type: "inventory", collection: "inventory", uidField: "id", routable: false },
],
},
routes: [
{ type: "branch", pattern: "/branches/:slug", getPath: (doc) => "/branches/" + doc.uid },
{
type: "menu_item",
pattern: "/menu/:branch/:slug",
getPath: (doc) => {
if (doc.url) return doc.url;
const branchSlug = readCmsDataPath(doc.data, "branch.slug");
if (typeof branchSlug !== "string") {
throw new Error("menu_item is missing branch.slug route data");
}
return "/menu/" + branchSlug + "/" + doc.uid;
},
},
],
});Required fields
Required field checks are useful today for high-volume collections and junction collections. Relationship checks cover the first useful cross-document invariant: whether one collection has matching related rows in another collection.
checks: {
fields: {
required: [
{ type: "branch", path: "name" },
{ type: "branch", path: "slug" },
{ type: "branch", path: "city", severity: "warning" },
{ type: "menu_item", path: "name" },
{ type: "menu_item", path: "slug" },
{ type: "menu_item", path: "description", severity: "warning" },
{ type: "menu_item", path: "base_price", severity: "warning" },
{ type: "pricing", path: "menu_item_id" },
{ type: "pricing", path: "branch_id" },
{ type: "pricing", path: "price" },
{ type: "inventory", path: "current_stock", severity: "warning" },
],
},
relationships: [
{
from: "menu_item",
to: "pricing",
where: { fromField: "id", toField: "menu_item_id" },
min: 1,
severity: "warning",
},
{
from: "branch",
to: "pricing",
where: { fromField: "id", toField: "branch_id" },
min: 1,
severity: "warning",
},
],
}Images and alt text
If Directus stores images as file objects, cms-lab can use file descriptions as alt text. If a project stores only external image URLs, add a separate text field such as image_alt and require it.
checks: {
fields: {
required: [
{ type: "menu_item", path: "image_url", severity: "warning" },
{ type: "menu_item", path: "image_alt", severity: "warning" },
],
},
}What this catches today
- Branch pages that return 404 or 500.
- Menu item route builders that cannot produce a valid path.
- Missing slugs, required fields, SEO fields, and image alt text.
- Junction rows missing required IDs or price fields.
- Items or branches without any matching pricing rows.
What is not built in yet: richer conditional rules such as "every active menu item must have an available pricing row per active branch" or "ignore archived branches when checking availability." Keep those in project-specific checks until adapter-specific rules exist.
Next steps: Directus provider, backend-only workflow, and large catalog scanning.