@janga/norna 0.7.6 → 0.7.8

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 (139) hide show
  1. package/README.md +8 -7
  2. package/astro.config.mjs +76 -0
  3. package/bin/norna-cli.mjs +2 -2
  4. package/docs/README.md +3 -3
  5. package/docs/commands.md +7 -6
  6. package/docs/configuration.md +3 -2
  7. package/docs/content.md +202 -91
  8. package/docs/design/site-examples-structure.md +1 -1
  9. package/docs/engine-development.md +3 -3
  10. package/docs/getting-started.md +50 -8
  11. package/docs/images-and-metadata.md +117 -20
  12. package/docs/routes.md +20 -12
  13. package/docs/site-structure.md +9 -5
  14. package/docs/theme.md +12 -8
  15. package/docs/typography.md +9 -10
  16. package/examples/dog-gallery/site/content.md +38 -77
  17. package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +0 -10
  18. package/fixtures/basic/site/content.md +2 -4
  19. package/fixtures/content-model-v2/.astro/collections/site.schema.json +776 -0
  20. package/fixtures/content-model-v2/.astro/collections/theme.schema.json +668 -0
  21. package/fixtures/content-model-v2/.astro/content-assets.mjs +4 -0
  22. package/fixtures/content-model-v2/.astro/content-modules.mjs +1 -0
  23. package/fixtures/content-model-v2/.astro/content.d.ts +175 -0
  24. package/fixtures/content-model-v2/.astro/data-store.json +1 -0
  25. package/fixtures/content-model-v2/.astro/dev-local.json +8 -0
  26. package/fixtures/content-model-v2/.astro/dev.json +13 -0
  27. package/fixtures/content-model-v2/.astro/dev.log +231 -0
  28. package/fixtures/content-model-v2/.astro/settings.json +5 -0
  29. package/fixtures/content-model-v2/.astro/types.d.ts +2 -0
  30. package/fixtures/content-model-v2/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  31. package/fixtures/content-model-v2/dist/_astro/SitePage.BbfZub4g.css +1 -0
  32. package/fixtures/content-model-v2/dist/guide/index.html +3 -0
  33. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-480.webp +0 -0
  34. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-768.webp +0 -0
  35. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-971.webp +0 -0
  36. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-480.webp +0 -0
  37. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-768.webp +0 -0
  38. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-971.webp +0 -0
  39. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-480.webp +0 -0
  40. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-768.webp +0 -0
  41. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-971.webp +0 -0
  42. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1080.webp +0 -0
  43. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1440.webp +0 -0
  44. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1600.webp +0 -0
  45. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-480.webp +0 -0
  46. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-768.webp +0 -0
  47. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-480.webp +0 -0
  48. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-768.webp +0 -0
  49. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-960.webp +0 -0
  50. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1080.webp +0 -0
  51. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1440.webp +0 -0
  52. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1600.webp +0 -0
  53. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-480.webp +0 -0
  54. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-768.webp +0 -0
  55. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1080.webp +0 -0
  56. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1440.webp +0 -0
  57. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1600.webp +0 -0
  58. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-480.webp +0 -0
  59. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-768.webp +0 -0
  60. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1080.webp +0 -0
  61. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1440.webp +0 -0
  62. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1600.webp +0 -0
  63. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-480.webp +0 -0
  64. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-768.webp +0 -0
  65. package/fixtures/content-model-v2/dist/index.html +8 -0
  66. package/fixtures/content-model-v2/node_modules/.vite/deps/_metadata.json +80 -0
  67. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  68. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  69. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  70. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  71. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  72. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  73. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  74. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  75. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  76. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  77. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  78. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  79. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js +1389 -0
  80. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  81. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  82. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  83. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  84. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  85. package/fixtures/content-model-v2/node_modules/.vite/deps/package.json +3 -0
  86. package/fixtures/content-model-v2/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  87. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  88. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  89. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  90. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  91. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  92. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  93. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  94. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  95. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  96. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  97. package/fixtures/content-model-v2/site/.norna/generated-images.json +210 -0
  98. package/fixtures/content-model-v2/site/config.mjs +11 -0
  99. package/fixtures/content-model-v2/site/content.md +84 -0
  100. package/fixtures/content-model-v2/site/images/card-section/adopt.svg +9 -0
  101. package/fixtures/content-model-v2/site/images/card-section/foster.svg +12 -0
  102. package/fixtures/content-model-v2/site/images/intro/detail.jpg +0 -0
  103. package/fixtures/content-model-v2/site/images/intro/duplicate.jpg +0 -0
  104. package/fixtures/content-model-v2/site/images/intro/hero.jpg +0 -0
  105. package/fixtures/content-model-v2/site/images/plain/image.jpg +0 -0
  106. package/fixtures/content-model-v2/site/images/timed/slide-one.jpg +0 -0
  107. package/fixtures/content-model-v2/site/images/timed/slide-three.jpg +0 -0
  108. package/fixtures/content-model-v2/site/images/timed/slide-two.jpg +0 -0
  109. package/fixtures/content-model-v2/site/routes/010-guide/images/intro/duplicate.jpg +0 -0
  110. package/fixtures/content-model-v2/site/routes/010-guide/route-content.md +17 -0
  111. package/fixtures/content-model-v2/site/theme.md +9 -0
  112. package/package.json +3 -2
  113. package/scripts/deploy-site.mjs +11 -4
  114. package/scripts/dev-local.mjs +6 -0
  115. package/scripts/generate-images.mjs +132 -56
  116. package/scripts/lib/image-dimensions.mjs +26 -0
  117. package/scripts/lib/norna-markdown-blocks.mjs +643 -0
  118. package/scripts/lib/site-content.mjs +31 -29
  119. package/scripts/show-typography.mjs +17 -9
  120. package/scripts/sync-content-sections.mjs +414 -412
  121. package/scripts/test-content-check.mjs +59 -45
  122. package/scripts/test-content-model-v2.mjs +1230 -0
  123. package/scripts/test-engine-commands.mjs +1 -1
  124. package/scripts/test-package-check.mjs +36 -8
  125. package/scripts/test-temporary-visibility.mjs +3 -6
  126. package/src/components/CardList.astro +77 -0
  127. package/src/components/GalleryGrid.astro +22 -12
  128. package/src/components/SiteNavigation.astro +5 -4
  129. package/src/components/SitePage.astro +9 -2
  130. package/src/components/SiteSection.astro +35 -13
  131. package/src/content.config.ts +4 -17
  132. package/src/lib/generatedImages.ts +50 -10
  133. package/src/lib/sectionContent.ts +130 -35
  134. package/src/styles/global.css +187 -0
  135. package/starters/basic/README.md +4 -2
  136. package/starters/basic/site/content.md +9 -11
  137. package/starters/project/README.md +7 -3
  138. package/starters/project/site/content.md +0 -9
  139. package/starters/project/site/routes/010-guide/route-content.md +0 -5
package/README.md CHANGED
@@ -25,15 +25,16 @@ A `norna` site is file-driven:
25
25
  base path, in `site/config.mjs`.
26
26
  3. The site keeps site-wide visual theme defaults, including layout, image
27
27
  sizing, font, typography, colors, and frame colors, in `site/theme.md`.
28
- 4. The site keeps homepage content, section order, image rows, alt text, and
29
- captions in `site/content.md`.
28
+ 4. The site keeps homepage content, Markdown section order, managed media
29
+ blocks, alt text, captions, and optional section metadata in
30
+ `site/content.md`.
30
31
  5. Optional route pages live under
31
- `site/routes/<route-folder>/route-content.md`.
32
+ `site/routes/<NNN-route-id>/route-content.md`.
32
33
  6. Source images live under `site/images/<section-id>/` for the homepage, or
33
- under `site/routes/<route-folder>/images/<section-id>/` for a route.
34
+ under `site/routes/<NNN-route-id>/images/<section-id>/` for a route.
34
35
  7. Static public files live under `site/public/`.
35
- 8. `norna` validates the files, generates WebP variants, builds static
36
- Astro pages, and can help publish the committed branch through GitHub Pages.
36
+ 8. `norna` validates the files, prepares managed images, builds static Astro
37
+ pages, and can help publish the committed branch through GitHub Pages.
37
38
 
38
39
  The default site directory is `site/`. Commands can also use another directory
39
40
  with `NORNA_SITE_DIR` or `norna --site-dir <path>`. Without an explicit site
@@ -75,7 +76,7 @@ Actions use the same engine version.
75
76
 
76
77
  - Create a site: [Getting Started](docs/getting-started.md)
77
78
  - Understand required site files: [Site Structure](docs/site-structure.md)
78
- - Edit sections and image rows: [Content](docs/content.md)
79
+ - Edit sections and managed media blocks: [Content](docs/content.md)
79
80
  - Configure a site: [Configuration](docs/configuration.md)
80
81
  - Publish under a GitHub Pages project path:
81
82
  [`site.basePath`](docs/configuration.md#sitebasepath)
package/astro.config.mjs CHANGED
@@ -1,14 +1,87 @@
1
1
  // @ts-check
2
+ import { execFile } from 'node:child_process';
2
3
  import path from 'node:path';
4
+ import { promisify } from 'node:util';
3
5
  import { defineConfig } from 'astro/config';
4
6
  import {
5
7
  astroCacheDir,
6
8
  astroDistDir,
7
9
  astroPublicDir,
8
10
  engineRoot,
11
+ generatedImagesManifestPath,
12
+ siteContentPath,
13
+ siteImagesDir,
14
+ siteProjectRoot,
15
+ siteRoutesDir,
9
16
  } from './scripts/lib/site-paths.mjs';
10
17
  import projectConfig from './scripts/lib/project-config.mjs';
11
18
 
19
+ const execFileAsync = promisify(execFile);
20
+
21
+ const isWithinDirectory = (parentDirectory, filePath) => {
22
+ const relativePath = path.relative(parentDirectory, filePath);
23
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
24
+ };
25
+
26
+ const runGenerateImages = () => execFileAsync(process.execPath, [path.join(engineRoot, 'scripts', 'generate-images.mjs')], {
27
+ cwd: siteProjectRoot,
28
+ maxBuffer: 1024 * 1024 * 10,
29
+ });
30
+
31
+ const nornaGeneratedImagesWatcher = () => ({
32
+ name: 'norna-generated-images-watcher',
33
+ configureServer(server) {
34
+ const manifestPath = path.resolve(generatedImagesManifestPath);
35
+ const watchedSourcePaths = [
36
+ siteContentPath,
37
+ siteImagesDir,
38
+ siteRoutesDir,
39
+ ].map((watchedPath) => path.resolve(watchedPath));
40
+ let refreshTimer;
41
+ let refreshPromise = Promise.resolve();
42
+
43
+ const isRelevantSourcePath = (changedPath) => {
44
+ const resolvedPath = path.resolve(changedPath);
45
+ return watchedSourcePaths.some((watchedPath) => (
46
+ resolvedPath === watchedPath || isWithinDirectory(watchedPath, resolvedPath)
47
+ ));
48
+ };
49
+
50
+ const refreshImages = () => {
51
+ refreshPromise = refreshPromise
52
+ .catch(() => {})
53
+ .then(async () => {
54
+ try {
55
+ await runGenerateImages();
56
+ } catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error);
58
+ server.config.logger.error(`Norna image refresh failed:\n${message}`);
59
+ }
60
+ });
61
+ };
62
+
63
+ const scheduleRefreshImages = (changedPath) => {
64
+ if (!isRelevantSourcePath(changedPath)) return;
65
+
66
+ clearTimeout(refreshTimer);
67
+ refreshTimer = setTimeout(refreshImages, 250);
68
+ };
69
+
70
+ server.watcher.add(manifestPath);
71
+ server.watcher.add(watchedSourcePaths);
72
+ server.watcher.on('change', (changedPath) => {
73
+ if (path.resolve(changedPath) !== manifestPath) return;
74
+
75
+ server.ws.send({ type: 'full-reload' });
76
+ });
77
+ server.watcher.on('add', scheduleRefreshImages);
78
+ server.watcher.on('change', scheduleRefreshImages);
79
+ server.watcher.on('unlink', scheduleRefreshImages);
80
+ server.watcher.on('addDir', scheduleRefreshImages);
81
+ server.watcher.on('unlinkDir', scheduleRefreshImages);
82
+ },
83
+ });
84
+
12
85
  // https://astro.build/config
13
86
  export default defineConfig({
14
87
  base: projectConfig.site.basePath,
@@ -16,4 +89,7 @@ export default defineConfig({
16
89
  outDir: astroDistDir,
17
90
  publicDir: astroPublicDir,
18
91
  srcDir: path.join(engineRoot, 'src'),
92
+ vite: {
93
+ plugins: [nornaGeneratedImagesWatcher()],
94
+ },
19
95
  });
package/bin/norna-cli.mjs CHANGED
@@ -16,7 +16,7 @@ Commands:
16
16
  dev:stop Stop local dev server
17
17
  config:check Validate site/config.mjs
18
18
  content:check Validate site/content.md and image references
19
- content:sync Rewrite section order and move misplaced images
19
+ content:sync Move misplaced Norna-managed images and refresh generated images
20
20
  typography presets Show built-in typography preset values
21
21
  typography show Show resolved typography for the selected site
22
22
  site:public Sync site/public/ to public/
@@ -128,9 +128,9 @@ try {
128
128
  await runScript('scripts/check-config.mjs', rest);
129
129
  } else if (command === 'content:check') {
130
130
  await runScript('scripts/sync-content-sections.mjs', ['--check', ...rest]);
131
- await runAstroInherit(['sync']);
132
131
  } else if (command === 'content:sync') {
133
132
  await runScript('scripts/sync-content-sections.mjs', ['--write', ...rest]);
133
+ await runScript('scripts/generate-images.mjs', rest);
134
134
  } else if (command === 'typography:presets' || (command === 'typography' && subcommand === 'presets')) {
135
135
  const scriptArgs = command === 'typography' ? subcommandRest : rest;
136
136
  await runScript('scripts/show-typography.mjs', ['presets', ...scriptArgs]);
package/docs/README.md CHANGED
@@ -38,9 +38,9 @@ For a quick reference:
38
38
  - [Configuration](configuration.md) describes technical `site/config.mjs`
39
39
  fields such as URL, base path, locale labels, footer, GitHub, and deploy
40
40
  settings.
41
- - [Content](content.md) describes page frontmatter, section frontmatter,
42
- image rows, carousels, temporary sections, Markdown section matching, and
43
- content validation.
41
+ - [Content](content.md) describes page frontmatter, Markdown-authored
42
+ sections, optional section metadata, managed media blocks, temporary
43
+ sections, and content validation.
44
44
  - [Theme](theme.md) describes site-wide layout, layout density, image sizing,
45
45
  font, typography rhythm, presentation colors, frame colors, and inline styles.
46
46
  - [Typography](typography.md) describes presets, rhythms, roles, overrides,
package/docs/commands.md CHANGED
@@ -101,10 +101,11 @@ should normally mean the repository's complete publishable artifact, while
101
101
  cache path.
102
102
  - `config:check`: validates `site/config.mjs` against the runtime config
103
103
  reader.
104
- - `content:check`: validates section structure and image references, then
105
- runs `astro sync`.
106
- - `content:sync` / `norna:sync`: rewrites Markdown section order and moves misplaced referenced
107
- image files after confirmation.
104
+ - `content:check`: validates section structure, managed media references,
105
+ inline styles, and common content mistakes.
106
+ - `content:sync` / `norna:sync`: moves misplaced referenced image files after
107
+ confirmation when the intended move is unambiguous. Moving files between the
108
+ homepage and routes, or between routes, requires a clean Git working tree.
108
109
  - `typography presets`: prints the exact built-in values for typography
109
110
  presets and rhythms.
110
111
  - `typography show`: prints the selected site's resolved typography for the
@@ -112,8 +113,8 @@ should normally mean the repository's complete publishable artifact, while
112
113
  and inherited page or section values are marked with `inherited: true`.
113
114
  - `site:public`: copies `site/public/` into `site/.norna/public/` and
114
115
  removes stale copied static files.
115
- - `images`: generates WebP variants and writes
116
- `site/.norna/generated-images.json`.
116
+ - `images`: generates WebP variants for raster images, copies managed SVG
117
+ images, and writes `site/.norna/generated-images.json`.
117
118
  - `engine:update [version|latest]`: updates the site repository's
118
119
  `@janga/norna` dependency with `npm install --save-exact`, normalizes
119
120
  `package-lock.json` for the pinned GitHub Actions Linux/npm environment, and
@@ -39,7 +39,7 @@ site: {
39
39
  `?`, `#`, or `//`.
40
40
  - Consequence: use `/` for a root site or custom domain. Use
41
41
  `/repository-name/` for a GitHub Pages project site without a custom domain.
42
- Source Markdown can still use root-relative paths such as `/workflow.svg` or
42
+ Source Markdown can still use root-relative paths such as `/favicon.svg` or
43
43
  `/getting-started/`; Norna prefixes them during rendering.
44
44
 
45
45
  Examples:
@@ -141,7 +141,8 @@ Fields:
141
141
  `Sections`.
142
142
  - `locale.labels.menu`: mobile menu summary text, default `Menu`.
143
143
  - `locale.labels.closeMenu`: reserved close-menu label, default `Close menu`.
144
- - `locale.labels.gallery`: image row ARIA label prefix, default `Images`.
144
+ - `locale.labels.gallery`: Norna managed media ARIA label prefix, default
145
+ `Images`.
145
146
 
146
147
  Example:
147
148
 
package/docs/content.md CHANGED
@@ -1,10 +1,9 @@
1
1
  # Content
2
2
 
3
- `site/content.md` is the homepage page file for a `norna` site. It defines the
4
- page metadata, section order, section ids, image rows, and Markdown text for
5
- the homepage.
3
+ `site/content.md` is the homepage page file for a Norna site. It contains page
4
+ metadata in frontmatter and the homepage content in Markdown.
6
5
 
7
- Route pages use the same page and section model in
6
+ Route pages use the same page model in
8
7
  `site/routes/<NNN-route-id>/route-content.md`. See [Routes](routes.md) for the
9
8
  route-specific rules.
10
9
 
@@ -22,8 +21,7 @@ The Astro content schema validates these top-level fields in page files:
22
21
  - `presentation`: optional page-level presentation overrides. See
23
22
  [Theme](theme.md) and [Typography](typography.md).
24
23
  - `frame`: optional page-level frame color source. See [Theme](theme.md).
25
- - `sections`: required non-empty array. Defines section order, ids,
26
- presentation overrides, and image rows.
24
+ - `sections`: optional section metadata keyed by section id.
27
25
 
28
26
  Minimal homepage:
29
27
 
@@ -31,8 +29,6 @@ Minimal homepage:
31
29
  ---
32
30
  title: My Site
33
31
  description: A small Norna site.
34
- sections:
35
- - id: intro
36
32
  ---
37
33
 
38
34
  ## Intro {#intro}
@@ -42,99 +38,217 @@ Text...
42
38
 
43
39
  ## Sections
44
40
 
45
- Each `sections[]` item has:
41
+ Markdown level 2 headings define the page sections and their order:
46
42
 
47
- - `id`: required string matching `^[a-z0-9-]+$`. Used for anchors, navigation,
48
- image directories, and Markdown heading ids.
49
- - `visible`: optional date window that controls whether the section is rendered.
50
- - `presentation`: optional section-level visual overrides.
51
- - `gallery`: optional array, defaulting to `[]`.
43
+ ```md
44
+ ## About {#about}
52
45
 
53
- Example:
46
+ ...
54
47
 
55
- ```yaml
56
- sections:
57
- - id: work
58
- gallery:
59
- - image: work.jpg
60
- alt: "A woven artwork on a white wall."
61
- caption: "Work in progress."
48
+ ## Work {#work}
49
+
50
+ ...
51
+
52
+ ## Contact {#contact}
53
+
54
+ ...
62
55
  ```
63
56
 
64
- Every frontmatter section must have a matching level 2 Markdown heading with an
65
- explicit id:
57
+ Every section heading must have an explicit id:
66
58
 
67
59
  ```md
68
60
  ## Work {#work}
61
+ ```
62
+
63
+ The id must match `^[a-z0-9-]+$`. It is used for anchors, navigation, image
64
+ directories, and optional section metadata. The visible section navigation
65
+ label comes from the Markdown heading text.
66
+
67
+ Markdown section content starts at the level 2 heading and continues until the
68
+ next level 2 heading. `###` and `####` headings are body subheadings within the
69
+ current section, not new sections.
70
+
71
+ ## Section Metadata
69
72
 
70
- Introductory text.
73
+ Use `sections` only when a section needs structured metadata that is not
74
+ naturally expressed by Markdown, such as visibility or presentation overrides.
75
+
76
+ ```yaml
77
+ sections:
78
+ work:
79
+ presentation:
80
+ typography:
81
+ preset: statement
71
82
  ```
72
83
 
73
- Keep these values aligned:
84
+ Each `sections.<section-id>` key must match a Markdown heading id in the same
85
+ page file:
74
86
 
75
- - the frontmatter `sections[].id`
76
- - the Markdown heading id
77
- - the source image directory `site/images/<section-id>/`
87
+ ```md
88
+ ## Work {#work}
89
+ ```
78
90
 
79
- The visible section navigation label comes from the Markdown heading text, not
80
- from the frontmatter id.
91
+ Do not list sections in frontmatter just to define order. Section order comes
92
+ from the Markdown heading order.
81
93
 
82
- ## Image Rows
94
+ ## Managed Media Blocks
83
95
 
84
- Each image row can contain a single image:
96
+ Norna-managed local images and cards are written in Markdown fenced blocks at
97
+ the point where they should appear in the section. Markdown determines
98
+ placement: move the fenced block in the page file to move the rendered image,
99
+ carousel, or card list.
85
100
 
86
- ```yaml
87
- gallery:
88
- - image: work.jpg
89
- alt: "A woven artwork on a white wall."
90
- caption: "Work in progress."
101
+ Use `norna-image-stack` for one or more stacked images:
102
+
103
+ ````md
104
+ ```norna-image-stack
105
+ - image: work.jpg
106
+ alt: A woven artwork on a white wall.
107
+ caption: Work in progress.
91
108
  ```
109
+ ````
110
+
111
+ Use `norna-image-carousel` for a carousel:
112
+
113
+ ````md
114
+ ```norna-image-carousel
115
+ - image: first.jpg
116
+ alt: First image.
117
+ caption: First caption.
118
+ - image: second.jpg
119
+ alt: Second image.
120
+ caption: Second caption.
121
+ ```
122
+ ````
123
+
124
+ Use `norna-card-list` for a list of compact cards. Cards can include text,
125
+ managed images, links, and optional badge text:
126
+
127
+ ````md
128
+ ```norna-card-list
129
+ layout: image-top
130
+ flow: grid
131
+ size: m
132
+ width: normal
133
+
134
+ - title: Adopt
135
+ text: Give a dog a new home.
136
+ image: adopt.svg
137
+ link: /adopt/
138
+ badge-text: Recommended
139
+ - title: Foster
140
+ text: Help for a shorter period.
141
+ image: foster.svg
142
+ ```
143
+ ````
92
144
 
93
- Image rows support:
145
+ Card-list options:
94
146
 
95
- - `image`: required filename matching `^[a-z0-9][a-z0-9.-]*\.(jpe?g|png)$`.
96
- It must be a filename, not a path.
97
- - `alt`: required string.
98
- - `caption`: optional string.
147
+ - `layout`: `image-top`, `image-left`, or `image-right`.
148
+ - `flow`: `grid` or `stack`.
149
+ - `size`: `s`, `m`, `l`, or `xl`.
150
+ - `width`: `text`, `narrow`, `normal`, or `wide`.
99
151
 
100
- Source image filenames must be unique across the selected page's image tree.
101
- The homepage reads images from `site/images/<section-id>/`. Route pages read
102
- images from `site/routes/<NNN-route-id>/images/<section-id>/`.
152
+ Each card starts with `- title: Card title`. Card fields use two spaces of
153
+ indentation. Supported fields are `text`, `image`, `link`, and `badge-text`.
154
+ Each card must include at least one of `text`, `image`, or `link`.
103
155
 
104
- ## Carousels
156
+ Each image entry supports:
105
157
 
106
- An image row can contain a carousel instead of a single image:
158
+ - `image`: required filename matching
159
+ `^[a-z0-9][a-z0-9.-]*\.(jpe?g|png|svg)$`.
160
+ It must be a filename, not a path.
161
+ - `alt`: optional alt text. If omitted, Norna renders an empty alt attribute.
162
+ - `caption`: optional caption.
107
163
 
108
- ```yaml
109
- gallery:
110
- - carousel:
111
- - image: first.jpg
112
- alt: "First image."
113
- caption: "First caption."
114
- - image: second.jpg
115
- alt: "Second image."
116
- caption: "Second caption."
164
+ Start every image entry in image stacks and carousels with
165
+ `- image: filename.jpg`. Optional fields use two spaces of indentation:
166
+
167
+ ````md
168
+ ```norna-image-stack
169
+ - image: filename.jpg
170
+ alt: Optional alt text.
171
+ caption: Optional caption.
117
172
  ```
173
+ ````
118
174
 
119
- Each carousel item has the same `image`, `alt`, and `caption` fields as a
120
- single image row.
175
+ Use three or more matching backticks or tildes for fenced blocks. If you need
176
+ to document a Norna image block inside another Markdown code sample, make the
177
+ outer fence longer than the inner fence:
178
+
179
+ ````md
180
+ ````
181
+ ```norna-image-stack
182
+ - image: filename.jpg
183
+ ```
184
+ ````
185
+ ````
121
186
 
122
187
  `content:check` warns when carousel images have different aspect ratios. Exact
123
188
  matching proportions are recommended because mixed proportions can make the
124
189
  layout move while the user changes slides.
125
190
 
191
+ SVG files are allowed in image stacks and carousels. When an SVG has a
192
+ `viewBox` or numeric `width` and `height`, Norna uses that ratio in the same
193
+ layout logic as raster images. SVG files without an intrinsic aspect ratio are
194
+ rendered directly, but carousel usage produces a warning because stable slide
195
+ sizing is less predictable.
196
+
197
+ ## Image Files
198
+
199
+ Homepage images live under:
200
+
201
+ ```text
202
+ site/images/<section-id>/
203
+ ```
204
+
205
+ Route images live under:
206
+
207
+ ```text
208
+ site/routes/<NNN-route-id>/images/<section-id>/
209
+ ```
210
+
211
+ Image references in Norna managed media blocks use only the filename:
212
+
213
+ ````md
214
+ ```norna-image-stack
215
+ - image: portrait.jpg
216
+ ```
217
+ ````
218
+
219
+ If `portrait.jpg` is referenced from `## Team {#team}`, the expected homepage
220
+ location is `site/images/team/portrait.jpg`.
221
+
222
+ Filenames do not have to be globally unique for the site to be valid. Automatic
223
+ sync only moves files when the filename identifies exactly one source candidate
224
+ across the site's page and route image roots. If more than one candidate
225
+ exists, Norna reports the ambiguity instead of guessing.
226
+
227
+ ## Markdown Images
228
+
229
+ Markdown image syntax is allowed for external images and public static assets:
230
+
231
+ ```md
232
+ ![External image](https://example.com/image.jpg)
233
+ ![Public asset](/favicon.svg)
234
+ ```
235
+
236
+ Relative local Markdown images such as `![Portrait](portrait.jpg)` are not
237
+ managed by Norna. Use `norna-image-stack`, `norna-image-carousel`, or
238
+ `norna-card-list` for local site images that should be validated, processed and
239
+ synced.
240
+
126
241
  ## Temporary Sections
127
242
 
128
- Use `sections[].visible` for sections that should be rendered only during a
129
- date window:
243
+ Use `sections.<section-id>.visible` for sections that should be rendered only
244
+ during a date window:
130
245
 
131
246
  ```yaml
132
247
  sections:
133
- - id: exhibition
248
+ exhibition:
134
249
  visible:
135
250
  from: "2026-08-01"
136
251
  until: "2026-09-16"
137
- gallery: []
138
252
  ```
139
253
 
140
254
  `from` is inclusive. `until` is exclusive. With the example above, the section
@@ -156,17 +270,6 @@ NORNA_TODAY=2026-08-15 npm run norna:build
156
270
 
157
271
  ## Markdown Text
158
272
 
159
- Section Markdown starts at the matching level 2 heading and continues until the
160
- next level 2 heading.
161
-
162
- ```md
163
- ## Intro {#intro}
164
-
165
- Paragraph text.
166
-
167
- Another paragraph.
168
- ```
169
-
170
273
  Inline styles use this Markdown form:
171
274
 
172
275
  ```md
@@ -184,16 +287,17 @@ Run:
184
287
  npm run norna:content:check
185
288
  ```
186
289
 
187
- This checks section order and heading ids, duplicate image names, missing image
188
- files, misplaced referenced images, duplicate image references, invalid image
189
- references, unreferenced images, undefined inline styles, and common
190
- frontmatter indentation and structure mistakes.
290
+ This checks section heading ids, section metadata, duplicate image names,
291
+ missing image files, misplaced referenced images, duplicate image references,
292
+ invalid managed media blocks, unreferenced images, undefined inline styles,
293
+ Markdown image references to unmanaged local files, and common frontmatter
294
+ indentation and structure mistakes.
191
295
 
192
296
  Frontmatter uses YAML indentation. Use ordinary spaces, not tabs or
193
297
  non-breaking spaces. `content:check` reports a focused error when indentation is
194
298
  invalid, when a key is indented under a line that already has a value, or when a
195
- known nested key such as `gallery` or section-specific `typography` appears at
196
- the top level:
299
+ known nested key such as section-specific `typography` appears at the top
300
+ level:
197
301
 
198
302
  ```yaml
199
303
  presentation:
@@ -205,14 +309,7 @@ presentation:
205
309
  ```
206
310
 
207
311
  Top-level page frontmatter may contain only `title`, `description`,
208
- `navigation`, `presentation`, `frame`, and `sections`. A `gallery` key belongs
209
- under one `sections[]` item:
210
-
211
- ```yaml
212
- sections:
213
- - id: work
214
- gallery: []
215
- ```
312
+ `navigation`, `presentation`, `frame`, and `sections`.
216
313
 
217
314
  Run:
218
315
 
@@ -220,6 +317,20 @@ Run:
220
317
  npm run norna:sync
221
318
  ```
222
319
 
223
- This rewrites Markdown sections into frontmatter order and moves referenced
224
- image files into the directory matching their section. It prompts before
225
- writing unless `--yes` is passed.
320
+ This moves referenced image files into the section directory shown by the
321
+ Markdown placement. It prompts before writing unless `--yes` is passed.
322
+
323
+ `content:sync` is intentionally conservative. It only moves a file when the
324
+ filename identifies exactly one matching source candidate across the site's
325
+ page and route image roots, and when the move will not break another reference.
326
+ If the intended move is ambiguous, rename or move the file manually and run
327
+ `content:check` again.
328
+
329
+ Duplicate filenames are allowed when files already live where their Markdown
330
+ references expect them. Automatic relocation only requires site-wide filename
331
+ uniqueness for the file being moved.
332
+
333
+ When `content:sync` needs to move an image between the homepage and a route, or
334
+ between two routes, the Git working tree must be clean before the write. This
335
+ keeps cross-route sync easy to roll back. `content:check` only reports issues
336
+ and does not require a clean working tree.
@@ -26,7 +26,7 @@ site repository. It should not be a showcase for every feature.
26
26
  An example site is a runnable site that demonstrates one or more features.
27
27
 
28
28
  Examples can be richer than the starter. They may show routes, navigation,
29
- typography presets, typography rhythms, galleries, image carousels, inline
29
+ typography presets, typography rhythms, image stacks, image carousels, inline
30
30
  styles, and site-specific configuration choices.
31
31
 
32
32
  ### Documentation Site
@@ -9,13 +9,13 @@ This document is for work on the reusable `norna` package itself.
9
9
  - `scripts/lib/site-paths.mjs`: engine/site path resolution.
10
10
  - `scripts/lib/project-config.mjs`: `site/config.mjs` validation and defaults.
11
11
  - `scripts/sync-content-sections.mjs`: content validation and sync behavior.
12
- - `scripts/generate-images.mjs`: WebP image pipeline and manifest.
12
+ - `scripts/generate-images.mjs`: managed image pipeline and manifest.
13
13
  - `scripts/sync-site-public.mjs`: static public file sync.
14
14
  - `scripts/deploy-site.mjs`: deploy and deploy:commit behavior.
15
15
  - `scripts/watch-pages-deploy.mjs`: GitHub Pages workflow monitor.
16
16
  - `src/content.config.ts`: Astro content schema.
17
- - `src/components/` and `src/layouts/`: rendered page, navigation, image rows,
18
- and layout.
17
+ - `src/components/` and `src/layouts/`: rendered page, navigation, image
18
+ blocks, and layout.
19
19
  - `tests/`: Playwright navigation diagnostics.
20
20
  - `fixtures/basic/site/`: minimal site used for engine checks.
21
21
  - `starters/basic/`: copyable site starter.