@janga/norna 0.7.17 → 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 (262) hide show
  1. package/README.md +15 -6
  2. package/bin/norna-cli.mjs +1 -1
  3. package/docs/README.md +11 -9
  4. package/docs/configuration.md +25 -2
  5. package/docs/content.md +21 -12
  6. package/docs/design/navigation-and-theme-plan.md +90 -19
  7. package/docs/design/site-examples-structure.md +7 -3
  8. package/docs/engine-development.md +19 -22
  9. package/docs/getting-started.md +3 -3
  10. package/docs/images-and-metadata.md +6 -4
  11. package/docs/pages.md +120 -64
  12. package/docs/public-files.md +1 -1
  13. package/docs/site-files.md +36 -29
  14. package/docs/sitewide-content.md +1 -1
  15. package/docs/theme.md +107 -141
  16. package/docs/typography.md +15 -27
  17. package/examples/README.md +8 -2
  18. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +5 -5
  19. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +10 -10
  20. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +35 -43
  21. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  22. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/content.md +2 -1
  23. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/theme.yaml +2 -3
  24. package/examples/feature-demos/theme-preset-documentation/README.md +16 -0
  25. package/examples/feature-demos/theme-preset-documentation/site/.norna/generated-images.json +74 -0
  26. package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/content.md +1 -1
  27. package/examples/feature-demos/theme-preset-portfolio/README.md +16 -0
  28. package/examples/feature-demos/theme-preset-portfolio/site/.norna/generated-images.json +74 -0
  29. package/examples/feature-demos/theme-preset-portfolio/site/config.yaml +1 -0
  30. package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/content.md +1 -1
  31. package/examples/feature-demos/theme-preset-portfolio/site/public/robots.txt +2 -0
  32. package/examples/feature-demos/theme-preset-portfolio/site/sitewide-content.yaml +3 -0
  33. package/examples/feature-demos/theme-preset-project/README.md +16 -0
  34. package/examples/feature-demos/theme-preset-project/site/.norna/generated-images.json +74 -0
  35. package/examples/feature-demos/theme-preset-project/site/config.yaml +1 -0
  36. package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/content.md +1 -1
  37. package/examples/feature-demos/theme-preset-project/site/public/robots.txt +2 -0
  38. package/examples/feature-demos/theme-preset-project/site/sitewide-content.yaml +3 -0
  39. package/examples/feature-demos/theme-preset-statement/README.md +16 -0
  40. package/examples/feature-demos/theme-preset-statement/site/.norna/generated-images.json +74 -0
  41. package/examples/feature-demos/theme-preset-statement/site/config.yaml +1 -0
  42. package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/content.md +1 -1
  43. package/examples/feature-demos/theme-preset-statement/site/public/robots.txt +2 -0
  44. package/examples/feature-demos/theme-preset-statement/site/sitewide-content.yaml +3 -0
  45. package/fixtures/basic/site/theme.yaml +1 -8
  46. package/fixtures/nested-pages/.astro/collections/site.schema.json +35 -0
  47. package/fixtures/nested-pages/.astro/collections/sitewide.schema.json +97 -0
  48. package/fixtures/nested-pages/.astro/collections/theme.schema.json +844 -0
  49. package/fixtures/nested-pages/.astro/content-assets.mjs +4 -0
  50. package/fixtures/nested-pages/.astro/content-modules.mjs +1 -0
  51. package/fixtures/nested-pages/.astro/content.d.ts +183 -0
  52. package/fixtures/nested-pages/.astro/data-store.json +1 -0
  53. package/fixtures/nested-pages/.astro/dev-local.json +8 -0
  54. package/fixtures/nested-pages/.astro/dev.log +1128 -0
  55. package/fixtures/nested-pages/.astro/settings.json +5 -0
  56. package/fixtures/nested-pages/.astro/types.d.ts +2 -0
  57. package/fixtures/nested-pages/node_modules/.vite/deps/_metadata.json +74 -0
  58. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  59. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  60. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  61. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  62. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  63. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  64. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  65. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  66. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  67. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  68. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  69. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  70. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  71. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  72. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  73. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  74. package/fixtures/nested-pages/node_modules/.vite/deps/package.json +3 -0
  75. package/fixtures/nested-pages/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  76. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  77. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  78. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  79. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  80. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  81. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  82. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  83. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  84. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  85. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  86. package/fixtures/nested-pages/site/config.yaml +2 -0
  87. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +4 -0
  88. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/theme.yaml +2 -0
  89. package/fixtures/nested-pages/site/pages/010-guides/pages/030-release-notes/content.md +9 -0
  90. package/fixtures/nested-pages/site/pages/010-guides/pages/040-configuration/content.md +3 -0
  91. package/fixtures/nested-pages/site/pages/010-guides/pages/050-content/content.md +3 -0
  92. package/fixtures/nested-pages/site/pages/010-guides/pages/060-themes/content.md +3 -0
  93. package/fixtures/nested-pages/site/pages/010-guides/pages/070-images/content.md +3 -0
  94. package/fixtures/nested-pages/site/pages/010-guides/pages/080-cards/content.md +3 -0
  95. package/fixtures/nested-pages/site/pages/010-guides/pages/090-notes/content.md +3 -0
  96. package/fixtures/nested-pages/site/pages/010-guides/pages/100-validation/content.md +3 -0
  97. package/fixtures/nested-pages/site/pages/010-guides/pages/110-synchronization/content.md +3 -0
  98. package/fixtures/nested-pages/site/pages/010-guides/pages/120-building/content.md +3 -0
  99. package/fixtures/nested-pages/site/pages/010-guides/pages/130-publishing/content.md +3 -0
  100. package/fixtures/nested-pages/site/pages/010-guides/pages/140-troubleshooting/content.md +3 -0
  101. package/fixtures/nested-pages/site/pages/010-guides/pages/150-migration/content.md +3 -0
  102. package/fixtures/nested-pages/site/pages/010-guides/pages/160-accessibility/content.md +3 -0
  103. package/fixtures/nested-pages/site/pages/010-guides/pages/170-performance/content.md +3 -0
  104. package/fixtures/nested-pages/site/pages/010-guides/pages/180-security/content.md +3 -0
  105. package/fixtures/nested-pages/site/pages/010-guides/pages/190-localization/content.md +3 -0
  106. package/fixtures/nested-pages/site/pages/010-guides/pages/200-upgrading/content.md +3 -0
  107. package/fixtures/nested-pages/site/pages/010-guides/pages/210-faq/content.md +3 -0
  108. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +7 -3
  109. package/fixtures/nested-pages/site/theme.yaml +1 -3
  110. package/package.json +2 -1
  111. package/schemas/config.schema.json +45 -4
  112. package/schemas/content-frontmatter.schema.json +5 -5
  113. package/schemas/manifest.json +3 -2
  114. package/schemas/page-theme.schema.json +192 -0
  115. package/schemas/sitewide-content.schema.json +17 -17
  116. package/schemas/theme.schema.json +188 -216
  117. package/scripts/check-config.mjs +2 -1
  118. package/scripts/deploy-site.mjs +1 -1
  119. package/scripts/generate-images.mjs +7 -14
  120. package/scripts/generate-schemas.mjs +4 -1
  121. package/scripts/lib/editor-language-service.mjs +0 -14
  122. package/scripts/lib/example-sites.mjs +15 -3
  123. package/scripts/lib/page-model.mjs +26 -7
  124. package/scripts/lib/presentation.mjs +25 -17
  125. package/scripts/lib/project-config.mjs +26 -16
  126. package/scripts/lib/schema-definitions.mjs +42 -14
  127. package/scripts/lib/schema-editor-metadata.mjs +70 -26
  128. package/scripts/lib/schema-value-definitions.mjs +14 -0
  129. package/scripts/lib/site-content.mjs +50 -25
  130. package/scripts/lib/site-conventions.mjs +1 -0
  131. package/scripts/lib/site-paths.mjs +9 -3
  132. package/scripts/lib/theme-config.mjs +3 -2
  133. package/scripts/lib/theme-presets.mjs +54 -21
  134. package/scripts/show-typography.mjs +1 -33
  135. package/scripts/sync-content-sections.mjs +1 -3
  136. package/scripts/test-banner-visibility.mjs +2 -2
  137. package/scripts/test-client-javascript.mjs +11 -4
  138. package/scripts/test-content-check.mjs +25 -25
  139. package/scripts/test-content-model-v2.mjs +165 -101
  140. package/scripts/test-documentation.mjs +4 -2
  141. package/scripts/test-editor-language-service.mjs +11 -8
  142. package/scripts/test-engine-commands.mjs +5 -5
  143. package/scripts/test-examples.mjs +30 -1
  144. package/scripts/test-heading-ids.mjs +2 -2
  145. package/scripts/test-navigation.mjs +11 -2
  146. package/scripts/test-nested-pages.mjs +25 -10
  147. package/scripts/test-package-check.mjs +29 -60
  148. package/scripts/test-page-model.mjs +30 -2
  149. package/scripts/test-project-config.mjs +5 -10
  150. package/scripts/test-schemas.mjs +18 -3
  151. package/scripts/test-theme-presets.mjs +33 -27
  152. package/src/components/NavigationPageTree.astro +114 -25
  153. package/src/components/PageContentsLinks.astro +40 -0
  154. package/src/components/SectionNavigationScript.astro +12 -5
  155. package/src/components/SiteBreadcrumbs.astro +17 -14
  156. package/src/components/SiteNavigation.astro +45 -12
  157. package/src/components/SitePage.astro +37 -23
  158. package/src/components/SiteSection.astro +2 -7
  159. package/src/components/SiteTreeNavigation.astro +35 -0
  160. package/src/components/TreeNavigationScript.astro +83 -0
  161. package/src/content.config.ts +2 -8
  162. package/src/layouts/BaseLayout.astro +10 -0
  163. package/src/lib/pageThemes.ts +11 -9
  164. package/src/lib/sectionContent.ts +1 -5
  165. package/src/lib/siteNavigation.ts +2 -1
  166. package/src/lib/sitePages.ts +25 -14
  167. package/src/pages/index.astro +2 -1
  168. package/src/styles/global.css +411 -43
  169. package/starters/basic/README.md +7 -7
  170. package/starters/basic/site/theme.yaml +1 -105
  171. package/starters/project/README.md +8 -8
  172. package/starters/project/site/theme.yaml +1 -31
  173. package/examples/feature-demos/theme-presets/README.md +0 -18
  174. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +0 -290
  175. package/examples/feature-demos/theme-presets/site/content.md +0 -14
  176. package/examples/feature-demos/theme-presets/site/theme.yaml +0 -1
  177. /package/examples/complete-sites/dog-shelter-multi-page/site/{content.md → pages/000-home/content.md} +0 -0
  178. /package/examples/complete-sites/dog-shelter-multi-page/site/{images → pages/000-home/images}/shelter-dog.jpg +0 -0
  179. /package/examples/complete-sites/dog-shelter-single-page/site/{content.md → pages/000-home/content.md} +0 -0
  180. /package/examples/complete-sites/dog-shelter-single-page/site/{images → pages/000-home/images}/foster-dog.jpg +0 -0
  181. /package/examples/complete-sites/dog-shelter-single-page/site/{images → pages/000-home/images}/rescue-dog.jpg +0 -0
  182. /package/examples/feature-demos/media-and-surfaces/site/{content.md → pages/000-home/content.md} +0 -0
  183. /package/examples/feature-demos/sitewide-content/site/{content.md → pages/000-home/content.md} +0 -0
  184. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/config.yaml +0 -0
  185. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/images/black-schnauzer.jpg +0 -0
  186. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/images/retriever-portrait.jpg +0 -0
  187. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site/pages/000-home}/images/schnauzer-portrait.jpg +0 -0
  188. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/public/robots.txt +0 -0
  189. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/sitewide-content.yaml +0 -0
  190. /package/examples/feature-demos/{theme-presets/site/pages/020-documentation → theme-preset-documentation/site}/theme.yaml +0 -0
  191. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/images/puppy-portrait.jpg +0 -0
  192. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/images/shepherd-portrait.jpg +0 -0
  193. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site/pages/000-home}/images/white-puppy.jpg +0 -0
  194. /package/examples/feature-demos/{theme-presets/site/pages/010-portfolio → theme-preset-portfolio/site}/theme.yaml +0 -0
  195. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/routeTheme.schema.json +0 -0
  196. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/site.schema.json +0 -0
  197. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/sitewide.schema.json +0 -0
  198. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/theme.schema.json +0 -0
  199. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-assets.mjs +0 -0
  200. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-modules.mjs +0 -0
  201. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content.d.ts +0 -0
  202. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/data-store.json +0 -0
  203. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev-local.json +0 -0
  204. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.json +0 -0
  205. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.log +0 -0
  206. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/settings.json +0 -0
  207. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/types.d.ts +0 -0
  208. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/_metadata.json +0 -0
  209. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
  210. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
  211. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
  212. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
  213. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
  214. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
  215. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
  216. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
  217. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
  218. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
  219. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
  220. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
  221. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js +0 -0
  222. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js.map +0 -0
  223. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
  224. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
  225. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
  226. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
  227. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/package.json +0 -0
  228. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
  229. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
  230. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
  231. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
  232. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
  233. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
  234. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
  235. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
  236. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
  237. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
  238. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
  239. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/images/boxer-portrait.jpg +0 -0
  240. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/images/grass-puppy.jpg +0 -0
  241. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site/pages/000-home}/images/grey-street-dog.jpg +0 -0
  242. /package/examples/feature-demos/{theme-presets/site/pages/030-project → theme-preset-project/site}/theme.yaml +0 -0
  243. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/images/black-pedigree.jpg +0 -0
  244. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/images/sheepdog-grass.jpg +0 -0
  245. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site/pages/000-home}/images/sheepdog-wide.jpg +0 -0
  246. /package/examples/feature-demos/{theme-presets/site/pages/040-statement → theme-preset-statement/site}/theme.yaml +0 -0
  247. /package/fixtures/basic/site/{content.md → pages/000-home/content.md} +0 -0
  248. /package/fixtures/basic/site/{images → pages/000-home/images}/.gitkeep +0 -0
  249. /package/fixtures/content-model-v2/site/{content.md → pages/000-home/content.md} +0 -0
  250. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/adopt.svg +0 -0
  251. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/detail.jpg +0 -0
  252. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/duplicate.jpg +0 -0
  253. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/foster.svg +0 -0
  254. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/hero.jpg +0 -0
  255. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/image.jpg +0 -0
  256. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/slide-one.jpg +0 -0
  257. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/slide-three.jpg +0 -0
  258. /package/fixtures/content-model-v2/site/{images → pages/000-home/images}/slide-two.jpg +0 -0
  259. /package/fixtures/nested-pages/site/{content.md → pages/000-home/content.md} +0 -0
  260. /package/starters/basic/site/{content.md → pages/000-home/content.md} +0 -0
  261. /package/starters/basic/site/{images → pages/000-home/images}/.gitkeep +0 -0
  262. /package/starters/project/site/{content.md → pages/000-home/content.md} +0 -0
@@ -50,7 +50,7 @@ const createTempSite = async ({ underRepoCache = false } = {}) => {
50
50
  await mkdir(tempParent, { recursive: true });
51
51
  const root = await mkdtemp(path.join(tempParent, 'norna-content-model-v2-'));
52
52
  const siteDir = path.join(root, 'site');
53
- await mkdir(siteDir, { recursive: true });
53
+ await mkdir(path.join(siteDir, 'pages', '000-home'), { recursive: true });
54
54
  await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
55
55
  await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
56
56
  profile: reading
@@ -109,7 +109,7 @@ test('content model v2 fixture checks and builds', async () => {
109
109
  await runNorna(['--site-dir', siteDir, 'build']);
110
110
 
111
111
  const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
112
- assert.ok(manifest['images/duplicate.jpg']);
112
+ assert.ok(manifest['pages/000-home/images/duplicate.jpg']);
113
113
  assert.ok(manifest['pages/010-guide/images/duplicate.jpg']);
114
114
 
115
115
  const homepageHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
@@ -121,11 +121,31 @@ test('content model v2 fixture checks and builds', async () => {
121
121
  }
122
122
  });
123
123
 
124
- test('the removed routes directory is rejected with a page migration hint', async () => {
124
+ test('the removed root page structure is rejected with a migration hint', async () => {
125
125
  const { root, siteDir } = await createTempSite();
126
126
  try {
127
127
  await writeFile(path.join(siteDir, 'content.md'), `# Home
128
128
  Home content.
129
+ `);
130
+
131
+ await assert.rejects(
132
+ runNorna(['--site-dir', siteDir, 'content:check']),
133
+ (error) => {
134
+ assert.match(error.output, /The old root-page structure is no longer supported: site\/content\.md\./);
135
+ assert.match(error.output, /Move the homepage content to site\/pages\/000-home\/content\.md/);
136
+ return true;
137
+ },
138
+ );
139
+ } finally {
140
+ await rm(root, { recursive: true, force: true });
141
+ }
142
+ });
143
+
144
+ test('the removed routes directory is rejected with a page migration hint', async () => {
145
+ const { root, siteDir } = await createTempSite();
146
+ try {
147
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `# Home
148
+ Home content.
129
149
  `);
130
150
  await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
131
151
  await writeFile(path.join(siteDir, 'routes', '010-guide', 'content.md'), 'Old page content.\n');
@@ -142,14 +162,41 @@ Home content.
142
162
  }
143
163
  });
144
164
 
145
- test('page theme replaces visual theme without changing site navigation', async () => {
165
+ test('the required home page cannot own child pages', async () => {
166
+ const { root, siteDir } = await createTempSite();
167
+ try {
168
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `# Home
169
+
170
+ ## Welcome {#welcome}
171
+
172
+ Home content.
173
+ `);
174
+ const homeChildDir = path.join(siteDir, 'pages', '000-home', 'pages', '010-news');
175
+ await mkdir(homeChildDir, { recursive: true });
176
+ await writeFile(path.join(homeChildDir, 'content.md'), '# News\n\nNews content.\n');
177
+
178
+ await assert.rejects(
179
+ runNorna(['--site-dir', siteDir, 'content:check']),
180
+ (error) => {
181
+ assert.match(error.output, /site\/pages\/000-home is the homepage and cannot contain child pages\./);
182
+ assert.match(error.output, /Move these page directories beside 000-home under site\/pages\//);
183
+ assert.match(error.output, /site\/pages\/000-home\/pages\/010-news/);
184
+ return true;
185
+ },
186
+ );
187
+ } finally {
188
+ await rm(root, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ test('page theme changes page presentation while preserving site visual identity', async () => {
146
193
  const { root, siteDir } = await createTempSite({ underRepoCache: true });
147
194
  try {
148
- await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
149
- profile: reading
195
+ await writeFile(path.join(siteDir, 'theme.yaml'), `preset: documentation
196
+ palette: dark
150
197
  `);
151
198
  await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
152
- await writeFile(path.join(siteDir, 'content.md'), `---
199
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
153
200
  page:
154
201
  description: Root page
155
202
  ---
@@ -168,16 +215,33 @@ page:
168
215
  # Guide
169
216
  Page content.
170
217
  `);
171
- await writeFile(path.join(siteDir, 'pages', '010-guide', 'theme.yaml'), `typography:
172
- profile: statement
173
- palette: light
218
+ await mkdir(path.join(siteDir, 'pages', '010-guide', 'pages', '010-detail'), { recursive: true });
219
+ await writeFile(path.join(siteDir, 'pages', '010-guide', 'pages', '010-detail', 'content.md'), `# Detail
220
+
221
+ Inherited page content.
222
+ `);
223
+ await writeFile(path.join(siteDir, 'pages', '010-guide', 'theme.yaml'), `layout:
224
+ contentSpacing: spacious
225
+ textWidth: wide
226
+ sections:
227
+ backgroundPattern: cycling
228
+ `);
229
+ await writeFile(path.join(siteDir, 'pages', '010-guide', 'pages', '010-detail', 'theme.yaml'), `layout:
230
+ textWidth: narrow
174
231
  `);
175
232
 
176
233
  await runNorna(['--site-dir', siteDir, 'build']);
177
234
  const rootHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
178
235
  const pageHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'guide', 'index.html'), 'utf8');
236
+ const detailHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'guide', 'detail', 'index.html'), 'utf8');
179
237
  assert.match(rootHtml, /--color-page: #000000/);
180
- assert.match(pageHtml, /--color-page: #ffffff/);
238
+ assert.match(pageHtml, /--color-page: #000000/);
239
+ assert.match(rootHtml, /--font-sans: Georgia, 'Times New Roman', serif/);
240
+ assert.match(pageHtml, /--font-sans: Georgia, 'Times New Roman', serif/);
241
+ assert.match(pageHtml, /--space-section-to-section-desktop: clamp\(2\.25rem, 5vw, 4\.5rem\)/);
242
+ assert.match(pageHtml, /--section-body-width-desktop: min\(72ch,/);
243
+ assert.match(detailHtml, /--space-section-to-section-desktop: clamp\(2\.25rem, 5vw, 4\.5rem\)/);
244
+ assert.match(detailHtml, /--section-body-width-desktop: min\(60ch,/);
181
245
  assert.match(pageHtml, /<a href="\/">\s*Root\s*<\/a>/);
182
246
  } finally {
183
247
  await rm(root, { recursive: true, force: true });
@@ -192,7 +256,7 @@ test('page metadata, navigation logo, and page listing have separate roles', asy
192
256
  await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `logo:
193
257
  height: 2rem
194
258
  `);
195
- await writeFile(path.join(siteDir, 'content.md'), `# Welcome
259
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `# Welcome
196
260
 
197
261
  ## Intro {#intro}
198
262
 
@@ -243,7 +307,7 @@ test('footer build information uses the site language and fixed formatting', asy
243
307
  await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `footer:
244
308
  buildInfo: true
245
309
  `);
246
- await writeFile(path.join(siteDir, 'content.md'), `---
310
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
247
311
  page:
248
312
  description: Footer build information.
249
313
  ---
@@ -267,7 +331,7 @@ test('page theme cannot define navigation logo settings', async () => {
267
331
  const { root, siteDir } = await createTempSite({ underRepoCache: true });
268
332
  try {
269
333
  await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
270
- await writeFile(path.join(siteDir, 'content.md'), `---
334
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
271
335
  page:
272
336
  description: Root page
273
337
  ---
@@ -346,7 +410,7 @@ test('multiple conventional navigation logo files stop config validation', async
346
410
  test('content:check warns for local Markdown images but not external Markdown images', async () => {
347
411
  const { root, siteDir } = await createTempSite();
348
412
  try {
349
- await writeFile(path.join(siteDir, 'content.md'), `---
413
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
350
414
  page:
351
415
  description: Fixture
352
416
  ---
@@ -374,7 +438,7 @@ page:
374
438
  test('managed SVG images are copied as static image output', async () => {
375
439
  const { root, siteDir } = await createTempSite();
376
440
  try {
377
- await writeFile(path.join(siteDir, 'content.md'), `---
441
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
378
442
  page:
379
443
  description: Fixture
380
444
  ---
@@ -388,18 +452,18 @@ page:
388
452
  alt: Diagram
389
453
  \`\`\`
390
454
  `);
391
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
392
- await writeFile(path.join(siteDir, 'images', 'diagram.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620"/></svg>\n');
455
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images'), { recursive: true });
456
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'diagram.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620"/></svg>\n');
393
457
 
394
458
  await runContentScript(siteDir, ['--check']);
395
459
  await runNorna(['--site-dir', siteDir, 'images']);
396
460
 
397
461
  const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
398
- const entry = manifest['images/diagram.svg'];
462
+ const entry = manifest['pages/000-home/images/diagram.svg'];
399
463
  assert.equal(entry.kind, 'static');
400
464
  assert.equal(entry.width, 900);
401
465
  assert.equal(entry.height, 620);
402
- assert.match(entry.src, /^\/images\/original\/images\/diagram-[a-f0-9]{8}\.svg$/);
466
+ assert.match(entry.src, /^\/images\/original\/pages\/000-home\/images\/diagram-[a-f0-9]{8}\.svg$/);
403
467
  assert.equal(entry.variants, undefined);
404
468
  assert.equal(await fileExists(path.join(siteDir, '.norna', 'public', entry.src.replace(/^\//, ''))), true);
405
469
  } finally {
@@ -410,7 +474,7 @@ page:
410
474
  test('inline notes render as linked numbered margin notes', async () => {
411
475
  const { root, siteDir } = await createTempSite({ underRepoCache: true });
412
476
  try {
413
- await writeFile(path.join(siteDir, 'content.md'), `---
477
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
414
478
  page:
415
479
  description: Fixture
416
480
  ---
@@ -450,7 +514,7 @@ The second paragraph has its own explanation.{note-ref}
450
514
  test('updated managed SVG images get updated static output', async () => {
451
515
  const { root, siteDir } = await createTempSite();
452
516
  try {
453
- await writeFile(path.join(siteDir, 'content.md'), `---
517
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
454
518
  page:
455
519
  description: Fixture
456
520
  ---
@@ -464,22 +528,22 @@ page:
464
528
  alt: Diagram
465
529
  \`\`\`
466
530
  `);
467
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
468
- const sourcePath = path.join(siteDir, 'images', 'diagram.svg');
531
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images'), { recursive: true });
532
+ const sourcePath = path.join(siteDir, 'pages', '000-home', 'images', 'diagram.svg');
469
533
  await writeFile(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620" fill="red"/></svg>\n');
470
534
 
471
535
  await runContentScript(siteDir, ['--check']);
472
536
  await runNorna(['--site-dir', siteDir, 'images']);
473
537
 
474
538
  const firstManifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
475
- const firstEntry = firstManifest['images/diagram.svg'];
539
+ const firstEntry = firstManifest['pages/000-home/images/diagram.svg'];
476
540
  const firstOutputPath = path.join(siteDir, '.norna', 'public', firstEntry.src.replace(/^\//, ''));
477
541
 
478
542
  await writeFile(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620" fill="blue"/></svg>\n');
479
543
  await runNorna(['--site-dir', siteDir, 'images']);
480
544
 
481
545
  const secondManifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
482
- const secondEntry = secondManifest['images/diagram.svg'];
546
+ const secondEntry = secondManifest['pages/000-home/images/diagram.svg'];
483
547
  const secondOutputPath = path.join(siteDir, '.norna', 'public', secondEntry.src.replace(/^\//, ''));
484
548
 
485
549
  assert.notEqual(secondEntry.sourceHash, firstEntry.sourceHash);
@@ -494,7 +558,7 @@ page:
494
558
  test('content:check warns when carousel SVG images have no intrinsic aspect ratio', async () => {
495
559
  const { root, siteDir } = await createTempSite();
496
560
  try {
497
- await writeFile(path.join(siteDir, 'content.md'), `---
561
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
498
562
  page:
499
563
  description: Fixture
500
564
  ---
@@ -510,9 +574,9 @@ page:
510
574
  alt: Second
511
575
  \`\`\`
512
576
  `);
513
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
514
- await writeFile(path.join(siteDir, 'images', 'first.svg'), '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>\n');
515
- await writeFile(path.join(siteDir, 'images', 'second.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100"/></svg>\n');
577
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images'), { recursive: true });
578
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'first.svg'), '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>\n');
579
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'second.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100"/></svg>\n');
516
580
 
517
581
  const { stdout } = await runContentScript(siteDir, ['--check']);
518
582
  assert.match(stdout, /Content check completed with warnings\./);
@@ -525,7 +589,7 @@ page:
525
589
  test('norna-card-list images are managed image references', async () => {
526
590
  const { root, siteDir } = await createTempSite({ underRepoCache: true });
527
591
  try {
528
- await writeFile(path.join(siteDir, 'content.md'), `---
592
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
529
593
  page:
530
594
  description: Fixture
531
595
  ---
@@ -549,15 +613,15 @@ width: narrow
549
613
  text: Support the shelter.
550
614
  \`\`\`
551
615
  `);
552
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
553
- await writeFile(path.join(siteDir, 'images', 'adopt.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100"><rect width="160" height="100"/></svg>\n');
616
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images'), { recursive: true });
617
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'adopt.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100"><rect width="160" height="100"/></svg>\n');
554
618
 
555
619
  const { stdout } = await runContentScript(siteDir, ['--check']);
556
620
  assert.match(stdout, /Content check passed\./);
557
621
 
558
622
  await runNorna(['--site-dir', siteDir, 'images']);
559
623
  const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
560
- assert.equal(manifest['images/adopt.svg'].kind, 'static');
624
+ assert.equal(manifest['pages/000-home/images/adopt.svg'].kind, 'static');
561
625
 
562
626
  await runNorna(['--site-dir', siteDir, 'build']);
563
627
  const html = await readFile(path.join(root, 'dist', 'index.html'), 'utf8');
@@ -569,7 +633,7 @@ width: narrow
569
633
  assert.match(html, /class="card-list-badge">Recommended<\/p>/);
570
634
  assert.match(html, /Give a dog a new home\./);
571
635
  assert.match(html, /href="\/adopt\/"/);
572
- assert.match(html, /src="\/images\/original\/images\/adopt-[a-f0-9]{8}\.svg"/);
636
+ assert.match(html, /src="\/images\/original\/pages\/000-home\/images\/adopt-[a-f0-9]{8}\.svg"/);
573
637
  } finally {
574
638
  await rm(root, { recursive: true, force: true });
575
639
  }
@@ -578,7 +642,7 @@ width: narrow
578
642
  test('content:check fails when a norna-card-list image file is missing', async () => {
579
643
  const { root, siteDir } = await createTempSite();
580
644
  try {
581
- await writeFile(path.join(siteDir, 'content.md'), `---
645
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
582
646
  page:
583
647
  description: Fixture
584
648
  ---
@@ -596,7 +660,7 @@ page:
596
660
  await assert.rejects(
597
661
  () => runContentScript(siteDir, ['--check']),
598
662
  (error) => {
599
- assert.match(error.output, /Image "missing\.svg" does not exist at .*site\/images\/missing\.svg or anywhere under any page image root\./);
663
+ assert.match(error.output, /Image "missing\.svg" does not exist at .*site\/pages\/000-home\/images\/missing\.svg or anywhere under any page image root\./);
600
664
  return true;
601
665
  },
602
666
  );
@@ -608,7 +672,7 @@ page:
608
672
  test('content:check fails for malformed norna-card-list blocks', async () => {
609
673
  const { root, siteDir } = await createTempSite();
610
674
  try {
611
- await writeFile(path.join(siteDir, 'content.md'), `---
675
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
612
676
  page:
613
677
  description: Fixture
614
678
  ---
@@ -637,7 +701,7 @@ page:
637
701
  test('content:check fails for invalid norna-card-list options', async () => {
638
702
  const { root, siteDir } = await createTempSite();
639
703
  try {
640
- await writeFile(path.join(siteDir, 'content.md'), `---
704
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
641
705
  page:
642
706
  description: Fixture
643
707
  ---
@@ -672,7 +736,7 @@ width: full
672
736
  test('content:check fails for invalid norna-card-list width', async () => {
673
737
  const { root, siteDir } = await createTempSite();
674
738
  try {
675
- await writeFile(path.join(siteDir, 'content.md'), `---
739
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
676
740
  page:
677
741
  description: Fixture
678
742
  ---
@@ -704,7 +768,7 @@ width: full
704
768
  test('content:check fails for invalid norna-card-list size', async () => {
705
769
  const { root, siteDir } = await createTempSite();
706
770
  try {
707
- await writeFile(path.join(siteDir, 'content.md'), `---
771
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
708
772
  page:
709
773
  description: Fixture
710
774
  ---
@@ -736,7 +800,7 @@ size: huge
736
800
  test('content:sync moves Norna-managed images inside the same page image root', async () => {
737
801
  const { root, siteDir } = await createTempSite();
738
802
  try {
739
- await writeFile(path.join(siteDir, 'content.md'), `---
803
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
740
804
  page:
741
805
  description: Fixture
742
806
  ---
@@ -750,12 +814,12 @@ page:
750
814
  alt: Moved image
751
815
  \`\`\`
752
816
  `);
753
- await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
754
- await writeFile(path.join(siteDir, 'images', 'old', 'moved.jpg'), 'fixture image');
817
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images', 'old'), { recursive: true });
818
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'old', 'moved.jpg'), 'fixture image');
755
819
 
756
820
  await runContentScript(siteDir, ['--write', '--yes']);
757
821
 
758
- const moved = await readFile(path.join(siteDir, 'images', 'moved.jpg'), 'utf8');
822
+ const moved = await readFile(path.join(siteDir, 'pages', '000-home', 'images', 'moved.jpg'), 'utf8');
759
823
  assert.equal(moved, 'fixture image');
760
824
  } finally {
761
825
  await rm(root, { recursive: true, force: true });
@@ -765,7 +829,7 @@ page:
765
829
  test('content:sync moves managed SVG images inside the same page image root', async () => {
766
830
  const { root, siteDir } = await createTempSite();
767
831
  try {
768
- await writeFile(path.join(siteDir, 'content.md'), `---
832
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
769
833
  page:
770
834
  description: Fixture
771
835
  ---
@@ -779,12 +843,12 @@ page:
779
843
  alt: Moved diagram
780
844
  \`\`\`
781
845
  `);
782
- await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
783
- await writeFile(path.join(siteDir, 'images', 'old', 'moved.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"></svg>\n');
846
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images', 'old'), { recursive: true });
847
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'old', 'moved.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"></svg>\n');
784
848
 
785
849
  await runContentScript(siteDir, ['--write', '--yes']);
786
850
 
787
- const moved = await readFile(path.join(siteDir, 'images', 'moved.svg'), 'utf8');
851
+ const moved = await readFile(path.join(siteDir, 'pages', '000-home', 'images', 'moved.svg'), 'utf8');
788
852
  assert.match(moved, /viewBox="0 0 10 10"/);
789
853
  } finally {
790
854
  await rm(root, { recursive: true, force: true });
@@ -798,7 +862,7 @@ test('content:sync moves Norna-managed images across page image roots when git i
798
862
  const targetPageDir = path.join(siteDir, 'pages', '020-target');
799
863
  await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
800
864
  await mkdir(targetPageDir, { recursive: true });
801
- await writeFile(path.join(siteDir, 'content.md'), `---
865
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
802
866
  page:
803
867
  description: Fixture
804
868
  ---
@@ -852,7 +916,7 @@ test('content:sync refuses cross-page image moves when git status is dirty', asy
852
916
  const targetPageDir = path.join(siteDir, 'pages', '020-target');
853
917
  await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
854
918
  await mkdir(targetPageDir, { recursive: true });
855
- await writeFile(path.join(siteDir, 'content.md'), `---
919
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
856
920
  page:
857
921
  description: Fixture
858
922
  ---
@@ -914,7 +978,7 @@ test('content:check reports cross-page image moves without requiring clean git s
914
978
  const targetPageDir = path.join(siteDir, 'pages', '020-target');
915
979
  await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
916
980
  await mkdir(targetPageDir, { recursive: true });
917
- await writeFile(path.join(siteDir, 'content.md'), `---
981
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
918
982
  page:
919
983
  description: Fixture
920
984
  ---
@@ -973,7 +1037,7 @@ test('content:sync refuses to move a cross-page image still referenced by its cu
973
1037
  const targetPageDir = path.join(siteDir, 'pages', '020-target');
974
1038
  await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
975
1039
  await mkdir(targetPageDir, { recursive: true });
976
- await writeFile(path.join(siteDir, 'content.md'), `---
1040
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
977
1041
  page:
978
1042
  description: Fixture
979
1043
  ---
@@ -1031,7 +1095,7 @@ page:
1031
1095
  test('CLI content:sync refreshes generated images after moving a carousel image', async () => {
1032
1096
  const { root, siteDir } = await createTempSite();
1033
1097
  try {
1034
- await writeFile(path.join(siteDir, 'content.md'), `---
1098
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1035
1099
  page:
1036
1100
  description: Fixture
1037
1101
  ---
@@ -1047,16 +1111,16 @@ page:
1047
1111
  alt: Slide two
1048
1112
  \`\`\`
1049
1113
  `);
1050
- await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
1051
- await cp(path.join(fixtureSiteDir, 'images', 'hero.jpg'), path.join(siteDir, 'images', 'old', 'slide-one.jpg'));
1052
- await cp(path.join(fixtureSiteDir, 'images', 'detail.jpg'), path.join(siteDir, 'images', 'slide-two.jpg'), { force: true });
1114
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images', 'old'), { recursive: true });
1115
+ await cp(path.join(fixtureSiteDir, 'pages', '000-home', 'images', 'hero.jpg'), path.join(siteDir, 'pages', '000-home', 'images', 'old', 'slide-one.jpg'));
1116
+ await cp(path.join(fixtureSiteDir, 'pages', '000-home', 'images', 'detail.jpg'), path.join(siteDir, 'pages', '000-home', 'images', 'slide-two.jpg'), { force: true });
1053
1117
 
1054
1118
  await runNorna(['--site-dir', siteDir, 'content:sync', '--yes']);
1055
1119
 
1056
1120
  const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
1057
- assert.equal(await fileExists(path.join(siteDir, 'images', 'slide-one.jpg')), true);
1058
- assert.ok(manifest['images/slide-one.jpg']);
1059
- assert.ok(manifest['images/slide-two.jpg']);
1121
+ assert.equal(await fileExists(path.join(siteDir, 'pages', '000-home', 'images', 'slide-one.jpg')), true);
1122
+ assert.ok(manifest['pages/000-home/images/slide-one.jpg']);
1123
+ assert.ok(manifest['pages/000-home/images/slide-two.jpg']);
1060
1124
  } finally {
1061
1125
  await rm(root, { recursive: true, force: true });
1062
1126
  }
@@ -1065,7 +1129,7 @@ page:
1065
1129
  test('automatic H2 and H3 ids render while explicit ids remain stable', async () => {
1066
1130
  const { root, siteDir } = await createTempSite({ underRepoCache: true });
1067
1131
  try {
1068
- await writeFile(path.join(siteDir, 'content.md'), `---
1132
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1069
1133
  page:
1070
1134
  description: Fixture
1071
1135
  ---
@@ -1098,7 +1162,7 @@ Explicit identifier.
1098
1162
  test('content:check fails for malformed Norna image blocks', async () => {
1099
1163
  const { root, siteDir } = await createTempSite();
1100
1164
  try {
1101
- await writeFile(path.join(siteDir, 'content.md'), `---
1165
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1102
1166
  page:
1103
1167
  description: Fixture
1104
1168
  ---
@@ -1128,9 +1192,9 @@ image hero.jpg
1128
1192
  test('content:check fails for unknown Norna block names', async () => {
1129
1193
  const { root, siteDir } = await createTempSite();
1130
1194
  try {
1131
- await mkdir(path.join(siteDir, 'images', 'plain'), { recursive: true });
1132
- await writeFile(path.join(siteDir, 'images', 'plain', 'image.jpg'), 'fixture image');
1133
- await writeFile(path.join(siteDir, 'content.md'), `---
1195
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images', 'plain'), { recursive: true });
1196
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'plain', 'image.jpg'), 'fixture image');
1197
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1134
1198
  page:
1135
1199
  description: Fixture
1136
1200
  ---
@@ -1161,7 +1225,7 @@ page:
1161
1225
  test('content:check rejects the removed norna-note block', async () => {
1162
1226
  const { root, siteDir } = await createTempSite();
1163
1227
  try {
1164
- await writeFile(path.join(siteDir, 'content.md'), `---
1228
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1165
1229
  page:
1166
1230
  description: Fixture
1167
1231
  ---
@@ -1190,9 +1254,9 @@ This block is no longer supported.
1190
1254
  test('content:check supports tilde fenced Norna blocks', async () => {
1191
1255
  const { root, siteDir } = await createTempSite();
1192
1256
  try {
1193
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
1194
- await writeFile(path.join(siteDir, 'images', 'hero.jpg'), 'fixture image');
1195
- await writeFile(path.join(siteDir, 'content.md'), `---
1257
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images'), { recursive: true });
1258
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'hero.jpg'), 'fixture image');
1259
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1196
1260
  page:
1197
1261
  description: Fixture
1198
1262
  ---
@@ -1216,7 +1280,7 @@ page:
1216
1280
  test('content:check ignores Norna blocks shown inside longer code fences', async () => {
1217
1281
  const { root, siteDir } = await createTempSite();
1218
1282
  try {
1219
- await writeFile(path.join(siteDir, 'content.md'), `---
1283
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1220
1284
  page:
1221
1285
  description: Fixture
1222
1286
  ---
@@ -1242,7 +1306,7 @@ page:
1242
1306
  test('content:check fails for likely Norna block names outside fences', async () => {
1243
1307
  const { root, siteDir } = await createTempSite();
1244
1308
  try {
1245
- await writeFile(path.join(siteDir, 'content.md'), `---
1309
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1246
1310
  page:
1247
1311
  description: Fixture
1248
1312
  ---
@@ -1271,7 +1335,7 @@ norna-image-stack
1271
1335
  test('content:check fails for short Norna block fences', async () => {
1272
1336
  const { root, siteDir } = await createTempSite();
1273
1337
  try {
1274
- await writeFile(path.join(siteDir, 'content.md'), `---
1338
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1275
1339
  page:
1276
1340
  description: Fixture
1277
1341
  ---
@@ -1301,7 +1365,7 @@ page:
1301
1365
  test('content:check fails for unclosed Norna blocks', async () => {
1302
1366
  const { root, siteDir } = await createTempSite();
1303
1367
  try {
1304
- await writeFile(path.join(siteDir, 'content.md'), `---
1368
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1305
1369
  page:
1306
1370
  description: Fixture
1307
1371
  ---
@@ -1331,7 +1395,7 @@ page:
1331
1395
  test('content:check continues after malformed Norna blocks', async () => {
1332
1396
  const { root, siteDir } = await createTempSite();
1333
1397
  try {
1334
- await writeFile(path.join(siteDir, 'content.md'), `---
1398
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1335
1399
  page:
1336
1400
  description: Fixture
1337
1401
  ---
@@ -1362,8 +1426,8 @@ image broken.jpg
1362
1426
  () => runContentScript(siteDir, ['--check']),
1363
1427
  (error) => {
1364
1428
  assert.match(error.output, /Invalid norna-image-stack entry "image broken\.jpg"\. Start each image with "- image: filename\.jpg"\./);
1365
- assert.match(error.output, /Image "missing-intro\.jpg" does not exist at .*site\/images\/missing-intro\.jpg or anywhere under any page image root\./);
1366
- assert.match(error.output, /Image "missing-more\.jpg" does not exist at .*site\/images\/missing-more\.jpg or anywhere under any page image root\./);
1429
+ assert.match(error.output, /Image "missing-intro\.jpg" does not exist at .*site\/pages\/000-home\/images\/missing-intro\.jpg or anywhere under any page image root\./);
1430
+ assert.match(error.output, /Image "missing-more\.jpg" does not exist at .*site\/pages\/000-home\/images\/missing-more\.jpg or anywhere under any page image root\./);
1367
1431
  return true;
1368
1432
  },
1369
1433
  );
@@ -1375,9 +1439,9 @@ image broken.jpg
1375
1439
  test('content:check allows Norna images without alt text', async () => {
1376
1440
  const { root, siteDir } = await createTempSite();
1377
1441
  try {
1378
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
1379
- await writeFile(path.join(siteDir, 'images', 'hero.jpg'), 'fixture image');
1380
- await writeFile(path.join(siteDir, 'content.md'), `---
1442
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images'), { recursive: true });
1443
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'hero.jpg'), 'fixture image');
1444
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1381
1445
  page:
1382
1446
  description: Fixture
1383
1447
  ---
@@ -1401,7 +1465,7 @@ page:
1401
1465
  test('content:check reports single-image carousel and missing file together', async () => {
1402
1466
  const { root, siteDir } = await createTempSite();
1403
1467
  try {
1404
- await writeFile(path.join(siteDir, 'content.md'), `---
1468
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1405
1469
  page:
1406
1470
  description: Fixture
1407
1471
  ---
@@ -1419,7 +1483,7 @@ page:
1419
1483
  () => runContentScript(siteDir, ['--check']),
1420
1484
  (error) => {
1421
1485
  assert.match(error.output, /norna-image-carousel on line \d+ contains 1 image\. A carousel needs at least two images\./);
1422
- assert.match(error.output, /Image "foo\.jpg" does not exist at .*site\/images\/foo\.jpg or anywhere under any page image root\./);
1486
+ assert.match(error.output, /Image "foo\.jpg" does not exist at .*site\/pages\/000-home\/images\/foo\.jpg or anywhere under any page image root\./);
1423
1487
  return true;
1424
1488
  },
1425
1489
  );
@@ -1431,7 +1495,7 @@ page:
1431
1495
  test('content:check fails when a Norna-managed image file is missing', async () => {
1432
1496
  const { root, siteDir } = await createTempSite();
1433
1497
  try {
1434
- await writeFile(path.join(siteDir, 'content.md'), `---
1498
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1435
1499
  page:
1436
1500
  description: Fixture
1437
1501
  ---
@@ -1449,7 +1513,7 @@ page:
1449
1513
  await assert.rejects(
1450
1514
  () => runContentScript(siteDir, ['--check']),
1451
1515
  (error) => {
1452
- assert.match(error.output, /Image "missing\.jpg" does not exist at .*site\/images\/missing\.jpg or anywhere under any page image root\./);
1516
+ assert.match(error.output, /Image "missing\.jpg" does not exist at .*site\/pages\/000-home\/images\/missing\.jpg or anywhere under any page image root\./);
1453
1517
  return true;
1454
1518
  },
1455
1519
  );
@@ -1461,7 +1525,7 @@ page:
1461
1525
  test('content:check fails when a page repeats a section id', async () => {
1462
1526
  const { root, siteDir } = await createTempSite();
1463
1527
  try {
1464
- await writeFile(path.join(siteDir, 'content.md'), `---
1528
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1465
1529
  page:
1466
1530
  description: Fixture
1467
1531
  ---
@@ -1492,7 +1556,7 @@ Second.
1492
1556
  test('content:check reports collisions between automatic H2 and H3 ids', async () => {
1493
1557
  const { root, siteDir } = await createTempSite();
1494
1558
  try {
1495
- await writeFile(path.join(siteDir, 'content.md'), `---
1559
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1496
1560
  page:
1497
1561
  description: Fixture
1498
1562
  ---
@@ -1525,7 +1589,7 @@ Topic text.
1525
1589
  test('content:check rejects removed sections frontmatter', async () => {
1526
1590
  const { root, siteDir } = await createTempSite();
1527
1591
  try {
1528
- await writeFile(path.join(siteDir, 'content.md'), `---
1592
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1529
1593
  page:
1530
1594
  description: Fixture
1531
1595
  sections:
@@ -1554,7 +1618,7 @@ Text.
1554
1618
  test('content:sync refuses ambiguous image relocation', async () => {
1555
1619
  const { root, siteDir } = await createTempSite();
1556
1620
  try {
1557
- await writeFile(path.join(siteDir, 'content.md'), `---
1621
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1558
1622
  page:
1559
1623
  description: Fixture
1560
1624
  ---
@@ -1568,24 +1632,24 @@ page:
1568
1632
  alt: Shared
1569
1633
  \`\`\`
1570
1634
  `);
1571
- await mkdir(path.join(siteDir, 'images', 'old-a'), { recursive: true });
1572
- await mkdir(path.join(siteDir, 'images', 'old-b'), { recursive: true });
1573
- await writeFile(path.join(siteDir, 'images', 'old-a', 'shared.jpg'), 'a');
1574
- await writeFile(path.join(siteDir, 'images', 'old-b', 'shared.jpg'), 'b');
1635
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images', 'old-a'), { recursive: true });
1636
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images', 'old-b'), { recursive: true });
1637
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'old-a', 'shared.jpg'), 'a');
1638
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'old-b', 'shared.jpg'), 'b');
1575
1639
 
1576
1640
  await assert.rejects(
1577
1641
  () => runContentScript(siteDir, ['--write', '--yes']),
1578
1642
  (error) => {
1579
1643
  assert.match(error.output, /Cannot relocate "shared\.jpg"\. Multiple files with this filename were found:/);
1580
- assert.match(error.output, /site\/images\/old-a\/shared\.jpg/);
1581
- assert.match(error.output, /site\/images\/old-b\/shared\.jpg/);
1644
+ assert.match(error.output, /site\/pages\/000-home\/images\/old-a\/shared\.jpg/);
1645
+ assert.match(error.output, /site\/pages\/000-home\/images\/old-b\/shared\.jpg/);
1582
1646
  return true;
1583
1647
  },
1584
1648
  );
1585
1649
 
1586
- assert.equal(await fileExists(path.join(siteDir, 'images', 'shared.jpg')), false);
1587
- assert.equal(await fileExists(path.join(siteDir, 'images', 'old-a', 'shared.jpg')), true);
1588
- assert.equal(await fileExists(path.join(siteDir, 'images', 'old-b', 'shared.jpg')), true);
1650
+ assert.equal(await fileExists(path.join(siteDir, 'pages', '000-home', 'images', 'shared.jpg')), false);
1651
+ assert.equal(await fileExists(path.join(siteDir, 'pages', '000-home', 'images', 'old-a', 'shared.jpg')), true);
1652
+ assert.equal(await fileExists(path.join(siteDir, 'pages', '000-home', 'images', 'old-b', 'shared.jpg')), true);
1589
1653
  } finally {
1590
1654
  await rm(root, { recursive: true, force: true });
1591
1655
  }
@@ -1594,7 +1658,7 @@ page:
1594
1658
  test('content:sync moves one page-local image once when multiple sections reference it', async () => {
1595
1659
  const { root, siteDir } = await createTempSite();
1596
1660
  try {
1597
- await writeFile(path.join(siteDir, 'content.md'), `---
1661
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'content.md'), `---
1598
1662
  page:
1599
1663
  description: Fixture
1600
1664
  ---
@@ -1615,13 +1679,13 @@ page:
1615
1679
  alt: Shared copy
1616
1680
  \`\`\`
1617
1681
  `);
1618
- await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
1619
- await writeFile(path.join(siteDir, 'images', 'intro', 'shared.jpg'), 'shared image');
1682
+ await mkdir(path.join(siteDir, 'pages', '000-home', 'images', 'intro'), { recursive: true });
1683
+ await writeFile(path.join(siteDir, 'pages', '000-home', 'images', 'intro', 'shared.jpg'), 'shared image');
1620
1684
 
1621
1685
  await runContentScript(siteDir, ['--write', '--yes']);
1622
1686
 
1623
- assert.equal(await fileExists(path.join(siteDir, 'images', 'intro', 'shared.jpg')), false);
1624
- assert.equal(await fileExists(path.join(siteDir, 'images', 'shared.jpg')), true);
1687
+ assert.equal(await fileExists(path.join(siteDir, 'pages', '000-home', 'images', 'intro', 'shared.jpg')), false);
1688
+ assert.equal(await fileExists(path.join(siteDir, 'pages', '000-home', 'images', 'shared.jpg')), true);
1625
1689
  await runContentScript(siteDir, ['--check']);
1626
1690
  } finally {
1627
1691
  await rm(root, { recursive: true, force: true });