@natilon/astro-cms 0.5.0 → 0.9.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 (3) hide show
  1. package/README.md +172 -47
  2. package/package.json +2 -2
  3. package/src/index.mjs +71 -0
package/README.md CHANGED
@@ -1,70 +1,195 @@
1
1
  # @natilon/astro-cms
2
2
 
3
- Astro integration that mounts [`@natilon/cms-server`](../cms-server) +
4
- [`@natilon/admin-ui`](../admin-ui) into `astro dev`, so the site and
5
- its CMS run from a single command on a single origin.
6
-
7
- In production (`astro build`), this integration does nothing — the CMS
8
- is a separate Node process you deploy alongside (or behind) the static
9
- site. Use `@natilon/cms-server`'s `startCmsServer` for that.
3
+ Astro integration for [Natilon CMS](../../README.md). Mounts the CMS admin panel into `astro dev`, auto-generates content collection config, and exports the JSON loader + schema builder your site needs.
10
4
 
11
5
  ## Install
12
6
 
13
7
  ```sh
14
- npm i @natilon/astro-cms @natilon/cms-server @natilon/admin-ui
8
+ npm i @natilon/astro-cms @natilon/cms-server
15
9
  ```
16
10
 
17
- ## Usage
11
+ ## Quick start
12
+
13
+ ### 1. Create `cms.config.mjs`
18
14
 
19
15
  ```js
20
- // astro.config.mjs
21
- import { defineConfig } from "astro/config";
22
- import natilonCms from "@natilon/astro-cms";
23
- import cmsConfig, { publicConfig } from "./cms.config.mjs";
24
- import path from "path";
25
- import { fileURLToPath } from "url";
16
+ export default {
17
+ mountPath: "/admin",
18
+ locales: ["en"],
19
+ defaultLocale: "en",
20
+
21
+ collections: {
22
+ blog: {
23
+ label: "Blog Posts",
24
+ listFields: ["title", "pubDate"],
25
+ metaFields: [
26
+ { key: "title", type: "text", label: "Title", required: true },
27
+ { key: "slug", type: "text", label: "Slug", required: true },
28
+ { key: "pubDate", type: "datetime", label: "Published" },
29
+ { key: "blocks", type: "blocks", label: "Content" },
30
+ ],
31
+ },
32
+ },
33
+
34
+ content: {
35
+ pagesDir: "src/pages-data",
36
+ publishBranch: "main",
37
+ commitMessage: (ts) => `Content updated ${ts}`,
38
+ },
39
+
40
+ media: { provider: "local" },
41
+
42
+ auth: {
43
+ provider: "basic",
44
+ userEnv: "ADMIN_USER",
45
+ passEnv: "ADMIN_PASS",
46
+ },
47
+ };
48
+ ```
49
+
50
+ ### 2. Mount in `astro.config.mjs`
26
51
 
27
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
52
+ ```js
53
+ import { defineConfig } from "astro/config";
54
+ import natilon from "@natilon/astro-cms";
55
+ import config from "./cms.config.mjs";
28
56
 
29
57
  export default defineConfig({
30
- integrations: [
31
- natilonCms({
32
- config: cmsConfig,
33
- publicConfig,
34
- rootDir: __dirname,
35
- adminUiSourceDir: path.join(__dirname, "node_modules/@natilon/admin-ui"),
36
- realm: "My Site Admin",
37
- }),
38
- ],
58
+ integrations: [natilon({ config })],
39
59
  });
40
60
  ```
41
61
 
42
- Now `npm run dev` boots Astro's Vite server with the admin mounted at
43
- `/admin`. No second process needed for local development.
62
+ ### 3. Run `astro dev` `src/content.config.ts` is generated automatically
44
63
 
45
- ## Content helpers
64
+ ```
65
+ ✔ Generated src/content.config.ts — covers blog. Edit freely.
66
+ ```
67
+
68
+ The generated file covers every collection in `cms.config.mjs`. You can commit it, and adding a new collection to `cms.config.mjs` requires no changes to it. Delete it to regenerate.
69
+
70
+ ### 4. Add env vars (`.env`)
71
+
72
+ ```
73
+ ADMIN_USER=admin
74
+ ADMIN_PASS=secret
75
+ ```
76
+
77
+ ### 5. Render blocks in your pages
78
+
79
+ ```astro
80
+ ---
81
+ import BlockRenderer from "@natilon/astro-blocks";
82
+ const { entry } = Astro.props;
83
+ ---
84
+ <BlockRenderer blocks={entry.data.blocks} />
85
+ ```
86
+
87
+ See [`@natilon/astro-blocks`](../astro-blocks/README.md) for the full block reference.
88
+
89
+ ---
90
+
91
+ ## `cms.config.mjs` reference
92
+
93
+ ### Collection options
46
94
 
47
95
  ```js
48
- import { listEntries, getEntry } from "@natilon/astro-cms/content";
49
-
50
- // In a .astro file:
51
- const posts = await listEntries({
52
- rootDir: process.cwd(),
53
- pagesDir: "src/pages-data",
54
- collection: "blog",
55
- locales: ["en", "tr"],
56
- });
96
+ collections: {
97
+ blog: {
98
+ label: "Blog Posts", // displayed in sidebar
99
+ listFields: ["title", "slug"], // columns shown in entry list
100
+ metaFields: [ ... ], // editable fields
101
+ defaultValues: { draft: true },
102
+ },
103
+ }
57
104
  ```
58
105
 
59
- These read the same JSON files the CMS writes. Use them from
60
- `getStaticPaths()` to drive Astro routes.
106
+ ### `metaFields` field types
107
+
108
+ | `type` | Editor control | Zod type (auto) |
109
+ |------------------|---------------------------------------|------------------------------|
110
+ | `text` | Single-line input | `z.string().optional()` |
111
+ | `textarea` | Multi-line input | `z.string().optional()` |
112
+ | `richtext` | Rich-text editor (HTML output) | `z.string().optional()` |
113
+ | `number` | Numeric input | `z.number().optional()` |
114
+ | `boolean` | Toggle | `z.boolean().optional()` |
115
+ | `date` | Date picker | `z.coerce.date().optional()` |
116
+ | `datetime` | Date + time picker | `z.coerce.date().optional()` |
117
+ | `select` | Dropdown (`options: [...]` required) | `z.enum([...]).optional()` |
118
+ | `image` | Media picker (CDN or local) | `z.string().optional()` |
119
+ | `meta-image` | OG/social image picker | `z.string().nullable().optional()` |
120
+ | `json` | Raw JSON textarea | `z.unknown().optional()` |
121
+ | `collection-ref` | Entry picker from another collection | `z.string().optional()` |
122
+ | `blocks` | Block content editor (see astro-blocks)| auto-included always |
123
+ | `code` | Code editor | `z.string().optional()` |
124
+
125
+ **Options:**
126
+
127
+ ```js
128
+ { key: "status", type: "select", label: "Status", options: ["draft", "published"], required: true }
129
+ { key: "cover", type: "image", label: "Cover image" }
130
+ { key: "source", type: "collection-ref", label: "Author", collection: "authors" }
131
+ ```
132
+
133
+ `required: true` shows a red `*` in the editor and blocks saving if the field is empty.
134
+
135
+ ### Custom block types
136
+
137
+ ```js
138
+ // cms.config.mjs — optional, extends the built-in block palette
139
+ blocks: {
140
+ hero: {
141
+ label: "Hero",
142
+ icon: "fa-star",
143
+ properties: {
144
+ heading: { type: "text", label: "Heading", required: true },
145
+ image: { type: "image", label: "Background image" },
146
+ cta: { type: "text", label: "Button text" },
147
+ ctaHref: { type: "text", label: "Button URL" },
148
+ },
149
+ defaults: { heading: "Welcome" },
150
+ },
151
+ },
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Content helpers
157
+
158
+ `jsonContentLoader` and `buildCollectionSchema` are re-exported from the main entry:
159
+
160
+ ```ts
161
+ import { jsonContentLoader, buildCollectionSchema } from "@natilon/astro-cms";
162
+ ```
163
+
164
+ ### `jsonContentLoader(collection, opts?)`
165
+
166
+ Astro Content Layer loader. Reads `src/pages-data/{collection}/*.json` (or `opts.pagesDir`).
167
+
168
+ ```ts
169
+ loader: jsonContentLoader("blog")
170
+ loader: jsonContentLoader("blog", { pagesDir: "content/pages" })
171
+ ```
172
+
173
+ ### `buildCollectionSchema(collectionConfig, { z })`
174
+
175
+ Generates a Zod schema from a collection's `metaFields`. Always includes `slug`, `lang`, `draft`, `publishAt`, `blocks`, and standard taxonomy arrays.
176
+
177
+ ```ts
178
+ // Extend to tighten types:
179
+ schema: buildCollectionSchema(config.collections.blog, { z }).extend({
180
+ pubDate: z.coerce.date(), // make date required (not optional)
181
+ }),
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Integration options
61
187
 
62
- ## Options
188
+ | Option | Type | Default |
189
+ |--------------------|------------|---------------------------------|
190
+ | `config` | `Object` | **required** — your cms.config |
191
+ | `publicConfig` | `Function` | auto-derived (strips secrets) |
192
+ | `rootDir` | `string` | Astro's `config.root` |
193
+ | `adminUiSourceDir` | `string` | path to admin-ui for HMR dev |
194
+ | `realm` | `string` | HTTP Basic auth realm |
63
195
 
64
- | Option | Type | Default |
65
- | ------------------- | -------------------------- | -------------------------------------- |
66
- | `config` | `Object` (required) | your `cms.config` object |
67
- | `publicConfig` | `() => Object` | sanitizer for `/api/config` |
68
- | `rootDir` | `string` | Astro's `config.root` |
69
- | `adminUiSourceDir` | `string` | path to `@natilon/admin-ui` package |
70
- | `realm` | `string` | HTTP basic-auth realm |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natilon/astro-cms",
3
- "version": "0.5.0",
3
+ "version": "0.9.0",
4
4
  "description": "Astro integration that mounts the Natilon CMS admin under /admin during `astro dev` and exposes content helpers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "peerDependencies": {
28
28
  "@natilon/cms-server": "*",
29
- "astro": "^4.0.0 || ^5.0.0",
29
+ "astro": "^4.0.0 || ^5.0.0 || ^6.0.0",
30
30
  "zod": "^3.0.0"
31
31
  },
32
32
  "peerDependenciesMeta": {
package/src/index.mjs CHANGED
@@ -1,6 +1,35 @@
1
1
  import { fileURLToPath } from "url";
2
+ import fs from "fs";
3
+ import path from "path";
2
4
  import { createCmsServer, mountAdminUi } from "@natilon/cms-server";
3
5
 
6
+ export { jsonContentLoader } from "./loader.mjs";
7
+ export { buildCollectionSchema } from "./schema.mjs";
8
+
9
+ /**
10
+ * Generate the content of src/content.config.ts for a new project.
11
+ * The generated file is safe to commit and customise.
12
+ */
13
+ function generateContentConfig() {
14
+ return `\
15
+ // Auto-generated by @natilon/astro-cms — safe to commit and customize.
16
+ // Delete this file and restart dev to regenerate from cms.config.mjs.
17
+ import { defineCollection, z } from "astro:content";
18
+ import { jsonContentLoader, buildCollectionSchema } from "@natilon/astro-cms";
19
+ import config from "../cms.config.mjs";
20
+
21
+ export const collections = Object.fromEntries(
22
+ Object.entries(config.collections).map(([name, col]) => [
23
+ name,
24
+ defineCollection({
25
+ loader: jsonContentLoader(name),
26
+ schema: buildCollectionSchema(col, { z }),
27
+ }),
28
+ ])
29
+ );
30
+ `;
31
+ }
32
+
4
33
  /**
5
34
  * Astro integration for the Natilon CMS.
6
35
  *
@@ -8,6 +37,9 @@ import { createCmsServer, mountAdminUi } from "@natilon/cms-server";
8
37
  * Astro's Vite server, so the site and `/admin` are served from one origin.
9
38
  * In `astro build`, does nothing — the CMS is a separate process in prod.
10
39
  *
40
+ * Auto-generates `src/content.config.ts` from `cms.config.mjs` if the file
41
+ * does not already exist, so new projects skip the manual wiring step.
42
+ *
11
43
  * @param {Object} opts
12
44
  * @param {Object} opts.config cms.config object (imported by the consumer)
13
45
  * @param {() => Object} [opts.publicConfig] returns sanitized config for the browser
@@ -20,6 +52,44 @@ export default function natilonCms(opts = {}) {
20
52
  return {
21
53
  name: "@natilon/astro-cms",
22
54
  hooks: {
55
+ "astro:config:setup": ({ config: astroConfig, logger }) => {
56
+ // Resolve project root — Astro 5 gives a URL object, earlier versions a string.
57
+ let rootDir = opts.rootDir;
58
+ if (!rootDir) {
59
+ const r = astroConfig.root;
60
+ if (!r) {
61
+ rootDir = process.cwd();
62
+ } else if (typeof r === "object" && typeof r.pathname === "string") {
63
+ // URL object (Astro 5+)
64
+ rootDir = fileURLToPath(r);
65
+ } else if (typeof r === "string" && r.startsWith("file://")) {
66
+ rootDir = fileURLToPath(new URL(r));
67
+ } else {
68
+ rootDir = String(r);
69
+ }
70
+ }
71
+
72
+ // Auto-generate content.config.ts only when all of these hold:
73
+ // 1. No existing content config file is found
74
+ // 2. The cms.config has at least one collection defined
75
+ const candidates = [
76
+ path.join(rootDir, "src", "content.config.ts"),
77
+ path.join(rootDir, "src", "content.config.js"),
78
+ path.join(rootDir, "src", "content.config.mjs"),
79
+ path.join(rootDir, "src", "content", "config.ts"),
80
+ path.join(rootDir, "src", "content", "config.js"),
81
+ ];
82
+
83
+ if (candidates.some((f) => fs.existsSync(f))) return;
84
+ if (!opts.config?.collections || Object.keys(opts.config.collections).length === 0) return;
85
+
86
+ const target = path.join(rootDir, "src", "content.config.ts");
87
+ fs.writeFileSync(target, generateContentConfig(), "utf8");
88
+ logger.info(
89
+ `Generated src/content.config.ts — covers ${Object.keys(opts.config.collections).join(", ")}. Edit freely.`
90
+ );
91
+ },
92
+
23
93
  "astro:server:setup": async ({ server, logger }) => {
24
94
  // server is Astro's underlying Vite dev server.
25
95
  const rootDir = (() => {
@@ -68,3 +138,4 @@ export default function natilonCms(opts = {}) {
68
138
  }
69
139
 
70
140
  export { createCmsServer, mountAdminUi };
141
+