@janga/norna 0.7.5 → 0.7.7

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 (142) hide show
  1. package/README.md +5 -5
  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 +6 -6
  6. package/docs/configuration.md +3 -2
  7. package/docs/content.md +154 -95
  8. package/docs/design/site-examples-structure.md +1 -1
  9. package/docs/engine-development.md +4 -4
  10. package/docs/getting-started.md +6 -4
  11. package/docs/images-and-metadata.md +112 -20
  12. package/docs/routes.md +52 -33
  13. package/docs/site-structure.md +7 -6
  14. package/docs/theme.md +8 -7
  15. package/docs/typography.md +5 -5
  16. package/examples/dog-gallery/site/content.md +41 -46
  17. package/examples/dog-gallery/site/routes/{dog-care → 010-dog-care}/route-content.md +1 -4
  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 +4 -2
  113. package/scripts/deploy-site.mjs +12 -5
  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/route-model.mjs +17 -0
  119. package/scripts/lib/site-content.mjs +45 -37
  120. package/scripts/show-typography.mjs +18 -10
  121. package/scripts/sync-content-sections.mjs +280 -423
  122. package/scripts/test-content-check.mjs +59 -45
  123. package/scripts/test-content-model-v2.mjs +998 -0
  124. package/scripts/test-engine-commands.mjs +1 -1
  125. package/scripts/test-package-check.mjs +38 -11
  126. package/scripts/test-route-model.mjs +32 -0
  127. package/scripts/test-temporary-visibility.mjs +3 -6
  128. package/src/components/CardList.astro +77 -0
  129. package/src/components/GalleryGrid.astro +22 -12
  130. package/src/components/SiteNavigation.astro +5 -4
  131. package/src/components/SitePage.astro +12 -6
  132. package/src/components/SiteSection.astro +35 -13
  133. package/src/content.config.ts +8 -23
  134. package/src/lib/generatedImages.ts +50 -10
  135. package/src/lib/sectionContent.ts +130 -35
  136. package/src/lib/sitePages.ts +36 -15
  137. package/src/pages/[slug].astro +3 -3
  138. package/src/styles/global.css +187 -0
  139. package/starters/basic/site/content.md +10 -6
  140. package/starters/project/README.md +2 -2
  141. package/starters/project/site/content.md +0 -10
  142. package/starters/project/site/routes/{guide → 010-guide}/route-content.md +0 -6
package/README.md CHANGED
@@ -25,15 +25,15 @@ 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, image blocks, alt
29
+ text, captions, and optional section metadata in `site/content.md`.
30
30
  5. Optional route pages live under
31
31
  `site/routes/<route-folder>/route-content.md`.
32
32
  6. Source images live under `site/images/<section-id>/` for the homepage, or
33
33
  under `site/routes/<route-folder>/images/<section-id>/` for a route.
34
34
  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.
35
+ 8. `norna` validates the files, prepares managed images, builds static Astro
36
+ pages, and can help publish the committed branch through GitHub Pages.
37
37
 
38
38
  The default site directory is `site/`. Commands can also use another directory
39
39
  with `NORNA_SITE_DIR` or `norna --site-dir <path>`. Without an explicit site
@@ -75,7 +75,7 @@ Actions use the same engine version.
75
75
 
76
76
  - Create a site: [Getting Started](docs/getting-started.md)
77
77
  - Understand required site files: [Site Structure](docs/site-structure.md)
78
- - Edit sections and image rows: [Content](docs/content.md)
78
+ - Edit sections and image blocks: [Content](docs/content.md)
79
79
  - Configure a site: [Configuration](docs/configuration.md)
80
80
  - Publish under a GitHub Pages project path:
81
81
  [`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, image stacks, carousels, 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,10 @@ 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, image references, inline
105
+ styles, and common content mistakes.
106
+ - `content:sync` / `norna:sync`: moves misplaced referenced image files after
107
+ confirmation.
108
108
  - `typography presets`: prints the exact built-in values for typography
109
109
  presets and rhythms.
110
110
  - `typography show`: prints the selected site's resolved typography for the
@@ -112,8 +112,8 @@ should normally mean the repository's complete publishable artifact, while
112
112
  and inherited page or section values are marked with `inherited: true`.
113
113
  - `site:public`: copies `site/public/` into `site/.norna/public/` and
114
114
  removes stale copied static files.
115
- - `images`: generates WebP variants and writes
116
- `site/.norna/generated-images.json`.
115
+ - `images`: generates WebP variants for raster images, copies managed SVG
116
+ images, and writes `site/.norna/generated-images.json`.
117
117
  - `engine:update [version|latest]`: updates the site repository's
118
118
  `@janga/norna` dependency with `npm install --save-exact`, normalizes
119
119
  `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 image block ARIA label prefix, default
145
+ `Images`.
145
146
 
146
147
  Example:
147
148
 
package/docs/content.md CHANGED
@@ -1,11 +1,10 @@
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
8
- `site/routes/<route-folder>/route-content.md`. See [Routes](routes.md) for the
6
+ Route pages use the same page model in
7
+ `site/routes/<NNN-route-id>/route-content.md`. See [Routes](routes.md) for the
9
8
  route-specific rules.
10
9
 
11
10
  Site-wide visual defaults belong in [Theme](theme.md). Typography presets and
@@ -18,14 +17,11 @@ The Astro content schema validates these top-level fields in page files:
18
17
 
19
18
  - `title`: required string. Rendered as the document title.
20
19
  - `description`: required string. Rendered as the meta description.
21
- - `slug`: optional route URL slug. It is ignored on the homepage. If omitted on
22
- a route page, the route folder name is used.
23
20
  - `navigation`: optional page navigation metadata. See [Routes](routes.md).
24
21
  - `presentation`: optional page-level presentation overrides. See
25
22
  [Theme](theme.md) and [Typography](typography.md).
26
23
  - `frame`: optional page-level frame color source. See [Theme](theme.md).
27
- - `sections`: required non-empty array. Defines section order, ids,
28
- presentation overrides, and image rows.
24
+ - `sections`: optional section metadata keyed by section id.
29
25
 
30
26
  Minimal homepage:
31
27
 
@@ -33,8 +29,6 @@ Minimal homepage:
33
29
  ---
34
30
  title: My Site
35
31
  description: A small Norna site.
36
- sections:
37
- - id: intro
38
32
  ---
39
33
 
40
34
  ## Intro {#intro}
@@ -44,99 +38,181 @@ Text...
44
38
 
45
39
  ## Sections
46
40
 
47
- Each `sections[]` item has:
41
+ Markdown level 2 headings define the page sections and their order:
48
42
 
49
- - `id`: required string matching `^[a-z0-9-]+$`. Used for anchors, navigation,
50
- image directories, and Markdown heading ids.
51
- - `visible`: optional date window that controls whether the section is rendered.
52
- - `presentation`: optional section-level visual overrides.
53
- - `gallery`: optional array, defaulting to `[]`.
43
+ ```md
44
+ ## About {#about}
54
45
 
55
- Example:
46
+ ...
56
47
 
57
- ```yaml
58
- sections:
59
- - id: work
60
- gallery:
61
- - image: work.jpg
62
- alt: "A woven artwork on a white wall."
63
- caption: "Work in progress."
48
+ ## Work {#work}
49
+
50
+ ...
51
+
52
+ ## Contact {#contact}
53
+
54
+ ...
64
55
  ```
65
56
 
66
- Every frontmatter section must have a matching level 2 Markdown heading with an
67
- explicit id:
57
+ Every section heading must have an explicit id:
68
58
 
69
59
  ```md
70
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.
71
70
 
72
- Introductory text.
71
+ ## Section Metadata
72
+
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
73
82
  ```
74
83
 
75
- Keep these values aligned:
84
+ Each `sections.<section-id>` key must match a Markdown heading id in the same
85
+ page file:
76
86
 
77
- - the frontmatter `sections[].id`
78
- - the Markdown heading id
79
- - the source image directory `site/images/<section-id>/`
87
+ ```md
88
+ ## Work {#work}
89
+ ```
80
90
 
81
- The visible section navigation label comes from the Markdown heading text, not
82
- from the frontmatter id.
91
+ Do not list sections in frontmatter just to define order. Section order comes
92
+ from the Markdown heading order.
83
93
 
84
- ## Image Rows
94
+ ## Image Blocks
85
95
 
86
- Each image row can contain a single image:
96
+ Norna-managed local images are written in Markdown fenced blocks at the point
97
+ where they should appear in the section.
87
98
 
88
- ```yaml
89
- gallery:
90
- - image: work.jpg
91
- alt: "A woven artwork on a white wall."
92
- caption: "Work in progress."
99
+ Use `norna-image-stack` for one or more stacked images:
100
+
101
+ ````md
102
+ ```norna-image-stack
103
+ - image: work.jpg
104
+ alt: A woven artwork on a white wall.
105
+ caption: Work in progress.
106
+ ```
107
+ ````
108
+
109
+ Use `norna-image-carousel` for a carousel:
110
+
111
+ ````md
112
+ ```norna-image-carousel
113
+ - image: first.jpg
114
+ alt: First image.
115
+ caption: First caption.
116
+ - image: second.jpg
117
+ alt: Second image.
118
+ caption: Second caption.
93
119
  ```
120
+ ````
94
121
 
95
- Image rows support:
122
+ Each image entry supports:
96
123
 
97
- - `image`: required filename matching `^[a-z0-9][a-z0-9.-]*\.(jpe?g|png)$`.
124
+ - `image`: required filename matching
125
+ `^[a-z0-9][a-z0-9.-]*\.(jpe?g|png|svg)$`.
98
126
  It must be a filename, not a path.
99
- - `alt`: required string.
100
- - `caption`: optional string.
127
+ - `alt`: optional alt text. If omitted, Norna renders an empty alt attribute.
128
+ - `caption`: optional caption.
101
129
 
102
- Source image filenames must be unique across the selected page's image tree.
103
- The homepage reads images from `site/images/<section-id>/`. Route pages read
104
- images from `site/routes/<route-folder>/images/<section-id>/`.
130
+ Start every image entry with `- image: filename.jpg`. Optional fields use two
131
+ spaces of indentation:
105
132
 
106
- ## Carousels
133
+ ````md
134
+ ```norna-image-stack
135
+ - image: filename.jpg
136
+ alt: Optional alt text.
137
+ caption: Optional caption.
138
+ ```
139
+ ````
107
140
 
108
- An image row can contain a carousel instead of a single image:
141
+ Use three or more matching backticks or tildes for fenced blocks. If you need
142
+ to document a Norna image block inside another Markdown code sample, make the
143
+ outer fence longer than the inner fence:
109
144
 
110
- ```yaml
111
- gallery:
112
- - carousel:
113
- - image: first.jpg
114
- alt: "First image."
115
- caption: "First caption."
116
- - image: second.jpg
117
- alt: "Second image."
118
- caption: "Second caption."
145
+ ````md
146
+ ````
147
+ ```norna-image-stack
148
+ - image: filename.jpg
119
149
  ```
120
-
121
- Each carousel item has the same `image`, `alt`, and `caption` fields as a
122
- single image row.
150
+ ````
151
+ ````
123
152
 
124
153
  `content:check` warns when carousel images have different aspect ratios. Exact
125
154
  matching proportions are recommended because mixed proportions can make the
126
155
  layout move while the user changes slides.
127
156
 
157
+ SVG files are allowed in image stacks and carousels. When an SVG has a
158
+ `viewBox` or numeric `width` and `height`, Norna uses that ratio in the same
159
+ layout logic as raster images. SVG files without an intrinsic aspect ratio are
160
+ rendered directly, but carousel usage produces a warning because stable slide
161
+ sizing is less predictable.
162
+
163
+ ## Image Files
164
+
165
+ Homepage images live under:
166
+
167
+ ```text
168
+ site/images/<section-id>/
169
+ ```
170
+
171
+ Route images live under:
172
+
173
+ ```text
174
+ site/routes/<NNN-route-id>/images/<section-id>/
175
+ ```
176
+
177
+ Image references in Norna image blocks use only the filename:
178
+
179
+ ````md
180
+ ```norna-image-stack
181
+ - image: portrait.jpg
182
+ ```
183
+ ````
184
+
185
+ If `portrait.jpg` is referenced from `## Team {#team}`, the expected homepage
186
+ location is `site/images/team/portrait.jpg`.
187
+
188
+ Filenames do not have to be globally unique for the site to be valid. Automatic
189
+ sync only moves files when the filename identifies exactly one source candidate
190
+ within the current page or route.
191
+
192
+ ## Markdown Images
193
+
194
+ Markdown image syntax is allowed for external images and public static assets:
195
+
196
+ ```md
197
+ ![External image](https://example.com/image.jpg)
198
+ ![Public asset](/favicon.svg)
199
+ ```
200
+
201
+ Relative local Markdown images such as `![Portrait](portrait.jpg)` are not
202
+ managed by Norna. Use `norna-image-stack` or `norna-image-carousel` for local
203
+ site images that should be validated, processed and synced.
204
+
128
205
  ## Temporary Sections
129
206
 
130
- Use `sections[].visible` for sections that should be rendered only during a
131
- date window:
207
+ Use `sections.<section-id>.visible` for sections that should be rendered only
208
+ during a date window:
132
209
 
133
210
  ```yaml
134
211
  sections:
135
- - id: exhibition
212
+ exhibition:
136
213
  visible:
137
214
  from: "2026-08-01"
138
215
  until: "2026-09-16"
139
- gallery: []
140
216
  ```
141
217
 
142
218
  `from` is inclusive. `until` is exclusive. With the example above, the section
@@ -158,17 +234,6 @@ NORNA_TODAY=2026-08-15 npm run norna:build
158
234
 
159
235
  ## Markdown Text
160
236
 
161
- Section Markdown starts at the matching level 2 heading and continues until the
162
- next level 2 heading.
163
-
164
- ```md
165
- ## Intro {#intro}
166
-
167
- Paragraph text.
168
-
169
- Another paragraph.
170
- ```
171
-
172
237
  Inline styles use this Markdown form:
173
238
 
174
239
  ```md
@@ -186,16 +251,17 @@ Run:
186
251
  npm run norna:content:check
187
252
  ```
188
253
 
189
- This checks section order and heading ids, duplicate image names, missing image
190
- files, misplaced referenced images, duplicate image references, invalid image
191
- references, unreferenced images, undefined inline styles, and common
192
- frontmatter indentation and structure mistakes.
254
+ This checks section heading ids, section metadata, duplicate image names,
255
+ missing image files, misplaced referenced images, duplicate image references,
256
+ invalid image blocks, unreferenced images, undefined inline styles, Markdown
257
+ image references to unmanaged local files, and common frontmatter indentation
258
+ and structure mistakes.
193
259
 
194
260
  Frontmatter uses YAML indentation. Use ordinary spaces, not tabs or
195
261
  non-breaking spaces. `content:check` reports a focused error when indentation is
196
262
  invalid, when a key is indented under a line that already has a value, or when a
197
- known nested key such as `gallery` or section-specific `typography` appears at
198
- the top level:
263
+ known nested key such as section-specific `typography` appears at the top
264
+ level:
199
265
 
200
266
  ```yaml
201
267
  presentation:
@@ -206,15 +272,8 @@ presentation:
206
272
  paragraphSpacing: 0.8em
207
273
  ```
208
274
 
209
- Top-level page frontmatter may contain only `title`, `description`, `slug`,
210
- `navigation`, `presentation`, `frame`, and `sections`. A `gallery` key belongs
211
- under one `sections[]` item:
212
-
213
- ```yaml
214
- sections:
215
- - id: work
216
- gallery: []
217
- ```
275
+ Top-level page frontmatter may contain only `title`, `description`,
276
+ `navigation`, `presentation`, `frame`, and `sections`.
218
277
 
219
278
  Run:
220
279
 
@@ -222,6 +281,6 @@ Run:
222
281
  npm run norna:sync
223
282
  ```
224
283
 
225
- This rewrites Markdown sections into frontmatter order and moves referenced
226
- image files into the directory matching their section. It prompts before
227
- writing unless `--yes` is passed.
284
+ This moves referenced image files into the section directory shown by the
285
+ Markdown placement. It prompts before writing unless `--yes` is passed. Sync is
286
+ currently page- or route-local: it does not move images across routes.
@@ -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.
@@ -160,7 +160,7 @@ and commit their updated `package-lock.json`.
160
160
  ## Rendering Notes
161
161
 
162
162
  The renderer builds the homepage at `/` and optional first-level routes from
163
- `site/routes/<route-folder>/route-content.md`.
163
+ `site/routes/<NNN-route-id>/route-content.md`.
164
164
 
165
165
  Navigation has two separate levels:
166
166
 
@@ -67,11 +67,13 @@ Commit the generated `package-lock.json` after the first install.
67
67
  2. Edit `site/theme.md` for site-wide layout, layout density, typography
68
68
  rhythm, image sizing, font, typography preset, colors, inline styles, and
69
69
  frame colors, or omit it to use engine defaults.
70
- 3. Edit `site/content.md` for homepage title, description, sections, text,
71
- image rows, page/section presentation overrides, alt text, and captions.
70
+ 3. Edit `site/content.md` for homepage title, description, Markdown sections,
71
+ text, Norna image blocks, page/section presentation overrides, alt text, and
72
+ captions.
72
73
  4. Put source images under `site/images/<section-id>/`.
73
74
  5. Add optional route pages under
74
- `site/routes/<route-folder>/route-content.md`.
75
+ `site/routes/<NNN-route-id>/route-content.md`, for example
76
+ `site/routes/010-about/route-content.md`.
75
77
  6. Put static files such as `robots.txt`, `CNAME`, and favicons under
76
78
  `site/public/`.
77
79
  7. Run:
@@ -103,7 +105,7 @@ site: {
103
105
  ```
104
106
 
105
107
  After that, content files may still use root-style internal links such as
106
- `/getting-started/` and `/workflow.svg`. Norna applies `site.basePath` during
108
+ `/getting-started/` and `/favicon.svg`. Norna applies `site.basePath` during
107
109
  rendering so the built links work under the GitHub Pages project path.
108
110
 
109
111
  The starter includes a GitHub Pages workflow in `.github/workflows/deploy.yml`.