@janga/norna 0.7.16 → 0.7.17

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 (200) hide show
  1. package/README.md +6 -6
  2. package/astro.config.mjs +35 -3
  3. package/docs/README.md +10 -10
  4. package/docs/commands.md +2 -2
  5. package/docs/content.md +82 -102
  6. package/docs/design/navigation-and-theme-plan.md +115 -0
  7. package/docs/design/norna-diagram-design.md +9 -9
  8. package/docs/design/site-examples-structure.md +9 -9
  9. package/docs/engine-development.md +9 -9
  10. package/docs/getting-started.md +20 -15
  11. package/docs/images-and-metadata.md +10 -9
  12. package/docs/pages.md +134 -0
  13. package/docs/public-files.md +49 -36
  14. package/docs/publishing.md +19 -3
  15. package/docs/site-files.md +29 -28
  16. package/docs/sitewide-content.md +20 -36
  17. package/docs/theme.md +20 -20
  18. package/docs/typography.md +10 -10
  19. package/examples/README.md +2 -2
  20. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
  21. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
  27. package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
  28. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
  29. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
  30. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +3 -4
  31. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
  32. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
  33. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
  34. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
  35. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
  36. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
  37. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
  38. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
  39. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
  40. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
  41. package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
  42. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
  43. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
  44. package/examples/complete-sites/dog-shelter-single-page/site/content.md +5 -2
  45. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
  46. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
  47. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
  48. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +185 -88
  49. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  50. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
  51. package/examples/feature-demos/media-and-surfaces/README.md +2 -2
  52. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
  53. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
  54. package/examples/feature-demos/media-and-surfaces/site/content.md +4 -7
  55. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
  56. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +7 -6
  57. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  58. package/examples/feature-demos/sitewide-content/README.md +3 -3
  59. package/examples/feature-demos/sitewide-content/site/content.md +8 -7
  60. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
  61. package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
  62. package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
  63. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
  64. package/examples/feature-demos/theme-presets/README.md +1 -1
  65. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +60 -60
  66. package/examples/feature-demos/theme-presets/site/content.md +4 -7
  67. package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/content.md +4 -7
  68. package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/content.md +4 -7
  69. package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/content.md +4 -7
  70. package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/content.md +4 -7
  71. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +1 -10
  72. package/fixtures/basic/site/content.md +5 -6
  73. package/fixtures/basic/site/sitewide-content.yaml +1 -8
  74. package/fixtures/content-model-v2/site/content.md +11 -13
  75. package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
  76. package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
  77. package/fixtures/nested-pages/site/config.yaml +1 -0
  78. package/fixtures/nested-pages/site/content.md +15 -0
  79. package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
  80. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
  81. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
  82. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +14 -0
  83. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
  84. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +3 -0
  85. package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
  86. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
  87. package/fixtures/nested-pages/site/theme.yaml +3 -0
  88. package/package.json +7 -4
  89. package/schemas/config.schema.json +5 -5
  90. package/schemas/content-frontmatter.schema.json +28 -63
  91. package/schemas/manifest.json +1 -0
  92. package/schemas/sitewide-content.schema.json +58 -132
  93. package/schemas/theme.schema.json +396 -86
  94. package/scripts/check-config.mjs +6 -5
  95. package/scripts/deploy-site.mjs +6 -8
  96. package/scripts/doctor.mjs +2 -2
  97. package/scripts/generate-images.mjs +11 -18
  98. package/scripts/generate-schemas.mjs +3 -217
  99. package/scripts/lib/base-path-redirect.mjs +9 -0
  100. package/scripts/lib/editor-language-service.mjs +126 -66
  101. package/scripts/lib/heading-ids.mjs +161 -0
  102. package/scripts/lib/navigation-model.mjs +51 -0
  103. package/scripts/lib/norna-markdown-blocks.mjs +3 -4
  104. package/scripts/lib/page-model.mjs +83 -0
  105. package/scripts/lib/project-config.mjs +28 -0
  106. package/scripts/lib/schema-definitions.mjs +41 -46
  107. package/scripts/lib/schema-editor-metadata.mjs +475 -0
  108. package/scripts/lib/schema-value-definitions.mjs +6 -6
  109. package/scripts/lib/site-content.mjs +124 -87
  110. package/scripts/lib/site-paths.mjs +2 -2
  111. package/scripts/lib/theme-config.mjs +9 -16
  112. package/scripts/lib/theme-presets.mjs +9 -1
  113. package/scripts/release.mjs +1 -1
  114. package/scripts/show-typography.mjs +26 -20
  115. package/scripts/sync-content-sections.mjs +106 -65
  116. package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +7 -24
  117. package/scripts/test-client-javascript.mjs +39 -42
  118. package/scripts/test-content-check.mjs +138 -54
  119. package/scripts/test-content-model-v2.mjs +445 -228
  120. package/scripts/test-documentation.mjs +6 -1
  121. package/scripts/test-editor-language-service.mjs +168 -24
  122. package/scripts/test-engine-commands.mjs +2 -1
  123. package/scripts/test-heading-ids.mjs +68 -0
  124. package/scripts/test-navigation-model.mjs +63 -0
  125. package/scripts/test-nested-pages.mjs +110 -0
  126. package/scripts/test-package-check.mjs +31 -40
  127. package/scripts/test-page-model.mjs +80 -0
  128. package/scripts/test-project-config.mjs +25 -1
  129. package/scripts/test-schemas.mjs +66 -14
  130. package/scripts/test-theme-presets.mjs +33 -24
  131. package/src/components/NavigationPageTree.astro +68 -0
  132. package/src/components/SectionNavigationScript.astro +110 -5
  133. package/src/components/SiteBreadcrumbs.astro +29 -0
  134. package/src/components/SiteNavigation.astro +65 -69
  135. package/src/components/SitePage.astro +70 -46
  136. package/src/components/SiteSection.astro +27 -20
  137. package/src/content.config.ts +9 -5
  138. package/src/layouts/BaseLayout.astro +11 -20
  139. package/src/lib/pageThemes.ts +42 -0
  140. package/src/lib/sectionContent.ts +109 -106
  141. package/src/lib/siteNavigation.ts +48 -0
  142. package/src/lib/sitePages.ts +85 -38
  143. package/src/pages/{[slug].astro → [...slug].astro} +2 -2
  144. package/src/styles/global.css +373 -55
  145. package/starters/basic/README.md +5 -7
  146. package/starters/basic/site/content.md +7 -4
  147. package/starters/basic/site/sitewide-content.yaml +1 -10
  148. package/starters/project/README.md +8 -8
  149. package/starters/project/site/content.md +5 -4
  150. package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
  151. package/starters/project/site/sitewide-content.yaml +1 -10
  152. package/docs/routes.md +0 -121
  153. package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
  154. package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
  155. package/scripts/lib/route-model.mjs +0 -17
  156. package/scripts/test-route-model.mjs +0 -32
  157. package/src/lib/routeThemes.ts +0 -41
  158. /package/examples/complete-sites/dog-shelter-multi-page/site/images/{intro/shelter-dog.jpg → shelter-dog.jpg} +0 -0
  159. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
  160. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
  161. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
  162. /package/examples/complete-sites/dog-shelter-single-page/site/images/{you-can-help/foster-dog.jpg → foster-dog.jpg} +0 -0
  163. /package/examples/complete-sites/dog-shelter-single-page/site/images/{what-we-do/rescue-dog.jpg → rescue-dog.jpg} +0 -0
  164. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
  165. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
  166. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
  167. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
  168. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
  169. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
  170. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
  171. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
  172. /package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/theme.yaml +0 -0
  173. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/puppy-portrait.jpg +0 -0
  174. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/shepherd-portrait.jpg +0 -0
  175. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/white-puppy.jpg +0 -0
  176. /package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/theme.yaml +0 -0
  177. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/black-schnauzer.jpg +0 -0
  178. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/retriever-portrait.jpg +0 -0
  179. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/schnauzer-portrait.jpg +0 -0
  180. /package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/theme.yaml +0 -0
  181. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/boxer-portrait.jpg +0 -0
  182. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grass-puppy.jpg +0 -0
  183. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grey-street-dog.jpg +0 -0
  184. /package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/theme.yaml +0 -0
  185. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/black-pedigree.jpg +0 -0
  186. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-grass.jpg +0 -0
  187. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-wide.jpg +0 -0
  188. /package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/theme.yaml +0 -0
  189. /package/fixtures/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
  190. /package/fixtures/content-model-v2/site/images/{card-section/adopt.svg → adopt.svg} +0 -0
  191. /package/fixtures/content-model-v2/site/images/{intro/detail.jpg → detail.jpg} +0 -0
  192. /package/fixtures/content-model-v2/site/images/{intro/duplicate.jpg → duplicate.jpg} +0 -0
  193. /package/fixtures/content-model-v2/site/images/{card-section/foster.svg → foster.svg} +0 -0
  194. /package/fixtures/content-model-v2/site/images/{intro/hero.jpg → hero.jpg} +0 -0
  195. /package/fixtures/content-model-v2/site/images/{plain/image.jpg → image.jpg} +0 -0
  196. /package/fixtures/content-model-v2/site/images/{timed/slide-one.jpg → slide-one.jpg} +0 -0
  197. /package/fixtures/content-model-v2/site/images/{timed/slide-three.jpg → slide-three.jpg} +0 -0
  198. /package/fixtures/content-model-v2/site/images/{timed/slide-two.jpg → slide-two.jpg} +0 -0
  199. /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
  200. /package/starters/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
@@ -69,12 +69,12 @@ const withTempProject = async ({ site, theme = defaultTheme, files, siteDirector
69
69
  };
70
70
 
71
71
  const brokenSite = `---
72
- sections:
73
- karin-walde: {}
74
- min-konst: {}
75
- mitt-hem: {}
76
-
72
+ page:
73
+ description: Exercises grouped content diagnostics.
77
74
  ---
75
+
76
+ # Broken content fixture
77
+
78
78
  ## Karin Walde {#karin-walde}
79
79
  Text.
80
80
 
@@ -112,10 +112,10 @@ test('content:check groups section issues, global issues, and unreferenced image
112
112
  await withTempProject({
113
113
  site: brokenSite,
114
114
  files: [
115
- 'site/images/karin-walde/karin.jpg',
115
+ 'site/images/karin.jpg',
116
116
  'site/images/karin-walde/unreferenced.jpg',
117
117
  'site/images/min-konst/duplicate.jpg',
118
- 'site/images/mitt-hem/home.jpg',
118
+ 'site/images/home.jpg',
119
119
  'site/images/mitt-hem/vav.jpeg',
120
120
  ],
121
121
  }, async (root) => {
@@ -126,15 +126,14 @@ test('content:check groups section issues, global issues, and unreferenced image
126
126
  assert.match(output, /^Content check failed\./m);
127
127
  assert.match(output, /Content Issues\n\n\[site\/content\.md \[min-konst\]\]\n Errors:/);
128
128
  assert.match(output, /Image "vav\.jpeg" is used here but is located in site\/images\/mitt-hem\/vav\.jpeg\./);
129
- assert.match(output, /Image "missing\.jpeg" does not exist at site\/images\/min-konst\/missing\.jpeg or anywhere under any page or route image root\./);
130
- assert.match(output, /Unreferenced Images\nThese files are kept under page or route image roots but are not referenced by Norna-managed image references:/);
129
+ assert.match(output, /Image "missing\.jpeg" does not exist at site\/images\/missing\.jpeg or anywhere under any page image root\./);
130
+ assert.match(output, /Unreferenced Images\nThese files are kept under page image roots but are not referenced by Norna-managed image references:/);
131
131
  assert.match(output, /site\/images\/karin-walde\/unreferenced\.jpg/);
132
132
  });
133
133
  });
134
134
 
135
- const carouselAspectRatioSite = `---
136
- title: Carousel Aspect Ratio
137
- ---
135
+ const carouselAspectRatioSite = `# Carousel Aspect Ratio
136
+
138
137
  ## Puppies {#puppies}
139
138
  Text.
140
139
 
@@ -151,11 +150,11 @@ test('content:check warns when carousel images use different aspect ratios', asy
151
150
  site: carouselAspectRatioSite,
152
151
  files: [
153
152
  {
154
- path: 'site/images/puppies/wide.png',
153
+ path: 'site/images/wide.png',
155
154
  contents: makePngHeader({ width: 400, height: 300 }),
156
155
  },
157
156
  {
158
- path: 'site/images/puppies/wider.png',
157
+ path: 'site/images/wider.png',
159
158
  contents: makePngHeader({ width: 600, height: 300 }),
160
159
  },
161
160
  ],
@@ -165,14 +164,13 @@ test('content:check warns when carousel images use different aspect ratios', asy
165
164
 
166
165
  assert.equal(result.status, 0, output);
167
166
  assert.match(output, /^Content check completed with warnings\./m);
168
- assert.match(output, /Carousel on line 7 uses images with different aspect ratios: wide\.png \(4:3\), wider\.png \(2:1\)\./);
167
+ assert.match(output, /Carousel on line 6 uses images with different aspect ratios: wide\.png \(4:3\), wider\.png \(2:1\)\./);
169
168
  assert.match(output, /Use images with exactly matching proportions in the same carousel/);
170
169
  });
171
170
  });
172
171
 
173
- const inlineStyleSite = `---
174
- title: Inline Style
175
- ---
172
+ const inlineStyleSite = `# Inline Style
173
+
176
174
  ## Intro {#intro}
177
175
  This has [known text]{.highlight} and [unknown text]{.missing}.
178
176
  `;
@@ -194,9 +192,8 @@ test('content:check fails when Markdown uses removed inline color styles', async
194
192
 
195
193
  test('content:check suggests semantic Markdown instead of inline color styles', async () => {
196
194
  await withTempProject({
197
- site: `---
198
- title: Invalid Inline Color
199
- ---
195
+ site: `# Invalid Inline Color
196
+
200
197
  ## Intro {#intro}
201
198
  This has [yellow text]{.yellow}.
202
199
  `,
@@ -212,9 +209,12 @@ This has [yellow text]{.yellow}.
212
209
  });
213
210
 
214
211
  const inlineNoteSite = `---
215
- title: Inline notes
216
- description: Fixture
212
+ page:
213
+ description: Fixture
217
214
  ---
215
+
216
+ # Inline notes
217
+
218
218
  ## Intro {#intro}
219
219
  This paragraph has a note reference.{note-ref}
220
220
 
@@ -239,9 +239,12 @@ test('content:check accepts valid inline notes and ignores note syntax in code f
239
239
  test('content:check accepts wrapped and explicit multiline inline notes', async () => {
240
240
  await withTempProject({
241
241
  site: `---
242
- title: Multiline inline notes
243
- description: Fixture
242
+ page:
243
+ description: Fixture
244
244
  ---
245
+
246
+ # Multiline inline notes
247
+
245
248
  ## Wrapped {#wrapped}
246
249
  This paragraph has a formatter-wrapped note.{note-ref}
247
250
 
@@ -269,9 +272,12 @@ This paragraph has an explicit multiline note.{note-ref}
269
272
  test('content:check reports an unclosed multiline inline note', async () => {
270
273
  await withTempProject({
271
274
  site: `---
272
- title: Unclosed inline note
273
- description: Fixture
275
+ page:
276
+ description: Fixture
274
277
  ---
278
+
279
+ # Unclosed inline note
280
+
275
281
  ## Intro {#intro}
276
282
  This paragraph has a note reference.{note-ref}
277
283
 
@@ -292,9 +298,12 @@ and continues to the end of the file.
292
298
  test('content:check rejects nested note syntax before a multiline note is closed', async () => {
293
299
  await withTempProject({
294
300
  site: `---
295
- title: Nested inline note syntax
296
- description: Fixture
301
+ page:
302
+ description: Fixture
297
303
  ---
304
+
305
+ # Nested inline note syntax
306
+
298
307
  ## Nested note {#nested-note}
299
308
  This paragraph has a note reference.{note-ref}
300
309
 
@@ -321,9 +330,12 @@ and contains {note-ref} before closing.}
321
330
  test('content:check allows escaped and inline-code note syntax inside notes', async () => {
322
331
  await withTempProject({
323
332
  site: `---
324
- title: Literal inline note syntax
325
- description: Fixture
333
+ page:
334
+ description: Fixture
326
335
  ---
336
+
337
+ # Literal inline note syntax
338
+
327
339
  ## Intro {#intro}
328
340
  This paragraph has a note reference.{note-ref}
329
341
 
@@ -342,9 +354,12 @@ This paragraph has a note reference.{note-ref}
342
354
  test('content:check rejects unpaired and repeated inline notes', async () => {
343
355
  await withTempProject({
344
356
  site: `---
345
- title: Invalid inline notes
346
- description: Fixture
357
+ page:
358
+ description: Fixture
347
359
  ---
360
+
361
+ # Invalid inline notes
362
+
348
363
  ## Orphan {#orphan}
349
364
  {note: This note has no reference.}
350
365
 
@@ -372,9 +387,6 @@ typography:
372
387
  \u00a0\u00a0overrides:
373
388
  body:
374
389
  paragraphSpacing: 0.3em
375
- sections:
376
- intro: {}
377
-
378
390
  ---
379
391
  ## Intro {#intro}
380
392
  Text.
@@ -401,9 +413,6 @@ typography:
401
413
  overrides:
402
414
  body:
403
415
  paragraphSpacing: 0.3em
404
- sections:
405
- intro: {}
406
-
407
416
  ---
408
417
  ## Intro {#intro}
409
418
  Text.
@@ -425,14 +434,14 @@ test('content:check explains frontmatter indentation below scalar values', async
425
434
  });
426
435
 
427
436
  const topLevelImagesSite = `---
428
- title: Example
429
- description: Example site.
430
- sections:
431
- intro: {}
437
+ page:
438
+ description: Example site.
432
439
  images:
433
440
  - image: intro.jpg
434
-
435
441
  ---
442
+
443
+ # Example
444
+
436
445
  ## Intro {#intro}
437
446
  Text.
438
447
  `;
@@ -447,7 +456,7 @@ test('content:check explains likely misindented frontmatter keys', async () => {
447
456
 
448
457
  assert.equal(result.status, 1, output);
449
458
  assert.match(output, /^Content check failed\./m);
450
- assert.match(output, /Frontmatter line 6 defines "images" at the top level, but it is not a valid top-level content field\./);
459
+ assert.match(output, /Frontmatter line 4 defines "images" at the top level, but it is not a valid top-level content field\./);
451
460
  assert.match(output, /Put local image references in norna-image-stack or norna-image-carousel blocks in the Markdown body\./);
452
461
  });
453
462
  });
@@ -457,8 +466,8 @@ test('content:check respects NORNA_SITE_DIR', async () => {
457
466
  site: movableSite,
458
467
  siteDirectory: 'custom-site',
459
468
  files: [
460
- 'custom-site/images/min-konst/move-me.jpg',
461
- 'custom-site/images/mitt-hem/home.jpg',
469
+ 'custom-site/images/move-me.jpg',
470
+ 'custom-site/images/home.jpg',
462
471
  ],
463
472
  }, async (root) => {
464
473
  const result = runContentScript(root, ['--check'], { NORNA_SITE_DIR: 'custom-site' });
@@ -470,11 +479,12 @@ test('content:check respects NORNA_SITE_DIR', async () => {
470
479
  });
471
480
 
472
481
  const movableSite = `---
473
- sections:
474
- min-konst: {}
475
- mitt-hem: {}
476
-
482
+ page:
483
+ description: Exercises content sync.
477
484
  ---
485
+
486
+ # Movable image fixture
487
+
478
488
  ## Min konst {#min-konst}
479
489
  Text.
480
490
 
@@ -496,7 +506,7 @@ test('content:sync moves referenced images and keeps unreferenced images in plac
496
506
  await withTempProject({
497
507
  site: movableSite,
498
508
  files: [
499
- 'site/images/mitt-hem/home.jpg',
509
+ 'site/images/home.jpg',
500
510
  'site/images/mitt-hem/move-me.jpg',
501
511
  'site/images/mitt-hem/unreferenced.jpg',
502
512
  ],
@@ -505,8 +515,8 @@ test('content:sync moves referenced images and keeps unreferenced images in plac
505
515
  const syncOutput = getOutput(syncResult);
506
516
 
507
517
  assert.equal(syncResult.status, 0, syncOutput);
508
- assert.match(syncOutput, /Moved image "move-me\.jpg" to site\/images\/min-konst\/\./);
509
- assert.equal(await fileExists(path.join(root, 'site/images/min-konst/move-me.jpg')), true);
518
+ assert.match(syncOutput, /Moved image "move-me\.jpg" to site\/images\/\./);
519
+ assert.equal(await fileExists(path.join(root, 'site/images/move-me.jpg')), true);
510
520
  assert.equal(await fileExists(path.join(root, 'site/images/mitt-hem/move-me.jpg')), false);
511
521
  assert.equal(await fileExists(path.join(root, 'site/images/mitt-hem/unreferenced.jpg')), true);
512
522
 
@@ -519,6 +529,80 @@ test('content:sync moves referenced images and keeps unreferenced images in plac
519
529
  });
520
530
  });
521
531
 
532
+ test('content:check accepts a frontmatter-free page with one H1 title', async () => {
533
+ await withTempProject({
534
+ site: `# About
535
+
536
+ Introductory text.
537
+
538
+ \`\`\`md
539
+ # Example heading in code
540
+ \`\`\`
541
+
542
+ ## Details {#details}
543
+
544
+ More text.
545
+ `,
546
+ files: [],
547
+ }, async (root) => {
548
+ const result = runContentScript(root, ['--check']);
549
+ assert.equal(result.status, 0, getOutput(result));
550
+ });
551
+ });
552
+
553
+ test('content:check requires exactly one H1 before level 2 sections', async () => {
554
+ await withTempProject({
555
+ site: `## Details {#details}
556
+
557
+ Text.
558
+ `,
559
+ files: [],
560
+ }, async (root) => {
561
+ const result = runContentScript(root, ['--check']);
562
+ const output = getOutput(result);
563
+ assert.equal(result.status, 1, output);
564
+ assert.match(output, /The page is missing its Markdown H1 title\./);
565
+ assert.match(output, /a Norna page must start with its H1 title\./);
566
+ });
567
+ });
568
+
569
+ test('content:check rejects repeated H1 page titles', async () => {
570
+ await withTempProject({
571
+ site: `# First title
572
+
573
+ Text.
574
+
575
+ # Second title
576
+
577
+ More text.
578
+ `,
579
+ files: [],
580
+ }, async (root) => {
581
+ const result = runContentScript(root, ['--check']);
582
+ const output = getOutput(result);
583
+ assert.equal(result.status, 1, output);
584
+ assert.match(output, /The page contains 2 Markdown H1 headings\./);
585
+ });
586
+ });
587
+
588
+ test('content:check rejects section ids and content before the H1 page title', async () => {
589
+ await withTempProject({
590
+ site: `Text before the title.
591
+
592
+ # About {#about}
593
+
594
+ Page text.
595
+ `,
596
+ files: [],
597
+ }, async (root) => {
598
+ const result = runContentScript(root, ['--check']);
599
+ const output = getOutput(result);
600
+ assert.equal(result.status, 1, output);
601
+ assert.match(output, /The page contains content before its first heading\./);
602
+ assert.match(output, /The Markdown H1 is the page title and must not have an id\./);
603
+ });
604
+ });
605
+
522
606
  let failed = 0;
523
607
 
524
608
  for (const { name, run } of tests) {