@koehler8/cms 1.0.0-beta.9 → 1.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.
- package/CHANGELOG.md +1522 -0
- package/README.md +108 -28
- package/bin/cms-create-extension.js +7 -0
- package/bin/cms-create-site.js +7 -0
- package/bin/cms-create-theme.js +7 -0
- package/bin/cms-ssg-build.js +13 -0
- package/bin/cms-validate-extensions.js +4 -0
- package/bin/cms-validate-themes.js +11 -0
- package/extensions/manifest.schema.json +1 -1
- package/package.json +33 -17
- package/scripts/create-extension.mjs +11 -0
- package/scripts/create-site.mjs +11 -0
- package/scripts/create-theme.mjs +11 -0
- package/scripts/generate-public-assets.js +70 -36
- package/scripts/image-variants/cache.js +40 -0
- package/scripts/image-variants/classifier.js +53 -0
- package/scripts/image-variants/config.js +54 -0
- package/scripts/image-variants/index.js +18 -0
- package/scripts/image-variants/manifest.js +76 -0
- package/scripts/image-variants/planner.js +95 -0
- package/scripts/image-variants/reconcile.js +194 -0
- package/scripts/image-variants/renderer.js +51 -0
- package/scripts/lib/extension-manifests.mjs +70 -0
- package/scripts/scaffold.mjs +242 -0
- package/scripts/ssg-build/findEmptyRenders.mjs +42 -0
- package/scripts/ssg-build/index.mjs +253 -0
- package/scripts/validate-extensions.mjs +50 -382
- package/scripts/validate-themes.mjs +37 -5
- package/src/components/About.vue +4 -2
- package/src/components/AboutValue.vue +5 -4
- package/src/components/ComingSoon.vue +12 -1
- package/src/components/ComingSoonModal.vue +33 -61
- package/src/components/Contact.vue +216 -120
- package/src/components/DraftGate.vue +211 -0
- package/src/components/Footer.vue +15 -2
- package/src/components/FooterMinimal.vue +13 -12
- package/src/components/Header.vue +365 -10
- package/src/components/Hero.vue +17 -8
- package/src/components/Home.vue +82 -66
- package/src/components/Intro.vue +4 -2
- package/src/components/IntroGate.vue +30 -73
- package/src/components/NotFound.vue +100 -0
- package/src/components/Plan.vue +10 -4
- package/src/components/Portfolio.vue +29 -3
- package/src/components/Principles.vue +3 -1
- package/src/components/Spacer.vue +23 -0
- package/src/components/Spacer15.vue +3 -4
- package/src/components/Spacer30.vue +3 -4
- package/src/components/Spacer40.vue +3 -4
- package/src/components/Spacer60.vue +3 -4
- package/src/components/StickyCTA.vue +23 -1
- package/src/components/Team.vue +3 -1
- package/src/composables/useComponentResolver.js +49 -1
- package/src/composables/useDraftGate.js +93 -0
- package/src/composables/useFocusTrap.js +85 -0
- package/src/composables/usePageConfig.js +136 -36
- package/src/composables/usePageMeta.js +178 -5
- package/src/composables/usePromoBackgroundStyles.js +30 -14
- package/src/extensions/extensionLoader.js +34 -24
- package/src/main.js +69 -6
- package/src/router/index.js +98 -37
- package/src/styles/base.css +11 -2
- package/src/themes/buildCssVarMap.js +34 -0
- package/src/utils/appInfo.js +8 -5
- package/src/utils/breadcrumbs.js +121 -0
- package/src/utils/canonicalUrl.js +53 -0
- package/src/utils/componentRegistry.js +98 -16
- package/src/utils/cookieConsent.js +38 -24
- package/src/utils/draftMode.js +65 -0
- package/src/utils/imageSources.js +9 -1
- package/src/utils/jsonLd.js +66 -0
- package/src/utils/loadConfig.js +106 -6
- package/src/utils/notFound.js +52 -0
- package/src/utils/robotsGenerator.js +95 -0
- package/src/utils/sha256.js +20 -0
- package/src/utils/sitemapGenerator.js +119 -0
- package/src/utils/socialMeta.js +100 -0
- package/src/utils/ssgRoutes.js +40 -0
- package/src/utils/trackingContext.js +16 -22
- package/src/utils/webAppManifest.js +85 -0
- package/src/utils/webStorage.js +82 -0
- package/templates/index.html +17 -24
- package/templates/scaffolds/extension/README.md +53 -0
- package/templates/scaffolds/extension/extension.config.json +10 -0
- package/templates/scaffolds/extension/index.js +37 -0
- package/templates/scaffolds/extension/package.json +22 -0
- package/templates/scaffolds/site/.env.example +7 -0
- package/templates/scaffolds/site/.nvmrc +1 -0
- package/templates/scaffolds/site/CLAUDE.md +121 -0
- package/templates/scaffolds/site/README.md +25 -0
- package/templates/scaffolds/site/amplify.yml +22 -0
- package/templates/scaffolds/site/package.json +26 -0
- package/templates/scaffolds/site/site/assets/img/.gitkeep +0 -0
- package/templates/scaffolds/site/site/components/.gitkeep +0 -0
- package/templates/scaffolds/site/site/content/content.config.json +3 -0
- package/templates/scaffolds/site/site/content/en/pages/home.json +8 -0
- package/templates/scaffolds/site/site/content/en/shared.json +5 -0
- package/templates/scaffolds/site/site/content/en/site.json +10 -0
- package/templates/scaffolds/site/site/style.css +4 -0
- package/templates/scaffolds/site/vite.config.js +15 -0
- package/templates/scaffolds/theme/README.md +50 -0
- package/templates/scaffolds/theme/index.js +8 -0
- package/templates/scaffolds/theme/package.json +21 -0
- package/templates/scaffolds/theme/theme.config.js +228 -0
- package/templates/scaffolds/theme/theme.css +14 -0
- package/themes/base/theme.config.js +100 -57
- package/vite-plugin.js +549 -32
- package/public/robots.txt +0 -5
- package/scripts/check-overflow.mjs +0 -33
- package/scripts/migrate-to-content-dirs.mjs +0 -223
- package/scripts/patch-lru-cache-tla.js +0 -164
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Vertex CMS
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](https://www.npmjs.com/package/@koehler8/cms)
|
|
5
5
|
|
|
6
6
|
Vertex CMS is a lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support.
|
|
7
7
|
|
|
8
|
+
## Accessibility
|
|
9
|
+
|
|
10
|
+
Vertex CMS targets **[WCAG 2.2 Level AA](https://www.w3.org/WAI/WCAG22/quickref/?currentsidebar=%23col_overview&versions=2.2&levels=aaa)** as a hard requirement for the bundled components, the `base` theme, and the page wrapper. Sites that ship on this framework with the bundled pieces inherit a skip link, single `<main>` landmark, real form labels with `aria-live` errors, visible focus rings, modal focus trap+restore, AA-verified palette contrast, and `prefers-reduced-motion` support out of the box.
|
|
11
|
+
|
|
12
|
+
When extending or theming, **don't regress AA**. See the "Accessibility" section of [CLAUDE.md](CLAUDE.md#accessibility-wcag-22-level-aa--hard-requirement) for the requirements, the token naming conventions for safe colors, and the audit checklist to run when changing palette/markup/CSS.
|
|
13
|
+
|
|
8
14
|
## Quick Start
|
|
9
15
|
|
|
10
16
|
### 1. Create a site repo
|
|
@@ -12,30 +18,33 @@ Vertex CMS is a lightweight, config-driven Vue 3 framework with theming, extensi
|
|
|
12
18
|
```
|
|
13
19
|
my-site/
|
|
14
20
|
site/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
content/
|
|
22
|
+
content.config.json # { "baseLocale": "en" }
|
|
23
|
+
en/
|
|
24
|
+
site.json # Site metadata and settings
|
|
25
|
+
shared.json # Shared content blocks
|
|
26
|
+
pages/
|
|
27
|
+
home.json # Per-page content
|
|
28
|
+
assets/img/ # Images and media
|
|
29
|
+
components/ # Site-local Vue components (optional)
|
|
30
|
+
style.css # Site-specific overrides (optional)
|
|
22
31
|
.env
|
|
23
32
|
package.json
|
|
24
33
|
vite.config.js
|
|
25
34
|
```
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
Or scaffold all of it in one step:
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
@koehler8:registry=https://npm.pkg.github.com
|
|
38
|
+
```bash
|
|
39
|
+
npx --package @koehler8/cms@^1.0.0 cms-create-site my-site
|
|
33
40
|
```
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
### 2. Install
|
|
43
|
+
|
|
44
|
+
Vertex CMS is published to the public npm registry (`registry.npmjs.org`) — no scope configuration needed:
|
|
36
45
|
|
|
37
46
|
```bash
|
|
38
|
-
npm install @koehler8/cms vue vue-router vite vite-ssg @vitejs/plugin-vue
|
|
47
|
+
npm install @koehler8/cms@^1.0.0 vue vue-router vite vite-ssg @vitejs/plugin-vue
|
|
39
48
|
```
|
|
40
49
|
|
|
41
50
|
### 3. Configure Vite
|
|
@@ -58,11 +67,15 @@ export default {
|
|
|
58
67
|
### 4. Run
|
|
59
68
|
|
|
60
69
|
```bash
|
|
61
|
-
npm run dev
|
|
62
|
-
npm run build
|
|
63
|
-
|
|
70
|
+
npm run dev # Development server
|
|
71
|
+
npm run build # Production build (SPA)
|
|
72
|
+
npx cms-ssg-build # Static site generation — memory-bounded shards + blank-page gate
|
|
64
73
|
```
|
|
65
74
|
|
|
75
|
+
(`vite-ssg build` also works directly, but `cms-ssg-build` wraps it with a
|
|
76
|
+
bounded heap, route sharding for large sites, and a post-render check that
|
|
77
|
+
fails the build if any page comes out blank.)
|
|
78
|
+
|
|
66
79
|
## Plugin Options
|
|
67
80
|
|
|
68
81
|
| Option | Default | Description |
|
|
@@ -97,6 +110,20 @@ All files use flat dot-notation keys in alphabetical order. The base locale (spe
|
|
|
97
110
|
|
|
98
111
|
**Supported locales:** en, fr, es, de, ja, ko, pt, ru, tr, vi, id, zh, th, hi, fil
|
|
99
112
|
|
|
113
|
+
### Analytics & consent
|
|
114
|
+
|
|
115
|
+
Set `"googleId"` in `site.json` to enable Google Analytics. How the
|
|
116
|
+
not-yet-answered consent state is treated is configurable:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{ "analytics.consentMode": "opt-in" }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- `"opt-out"` (default) — analytics load while consent is pending and stop if
|
|
123
|
+
the visitor declines. Common US posture.
|
|
124
|
+
- `"opt-in"` — analytics stay off until the visitor explicitly accepts.
|
|
125
|
+
Use this for sites with EU/EEA audiences (GDPR / ePrivacy).
|
|
126
|
+
|
|
100
127
|
## Themes
|
|
101
128
|
|
|
102
129
|
Themes export a manifest with design tokens (palette, typography, surfaces, CTAs, etc.) that are applied as CSS variables at runtime.
|
|
@@ -118,10 +145,10 @@ Each extension has an `extension.config.json` manifest defining:
|
|
|
118
145
|
- **assets** -- CSS and static file references
|
|
119
146
|
- **dependencies** -- Required npm packages
|
|
120
147
|
|
|
121
|
-
|
|
148
|
+
The manifest JSON Schema ships with the package — point your editor or JSON-schema tooling at it:
|
|
122
149
|
|
|
123
|
-
```
|
|
124
|
-
|
|
150
|
+
```json
|
|
151
|
+
{ "$schema": "node_modules/@koehler8/cms/extensions/manifest.schema.json" }
|
|
125
152
|
```
|
|
126
153
|
|
|
127
154
|
## Built-in Components
|
|
@@ -163,6 +190,27 @@ npx cms-validate-extensions --site-dir ./site
|
|
|
163
190
|
| `useLazyImage` | Lazy image loading with IntersectionObserver |
|
|
164
191
|
| `usePromoBackgroundStyles` | Promo section background styling |
|
|
165
192
|
|
|
193
|
+
## Draft Mode
|
|
194
|
+
|
|
195
|
+
Pages can be gated behind a site-wide password before launch: `"draft": true`
|
|
196
|
+
on a page, `"draftPaths": [...]` prefixes, or `"draft": true` site-wide in
|
|
197
|
+
`site.json`, with the password in `"draftPassword"`. The build replaces the
|
|
198
|
+
plaintext password with a SHA-256 hash (plaintext never ships), gated pages
|
|
199
|
+
render only the password gate in the SSG HTML, are marked `noindex`, and are
|
|
200
|
+
omitted from `sitemap.xml`.
|
|
201
|
+
|
|
202
|
+
**Know what the gate is — and isn't.** Draft mode is a pre-launch
|
|
203
|
+
convenience, not access control:
|
|
204
|
+
|
|
205
|
+
- The password hash ships in the public bundle (unsalted SHA-256 — a weak or
|
|
206
|
+
guessable password is trivially recoverable).
|
|
207
|
+
- The gated page's `content.*` keys still ship in the page's hydration JSON
|
|
208
|
+
so Vue can mount after unlock — a determined reader can parse them out of
|
|
209
|
+
the HTML without the password.
|
|
210
|
+
|
|
211
|
+
For genuinely confidential content (NDA material, unannounced listings),
|
|
212
|
+
don't put it in the page JSON until launch.
|
|
213
|
+
|
|
166
214
|
## Testing
|
|
167
215
|
|
|
168
216
|
The framework uses [Vitest](https://vitest.dev/) with happy-dom for unit testing.
|
|
@@ -178,34 +226,66 @@ Tests live in `tests/` mirroring the source structure (`tests/utils/`, `tests/co
|
|
|
178
226
|
## CLI Commands
|
|
179
227
|
|
|
180
228
|
```bash
|
|
229
|
+
# Scaffold a new site / theme / extension
|
|
230
|
+
npx cms-create-site my-site
|
|
231
|
+
npx cms-create-theme my-theme
|
|
232
|
+
npx cms-create-extension my-extension
|
|
233
|
+
|
|
234
|
+
# Static site generation (memory-bounded shards + blank-page gate)
|
|
235
|
+
npx cms-ssg-build
|
|
236
|
+
|
|
181
237
|
# Generate favicon.ico, logo.png, og-image.jpg from source assets
|
|
182
238
|
npx cms-generate-public-assets --site-dir ./site
|
|
183
239
|
|
|
184
|
-
# Validate theme manifests
|
|
185
|
-
npx cms-validate-themes
|
|
240
|
+
# Validate theme manifests (bundled + your site-local themes/)
|
|
241
|
+
npx cms-validate-themes --site-dir ./site
|
|
186
242
|
|
|
187
|
-
# Validate extension manifests
|
|
188
|
-
npx cms-validate-extensions --site-dir ./site
|
|
243
|
+
# Validate extension manifests (site-local extensions/ + named packages)
|
|
244
|
+
npx cms-validate-extensions --site-dir ./site @koehler8/cms-ext-compliance
|
|
189
245
|
```
|
|
190
246
|
|
|
247
|
+
(The Vite plugin also runs the same extension-manifest validation on every
|
|
248
|
+
build and fails loudly on an invalid manifest — the CLIs are for checking
|
|
249
|
+
outside a build.)
|
|
250
|
+
|
|
191
251
|
## Exports
|
|
192
252
|
|
|
253
|
+
The supported public API surface — these specifiers are covered by semver:
|
|
254
|
+
|
|
193
255
|
```js
|
|
194
|
-
|
|
256
|
+
// Build integration
|
|
257
|
+
import cmsPlugin from '@koehler8/cms/vite'; // Vite plugin
|
|
195
258
|
import { createCmsApp } from '@koehler8/cms/app'; // App factory
|
|
259
|
+
|
|
260
|
+
// Utils commonly used by site components and extensions
|
|
196
261
|
import { loadConfigData } from '@koehler8/cms/utils/loadConfig';
|
|
197
262
|
import { resolveAsset, resolveMedia } from '@koehler8/cms/utils/assetResolver';
|
|
198
263
|
import { useResponsiveImage } from '@koehler8/cms/utils/imageSources';
|
|
199
|
-
|
|
264
|
+
import { trackEvent } from '@koehler8/cms/utils/analytics';
|
|
265
|
+
import { hasAcceptedConsent } from '@koehler8/cms/utils/cookieConsent';
|
|
266
|
+
import { formatTokenAmount } from '@koehler8/cms/utils/formatNumber';
|
|
267
|
+
import { resolveThemeColor } from '@koehler8/cms/utils/themeColors';
|
|
268
|
+
|
|
269
|
+
// Bundled components and composables (by file)
|
|
270
|
+
import Header from '@koehler8/cms/components/Header.vue';
|
|
271
|
+
import { useComingSoonConfig } from '@koehler8/cms/composables/useComingSoonConfig';
|
|
200
272
|
```
|
|
201
273
|
|
|
274
|
+
The extension manifest schema is exported at
|
|
275
|
+
`@koehler8/cms/extensions/manifest.schema.json`.
|
|
276
|
+
|
|
277
|
+
Other `utils/*`, `composables/*`, `components/*`, `themes/*`, and
|
|
278
|
+
`extensions/*` deep imports resolve too, but anything not listed above is
|
|
279
|
+
**internal** — it may move or change between minor versions.
|
|
280
|
+
|
|
202
281
|
## Environment Variables
|
|
203
282
|
|
|
204
283
|
| Variable | Used By | Description |
|
|
205
284
|
|----------|---------|-------------|
|
|
206
285
|
| `VITE_SHOW_COOKIE_BANNER` | cookieConsent | Enable cookie consent banner |
|
|
286
|
+
| `VITE_APP_VERSION` | appInfo | Override the framework version string reported in the bundle |
|
|
207
287
|
| `CMS_SITE_DIR` | generate-public-assets | Site directory path (build scripts) |
|
|
208
|
-
| `FAVICON_BG` / `FAVICON_FG` | generate-public-assets |
|
|
288
|
+
| `FAVICON_BG` / `FAVICON_FG` | generate-public-assets | Fallback favicon/og colors (default: stable per-title pair) |
|
|
209
289
|
|
|
210
290
|
## License
|
|
211
291
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'create-extension.mjs');
|
|
7
|
+
await import(pathToFileURL(scriptPath).href);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'create-site.mjs');
|
|
7
|
+
await import(pathToFileURL(scriptPath).href);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'create-theme.mjs');
|
|
7
|
+
await import(pathToFileURL(scriptPath).href);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin wrapper that runs the memory-bounded SSG build orchestrator from the
|
|
3
|
+
// consuming site's cwd. The script does its own argument handling, spawns the
|
|
4
|
+
// per-shard `vite-ssg build` processes, and prints its own status.
|
|
5
|
+
// See scripts/ssg-build/index.mjs and SSG-MEMORY-PLAN.md.
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
await import(path.resolve(__dirname, '..', 'scripts', 'ssg-build', 'index.mjs'));
|
|
@@ -4,14 +4,18 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
let siteDir = './site';
|
|
7
|
+
const packages = [];
|
|
7
8
|
for (let i = 0; i < args.length; i++) {
|
|
8
9
|
if (args[i] === '--site-dir' && args[i + 1]) {
|
|
9
10
|
siteDir = args[i + 1];
|
|
10
11
|
i++;
|
|
12
|
+
} else if (!args[i].startsWith('-')) {
|
|
13
|
+
packages.push(args[i]);
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
process.env.CMS_SITE_DIR = path.resolve(process.cwd(), siteDir);
|
|
18
|
+
process.env.CMS_VALIDATE_PACKAGES = packages.join(',');
|
|
15
19
|
|
|
16
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
21
|
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'validate-extensions.mjs');
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
let siteDir = './site';
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
if (args[i] === '--site-dir' && args[i + 1]) {
|
|
9
|
+
siteDir = args[i + 1];
|
|
10
|
+
i++;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
process.env.CMS_SITE_DIR = path.resolve(process.cwd(), siteDir);
|
|
15
|
+
|
|
5
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
17
|
const scriptPath = path.resolve(__dirname, '..', 'scripts', 'validate-themes.mjs');
|
|
7
18
|
await import(pathToFileURL(scriptPath).href);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koehler8/cms",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Vertex CMS — lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/koehler8/cms/issues"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"vue",
|
|
18
|
+
"cms",
|
|
19
|
+
"vite",
|
|
20
|
+
"ssg",
|
|
21
|
+
"theming",
|
|
22
|
+
"extensions",
|
|
23
|
+
"config-driven"
|
|
24
|
+
],
|
|
17
25
|
"scripts": {
|
|
18
|
-
"postinstall": "node scripts/patch-lru-cache-tla.js",
|
|
19
26
|
"test": "vitest run",
|
|
20
27
|
"test:watch": "vitest",
|
|
21
28
|
"test:coverage": "vitest run --coverage"
|
|
@@ -33,21 +40,21 @@
|
|
|
33
40
|
"./components/*.vue": "./src/components/*.vue",
|
|
34
41
|
"./components/*.js": "./src/components/*.js",
|
|
35
42
|
"./components/*": "./src/components/*",
|
|
36
|
-
"./router": "./src/router/index.js",
|
|
37
43
|
"./themes/*.js": "./src/themes/*.js",
|
|
38
44
|
"./themes/*": "./src/themes/*.js",
|
|
45
|
+
"./extensions/manifest.schema.json": "./extensions/manifest.schema.json",
|
|
39
46
|
"./extensions/*.js": "./src/extensions/*.js",
|
|
40
47
|
"./extensions/*": "./src/extensions/*.js",
|
|
41
|
-
"./constants/*.js": "./src/constants/*.js",
|
|
42
|
-
"./constants/*": "./src/constants/*.js",
|
|
43
|
-
"./plugins/*.js": "./src/plugins/*.js",
|
|
44
|
-
"./plugins/*": "./src/plugins/*.js",
|
|
45
48
|
"./package.json": "./package.json"
|
|
46
49
|
},
|
|
47
50
|
"bin": {
|
|
48
51
|
"cms-generate-public-assets": "./bin/cms-generate-public-assets.js",
|
|
52
|
+
"cms-ssg-build": "./bin/cms-ssg-build.js",
|
|
49
53
|
"cms-validate-themes": "./bin/cms-validate-themes.js",
|
|
50
|
-
"cms-validate-extensions": "./bin/cms-validate-extensions.js"
|
|
54
|
+
"cms-validate-extensions": "./bin/cms-validate-extensions.js",
|
|
55
|
+
"cms-create-theme": "./bin/cms-create-theme.js",
|
|
56
|
+
"cms-create-extension": "./bin/cms-create-extension.js",
|
|
57
|
+
"cms-create-site": "./bin/cms-create-site.js"
|
|
51
58
|
},
|
|
52
59
|
"files": [
|
|
53
60
|
"src",
|
|
@@ -58,7 +65,8 @@
|
|
|
58
65
|
"templates",
|
|
59
66
|
"bin",
|
|
60
67
|
"vite-plugin.js",
|
|
61
|
-
"README.md"
|
|
68
|
+
"README.md",
|
|
69
|
+
"CHANGELOG.md"
|
|
62
70
|
],
|
|
63
71
|
"peerDependencies": {
|
|
64
72
|
"@vitejs/plugin-vue": "^6.0.0",
|
|
@@ -68,21 +76,29 @@
|
|
|
68
76
|
"vue-router": "^5.0.0"
|
|
69
77
|
},
|
|
70
78
|
"dependencies": {
|
|
71
|
-
"@unhead/vue": "^2.
|
|
72
|
-
"ajv": "^8.
|
|
79
|
+
"@unhead/vue": "^2.1.15",
|
|
80
|
+
"ajv": "^8.20.0",
|
|
73
81
|
"ajv-formats": "^3.0.1",
|
|
82
|
+
"pinia": "^3.0.1",
|
|
83
|
+
"sharp": "^0.35.3"
|
|
84
|
+
},
|
|
85
|
+
"optionalDependencies": {
|
|
74
86
|
"canvas": "^3.2.0",
|
|
75
87
|
"dotenv": "^17.2.3",
|
|
76
|
-
"pinia": "^3.0.1",
|
|
77
88
|
"png-to-ico": "^3.0.1"
|
|
78
89
|
},
|
|
79
90
|
"devDependencies": {
|
|
80
|
-
"@
|
|
81
|
-
"
|
|
82
|
-
"
|
|
91
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
92
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
93
|
+
"@vue/test-utils": "^2.4.10",
|
|
94
|
+
"happy-dom": "^20.9.0",
|
|
95
|
+
"vite": "^8.2.1",
|
|
96
|
+
"vitest": "^4.1.6",
|
|
97
|
+
"vue": "^3.5.39",
|
|
98
|
+
"vue-router": "^5.0.7"
|
|
83
99
|
},
|
|
84
100
|
"engines": {
|
|
85
101
|
"node": ">=20.19.0",
|
|
86
|
-
"npm": "
|
|
102
|
+
"npm": ">=10.8.0"
|
|
87
103
|
}
|
|
88
104
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runScaffold } from './scaffold.mjs';
|
|
2
|
+
|
|
3
|
+
const result = await runScaffold({
|
|
4
|
+
kind: 'extension',
|
|
5
|
+
argv: process.argv.slice(2),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if (result.message) {
|
|
9
|
+
process[result.ok ? 'stdout' : 'stderr'].write(result.message + '\n');
|
|
10
|
+
}
|
|
11
|
+
process.exit(result.ok ? 0 : 1);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runScaffold } from './scaffold.mjs';
|
|
2
|
+
|
|
3
|
+
const result = await runScaffold({
|
|
4
|
+
kind: 'site',
|
|
5
|
+
argv: process.argv.slice(2),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if (result.message) {
|
|
9
|
+
process[result.ok ? 'stdout' : 'stderr'].write(result.message + '\n');
|
|
10
|
+
}
|
|
11
|
+
process.exit(result.ok ? 0 : 1);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runScaffold } from './scaffold.mjs';
|
|
2
|
+
|
|
3
|
+
const result = await runScaffold({
|
|
4
|
+
kind: 'theme',
|
|
5
|
+
argv: process.argv.slice(2),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
if (result.message) {
|
|
9
|
+
process[result.ok ? 'stdout' : 'stderr'].write(result.message + '\n');
|
|
10
|
+
}
|
|
11
|
+
process.exit(result.ok ? 0 : 1);
|
|
@@ -6,13 +6,35 @@
|
|
|
6
6
|
// {CMS_SITE_DIR}/content/{baseLocale}/site.json, output is written to
|
|
7
7
|
// {dirname(CMS_SITE_DIR)}/public/.
|
|
8
8
|
|
|
9
|
-
import 'dotenv/config';
|
|
10
|
-
|
|
11
9
|
import fs from 'fs';
|
|
12
10
|
import path from 'path';
|
|
13
11
|
import { fileURLToPath } from 'url';
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
|
|
13
|
+
// dotenv is optional — without it, config comes from the process environment.
|
|
14
|
+
try {
|
|
15
|
+
await import('dotenv/config');
|
|
16
|
+
} catch {
|
|
17
|
+
/* optional dependency not installed */
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// canvas (native) and png-to-ico are optionalDependencies: npm skips them
|
|
21
|
+
// when their platform build fails, so a consumer that never runs this
|
|
22
|
+
// command still installs cleanly. This command DOES need them — fail loudly
|
|
23
|
+
// with the fix instead of a bare module-not-found stack.
|
|
24
|
+
let createCanvas;
|
|
25
|
+
let registerFont;
|
|
26
|
+
let pngToIco;
|
|
27
|
+
try {
|
|
28
|
+
({ createCanvas, registerFont } = await import('canvas'));
|
|
29
|
+
({ default: pngToIco } = await import('png-to-ico'));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('❌ cms-generate-public-assets requires the optional dependencies `canvas` and `png-to-ico`.');
|
|
32
|
+
console.error(' They were skipped (or failed to build) during install — canvas is a native module');
|
|
33
|
+
console.error(' and needs system libraries (cairo, pango) on platforms without prebuilds.');
|
|
34
|
+
console.error(' Install them explicitly: npm install canvas png-to-ico');
|
|
35
|
+
console.error(` Underlying error: ${err.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
40
|
const __dirname = path.dirname(__filename);
|
|
@@ -83,6 +105,37 @@ function luminance({ r, g, b }) {
|
|
|
83
105
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
84
106
|
}
|
|
85
107
|
|
|
108
|
+
const FALLBACK_COLOR_PAIRS = [
|
|
109
|
+
['#ffffff', '#000000'],
|
|
110
|
+
['#000000', '#ffffff'],
|
|
111
|
+
['#ff0000', '#ffffff'],
|
|
112
|
+
['#0000ff', '#ffffff'],
|
|
113
|
+
['#008000', '#ffffff'],
|
|
114
|
+
['#ffff00', '#000000'],
|
|
115
|
+
['#ff00ff', '#000000'],
|
|
116
|
+
['#00ffff', '#000000'],
|
|
117
|
+
['#1e90ff', '#ffffff'],
|
|
118
|
+
['#ff8c00', '#000000'],
|
|
119
|
+
['#4b0082', '#ffffff'],
|
|
120
|
+
['#32cd32', '#000000'],
|
|
121
|
+
['#8b0000', '#ffffff'],
|
|
122
|
+
['#ffd700', '#000000'],
|
|
123
|
+
['#ff69b4', '#000000'],
|
|
124
|
+
['#2f4f4f', '#ffffff']
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
// Deterministic per-site pick: the same title always yields the same pair,
|
|
128
|
+
// so generated assets keep their colors across deploys (public/ is rebuilt
|
|
129
|
+
// on every build).
|
|
130
|
+
function getStableColorPair(seed) {
|
|
131
|
+
const s = String(seed || '');
|
|
132
|
+
let hash = 5381;
|
|
133
|
+
for (let i = 0; i < s.length; i++) {
|
|
134
|
+
hash = ((hash * 33) ^ s.charCodeAt(i)) >>> 0;
|
|
135
|
+
}
|
|
136
|
+
return FALLBACK_COLOR_PAIRS[hash % FALLBACK_COLOR_PAIRS.length];
|
|
137
|
+
}
|
|
138
|
+
|
|
86
139
|
// Try to register a professional-looking font if available
|
|
87
140
|
function tryRegisterFonts() {
|
|
88
141
|
const candidates = [
|
|
@@ -336,44 +389,16 @@ async function generateFavicon() {
|
|
|
336
389
|
shouldGenerateOg = true;
|
|
337
390
|
}
|
|
338
391
|
|
|
339
|
-
const providedFavicon = resolveSiteAssetFile(['favicon.ico']);
|
|
340
|
-
if (providedFavicon) {
|
|
341
|
-
fs.copyFileSync(providedFavicon, icoPath);
|
|
342
|
-
console.log(`✅ Found existing favicon; copied from ${providedFavicon}`);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
console.log(`Generating favicon.ico using letter "${letter}" from site "${siteTitle}"`);
|
|
347
|
-
|
|
348
|
-
function getRandomColorPair() {
|
|
349
|
-
const colors = [
|
|
350
|
-
['#ffffff', '#000000'],
|
|
351
|
-
['#000000', '#ffffff'],
|
|
352
|
-
['#ff0000', '#ffffff'],
|
|
353
|
-
['#0000ff', '#ffffff'],
|
|
354
|
-
['#008000', '#ffffff'],
|
|
355
|
-
['#ffff00', '#000000'],
|
|
356
|
-
['#ff00ff', '#000000'],
|
|
357
|
-
['#00ffff', '#000000'],
|
|
358
|
-
['#1e90ff', '#ffffff'],
|
|
359
|
-
['#ff8c00', '#000000'],
|
|
360
|
-
['#4b0082', '#ffffff'],
|
|
361
|
-
['#32cd32', '#000000'],
|
|
362
|
-
['#8b0000', '#ffffff'],
|
|
363
|
-
['#ffd700', '#000000'],
|
|
364
|
-
['#ff69b4', '#000000'],
|
|
365
|
-
['#2f4f4f', '#ffffff']
|
|
366
|
-
];
|
|
367
|
-
return colors[Math.floor(Math.random() * colors.length)];
|
|
368
|
-
}
|
|
369
|
-
|
|
370
392
|
let bg = process.env.FAVICON_BG;
|
|
371
393
|
let fg = process.env.FAVICON_FG;
|
|
372
394
|
|
|
373
395
|
if (!bg || !fg) {
|
|
374
|
-
[bg, fg] =
|
|
396
|
+
[bg, fg] = getStableColorPair(siteTitle);
|
|
375
397
|
}
|
|
376
398
|
|
|
399
|
+
// The fallback og-image must generate before the provided-favicon early
|
|
400
|
+
// return: socialMeta emits og:image → /og-image.jpg on every page, so a
|
|
401
|
+
// site that ships its own favicon.ico but no og-image still needs one.
|
|
377
402
|
if (shouldGenerateOg) {
|
|
378
403
|
try {
|
|
379
404
|
const ogBuffer = renderOgImageBuffer(siteTitle, { width: 1200, height: 630, bg, fg });
|
|
@@ -384,6 +409,15 @@ async function generateFavicon() {
|
|
|
384
409
|
}
|
|
385
410
|
}
|
|
386
411
|
|
|
412
|
+
const providedFavicon = resolveSiteAssetFile(['favicon.ico']);
|
|
413
|
+
if (providedFavicon) {
|
|
414
|
+
fs.copyFileSync(providedFavicon, icoPath);
|
|
415
|
+
console.log(`✅ Found existing favicon; copied from ${providedFavicon}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log(`Generating favicon.ico using letter "${letter}" from site "${siteTitle}"`);
|
|
420
|
+
|
|
387
421
|
const sizes = [256, 128, 64, 48, 32, 16];
|
|
388
422
|
const pngBuffers = sizes.map((s) => renderLetterPng(letter, s, { bg, fg }));
|
|
389
423
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the per-site variant cache directory.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* {siteRoot}/node_modules/.cache/@koehler8/cms/image-variants/{siteKey}/
|
|
6
|
+
*
|
|
7
|
+
* where siteKey is the first 8 hex chars of sha1(siteDir). The keying
|
|
8
|
+
* prevents cache collisions when @koehler8/cms is hoisted in a workspace
|
|
9
|
+
* (one framework install serves multiple sites — without per-site keys,
|
|
10
|
+
* concurrent builds would clobber each other).
|
|
11
|
+
*
|
|
12
|
+
* The cache lives under the SITE's node_modules (matching Vite's own
|
|
13
|
+
* node_modules/.vite/ convention) so each site's cache is local to its
|
|
14
|
+
* own project tree.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
export function siteKeyFor(siteDir) {
|
|
21
|
+
return crypto
|
|
22
|
+
.createHash('sha1')
|
|
23
|
+
.update(path.resolve(siteDir))
|
|
24
|
+
.digest('hex')
|
|
25
|
+
.slice(0, 8);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function computeVariantCacheDir(siteRoot, siteDir) {
|
|
29
|
+
const root = path.resolve(siteRoot);
|
|
30
|
+
const key = siteKeyFor(siteDir || siteRoot);
|
|
31
|
+
return path.join(
|
|
32
|
+
root,
|
|
33
|
+
'node_modules',
|
|
34
|
+
'.cache',
|
|
35
|
+
'@koehler8',
|
|
36
|
+
'cms',
|
|
37
|
+
'image-variants',
|
|
38
|
+
key,
|
|
39
|
+
);
|
|
40
|
+
}
|