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

CollectionUseRouteFields to check
branchesPhysical locations/branches/:slugname, slug, city, is_active
menu_categoriesMenu grouping/categories/:slugname, slug
menu_itemsDishes or products/menu/:branch/:slug when route data existsname, slug, description, base_price, category_id
item_branch_pricingPer-branch pricing and availabilitynot directly routablemenu_item_id, branch_id, price, is_available
inventoryStock per item and branchnot directly routablemenu_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.