@janga/norna 0.7.16 → 0.7.18

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 (342) hide show
  1. package/README.md +18 -9
  2. package/astro.config.mjs +35 -3
  3. package/bin/norna-cli.mjs +1 -1
  4. package/docs/README.md +16 -14
  5. package/docs/commands.md +2 -2
  6. package/docs/configuration.md +25 -2
  7. package/docs/content.md +92 -103
  8. package/docs/design/navigation-and-theme-plan.md +186 -0
  9. package/docs/design/norna-diagram-design.md +9 -9
  10. package/docs/design/site-examples-structure.md +15 -11
  11. package/docs/engine-development.md +19 -22
  12. package/docs/getting-started.md +21 -16
  13. package/docs/images-and-metadata.md +12 -9
  14. package/docs/pages.md +190 -0
  15. package/docs/public-files.md +49 -36
  16. package/docs/publishing.md +19 -3
  17. package/docs/site-files.md +46 -38
  18. package/docs/sitewide-content.md +20 -36
  19. package/docs/theme.md +109 -143
  20. package/docs/typography.md +19 -31
  21. package/examples/README.md +10 -4
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
  27. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
  28. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
  29. package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
  30. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
  31. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
  32. package/examples/complete-sites/dog-shelter-multi-page/site/{content.md → pages/000-home/content.md} +3 -4
  33. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
  34. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
  35. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
  36. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
  37. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
  38. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
  39. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
  40. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
  41. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
  42. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
  43. package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
  44. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
  45. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
  46. package/examples/complete-sites/dog-shelter-single-page/site/{content.md → pages/000-home/content.md} +5 -2
  47. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
  48. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
  49. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
  50. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +180 -91
  51. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  52. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
  53. package/examples/feature-demos/media-and-surfaces/README.md +2 -2
  54. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
  55. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
  56. package/examples/feature-demos/media-and-surfaces/site/{content.md → pages/000-home/content.md} +4 -7
  57. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
  58. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +9 -7
  59. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/theme.yaml +2 -0
  60. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  61. package/examples/feature-demos/sitewide-content/README.md +3 -3
  62. package/examples/feature-demos/sitewide-content/site/pages/000-home/content.md +17 -0
  63. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
  64. package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
  65. package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
  66. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
  67. package/examples/feature-demos/theme-preset-documentation/README.md +16 -0
  68. package/examples/feature-demos/theme-preset-documentation/site/.norna/generated-images.json +74 -0
  69. package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site/pages/000-home}/content.md +4 -7
  70. package/examples/feature-demos/theme-preset-documentation/site/sitewide-content.yaml +3 -0
  71. package/examples/feature-demos/theme-preset-portfolio/README.md +16 -0
  72. package/examples/feature-demos/theme-preset-portfolio/site/.norna/generated-images.json +74 -0
  73. package/examples/feature-demos/theme-preset-portfolio/site/config.yaml +1 -0
  74. package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site/pages/000-home}/content.md +4 -7
  75. package/examples/feature-demos/theme-preset-portfolio/site/public/robots.txt +2 -0
  76. package/examples/feature-demos/theme-preset-portfolio/site/sitewide-content.yaml +3 -0
  77. package/examples/feature-demos/theme-preset-project/README.md +16 -0
  78. package/examples/feature-demos/theme-preset-project/site/.norna/generated-images.json +74 -0
  79. package/examples/feature-demos/theme-preset-project/site/config.yaml +1 -0
  80. package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site/pages/000-home}/content.md +4 -7
  81. package/examples/feature-demos/theme-preset-project/site/public/robots.txt +2 -0
  82. package/examples/feature-demos/theme-preset-project/site/sitewide-content.yaml +3 -0
  83. package/examples/feature-demos/theme-preset-statement/README.md +16 -0
  84. package/examples/feature-demos/theme-preset-statement/site/.norna/generated-images.json +74 -0
  85. package/examples/feature-demos/theme-preset-statement/site/config.yaml +1 -0
  86. package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site/pages/000-home}/content.md +4 -7
  87. package/examples/feature-demos/theme-preset-statement/site/public/robots.txt +2 -0
  88. package/examples/feature-demos/theme-preset-statement/site/sitewide-content.yaml +3 -0
  89. package/fixtures/basic/site/{content.md → pages/000-home/content.md} +5 -6
  90. package/fixtures/basic/site/sitewide-content.yaml +1 -8
  91. package/fixtures/basic/site/theme.yaml +1 -8
  92. package/fixtures/content-model-v2/site/{content.md → pages/000-home/content.md} +11 -13
  93. package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
  94. package/fixtures/nested-pages/.astro/collections/site.schema.json +35 -0
  95. package/fixtures/nested-pages/.astro/collections/sitewide.schema.json +97 -0
  96. package/fixtures/nested-pages/.astro/collections/theme.schema.json +844 -0
  97. package/fixtures/nested-pages/.astro/content-assets.mjs +4 -0
  98. package/fixtures/nested-pages/.astro/content-modules.mjs +1 -0
  99. package/fixtures/nested-pages/.astro/content.d.ts +183 -0
  100. package/fixtures/nested-pages/.astro/data-store.json +1 -0
  101. package/fixtures/nested-pages/.astro/dev-local.json +8 -0
  102. package/fixtures/nested-pages/.astro/dev.log +1128 -0
  103. package/fixtures/nested-pages/.astro/settings.json +5 -0
  104. package/fixtures/nested-pages/.astro/types.d.ts +2 -0
  105. package/fixtures/nested-pages/node_modules/.vite/deps/_metadata.json +74 -0
  106. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  107. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  108. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  109. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  110. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  111. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  112. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  113. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  114. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  115. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  116. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  117. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  118. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  119. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  120. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  121. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  122. package/fixtures/nested-pages/node_modules/.vite/deps/package.json +3 -0
  123. package/fixtures/nested-pages/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  124. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  125. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  126. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  127. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  128. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  129. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  130. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  131. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  132. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  133. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  134. package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
  135. package/fixtures/nested-pages/site/config.yaml +3 -0
  136. package/fixtures/nested-pages/site/pages/000-home/content.md +15 -0
  137. package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
  138. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
  139. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
  140. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +18 -0
  141. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/theme.yaml +2 -0
  142. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
  143. package/fixtures/nested-pages/site/pages/010-guides/pages/030-release-notes/content.md +9 -0
  144. package/fixtures/nested-pages/site/pages/010-guides/pages/040-configuration/content.md +3 -0
  145. package/fixtures/nested-pages/site/pages/010-guides/pages/050-content/content.md +3 -0
  146. package/fixtures/nested-pages/site/pages/010-guides/pages/060-themes/content.md +3 -0
  147. package/fixtures/nested-pages/site/pages/010-guides/pages/070-images/content.md +3 -0
  148. package/fixtures/nested-pages/site/pages/010-guides/pages/080-cards/content.md +3 -0
  149. package/fixtures/nested-pages/site/pages/010-guides/pages/090-notes/content.md +3 -0
  150. package/fixtures/nested-pages/site/pages/010-guides/pages/100-validation/content.md +3 -0
  151. package/fixtures/nested-pages/site/pages/010-guides/pages/110-synchronization/content.md +3 -0
  152. package/fixtures/nested-pages/site/pages/010-guides/pages/120-building/content.md +3 -0
  153. package/fixtures/nested-pages/site/pages/010-guides/pages/130-publishing/content.md +3 -0
  154. package/fixtures/nested-pages/site/pages/010-guides/pages/140-troubleshooting/content.md +3 -0
  155. package/fixtures/nested-pages/site/pages/010-guides/pages/150-migration/content.md +3 -0
  156. package/fixtures/nested-pages/site/pages/010-guides/pages/160-accessibility/content.md +3 -0
  157. package/fixtures/nested-pages/site/pages/010-guides/pages/170-performance/content.md +3 -0
  158. package/fixtures/nested-pages/site/pages/010-guides/pages/180-security/content.md +3 -0
  159. package/fixtures/nested-pages/site/pages/010-guides/pages/190-localization/content.md +3 -0
  160. package/fixtures/nested-pages/site/pages/010-guides/pages/200-upgrading/content.md +3 -0
  161. package/fixtures/nested-pages/site/pages/010-guides/pages/210-faq/content.md +3 -0
  162. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +7 -0
  163. package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
  164. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
  165. package/fixtures/nested-pages/site/theme.yaml +1 -0
  166. package/package.json +8 -4
  167. package/schemas/config.schema.json +46 -5
  168. package/schemas/content-frontmatter.schema.json +28 -63
  169. package/schemas/manifest.json +4 -2
  170. package/schemas/page-theme.schema.json +192 -0
  171. package/schemas/sitewide-content.schema.json +58 -132
  172. package/schemas/theme.schema.json +434 -152
  173. package/scripts/check-config.mjs +8 -6
  174. package/scripts/deploy-site.mjs +6 -8
  175. package/scripts/doctor.mjs +2 -2
  176. package/scripts/generate-images.mjs +15 -29
  177. package/scripts/generate-schemas.mjs +7 -218
  178. package/scripts/lib/base-path-redirect.mjs +9 -0
  179. package/scripts/lib/editor-language-service.mjs +125 -79
  180. package/scripts/lib/example-sites.mjs +15 -3
  181. package/scripts/lib/heading-ids.mjs +161 -0
  182. package/scripts/lib/navigation-model.mjs +51 -0
  183. package/scripts/lib/norna-markdown-blocks.mjs +3 -4
  184. package/scripts/lib/page-model.mjs +102 -0
  185. package/scripts/lib/presentation.mjs +25 -17
  186. package/scripts/lib/project-config.mjs +46 -8
  187. package/scripts/lib/schema-definitions.mjs +78 -55
  188. package/scripts/lib/schema-editor-metadata.mjs +519 -0
  189. package/scripts/lib/schema-value-definitions.mjs +20 -6
  190. package/scripts/lib/site-content.mjs +159 -97
  191. package/scripts/lib/site-conventions.mjs +1 -0
  192. package/scripts/lib/site-paths.mjs +11 -5
  193. package/scripts/lib/theme-config.mjs +12 -18
  194. package/scripts/lib/theme-presets.mjs +55 -14
  195. package/scripts/release.mjs +1 -1
  196. package/scripts/show-typography.mjs +4 -30
  197. package/scripts/sync-content-sections.mjs +107 -68
  198. package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +8 -25
  199. package/scripts/test-client-javascript.mjs +50 -46
  200. package/scripts/test-content-check.mjs +153 -69
  201. package/scripts/test-content-model-v2.mjs +566 -285
  202. package/scripts/test-documentation.mjs +10 -3
  203. package/scripts/test-editor-language-service.mjs +172 -25
  204. package/scripts/test-engine-commands.mjs +6 -5
  205. package/scripts/test-examples.mjs +30 -1
  206. package/scripts/test-heading-ids.mjs +68 -0
  207. package/scripts/test-navigation-model.mjs +63 -0
  208. package/scripts/test-navigation.mjs +11 -2
  209. package/scripts/test-nested-pages.mjs +125 -0
  210. package/scripts/test-package-check.mjs +55 -95
  211. package/scripts/test-page-model.mjs +108 -0
  212. package/scripts/test-project-config.mjs +22 -3
  213. package/scripts/test-schemas.mjs +82 -15
  214. package/scripts/test-theme-presets.mjs +57 -42
  215. package/src/components/NavigationPageTree.astro +157 -0
  216. package/src/components/PageContentsLinks.astro +40 -0
  217. package/src/components/SectionNavigationScript.astro +117 -5
  218. package/src/components/SiteBreadcrumbs.astro +32 -0
  219. package/src/components/SiteNavigation.astro +97 -68
  220. package/src/components/SitePage.astro +93 -55
  221. package/src/components/SiteSection.astro +29 -27
  222. package/src/components/SiteTreeNavigation.astro +35 -0
  223. package/src/components/TreeNavigationScript.astro +83 -0
  224. package/src/content.config.ts +8 -10
  225. package/src/layouts/BaseLayout.astro +21 -20
  226. package/src/lib/pageThemes.ts +44 -0
  227. package/src/lib/sectionContent.ts +106 -107
  228. package/src/lib/siteNavigation.ts +49 -0
  229. package/src/lib/sitePages.ts +96 -38
  230. package/src/pages/{[slug].astro → [...slug].astro} +2 -2
  231. package/src/pages/index.astro +2 -1
  232. package/src/styles/global.css +743 -57
  233. package/starters/basic/README.md +9 -11
  234. package/starters/basic/site/{content.md → pages/000-home/content.md} +7 -4
  235. package/starters/basic/site/sitewide-content.yaml +1 -10
  236. package/starters/basic/site/theme.yaml +1 -105
  237. package/starters/project/README.md +14 -14
  238. package/starters/project/site/{content.md → pages/000-home/content.md} +5 -4
  239. package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
  240. package/starters/project/site/sitewide-content.yaml +1 -10
  241. package/starters/project/site/theme.yaml +1 -31
  242. package/docs/routes.md +0 -121
  243. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +0 -3
  244. package/examples/feature-demos/sitewide-content/site/content.md +0 -16
  245. package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
  246. package/examples/feature-demos/theme-presets/README.md +0 -18
  247. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +0 -290
  248. package/examples/feature-demos/theme-presets/site/content.md +0 -17
  249. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +0 -12
  250. package/examples/feature-demos/theme-presets/site/theme.yaml +0 -1
  251. package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
  252. package/scripts/lib/route-model.mjs +0 -17
  253. package/scripts/test-route-model.mjs +0 -32
  254. package/src/lib/routeThemes.ts +0 -41
  255. /package/examples/complete-sites/dog-shelter-multi-page/site/{images/intro → pages/000-home/images}/shelter-dog.jpg +0 -0
  256. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
  257. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
  258. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
  259. /package/examples/complete-sites/dog-shelter-single-page/site/{images/you-can-help → pages/000-home/images}/foster-dog.jpg +0 -0
  260. /package/examples/complete-sites/dog-shelter-single-page/site/{images/what-we-do → pages/000-home/images}/rescue-dog.jpg +0 -0
  261. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
  262. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
  263. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
  264. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
  265. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
  266. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
  267. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
  268. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
  269. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/config.yaml +0 -0
  270. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/black-schnauzer.jpg +0 -0
  271. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/retriever-portrait.jpg +0 -0
  272. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/schnauzer-portrait.jpg +0 -0
  273. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/public/robots.txt +0 -0
  274. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site}/theme.yaml +0 -0
  275. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/puppy-portrait.jpg +0 -0
  276. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/shepherd-portrait.jpg +0 -0
  277. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/white-puppy.jpg +0 -0
  278. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site}/theme.yaml +0 -0
  279. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/routeTheme.schema.json +0 -0
  280. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/site.schema.json +0 -0
  281. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/sitewide.schema.json +0 -0
  282. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/theme.schema.json +0 -0
  283. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-assets.mjs +0 -0
  284. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-modules.mjs +0 -0
  285. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content.d.ts +0 -0
  286. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/data-store.json +0 -0
  287. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev-local.json +0 -0
  288. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.json +0 -0
  289. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.log +0 -0
  290. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/settings.json +0 -0
  291. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/types.d.ts +0 -0
  292. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/_metadata.json +0 -0
  293. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
  294. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
  295. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
  296. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
  297. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
  298. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
  299. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
  300. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
  301. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
  302. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
  303. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
  304. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
  305. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js +0 -0
  306. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js.map +0 -0
  307. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
  308. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
  309. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
  310. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
  311. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/package.json +0 -0
  312. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
  313. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
  314. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
  315. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
  316. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
  317. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
  318. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
  319. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
  320. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
  321. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
  322. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
  323. /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/boxer-portrait.jpg +0 -0
  324. /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/grass-puppy.jpg +0 -0
  325. /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/grey-street-dog.jpg +0 -0
  326. /package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site}/theme.yaml +0 -0
  327. /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/black-pedigree.jpg +0 -0
  328. /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/sheepdog-grass.jpg +0 -0
  329. /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/sheepdog-wide.jpg +0 -0
  330. /package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site}/theme.yaml +0 -0
  331. /package/fixtures/basic/site/{images/work → pages/000-home/images}/.gitkeep +0 -0
  332. /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/adopt.svg +0 -0
  333. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/detail.jpg +0 -0
  334. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/duplicate.jpg +0 -0
  335. /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/foster.svg +0 -0
  336. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/hero.jpg +0 -0
  337. /package/fixtures/content-model-v2/site/{images/plain → pages/000-home/images}/image.jpg +0 -0
  338. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-one.jpg +0 -0
  339. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-three.jpg +0 -0
  340. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-two.jpg +0 -0
  341. /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
  342. /package/starters/basic/site/{images/work → pages/000-home/images}/.gitkeep +0 -0
@@ -1,5 +1,3 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
1
  import {
4
2
  defaultTypography,
5
3
  resolveTypographyConfig,
@@ -13,18 +11,12 @@ import {
13
11
  readSiteFile,
14
12
  validateContentFrontmatterStructure,
15
13
  validateFrontmatterIndentation,
16
- validateRouteThemeYamlStructure,
17
14
  } from './lib/site-content.mjs';
18
15
  import {
19
- siteContentLabel,
20
- siteContentPath,
21
- siteRoutesDir,
22
16
  siteThemeLabel,
23
- siteThemePath,
24
17
  } from './lib/site-paths.mjs';
25
18
  import { readThemeConfig } from './lib/theme-config.mjs';
26
19
  import { resolveThemeConfig } from './lib/theme-presets.mjs';
27
- import { parseYamlConfig } from './lib/yaml-config.mjs';
28
20
 
29
21
  const mode = process.argv[2] ?? 'show';
30
22
 
@@ -197,24 +189,6 @@ const readThemeTypography = async () => {
197
189
  return resolveAnnotatedTypographyConfig(themeTypographyConfig ?? defaultTypography, siteThemeLabel);
198
190
  };
199
191
 
200
- const readRouteThemeTypography = async (contentFile, themeTypography) => {
201
- if (contentFile.isHome) return themeTypography;
202
-
203
- const routeThemePath = path.join(siteRoutesDir, contentFile.routeDirectory, 'theme.yaml');
204
- const routeThemeFile = await readFile(routeThemePath, 'utf8').catch((error) => {
205
- if (error?.code === 'ENOENT') return null;
206
- throw error;
207
- });
208
- if (!routeThemeFile) return themeTypography;
209
- const routeThemeLabel = `${contentFile.contentLabel.replace(/\/content\.md$/, '')}/theme.yaml`;
210
-
211
- const routeThemeConfig = resolveThemeConfig(parseYamlConfig(routeThemeFile, routeThemeLabel, {
212
- validateStructure: validateRouteThemeYamlStructure,
213
- }), routeThemeLabel);
214
- const typographyConfig = routeThemeConfig.typography;
215
- return resolveAnnotatedTypographyConfig(typographyConfig ?? defaultTypography, routeThemeLabel);
216
- };
217
-
218
192
  const readPageTypography = async (contentFile, siteThemeTypography) => {
219
193
  const { frontmatter, body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
220
194
  const indentationIssues = [];
@@ -227,8 +201,8 @@ const readPageTypography = async (contentFile, siteThemeTypography) => {
227
201
  ].join('\n'));
228
202
  }
229
203
 
230
- const pageTypography = await readRouteThemeTypography(contentFile, siteThemeTypography);
231
- const sections = getBodySections(body).sections
204
+ const pageTypography = siteThemeTypography;
205
+ const sections = (await getBodySections(body)).sections
232
206
  .filter((section) => section.id)
233
207
  .map((section) => {
234
208
  return {
@@ -239,7 +213,7 @@ const readPageTypography = async (contentFile, siteThemeTypography) => {
239
213
 
240
214
  return {
241
215
  source: contentFile.contentLabel,
242
- route: contentFile.isHome ? '/' : `/${contentFile.routeId}/`,
216
+ pathname: contentFile.isHome ? '/' : `/${contentFile.pagePath}/`,
243
217
  pageTypography,
244
218
  sections,
245
219
  };
@@ -259,7 +233,7 @@ if (mode === 'profiles') {
259
233
  typography: formatAnnotatedTypography(themeTypography),
260
234
  },
261
235
  pages: Object.fromEntries(pages.map((page) => [
262
- page.route,
236
+ page.pathname,
263
237
  {
264
238
  source: page.source,
265
239
  typography: formatAnnotatedTypography(page.pageTypography),
@@ -13,7 +13,6 @@ import {
13
13
  import {
14
14
  getBodySections,
15
15
  getContentFiles,
16
- getFrontmatterSections,
17
16
  getImageCandidatesByName,
18
17
  getDeprecatedInlineStyleReferences,
19
18
  readSiteFile,
@@ -23,6 +22,7 @@ import {
23
22
  validateFrontmatterIndentation,
24
23
  validateThemeYamlStructure,
25
24
  } from './lib/site-content.mjs';
25
+ import { getHeadingIdentifierIssues } from './lib/heading-ids.mjs';
26
26
  import { readImageDimensions } from './lib/image-dimensions.mjs';
27
27
  import {
28
28
  siteThemeLabel,
@@ -65,7 +65,7 @@ const promptForWrite = async () => {
65
65
  if (!process.stdin.isTTY) return false;
66
66
 
67
67
  const rl = createInterface({ input, output });
68
- const answer = await rl.question('This will move image files into the page or route image roots where Norna references them. Continue? [y/N] ');
68
+ const answer = await rl.question('This will move image files into the page image roots where Norna references them. Continue? [y/N] ');
69
69
  rl.close();
70
70
 
71
71
  return answer.trim().toLowerCase() === 'y';
@@ -136,7 +136,7 @@ const getReportLines = (title, unreferencedImages) => {
136
136
  lines.push(
137
137
  '',
138
138
  'Unreferenced Images',
139
- 'These files are kept under page or route image roots but are not referenced by Norna-managed image references:',
139
+ 'These files are kept under page image roots but are not referenced by Norna-managed image references:',
140
140
  );
141
141
 
142
142
  for (const imagePath of unreferencedImages) {
@@ -171,20 +171,18 @@ const getAspectRatioLabel = ({ width, height }) => {
171
171
  return `${width / divisor}:${height / divisor}`;
172
172
  };
173
173
 
174
- const getExpectedImagePath = (contentFile, sectionId, imageName) =>
175
- path.join(contentFile.imagesDir, sectionId, imageName);
174
+ const getExpectedImagePath = (contentFile, imageName) =>
175
+ path.join(contentFile.imagesDir, imageName);
176
176
 
177
- const getExpectedImageLabel = (contentFile, sectionId, imageName) =>
178
- `${contentFile.imagesLabel}/${sectionId}/${imageName}`;
177
+ const getExpectedImageLabel = (contentFile, imageName) =>
178
+ `${contentFile.imagesLabel}/${imageName}`;
179
179
 
180
180
  const getGlobalImageCandidates = (globalImageCandidatesByName, imageName, expectedPath) =>
181
181
  (globalImageCandidatesByName.get(imageName) ?? []).filter(({ imagePath }) => imagePath !== expectedPath);
182
182
 
183
- const getReferenceLabel = (contentFile, section) => `${contentFile.contentLabel} [${section.id}]`;
183
+ const getReferenceLabel = (contentFile, section) => `${contentFile.contentLabel} [${section.id ?? 'page title'}]`;
184
184
 
185
- const getRootLabel = (contentFile) => contentFile.isHome
186
- ? contentFile.imagesLabel
187
- : contentFile.imagesLabel.replace(/\/images$/, '');
185
+ const getRootLabel = (contentFile) => contentFile.imagesLabel.replace(/\/images$/, '');
188
186
 
189
187
  const getCandidateLabel = ({ contentFile, imagePath }) =>
190
188
  `${contentFile.imagesLabel}/${toPosixPath(path.relative(contentFile.imagesDir, imagePath))}`;
@@ -217,8 +215,8 @@ const assertCleanGitForCrossRootSync = async (moves) => {
217
215
  } catch (error) {
218
216
  addIssue({
219
217
  severity: 'error',
220
- message: 'Cross-route content sync requires a clean git working tree, but git status could not be checked.',
221
- fix: 'Run content:sync inside a Git worktree, or move cross-route image files manually.',
218
+ message: 'Cross-page content sync requires a clean git working tree, but git status could not be checked.',
219
+ fix: 'Run content:sync inside a Git worktree, or move cross-page image files manually.',
222
220
  });
223
221
  return;
224
222
  }
@@ -227,7 +225,7 @@ const assertCleanGitForCrossRootSync = async (moves) => {
227
225
 
228
226
  addIssue({
229
227
  severity: 'error',
230
- message: 'Cross-route content sync requires a clean git working tree before moving files between page or route image roots.',
228
+ message: 'Cross-page content sync requires a clean git working tree before moving files between page image roots.',
231
229
  fix: `Commit or stash your current changes, then run content:sync again. Current changes:\n${stdout.trim()}`,
232
230
  });
233
231
  };
@@ -256,9 +254,9 @@ const addConflictingMoveIssues = () => {
256
254
  if (destinations.size <= 1) continue;
257
255
 
258
256
  const firstMove = moves[0];
259
- addSectionIssue(firstMove.contentFile, { id: firstMove.sectionId, heading: firstMove.sectionId }, {
257
+ addSectionIssue(firstMove.contentFile, firstMove.section, {
260
258
  severity: 'error',
261
- message: `Cannot relocate "${firstMove.imageName}" because the same source file is referenced from multiple destinations: ${moves.map((move) => getExpectedImageLabel(move.contentFile, move.sectionId, move.imageName)).join(', ')}.`,
259
+ message: `Cannot relocate "${firstMove.imageName}" because the same source file is referenced from multiple destinations: ${moves.map((move) => getExpectedImageLabel(move.contentFile, move.imageName)).join(', ')}.`,
262
260
  fix: 'Duplicate the image manually or rename one of the image files so each move has a single destination.',
263
261
  });
264
262
  }
@@ -268,9 +266,9 @@ const addConflictingMoveIssues = () => {
268
266
  if (sources.size <= 1) continue;
269
267
 
270
268
  const firstMove = moves[0];
271
- addSectionIssue(firstMove.contentFile, { id: firstMove.sectionId, heading: firstMove.sectionId }, {
269
+ addSectionIssue(firstMove.contentFile, firstMove.section, {
272
270
  severity: 'error',
273
- message: `Cannot relocate "${firstMove.imageName}" because multiple source files would move to ${getExpectedImageLabel(firstMove.contentFile, firstMove.sectionId, firstMove.imageName)}: ${moves.map((move) => getCandidateLabel(move.sourceContentFile ? { contentFile: move.sourceContentFile, imagePath: move.from } : move)).join(', ')}.`,
271
+ message: `Cannot relocate "${firstMove.imageName}" because multiple source files would move to ${getExpectedImageLabel(firstMove.contentFile, firstMove.imageName)}: ${moves.map((move) => getCandidateLabel(move.sourceContentFile ? { contentFile: move.sourceContentFile, imagePath: move.from } : move)).join(', ')}.`,
274
272
  fix: 'Move the intended file manually or rename files so the destination is unambiguous.',
275
273
  });
276
274
  }
@@ -284,8 +282,8 @@ const validateImageReference = async (
284
282
  expectedReferencesByPath,
285
283
  ) => {
286
284
  const imageName = reference.image;
287
- const expectedPath = getExpectedImagePath(contentFile, section.id, imageName);
288
- const expectedLabel = getExpectedImageLabel(contentFile, section.id, imageName);
285
+ const expectedPath = getExpectedImagePath(contentFile, imageName);
286
+ const expectedLabel = getExpectedImageLabel(contentFile, imageName);
289
287
  const expectedExists = await stat(expectedPath).then((entry) => entry.isFile()).catch(() => false);
290
288
 
291
289
  if (expectedExists) {
@@ -297,8 +295,8 @@ const validateImageReference = async (
297
295
  if (candidates.length === 0) {
298
296
  addSectionIssue(contentFile, section, {
299
297
  severity: 'error',
300
- message: `Image "${imageName}" does not exist at ${expectedLabel} or anywhere under any page or route image root.`,
301
- fix: `Add the source image to ${contentFile.imagesLabel}/${section.id}/ or remove the Norna-managed image reference.`,
298
+ message: `Image "${imageName}" does not exist at ${expectedLabel} or anywhere under any page image root.`,
299
+ fix: `Add the source image directly to ${contentFile.imagesLabel}/ or remove the Norna-managed image reference.`,
302
300
  });
303
301
  return null;
304
302
  }
@@ -315,7 +313,7 @@ const validateImageReference = async (
315
313
  const sourceCandidate = candidates[0];
316
314
  const sourcePath = sourceCandidate.imagePath;
317
315
  const referencesAtCurrentLocation = (expectedReferencesByPath.get(sourcePath) ?? [])
318
- .filter((expectedReference) => expectedReference.contentFile !== contentFile || expectedReference.section.id !== section.id);
316
+ .filter((expectedReference) => expectedReference.contentFile !== contentFile);
319
317
 
320
318
  if (referencesAtCurrentLocation.length > 0) {
321
319
  addSectionIssue(contentFile, section, {
@@ -326,15 +324,17 @@ const validateImageReference = async (
326
324
  return null;
327
325
  }
328
326
 
329
- imageMoves.push({
330
- imageName,
331
- from: sourcePath,
332
- to: expectedPath,
333
- contentFile,
334
- sourceContentFile: sourceCandidate.contentFile,
335
- sectionId: section.id,
336
- crossRoot: sourceCandidate.contentFile !== contentFile,
337
- });
327
+ if (!imageMoves.some((move) => move.from === sourcePath && move.to === expectedPath)) {
328
+ imageMoves.push({
329
+ imageName,
330
+ from: sourcePath,
331
+ to: expectedPath,
332
+ contentFile,
333
+ sourceContentFile: sourceCandidate.contentFile,
334
+ section,
335
+ crossRoot: sourceCandidate.contentFile !== contentFile,
336
+ });
337
+ }
338
338
  referencedImagePaths.add(sourcePath);
339
339
 
340
340
  if (!shouldWrite) {
@@ -342,8 +342,8 @@ const validateImageReference = async (
342
342
  severity: 'error',
343
343
  message: `Image "${imageName}" is used here but is located in ${getCandidateLabel(sourceCandidate)}.`,
344
344
  fix: sourceCandidate.contentFile === contentFile
345
- ? 'Run norna content:sync to move it inside the current page or route image root.'
346
- : `Run norna content:sync to move it from ${getRootLabel(sourceCandidate.contentFile)} to ${getRootLabel(contentFile)}. Cross-route sync requires a clean git working tree when files are moved.`,
345
+ ? 'Run norna content:sync to move it directly into the current page image root.'
346
+ : `Run norna content:sync to move it from ${getRootLabel(sourceCandidate.contentFile)} to ${getRootLabel(contentFile)}. Cross-page sync requires a clean git working tree when files are moved.`,
347
347
  });
348
348
  }
349
349
 
@@ -412,11 +412,27 @@ for (const contentFile of contentFiles) {
412
412
  validateFrontmatterIndentation(frontmatter, addIssue);
413
413
  validateContentFrontmatterStructure(frontmatter, addIssue);
414
414
 
415
- const frontmatterSections = getFrontmatterSections(frontmatter);
416
- const frontmatterIds = new Set(frontmatterSections.map((section) => section.id));
417
- const { sections } = getBodySections(body);
415
+ const { headings, pageHeadings, prelude, sections } = await getBodySections(body);
416
+ const headingIdentifierIssues = getHeadingIdentifierIssues(headings);
417
+ const invalidHeadingLines = new Set();
418
+ for (const issue of headingIdentifierIssues) {
419
+ invalidHeadingLines.add(issue.heading.line);
420
+ if (issue.otherHeading) invalidHeadingLines.add(issue.otherHeading.line);
421
+ const line = bodyLineOffset + issue.heading.line;
422
+ const relatedLine = issue.otherHeading
423
+ ? ` The other heading is on line ${bodyLineOffset + issue.otherHeading.line}.`
424
+ : '';
425
+ addIssue({
426
+ severity: 'error',
427
+ message: `${contentFile.contentLabel} line ${line}: ${issue.message}${relatedLine}`,
428
+ fix: issue.fix,
429
+ sectionId: `${contentFile.contentLabel}:heading:${issue.heading.line}`,
430
+ sectionLabel: `${contentFile.contentLabel} line ${line}`,
431
+ });
432
+ }
418
433
  const sectionsById = new Map();
419
- const blockResultsBySectionId = new Map();
434
+ const blockResultsBySection = new Map();
435
+ const validSections = new Set();
420
436
  const imageCandidatesByName = await getImageCandidatesByName(contentFile.imagesDir);
421
437
 
422
438
  for (const [imageName, candidates] of imageCandidatesByName) {
@@ -427,36 +443,71 @@ for (const contentFile of contentFiles) {
427
443
  }
428
444
  }
429
445
 
446
+ if (pageHeadings.length === 0) {
447
+ addContentIssue(contentFile, {
448
+ severity: 'error',
449
+ message: 'The page is missing its Markdown H1 title.',
450
+ fix: 'Add exactly one page title before any sections, for example "# About".',
451
+ });
452
+ } else if (pageHeadings.length > 1) {
453
+ addContentIssue(contentFile, {
454
+ severity: 'error',
455
+ message: `The page contains ${pageHeadings.length} Markdown H1 headings.`,
456
+ fix: 'Keep exactly one H1 page title. Use level 2 headings with explicit ids for sections.',
457
+ });
458
+ }
459
+
460
+ if (sections[0] && !sections[0].isPageTitle) {
461
+ addContentIssue(contentFile, {
462
+ severity: 'error',
463
+ message: `The first content heading is "${sections[0].heading}", but a Norna page must start with its H1 title.`,
464
+ fix: 'Add the page title first, for example "# About".',
465
+ });
466
+ }
467
+
468
+ if (prelude.trim()) {
469
+ addContentIssue(contentFile, {
470
+ severity: 'error',
471
+ message: 'The page contains content before its first heading.',
472
+ fix: 'Make the Markdown H1 page title the first content after optional frontmatter.',
473
+ });
474
+ }
475
+
430
476
  for (const section of sections) {
431
- if (!section.id) {
432
- addSectionIssue(contentFile, section, {
433
- severity: 'error',
434
- message: `Section heading "${section.heading.replace(/^##\s+/, '')}" is missing an explicit id.`,
435
- fix: `Write it as: ${section.heading} {#section-id}`,
436
- });
477
+ if (section.isPageTitle) {
478
+ if (
479
+ pageHeadings.length === 1
480
+ && sections[0] === section
481
+ && !invalidHeadingLines.has(section.line)
482
+ ) {
483
+ validSections.add(section);
484
+ }
437
485
  continue;
438
486
  }
439
487
 
488
+ if (invalidHeadingLines.has(section.line) || !section.id) continue;
489
+
440
490
  if (sectionsById.has(section.id)) {
441
491
  addSectionIssue(contentFile, section, {
442
492
  severity: 'error',
443
- message: `Duplicate Markdown section heading id "${section.id}".`,
444
- fix: 'Each Markdown level 2 section heading must use a unique explicit id within the page.',
493
+ message: `Duplicate Markdown heading id "${section.id}".`,
494
+ fix: 'Add a unique explicit id to at least one heading.',
445
495
  });
446
496
  continue;
447
497
  }
448
498
 
449
499
  sectionsById.set(section.id, section);
500
+ validSections.add(section);
450
501
  }
451
502
 
452
503
  for (const section of sections) {
453
- if (!section.id || !sectionsById.has(section.id)) continue;
504
+ if (!validSections.has(section)) continue;
454
505
 
455
506
  const blockResults = extractNornaMarkdownBlockDiagnostics(section.text, {
456
507
  label: contentFile.contentLabel,
457
508
  lineOffset: bodyLineOffset + section.line - 1,
458
509
  });
459
- blockResultsBySectionId.set(section.id, blockResults);
510
+ blockResultsBySection.set(section, blockResults);
460
511
 
461
512
  const inlineNoteResults = extractInlineNoteDiagnostics(section.text, {
462
513
  label: contentFile.contentLabel,
@@ -470,17 +521,16 @@ for (const contentFile of contentFiles) {
470
521
  }
471
522
 
472
523
  for (const reference of getNornaBlockImageReferences(blockResults.blocks)) {
473
- const expectedPath = getExpectedImagePath(contentFile, section.id, reference.image);
524
+ const expectedPath = getExpectedImagePath(contentFile, reference.image);
474
525
  addExpectedReference(globalExpectedReferencesByPath, expectedPath, { contentFile, section, reference });
475
526
  }
476
527
  }
477
528
 
478
529
  contentFileContexts.push({
479
530
  contentFile,
480
- frontmatterIds,
481
531
  sections,
482
- sectionsById,
483
- blockResultsBySectionId,
532
+ validSections,
533
+ blockResultsBySection,
484
534
  body,
485
535
  });
486
536
  }
@@ -488,23 +538,12 @@ for (const contentFile of contentFiles) {
488
538
  for (const context of contentFileContexts) {
489
539
  const {
490
540
  contentFile,
491
- frontmatterIds,
492
541
  sections,
493
- sectionsById,
494
- blockResultsBySectionId,
542
+ validSections,
543
+ blockResultsBySection,
495
544
  body,
496
545
  } = context;
497
546
 
498
- for (const id of frontmatterIds) {
499
- if (!sectionsById.has(id)) {
500
- addContentIssue(contentFile, {
501
- severity: 'error',
502
- message: `Section metadata "${id}" does not match any Markdown section.`,
503
- fix: `Add a level 2 Markdown heading, for example "## Heading {#${id}}", or remove sections.${id}.`,
504
- });
505
- }
506
- }
507
-
508
547
  for (const styleName of new Set(getDeprecatedInlineStyleReferences(body))) {
509
548
  addContentIssue(contentFile, {
510
549
  severity: 'error',
@@ -514,7 +553,7 @@ for (const context of contentFileContexts) {
514
553
  }
515
554
 
516
555
  for (const section of sections) {
517
- if (!section.id || !sectionsById.has(section.id)) continue;
556
+ if (!validSections.has(section)) continue;
518
557
 
519
558
  const markdownImages = extractMarkdownImageReferences(section.text);
520
559
  for (const reference of markdownImages) {
@@ -525,7 +564,7 @@ for (const context of contentFileContexts) {
525
564
  });
526
565
  }
527
566
 
528
- const { blocks, errors: blockErrors } = blockResultsBySectionId.get(section.id) ?? { blocks: [], errors: [] };
567
+ const { blocks, errors: blockErrors } = blockResultsBySection.get(section) ?? { blocks: [], errors: [] };
529
568
  for (const error of blockErrors) {
530
569
  addSectionIssue(contentFile, section, {
531
570
  severity: 'error',
@@ -601,5 +640,5 @@ if (!canWrite) {
601
640
  for (const move of imageMoves) {
602
641
  await mkdir(path.dirname(move.to), { recursive: true });
603
642
  await rename(move.from, move.to);
604
- console.log(`Moved image "${move.imageName}" to ${move.contentFile.imagesLabel}/${move.sectionId}/.`);
643
+ console.log(`Moved image "${move.imageName}" to ${move.contentFile.imagesLabel}/.`);
605
644
  }
@@ -8,7 +8,7 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
8
8
  const nornaBin = path.join(repoRoot, 'bin', 'norna.mjs');
9
9
  const tempParent = path.join(repoRoot, 'node_modules', '.cache');
10
10
  await mkdir(tempParent, { recursive: true });
11
- const tempRoot = await mkdtemp(path.join(tempParent, 'norna-temporary-visibility-'));
11
+ const tempRoot = await mkdtemp(path.join(tempParent, 'norna-banner-visibility-'));
12
12
  const siteDir = path.join(tempRoot, 'site');
13
13
 
14
14
  const runCli = (args, env = {}) => {
@@ -34,7 +34,7 @@ const runCli = (args, env = {}) => {
34
34
  };
35
35
 
36
36
  try {
37
- await mkdir(path.join(siteDir, 'images', 'work'), { recursive: true });
37
+ await mkdir(path.join(siteDir, 'pages', '000-home'), { recursive: true });
38
38
  await mkdir(path.join(siteDir, 'public'), { recursive: true });
39
39
  await writeFile(path.join(siteDir, 'public', 'robots.txt'), 'User-agent: *\nAllow: /\n');
40
40
  await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
@@ -55,29 +55,14 @@ palette: dark
55
55
  title: Active banner
56
56
  text: Active banner text.
57
57
  `);
58
- await writeFile(path.join(siteDir, 'content.md'), `---
59
- title: Temporary Visibility Test
60
- description: Test site for temporary sections.
61
- sections:
62
- expired:
63
- visible:
64
- until: "2026-01-01"
65
- active:
66
- visible:
67
- from: "2026-01-01"
68
- until: "2026-12-31"
69
- always: {}
70
-
58
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
59
+ page:
60
+ description: Test site for temporary banners.
71
61
  ---
72
- ## Expired {#expired}
73
-
74
- Expired section text.
75
-
76
- ## Active {#active}
77
62
 
78
- Active section text.
63
+ # Temporary Visibility Test
79
64
 
80
- ## Always {#always}
65
+ ## Content {#content}
81
66
 
82
67
  Always visible section text.
83
68
  `);
@@ -85,13 +70,11 @@ Always visible section text.
85
70
  runCli(['build'], { NORNA_TODAY: '2026-06-15' });
86
71
 
87
72
  const html = await readFile(path.join(tempRoot, 'dist', 'index.html'), 'utf8');
88
- assert.match(html, /Active section text/);
89
73
  assert.match(html, /Always visible section text/);
90
- assert.doesNotMatch(html, /Expired section text/);
91
74
  assert.match(html, /Active banner text/);
92
75
  assert.doesNotMatch(html, /Expired banner text/);
93
76
 
94
- console.log('Temporary visibility test passed.');
77
+ console.log('Temporary banner visibility test passed.');
95
78
  } finally {
96
79
  await rm(tempRoot, { force: true, recursive: true });
97
80
  }
@@ -10,7 +10,8 @@ const tempParent = path.join(repoRoot, 'node_modules', '.cache');
10
10
  await mkdir(tempParent, { recursive: true });
11
11
  const tempRoot = await mkdtemp(path.join(tempParent, 'norna-client-javascript-'));
12
12
  const siteDir = path.join(tempRoot, 'site');
13
- const routeDir = path.join(siteDir, 'routes', '010-details');
13
+ const homeDir = path.join(siteDir, 'pages', '000-home');
14
+ const pageDir = path.join(siteDir, 'pages', '010-details');
14
15
  const configPath = path.join(siteDir, 'config.yaml');
15
16
 
16
17
  const runBuild = () => {
@@ -54,22 +55,20 @@ const writeSvg = (filePath, color) => writeFile(filePath, [
54
55
  ].join('\n'));
55
56
 
56
57
  try {
58
+ await mkdir(homeDir, { recursive: true });
57
59
  await mkdir(path.join(siteDir, 'public'), { recursive: true });
58
60
  await writeConfig();
59
61
  await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
60
62
  profile: reading
61
63
  palette: paper
62
64
  `);
63
- await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `navigation:
64
- label: Test site
65
- `);
66
- await writeFile(path.join(siteDir, 'content.md'), `---
67
- title: Section navigation
68
- description: A single page with sticky section navigation.
69
- sections:
70
- intro: {}
71
- more: {}
65
+ await writeFile(path.join(homeDir, 'content.md'), `---
66
+ page:
67
+ description: A single page with sticky section navigation.
72
68
  ---
69
+
70
+ # Section navigation
71
+
73
72
  ## Intro {#intro}
74
73
 
75
74
  The section heading is a native anchor target.
@@ -89,6 +88,11 @@ The navigation enhancement keeps native anchors clear of the sticky header.
89
88
  assert.match(getScripts(instantHtml)[0], /site-top-anchor-offset/);
90
89
  assertScrollBehavior(instantHtml, 'auto', 'The default instant scroll mode');
91
90
  assert.match(instantHtml, /<nav class="page-nav"/);
91
+ assert.match(instantHtml, /<nav class="page-nav" aria-label="Page contents">/);
92
+ assert.doesNotMatch(instantHtml, /class="page-nav-label"/);
93
+ assert.doesNotMatch(instantHtml, /<h2 tabindex="-1">Page contents<\/h2>/);
94
+ assert.match(instantHtml, /class="page-nav-page-top" data-page-top href="\/">Section navigation<\/a>/);
95
+ assert.match(instantHtml, /class="mobile-nav-destination mobile-nav-page-top" data-page-top href="\/">/);
92
96
  assert.match(instantHtml, /href="#intro"/);
93
97
  assert.match(instantHtml, /href="#more"/);
94
98
 
@@ -104,27 +108,26 @@ The navigation enhancement keeps native anchors clear of the sticky header.
104
108
 
105
109
  await writeConfig();
106
110
 
107
- await mkdir(path.join(routeDir, 'images', 'details'), { recursive: true });
108
- await writeSvg(path.join(routeDir, 'images', 'details', 'first.svg'), '#7b8f78');
109
- await writeSvg(path.join(routeDir, 'images', 'details', 'second.svg'), '#a7664b');
110
- await writeFile(path.join(siteDir, 'content.md'), `---
111
- title: Static baseline
112
- description: A page without interactive features.
113
- sections:
114
- intro: {}
111
+ await mkdir(path.join(pageDir, 'images'), { recursive: true });
112
+ await writeSvg(path.join(pageDir, 'images', 'first.svg'), '#7b8f78');
113
+ await writeSvg(path.join(pageDir, 'images', 'second.svg'), '#a7664b');
114
+ await writeFile(path.join(homeDir, 'content.md'), `---
115
+ page:
116
+ description: A page without interactive features.
115
117
  ---
118
+
119
+ # Static baseline
120
+
116
121
  ## Intro {#intro}
117
122
 
118
123
  Ordinary Markdown needs no client-side JavaScript.
119
124
  `);
120
- await writeFile(path.join(routeDir, 'content.md'), `---
121
- title: Details
122
- description: Static managed images.
123
- sections:
124
- details: {}
125
+ await writeFile(path.join(pageDir, 'content.md'), `---
126
+ page:
127
+ description: Static managed images.
125
128
  ---
126
- ## Details {#details}
127
129
 
130
+ # Details
128
131
  Static image stacks remain static.
129
132
 
130
133
  \`\`\`norna-image-stack
@@ -147,24 +150,26 @@ Static image stacks remain static.
147
150
  'A page with managed image stacks and cards',
148
151
  );
149
152
 
150
- await writeFile(path.join(siteDir, 'content.md'), `---
151
- title: Notes
152
- description: A page with an enhanced sidenote.
153
- sections:
154
- intro: {}
153
+ await writeFile(path.join(homeDir, 'content.md'), `---
154
+ page:
155
+ description: A page with an enhanced sidenote.
155
156
  ---
157
+
158
+ # Notes
159
+
156
160
  ## Intro {#intro}
157
161
 
158
162
  This sentence has additional context.{note-ref}
159
163
 
160
164
  {note: The note remains readable without JavaScript, while alignment is enhanced when JavaScript runs.}
161
165
  `);
162
- await writeFile(path.join(routeDir, 'content.md'), `---
163
- title: Carousel
164
- description: An interactive image carousel.
165
- sections:
166
- details: {}
166
+ await writeFile(path.join(pageDir, 'content.md'), `---
167
+ page:
168
+ description: An interactive image carousel.
167
169
  ---
170
+
171
+ # Carousel
172
+
168
173
  ## Details {#details}
169
174
 
170
175
  \`\`\`norna-image-carousel
@@ -184,25 +189,24 @@ sections:
184
189
  assert.match(carouselHtml, /data-carousel/);
185
190
  assert.match(getScripts(carouselHtml)[0], /\ssrc=/, 'Carousel JavaScript should be emitted as a module asset.');
186
191
 
187
- await writeFile(path.join(siteDir, 'content.md'), `---
188
- title: Banner
189
- description: A page with a dismissible banner.
190
- sections:
191
- intro: {}
192
+ await writeFile(path.join(homeDir, 'content.md'), `---
193
+ page:
194
+ description: A page with a dismissible banner.
192
195
  ---
196
+
197
+ # Banner
198
+
193
199
  ## Intro {#intro}
194
200
 
195
201
  The only interactive feature is the banner.
196
202
  `);
197
- await writeFile(path.join(routeDir, 'content.md'), `---
198
- title: Details
199
- description: A second plain page.
200
- sections:
201
- details: {}
203
+ await writeFile(path.join(pageDir, 'content.md'), `---
204
+ page:
205
+ description: A second plain page.
202
206
  ---
203
- ## Details {#details}
204
207
 
205
- Plain route content.
208
+ # Details
209
+ Plain page content.
206
210
  `);
207
211
  await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `banners:
208
212
  - id: test-banner