octocms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/agentDocs-Z5BI2Y2G.js +38 -0
  2. package/dist/agentDocs-Z5BI2Y2G.js.map +1 -0
  3. package/dist/chunk-4MPOTHTY.js +9 -0
  4. package/dist/chunk-4MPOTHTY.js.map +1 -0
  5. package/dist/chunk-4VLN5EX2.js +9204 -0
  6. package/dist/chunk-4VLN5EX2.js.map +1 -0
  7. package/dist/chunk-6PHFHGTZ.js +35 -0
  8. package/dist/chunk-6PHFHGTZ.js.map +1 -0
  9. package/dist/chunk-7CFFE2I6.js +55 -0
  10. package/dist/chunk-7CFFE2I6.js.map +1 -0
  11. package/dist/chunk-B47VXAHT.js +28 -0
  12. package/dist/chunk-B47VXAHT.js.map +1 -0
  13. package/dist/chunk-BRTXBBVQ.js +46 -0
  14. package/dist/chunk-BRTXBBVQ.js.map +1 -0
  15. package/dist/chunk-C62C776U.js +79 -0
  16. package/dist/chunk-C62C776U.js.map +1 -0
  17. package/dist/chunk-I7KNSICQ.js +114 -0
  18. package/dist/chunk-I7KNSICQ.js.map +1 -0
  19. package/dist/chunk-Q73JSGXV.js +123 -0
  20. package/dist/chunk-Q73JSGXV.js.map +1 -0
  21. package/dist/chunk-W6QJTGBC.js +57 -0
  22. package/dist/chunk-W6QJTGBC.js.map +1 -0
  23. package/dist/cli/index.js +196 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/components/public/index.d.mts +40 -0
  26. package/dist/components/public/index.js +401 -0
  27. package/dist/components/public/index.js.map +1 -0
  28. package/dist/config.d.mts +4 -0
  29. package/dist/config.js +13 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/defineConfig.d.mts +126 -0
  32. package/dist/defineConfig.js +8 -0
  33. package/dist/defineConfig.js.map +1 -0
  34. package/dist/dev-QY534GEH.js +87 -0
  35. package/dist/dev-QY534GEH.js.map +1 -0
  36. package/dist/index.d.mts +5 -0
  37. package/dist/index.js +17 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/init-UGUTJFFI.js +145 -0
  40. package/dist/init-UGUTJFFI.js.map +1 -0
  41. package/dist/jiti-VYEW7A6R.js +3068 -0
  42. package/dist/jiti-VYEW7A6R.js.map +1 -0
  43. package/dist/localReader-I2THES24.js +40 -0
  44. package/dist/localReader-I2THES24.js.map +1 -0
  45. package/dist/query.d.mts +112 -0
  46. package/dist/query.js +11 -0
  47. package/dist/query.js.map +1 -0
  48. package/dist/types.d.mts +352 -0
  49. package/dist/types.js +1 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/typesGen-WBC6CNBG.js +241 -0
  52. package/dist/typesGen-WBC6CNBG.js.map +1 -0
  53. package/dist/update-RMGZMS56.js +57 -0
  54. package/dist/update-RMGZMS56.js.map +1 -0
  55. package/dist/validate-OTJ6ULMP.js +297 -0
  56. package/dist/validate-OTJ6ULMP.js.map +1 -0
  57. package/dist/withOctoCMS.d.mts +6 -0
  58. package/dist/withOctoCMS.js +9 -0
  59. package/dist/withOctoCMS.js.map +1 -0
  60. package/docs/index.md +27 -0
  61. package/docs/overview.md +113 -0
  62. package/docs/schema.md +279 -0
  63. package/globals.css +198 -0
  64. package/package.json +116 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../cli/lib/contentValidator.ts","../cli/commands/validate.ts"],"sourcesContent":["/**\n * Content entry validator — reads JSON files from disk and validates\n * against the CMS schema. Used by `octocms validate`.\n */\n\nimport { existsSync, readdirSync, readFileSync } from 'fs';\nimport { join } from 'path';\n\nimport type { Collection, CollectionField, Config } from '../../types';\n\nexport type ValidationError = {\n file: string;\n collection: string;\n field?: string;\n message: string;\n};\n\nexport type ValidationResult = {\n errors: ValidationError[];\n /** Number of entries checked per collection. */\n counts: Record<string, number>;\n};\n\n/**\n * Validate all content entries against the CMS schema.\n * Returns a list of errors (empty = everything valid).\n */\nexport function validateContent(projectRoot: string, config: Config): ValidationResult {\n const errors: ValidationError[] = [];\n const counts: Record<string, number> = {};\n const contentDir = join(projectRoot, config.contentFolder);\n\n for (const [collectionName, collection] of Object.entries(config.collections)) {\n const collDir = join(contentDir, collectionName);\n if (!existsSync(collDir)) {\n counts[collectionName] = 0;\n continue;\n }\n\n const jsonFiles = readdirSync(collDir).filter((f) => f.endsWith('.json'));\n counts[collectionName] = jsonFiles.length;\n\n for (const file of jsonFiles) {\n const filePath = join(collDir, file);\n const fileErrors = validateEntry(filePath, file, collectionName, collection, contentDir, config);\n errors.push(...fileErrors);\n }\n }\n\n return { errors, counts };\n}\n\nfunction validateEntry(\n filePath: string,\n fileName: string,\n collectionName: string,\n collection: Collection,\n contentDir: string,\n config: Config,\n): ValidationError[] {\n const errors: ValidationError[] = [];\n const ctx = { file: fileName, collection: collectionName };\n\n let raw: string;\n try {\n raw = readFileSync(filePath, 'utf8');\n } catch {\n errors.push({ ...ctx, message: 'Could not read file' });\n return errors;\n }\n\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n errors.push({ ...ctx, message: 'Invalid JSON' });\n return errors;\n }\n\n // Validate sys\n const sys = entry.sys as Record<string, unknown> | undefined;\n if (!sys || typeof sys !== 'object') {\n errors.push({ ...ctx, message: 'Missing sys object' });\n return errors;\n }\n if (!sys.id) {\n errors.push({ ...ctx, message: 'Missing sys.id' });\n }\n if (sys.type !== collectionName) {\n errors.push({ ...ctx, message: `sys.type is \"${String(sys.type)}\", expected \"${collectionName}\"` });\n }\n\n // Validate fields\n const fields = entry.fields as Record<string, unknown> | undefined;\n if (!fields || typeof fields !== 'object') {\n errors.push({ ...ctx, message: 'Missing fields object' });\n return errors;\n }\n\n for (const [fieldName, fieldDef] of Object.entries(collection.fields)) {\n const value = fields[fieldName];\n const fieldErrors = validateFieldValue(fieldName, fieldDef, value, contentDir, config, ctx);\n errors.push(...fieldErrors);\n }\n\n // Check companion files for markdown/richtext fields\n for (const [fieldName, fieldDef] of Object.entries(collection.fields)) {\n if (fieldDef.format === 'markdown') {\n const companionPath = filePath.replace(/\\.json$/, `.${fieldName}.md`);\n if (!existsSync(companionPath) && fieldDef.required) {\n errors.push({ ...ctx, field: fieldName, message: `Missing required companion file (.${fieldName}.md)` });\n }\n }\n if (fieldDef.format === 'richtext') {\n const companionPath = filePath.replace(/\\.json$/, `.${fieldName}.mdx`);\n if (!existsSync(companionPath) && fieldDef.required) {\n errors.push({ ...ctx, field: fieldName, message: `Missing required companion file (.${fieldName}.mdx)` });\n }\n }\n }\n\n return errors;\n}\n\nfunction validateFieldValue(\n fieldName: string,\n fieldDef: CollectionField,\n value: unknown,\n contentDir: string,\n config: Config,\n ctx: { file: string; collection: string },\n): ValidationError[] {\n const errors: ValidationError[] = [];\n const fctx = { ...ctx, field: fieldName };\n\n // Skip companion-file formats (not stored in JSON)\n if (fieldDef.format === 'markdown' || fieldDef.format === 'richtext') {\n return errors;\n }\n\n // Required check\n if (fieldDef.required && (value === undefined || value === null || value === '')) {\n errors.push({ ...fctx, message: `Required field \"${fieldDef.label}\" is empty` });\n return errors;\n }\n\n // Skip further checks if value is absent and not required\n if (value === undefined || value === null) return errors;\n\n switch (fieldDef.format) {\n case 'string': {\n if (fieldDef.list) {\n if (!Array.isArray(value)) {\n errors.push({ ...fctx, message: 'Expected string array' });\n } else if (!value.every((v) => typeof v === 'string')) {\n errors.push({ ...fctx, message: 'All list items must be strings' });\n }\n } else if (typeof value !== 'string') {\n errors.push({ ...fctx, message: 'Expected string' });\n }\n break;\n }\n case 'text':\n case 'slug':\n case 'url':\n case 'color':\n case 'image': {\n if (typeof value !== 'string') {\n errors.push({ ...fctx, message: `Expected string for ${fieldDef.format} field` });\n }\n break;\n }\n case 'boolean': {\n if (value !== 'true' && value !== 'false') {\n errors.push({ ...fctx, message: 'Expected \"true\" or \"false\"' });\n }\n break;\n }\n case 'number': {\n if (typeof value !== 'number' && value !== null) {\n errors.push({ ...fctx, message: 'Expected number or null' });\n }\n if (typeof value === 'number') {\n if (fieldDef.min != null && value < fieldDef.min) {\n errors.push({ ...fctx, message: `Value ${value} is below min ${fieldDef.min}` });\n }\n if (fieldDef.max != null && value > fieldDef.max) {\n errors.push({ ...fctx, message: `Value ${value} is above max ${fieldDef.max}` });\n }\n }\n break;\n }\n case 'datetime': {\n if (typeof value === 'string') {\n if (isNaN(Date.parse(value))) {\n errors.push({ ...fctx, message: 'Invalid datetime format' });\n }\n } else if (value !== null) {\n errors.push({ ...fctx, message: 'Expected ISO date string or null' });\n }\n break;\n }\n case 'select': {\n const validValues = new Set(fieldDef.options.map((o) => o.value));\n if (fieldDef.multiple) {\n if (!Array.isArray(value)) {\n errors.push({ ...fctx, message: 'Expected array for multi-select' });\n } else {\n for (const v of value) {\n if (!validValues.has(String(v))) {\n errors.push({ ...fctx, message: `Invalid select value \"${String(v)}\"` });\n }\n }\n }\n } else {\n if (typeof value !== 'string' || !validValues.has(value)) {\n errors.push({ ...fctx, message: `Invalid select value \"${String(value)}\"` });\n }\n }\n break;\n }\n case 'reference': {\n const cardinality = fieldDef.reference?.cardinality ?? 'many';\n if (cardinality === 'one') {\n if (typeof value === 'string' && value) {\n validateReferenceTarget(value, fieldDef.reference?.collections, contentDir, config, fctx, errors);\n }\n } else {\n const refs = parseRefs(value);\n for (const ref of refs) {\n validateReferenceTarget(ref, fieldDef.reference?.collections, contentDir, config, fctx, errors);\n }\n }\n break;\n }\n case 'json': {\n // Any JSON value is valid\n break;\n }\n case 'conditional': {\n if (typeof value !== 'object' || value === null) {\n errors.push({ ...fctx, message: 'Expected object for conditional field' });\n }\n break;\n }\n }\n\n return errors;\n}\n\nfunction parseRefs(value: unknown): string[] {\n if (typeof value === 'string') {\n try {\n const arr = JSON.parse(value);\n return Array.isArray(arr) ? arr.map(String) : [];\n } catch {\n return [];\n }\n }\n if (Array.isArray(value)) {\n return value.map(String);\n }\n return [];\n}\n\nfunction validateReferenceTarget(\n refKey: string,\n allowedCollections: string[] | undefined,\n contentDir: string,\n config: Config,\n ctx: { file: string; collection: string; field: string },\n errors: ValidationError[],\n): void {\n // Reference key format: `collection-id.json`\n const match = refKey.match(/^(\\w+)-(.+)\\.json$/);\n if (!match) {\n errors.push({ ...ctx, message: `Invalid reference key format: \"${refKey}\"` });\n return;\n }\n\n const [, refCollection] = match;\n if (allowedCollections && !allowedCollections.includes(refCollection)) {\n errors.push({\n ...ctx,\n message: `Reference \"${refKey}\" targets collection \"${refCollection}\" which is not allowed`,\n });\n return;\n }\n\n if (!config.collections[refCollection]) {\n errors.push({ ...ctx, message: `Reference \"${refKey}\" targets unknown collection \"${refCollection}\"` });\n return;\n }\n\n const targetPath = join(contentDir, refCollection, refKey);\n if (!existsSync(targetPath)) {\n errors.push({ ...ctx, message: `Reference target \"${refKey}\" does not exist` });\n }\n}\n","/**\n * `octocms validate` — Validate all content entries against the CMS schema.\n *\n * Reads every JSON file in `cms/content/`, validates structure, field types,\n * required fields, select option values, reference targets, and companion files.\n */\n\nimport { fmt, log } from '../lib/logger';\nimport { validateContent } from '../lib/contentValidator';\nimport { loadCollections, loadProjectConfig } from '../lib/project';\nimport { validateConfig } from '../lib/validateConfig';\n\nexport async function validateCommand(projectRoot: string): Promise<void> {\n log.header('Validate content');\n\n const config = await loadProjectConfig(projectRoot);\n const collections = await loadCollections(projectRoot);\n\n // Validate config first\n try {\n validateConfig(config, collections);\n } catch (e) {\n log.error(`Config error: ${(e as Error).message}`);\n process.exitCode = 1;\n return;\n }\n\n const collectionNames = Object.keys(config.collections);\n log.info(`Validating ${collectionNames.length} collections...`);\n log.blank();\n\n const result = validateContent(projectRoot, config);\n\n // Group errors by file\n const errorsByFile = new Map<string, typeof result.errors>();\n for (const err of result.errors) {\n const key = `${err.collection}/${err.file}`;\n if (!errorsByFile.has(key)) errorsByFile.set(key, []);\n errorsByFile.get(key)!.push(err);\n }\n\n // Report per collection\n let totalEntries = 0;\n for (const name of collectionNames) {\n const count = result.counts[name] ?? 0;\n totalEntries += count;\n\n const collErrors = result.errors.filter((e) => e.collection === name);\n if (collErrors.length === 0) {\n log.success(`${name} — ${count} ${count === 1 ? 'entry' : 'entries'}`);\n } else {\n const errorFiles = new Set(collErrors.map((e) => e.file));\n log.error(`${name} — ${count} ${count === 1 ? 'entry' : 'entries'}, ${errorFiles.size} with errors`);\n }\n }\n\n // Show detailed errors\n if (errorsByFile.size > 0) {\n log.blank();\n for (const [fileKey, fileErrors] of errorsByFile) {\n log.error(fileKey);\n for (const err of fileErrors) {\n const field = err.field ? `${fmt.dim(err.field)}: ` : '';\n log.info(` ${fmt.yellow('•')} ${field}${err.message}`);\n }\n }\n log.blank();\n log.error(`${result.errors.length} ${result.errors.length === 1 ? 'error' : 'errors'} found.`);\n process.exitCode = 1;\n } else {\n log.blank();\n log.success(`All ${totalEntries} ${totalEntries === 1 ? 'entry' : 'entries'} valid.`);\n }\n\n log.blank();\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,6 @@
1
+ import { NextConfig } from 'next';
2
+ import { Config } from './types.mjs';
3
+
4
+ declare function withOctoCMS(nextConfig: NextConfig | undefined, octoConfig: Config): NextConfig;
5
+
6
+ export { withOctoCMS };
@@ -0,0 +1,9 @@
1
+ import {
2
+ withOctoCMS
3
+ } from "./chunk-BRTXBBVQ.js";
4
+ import "./chunk-B47VXAHT.js";
5
+ import "./chunk-7CFFE2I6.js";
6
+ export {
7
+ withOctoCMS
8
+ };
9
+ //# sourceMappingURL=withOctoCMS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/docs/index.md ADDED
@@ -0,0 +1,27 @@
1
+ <!--
2
+ AUTO-GENERATED FILE — DO NOT EDIT.
3
+ Generated by scripts/generate-agent-docs.ts from cms/octocms.config.ts.
4
+ Run `npm run agent-docs:gen` to regenerate.
5
+ -->
6
+
7
+ # OctoCMS — AI Agent Documentation
8
+
9
+ These auto-generated docs describe how AI agents can manage content in an OctoCMS project.
10
+ Include them in your `AGENTS.md` or reference them directly.
11
+
12
+ ## Documents
13
+
14
+ - **[Content Management](./overview.md)** — How to find, create, update, and delete content entries via file operations
15
+ - **[Schema Reference](./schema.md)** — Per-collection field definitions, example JSON, and file path conventions
16
+
17
+ ## Usage in AGENTS.md
18
+
19
+ Reference these docs from your project's `AGENTS.md`:
20
+
21
+ ```markdown
22
+ ## Content Management
23
+
24
+ See `octocms/docs/overview.md` and `octocms/docs/schema.md` for how to manage CMS content directly.
25
+ ```
26
+
27
+ To regenerate after schema changes: `npm run agent-docs:gen`
@@ -0,0 +1,113 @@
1
+ <!--
2
+ AUTO-GENERATED FILE — DO NOT EDIT.
3
+ Generated by scripts/generate-agent-docs.ts from cms/octocms.config.ts.
4
+ Run `npm run agent-docs:gen` to regenerate.
5
+ -->
6
+
7
+ # OctoCMS — Content Management for AI Agents
8
+
9
+ This document describes how to manage content in an OctoCMS project directly via file operations.
10
+ Use it when an AI agent needs to create, read, update, or delete content entries without the admin UI.
11
+
12
+ ## How content is stored
13
+
14
+ Content lives in the `cms/content/` directory as JSON files. Each collection has its own subfolder.
15
+
16
+ **File naming:**
17
+
18
+ - **`hasMany` collections** (multiple entries): `cms/content/{type}/{type}-{uuid}.json` — UUID is generated via `crypto.randomUUID()`
19
+ - **Singleton collections** (single entry): `cms/content/{type}/{type}-0000.json` — fixed ID `0000`
20
+
21
+ ## Collections
22
+
23
+ | Collection | Label | Type |
24
+ | ---------- | --------- | --------- |
25
+ | `post` | Post | hasMany |
26
+ | `item` | Item | hasMany |
27
+ | `homePage` | Home Page | singleton |
28
+ | `blog` | Blog | singleton |
29
+ | `author` | Author | hasMany |
30
+ | `role` | Role | hasMany |
31
+
32
+ ## Entry JSON structure
33
+
34
+ Every entry file has this shape:
35
+
36
+ ```json
37
+ {
38
+ "sys": {
39
+ "id": "<uuid-or-fixed-id>",
40
+ "type": "<collectionName>",
41
+ "status": "draft"
42
+ },
43
+ "fields": {
44
+ "fieldName": "value"
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Entry status values
50
+
51
+ | Status | Meaning | Visible on public site |
52
+ | ----------- | ------------------------------------------ | ---------------------- |
53
+ | `draft` | Newly created, never published | No |
54
+ | `published` | Live on public site | Yes |
55
+ | `changed` | Edited after publish, not yet re-published | Yes (shows latest) |
56
+ | `merged` | On main branch, baseline state | Yes |
57
+ | `archived` | Soft-deleted | No |
58
+
59
+ When creating a new entry, set `"status": "draft"`.
60
+
61
+ ## Companion files (markdown & richtext)
62
+
63
+ Fields with `format: "markdown"` or `format: "richtext"` are **not** stored in the JSON entry.
64
+ Instead, they use companion files alongside the entry:
65
+
66
+ - **Markdown:** `{type}-{id}.{fieldName}.md`
67
+ - **Richtext:** `{type}-{id}.{fieldName}.mdx`
68
+
69
+ Example: A post entry `post-abc123.json` with a `body` markdown field stores its content in `post-abc123.body.md` in the same directory.
70
+
71
+ ## URL to collection mapping
72
+
73
+ Use this table to match a public URL to its content collection and entry:
74
+
75
+ | URL pattern | Collection | How to find the entry |
76
+ | ------------- | ------------- | ---------------------------------------- |
77
+ | `/blog/:slug` | `post` (Post) | Match `fields.slug` from the URL segment |
78
+
79
+ ## Creating a new entry
80
+
81
+ 1. Generate a UUID (e.g. `crypto.randomUUID()`)
82
+ 2. Create `cms/content/{type}/{type}-{uuid}.json` with the entry JSON structure above
83
+ 3. Set `sys.status` to `"draft"`
84
+ 4. Fill in all required fields (see schema reference)
85
+ 5. For any markdown/richtext fields, create the companion file in the same directory
86
+ 6. Run `npm run types:gen` if you modified `cms/octocms.config.ts`
87
+
88
+ ## Updating an entry
89
+
90
+ 1. Read the JSON file
91
+ 2. Modify values in the `fields` object
92
+ 3. Write the file back
93
+ 4. For markdown/richtext fields, edit the companion `.md`/`.mdx` file directly
94
+
95
+ ## Deleting an entry
96
+
97
+ 1. Delete the JSON file
98
+ 2. Delete any companion files (`{type}-{id}.*.md`, `{type}-{id}.*.mdx`)
99
+
100
+ ## Adding a new collection
101
+
102
+ 1. Edit `cms/octocms.config.ts` — add the collection to the `collections` object inside `defineConfig()`
103
+ 2. Create the content directory: `cms/content/{collectionName}/`
104
+ 3. Run `npm run types:gen` to regenerate TypeScript types
105
+ 4. Optionally create a public page route using the `query()` API:
106
+
107
+ ```typescript
108
+ import { query } from "octocms/query";
109
+
110
+ const entries = await query("collectionName")
111
+ .sort("fieldName", "desc")
112
+ .toArray();
113
+ ```
package/docs/schema.md ADDED
@@ -0,0 +1,279 @@
1
+ <!--
2
+ AUTO-GENERATED FILE — DO NOT EDIT.
3
+ Generated by scripts/generate-agent-docs.ts from cms/octocms.config.ts.
4
+ Run `npm run agent-docs:gen` to regenerate.
5
+ -->
6
+
7
+ # OctoCMS — Schema Reference for AI Agents
8
+
9
+ Per-collection field definitions and example JSON entries.
10
+
11
+ ## Field format storage reference
12
+
13
+ How each field format is stored in the JSON entry file:
14
+
15
+ | Format | Storage |
16
+ | --- | --- |
17
+ | `string` | Plain text string (or JSON array when `list: true`) |
18
+ | `text` | Plain text string |
19
+ | `markdown` | Companion `.md` file (not in JSON `fields`) |
20
+ | `richtext` | Companion `.mdx` file (not in JSON `fields`) |
21
+ | `boolean` | `"true"` or `"false"` (string, not JSON boolean) |
22
+ | `number` | JSON number or `null` |
23
+ | `datetime` | ISO 8601 string (e.g. `"2024-01-01T00:00:00.000Z"`) or `null` |
24
+ | `image` | Media entry UUID string |
25
+ | `json` | Any valid JSON value |
26
+ | `slug` | URL-safe string |
27
+ | `select` | Option value string (or JSON array when `multiple: true`) |
28
+ | `url` | URL string |
29
+ | `color` | `#rrggbb` hex string |
30
+ | `reference` | Reference key string `"type-id.json"` (or JSON array for cardinality `many`) |
31
+ | `conditional` | JSON object (structure varies by branch) |
32
+
33
+ ## Post (`post`)
34
+
35
+ - **Type:** hasMany (multiple entries)
36
+ - **Path:** `cms/content/post/post-<uuid>.json`
37
+ - **Companion files:** `post-{id}.body.md`
38
+
39
+ ### Fields
40
+
41
+ | Field | Label | Format | Required | Storage notes |
42
+ | --- | --- | --- | --- | --- |
43
+ | `title` | Entry title | `string` | yes | plain text |
44
+ | `slug` | URL slug | `slug` | yes | URL-safe string |
45
+ | `publishedAt` | Published | `datetime` | — | ISO 8601 string or `null` |
46
+ | `featuredImage` | Featured Image | `image` | — | media entry UUID string |
47
+ | `body` | Body | `markdown` | — | companion `.md` file (not in JSON) |
48
+ | `tags` | Tags | `string` | — | JSON array of strings |
49
+ | `meta` | JSON metadata | `json` | — | any valid JSON value |
50
+
51
+ ### Example entry JSON
52
+
53
+ ```json
54
+ {
55
+ "sys": {
56
+ "id": "<uuid>",
57
+ "type": "post",
58
+ "status": "draft"
59
+ },
60
+ "fields": {
61
+ "title": "Example entry title",
62
+ "slug": "example-slug",
63
+ "publishedAt": "2024-01-01T00:00:00.000Z",
64
+ "featuredImage": "<media-entry-uuid>",
65
+ "tags": ["tag1", "tag2"],
66
+ "meta": null
67
+ }
68
+ }
69
+ ```
70
+
71
+ Companion file `cms/content/post/post-<uuid>.body.md`:
72
+
73
+ ```markdown
74
+ # Example Body
75
+
76
+ Write your body content here in Markdown.
77
+ ```
78
+
79
+ ## Item (`item`)
80
+
81
+ - **Type:** hasMany (multiple entries)
82
+ - **Path:** `cms/content/item/item-<uuid>.json`
83
+ - **Companion files:** `item-{id}.body.md`
84
+
85
+ ### Fields
86
+
87
+ | Field | Label | Format | Required | Storage notes |
88
+ | --- | --- | --- | --- | --- |
89
+ | `title` | Title | `string` | — | plain text |
90
+ | `body` | Body | `markdown` | — | companion `.md` file (not in JSON) |
91
+ | `enabled` | Enabled | `boolean` | — | `"true"` or `"false"` (string, not boolean) |
92
+ | `category` | Category | `select` | — | option value string Options: `general`, `featured`. Default: `general`. |
93
+ | `flags` | Flags | `select` | — | JSON array of option values Options: `new`, `sale`. Defaults: `new`. |
94
+ | `sortOrder` | Sort order | `number` | — | JSON number or `null` type: int. |
95
+
96
+ ### Example entry JSON
97
+
98
+ ```json
99
+ {
100
+ "sys": {
101
+ "id": "<uuid>",
102
+ "type": "item",
103
+ "status": "draft"
104
+ },
105
+ "fields": {
106
+ "title": "Example title",
107
+ "enabled": "true",
108
+ "category": "general",
109
+ "flags": ["new"],
110
+ "sortOrder": 0
111
+ }
112
+ }
113
+ ```
114
+
115
+ Companion file `cms/content/item/item-<uuid>.body.md`:
116
+
117
+ ```markdown
118
+ # Example Body
119
+
120
+ Write your body content here in Markdown.
121
+ ```
122
+
123
+ ## Home Page (`homePage`)
124
+
125
+ - **Type:** singleton
126
+ - **Path:** `cms/content/homePage/homePage-0000.json`
127
+ - **Companion files:** `homePage-{id}.philosophyBody.md`
128
+
129
+ ### Fields
130
+
131
+ | Field | Label | Format | Required | Storage notes |
132
+ | --- | --- | --- | --- | --- |
133
+ | `title` | Title | `string` | — | plain text |
134
+ | `heroBadge` | Hero — Badge | `string` | — | plain text |
135
+ | `heroHeadline` | Hero — Headline | `string` | — | plain text |
136
+ | `heroHeadlineAccent` | Hero — Headline Accent | `string` | — | plain text |
137
+ | `heroDescription` | Hero — Description | `text` | — | plain text |
138
+ | `howItWorksTitle` | How It Works — Title | `string` | — | plain text |
139
+ | `howItWorksDescription` | How It Works — Description | `text` | — | plain text |
140
+ | `steps` | How It Works — Steps | `json` | — | any valid JSON value |
141
+ | `featuresTitle` | Features — Title | `string` | — | plain text |
142
+ | `features` | Features — Cards | `json` | — | any valid JSON value |
143
+ | `philosophyTitle` | Philosophy — Title | `string` | — | plain text |
144
+ | `philosophyBody` | Philosophy — Body | `markdown` | — | companion `.md` file (not in JSON) |
145
+ | `useCasesTitle` | Use Cases — Title | `string` | — | plain text |
146
+ | `useCases` | Use Cases — Cards | `json` | — | any valid JSON value |
147
+ | `ctaTitle` | CTA — Title | `string` | — | plain text |
148
+ | `ctaSubtitle` | CTA — Subtitle | `string` | — | plain text |
149
+ | `footerText` | Footer — Text | `string` | — | plain text |
150
+
151
+ ### Example entry JSON
152
+
153
+ ```json
154
+ {
155
+ "sys": {
156
+ "id": "0000",
157
+ "type": "homePage",
158
+ "status": "draft"
159
+ },
160
+ "fields": {
161
+ "title": "Example title",
162
+ "heroBadge": "Example hero — badge",
163
+ "heroHeadline": "Example hero — headline",
164
+ "heroHeadlineAccent": "Example hero — headline accent",
165
+ "heroDescription": "Example hero — description text",
166
+ "howItWorksTitle": "Example how it works — title",
167
+ "howItWorksDescription": "Example how it works — description text",
168
+ "steps": null,
169
+ "featuresTitle": "Example features — title",
170
+ "features": null,
171
+ "philosophyTitle": "Example philosophy — title",
172
+ "useCasesTitle": "Example use cases — title",
173
+ "useCases": null,
174
+ "ctaTitle": "Example cta — title",
175
+ "ctaSubtitle": "Example cta — subtitle",
176
+ "footerText": "Example footer — text"
177
+ }
178
+ }
179
+ ```
180
+
181
+ Companion file `cms/content/homePage/homePage-0000.philosophyBody.md`:
182
+
183
+ ```markdown
184
+ # Example Philosophy — Body
185
+
186
+ Write your philosophy — body content here in Markdown.
187
+ ```
188
+
189
+ ## Blog (`blog`)
190
+
191
+ - **Type:** singleton
192
+ - **Path:** `cms/content/blog/blog-0000.json`
193
+
194
+ ### Fields
195
+
196
+ | Field | Label | Format | Required | Storage notes |
197
+ | --- | --- | --- | --- | --- |
198
+ | `title` | Title | `string` | — | plain text |
199
+ | `posts` | Posts | `reference` | — | JSON array of reference key strings Collections: `post`. Cardinality: `many`. |
200
+
201
+ ### Example entry JSON
202
+
203
+ ```json
204
+ {
205
+ "sys": {
206
+ "id": "0000",
207
+ "type": "blog",
208
+ "status": "draft"
209
+ },
210
+ "fields": {
211
+ "title": "Example title",
212
+ "posts": ["post-<id>.json"]
213
+ }
214
+ }
215
+ ```
216
+
217
+ ## Author (`author`)
218
+
219
+ - **Type:** hasMany (multiple entries)
220
+ - **Path:** `cms/content/author/author-<uuid>.json`
221
+ - **Companion files:** `author-{id}.bio.md`
222
+
223
+ ### Fields
224
+
225
+ | Field | Label | Format | Required | Storage notes |
226
+ | --- | --- | --- | --- | --- |
227
+ | `name` | Name | `string` | — | plain text |
228
+ | `bio` | Bio | `markdown` | — | companion `.md` file (not in JSON) |
229
+ | `roles` | Roles | `reference` | — | JSON array of reference key strings Collections: `role`. Cardinality: `many`. |
230
+
231
+ ### Example entry JSON
232
+
233
+ ```json
234
+ {
235
+ "sys": {
236
+ "id": "<uuid>",
237
+ "type": "author",
238
+ "status": "draft"
239
+ },
240
+ "fields": {
241
+ "name": "Example name",
242
+ "roles": ["role-<id>.json"]
243
+ }
244
+ }
245
+ ```
246
+
247
+ Companion file `cms/content/author/author-<uuid>.bio.md`:
248
+
249
+ ```markdown
250
+ # Example Bio
251
+
252
+ Write your bio content here in Markdown.
253
+ ```
254
+
255
+ ## Role (`role`)
256
+
257
+ - **Type:** hasMany (multiple entries)
258
+ - **Path:** `cms/content/role/role-<uuid>.json`
259
+
260
+ ### Fields
261
+
262
+ | Field | Label | Format | Required | Storage notes |
263
+ | --- | --- | --- | --- | --- |
264
+ | `title` | Title | `string` | — | plain text |
265
+
266
+ ### Example entry JSON
267
+
268
+ ```json
269
+ {
270
+ "sys": {
271
+ "id": "<uuid>",
272
+ "type": "role",
273
+ "status": "draft"
274
+ },
275
+ "fields": {
276
+ "title": "Example title"
277
+ }
278
+ }
279
+ ```
package/globals.css ADDED
@@ -0,0 +1,198 @@
1
+ @import 'tailwindcss';
2
+
3
+ /* Scope dark: variants to descendants of any .dark element (admin-only, not <html>) */
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ /* ============================================================
7
+ Design tokens — mapped to Tailwind CSS via inline references
8
+ so that utilities like bg-background update at runtime when
9
+ .dark is toggled on an ancestor element.
10
+ ============================================================ */
11
+
12
+ @theme inline {
13
+ /* Layout */
14
+ --header-height: 56px;
15
+
16
+ /* Border radius */
17
+ --radius-sm: 0.375rem;
18
+ --radius-md: 0.5rem;
19
+ --radius-lg: 0.75rem;
20
+
21
+ /* shadcn/ui semantic colors — reference CSS vars so they
22
+ respond to .dark class toggling at runtime */
23
+ --color-background: var(--background);
24
+ --color-foreground: var(--foreground);
25
+ --color-primary: var(--primary);
26
+ --color-primary-foreground: var(--primary-foreground);
27
+ --color-secondary: var(--secondary);
28
+ --color-secondary-foreground: var(--secondary-foreground);
29
+ --color-destructive: var(--destructive);
30
+ --color-destructive-foreground: var(--destructive-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-muted-foreground: var(--muted-foreground);
33
+ --color-accent: var(--accent);
34
+ --color-accent-foreground: var(--accent-foreground);
35
+ --color-popover: var(--popover);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-card: var(--card);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-input: var(--input);
40
+ --color-border: var(--border);
41
+ --color-ring: var(--ring);
42
+ }
43
+
44
+ /* ============================================================
45
+ Light theme (default) — all semantic tokens
46
+ ============================================================ */
47
+ :root {
48
+ /* Layout */
49
+ --header-height: 56px;
50
+ --header-backdrop-filter: none;
51
+
52
+ /* shadcn/ui semantic tokens */
53
+ --background: #fff;
54
+ --foreground: #1f2328;
55
+ --card: #fff;
56
+ --card-foreground: #1f2328;
57
+ --popover: #fff;
58
+ --popover-foreground: #1f2328;
59
+ --primary: #0969da;
60
+ --primary-foreground: #fff;
61
+ --secondary: #f6f8fa;
62
+ --secondary-foreground: #1f2328;
63
+ --destructive: #d1242f;
64
+ --destructive-foreground: #fff;
65
+ --muted: #f6f8fa;
66
+ --muted-foreground: #656d76;
67
+ --accent: #ddf4ff;
68
+ --accent-foreground: #0969da;
69
+ --input: #d1d9e0;
70
+ --border: #d1d9e0;
71
+ --ring: #0969da;
72
+ --radius: 0.5rem;
73
+
74
+ /* CMS layout aliases (used by legacy CSS Module classes below) */
75
+ --color-layout-bg: var(--background);
76
+ --color-text: var(--foreground);
77
+ --color-header-bg: var(--background);
78
+ --color-header-text: var(--foreground);
79
+ --color-sidebar-bg: var(--secondary);
80
+ --color-border: var(--border);
81
+ }
82
+
83
+ /* ============================================================
84
+ Dark theme — applied when an ancestor has class="dark"
85
+ Colors sourced from the provided OKLCH reference palette.
86
+ ============================================================ */
87
+ .dark {
88
+ --background: oklch(0.145 0 0);
89
+ --foreground: oklch(0.985 0 0);
90
+ --card: oklch(0.145 0 0);
91
+ --card-foreground: oklch(0.985 0 0);
92
+ --popover: oklch(0.145 0 0);
93
+ --popover-foreground: oklch(0.985 0 0);
94
+ --primary: oklch(0.985 0 0);
95
+ --primary-foreground: oklch(0.205 0 0);
96
+ --secondary: oklch(0.269 0 0);
97
+ --secondary-foreground: oklch(0.985 0 0);
98
+ --destructive: oklch(0.396 0.141 25.723);
99
+ --destructive-foreground: oklch(0.637 0.237 25.331);
100
+ --muted: oklch(0.269 0 0);
101
+ --muted-foreground: oklch(0.708 0 0);
102
+ --accent: oklch(0.269 0 0);
103
+ --accent-foreground: oklch(0.985 0 0);
104
+ --input: oklch(0.269 0 0);
105
+ --border: oklch(0.269 0 0);
106
+ --ring: oklch(0.439 0 0);
107
+ }
108
+
109
+ /* Reset CSS */
110
+ *,
111
+ *::before,
112
+ *::after {
113
+ box-sizing: border-box;
114
+ }
115
+
116
+ ul[role='list'],
117
+ ol[role='list'] {
118
+ list-style: none;
119
+ }
120
+
121
+ html:focus-within {
122
+ scroll-behavior: smooth;
123
+ }
124
+
125
+ a:not([class]) {
126
+ text-decoration-skip-ink: auto;
127
+ }
128
+
129
+ img,
130
+ picture,
131
+ svg,
132
+ video,
133
+ canvas {
134
+ max-width: 100%;
135
+ height: auto;
136
+ vertical-align: middle;
137
+ font-style: italic;
138
+ background-repeat: no-repeat;
139
+ background-size: cover;
140
+ }
141
+
142
+ input,
143
+ button,
144
+ textarea,
145
+ select {
146
+ font: inherit;
147
+ }
148
+
149
+ @media (prefers-reduced-motion: reduce) {
150
+ html:focus-within {
151
+ scroll-behavior: auto;
152
+ }
153
+
154
+ *,
155
+ *::before,
156
+ *::after {
157
+ animation-duration: 0.01ms !important;
158
+ animation-iteration-count: 1 !important;
159
+ transition-duration: 0.01ms !important;
160
+ scroll-behavior: auto !important;
161
+ transition: none;
162
+ }
163
+ }
164
+
165
+ body,
166
+ html {
167
+ height: 100%;
168
+ scroll-behavior: smooth;
169
+ }
170
+
171
+ html {
172
+ font-size: 16px;
173
+ font-family:
174
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji',
175
+ 'Segoe UI Emoji', 'Segoe UI Symbol';
176
+ font-weight: normal;
177
+ -ms-text-size-adjust: 100%;
178
+ -webkit-text-size-adjust: 100%;
179
+ }
180
+
181
+ body {
182
+ margin: 0;
183
+ padding: 0;
184
+ color: var(--foreground);
185
+ background: var(--background);
186
+ }
187
+
188
+
189
+ .editor-markdown.mdxeditor {
190
+ background-color: var(--background);
191
+ padding: 5px;
192
+ border-radius: 8px;
193
+ border: 1px solid var(--border);
194
+ }
195
+
196
+ .editor-markdown--invalid.mdxeditor {
197
+ border-color: #dc2626;
198
+ }