@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
@@ -2,28 +2,31 @@ import { access, readdir, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {
4
4
  siteContentLabel,
5
- siteContentPath,
6
5
  siteDir,
7
- siteImagesDir,
6
+ siteDirLabel,
7
+ homePageDirectory,
8
8
  siteImagesLabel,
9
- siteRoutesDir,
10
- siteRoutesLabel,
9
+ sitePagesDir,
10
+ sitePagesLabel,
11
+ sitewideContentLabel,
11
12
  } from './site-paths.mjs';
12
- import { parseRouteDirectory } from './route-model.mjs';
13
+ import { parsePageDirectoryPath } from './page-model.mjs';
14
+ import { getMarkdownHeadings } from './heading-ids.mjs';
15
+ import { schemaTopLevelKeys } from './schema-definitions.mjs';
13
16
 
14
17
  export const rasterImageExtensions = new Set(['.jpg', '.jpeg', '.png']);
15
18
  export const staticImageExtensions = new Set(['.svg']);
16
19
  export const supportedImageExtensions = new Set([...rasterImageExtensions, ...staticImageExtensions]);
17
20
 
18
- const h2Regex = /^##\s+.*$/gm;
19
- const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
20
21
  const deprecatedInlineStyleReferenceRegex = /\[[^\]\n]+\]\{\.([a-z][a-z0-9-]*)\}/g;
21
22
  const frontmatterDelimiterRegex = /^---\s*$/;
22
- const knownConfigTopLevelFrontmatterKeys = new Set(['url', 'language', 'scrollBehavior']);
23
- const knownContentTopLevelFrontmatterKeys = new Set(['title', 'description', 'navigation', 'sections']);
24
- const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces']);
25
- const knownRouteThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces']);
26
- const knownSitewideTopLevelFrontmatterKeys = new Set(['navigation', 'banners', 'footer']);
23
+ const knownConfigTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.config);
24
+ const knownContentTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.content);
25
+ const knownThemeTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.theme);
26
+ const knownPageThemeTopLevelFrontmatterKeys = new Set(
27
+ schemaTopLevelKeys.pageTheme,
28
+ );
29
+ const knownSitewideTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.sitewide);
27
30
  const knownNestedFrontmatterKeys = new Set([
28
31
  'align',
29
32
  'alt',
@@ -31,7 +34,7 @@ const knownNestedFrontmatterKeys = new Set([
31
34
  'blockGap',
32
35
  'caption',
33
36
  'carousel',
34
- 'density',
37
+ 'contentSpacing',
35
38
  'desktop',
36
39
  'fontFamily',
37
40
  'from',
@@ -45,11 +48,10 @@ const knownNestedFrontmatterKeys = new Set([
45
48
  'heading',
46
49
  'headings',
47
50
  'id',
48
- 'include',
49
51
  'image',
50
52
  'imageGap',
51
53
  'images',
52
- 'label',
54
+ 'listed',
53
55
  'logo',
54
56
  'lineHeight',
55
57
  'maxAvailableHeightPercent',
@@ -63,13 +65,15 @@ const knownNestedFrontmatterKeys = new Set([
63
65
  'preset',
64
66
  'profile',
65
67
  'rhythm',
66
- 'sections',
67
68
  'sectionGap',
68
- 'sectionSurfaces',
69
+ 'backgroundPattern',
70
+ 'sections',
71
+ 'shape',
69
72
  'size',
70
73
  'spacingAfter',
71
74
  'spacingBefore',
72
75
  'theme',
76
+ 'textWidth',
73
77
  'typography',
74
78
  'until',
75
79
  'visible',
@@ -81,45 +85,99 @@ export const toPosixPath = (filePath) => filePath.split(path.sep).join('/');
81
85
  const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
82
86
 
83
87
  export const getContentFiles = async () => {
84
- const contentFiles = [{
85
- contentLabel: siteContentLabel,
86
- contentPath: siteContentPath,
87
- imagesDir: siteImagesDir,
88
- imagesLabel: siteImagesLabel,
89
- isHome: true,
90
- routeDirectory: null,
91
- routeId: null,
92
- routeOrder: 0,
93
- }];
94
- const routeEntries = await readdir(siteRoutesDir, { withFileTypes: true }).catch((error) => {
95
- if (error?.code === 'ENOENT') {
96
- return [];
97
- }
88
+ const legacyContentPath = path.join(siteDir, 'content.md');
89
+ const legacyImagesPath = path.join(siteDir, 'images');
90
+ const legacyPaths = [];
91
+ if (await fileExists(legacyContentPath)) legacyPaths.push(`${siteDirLabel}/content.md`);
92
+ if (await fileExists(legacyImagesPath)) legacyPaths.push(`${siteDirLabel}/images`);
93
+ if (legacyPaths.length > 0) {
94
+ throw new Error([
95
+ `The old root-page structure is no longer supported: ${legacyPaths.join(', ')}.`,
96
+ `Move the homepage content to ${sitePagesLabel}/${homePageDirectory}/content.md and its images to ${sitePagesLabel}/${homePageDirectory}/images/.`,
97
+ ].join('\n'));
98
+ }
98
99
 
100
+ const legacyRoutesDir = path.join(siteDir, 'routes');
101
+ const legacyRoutes = await readdir(legacyRoutesDir).catch((error) => {
102
+ if (error?.code === 'ENOENT') return null;
99
103
  throw error;
100
104
  });
105
+ if (legacyRoutes !== null) {
106
+ throw new Error(`${siteDirLabel}/routes is no longer supported. Rename it to ${sitePagesLabel} and use NNN-page-id directory names.`);
107
+ }
101
108
 
102
- for (const entry of routeEntries) {
103
- if (!entry.isDirectory()) continue;
109
+ const contentFiles = [];
110
+ const collectPageContentFiles = async (pagesDir, pagesLabel, parentPageDirectory = '') => {
111
+ const pageEntries = await readdir(pagesDir, { withFileTypes: true }).catch((error) => {
112
+ if (error?.code === 'ENOENT') {
113
+ return [];
114
+ }
104
115
 
105
- const routeDirectory = entry.name;
106
- const routeDir = path.join(siteRoutesDir, routeDirectory);
107
- const routeContentPath = path.join(routeDir, 'content.md');
116
+ throw error;
117
+ });
108
118
 
109
- if (!(await fileExists(routeContentPath))) continue;
119
+ for (const entry of pageEntries.sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
120
+ if (!entry.isDirectory()) continue;
121
+
122
+ const pageDirectory = parentPageDirectory
123
+ ? `${parentPageDirectory}/pages/${entry.name}`
124
+ : entry.name;
125
+ const pageLabel = `${pagesLabel}/${entry.name}`;
126
+ const pageDir = path.join(pagesDir, entry.name);
127
+ const pageContentPath = path.join(pageDir, 'content.md');
128
+
129
+ if (!(await fileExists(pageContentPath))) {
130
+ const childPageEntries = await readdir(path.join(pageDir, 'pages'), { withFileTypes: true }).catch((error) => {
131
+ if (error?.code === 'ENOENT') return [];
132
+ throw error;
133
+ });
134
+ if (childPageEntries.some((childEntry) => childEntry.isDirectory())) {
135
+ throw new Error(`${pageLabel} contains nested pages but has no content.md of its own.`);
136
+ }
137
+ continue;
138
+ }
139
+ const pageMetadata = parsePageDirectoryPath(pageDirectory, pageLabel);
140
+ const isHome = pageDirectory === homePageDirectory;
141
+
142
+ contentFiles.push({
143
+ contentLabel: `${pageLabel}/content.md`,
144
+ contentPath: pageContentPath,
145
+ imagesDir: path.join(pageDir, 'images'),
146
+ imagesLabel: `${pageLabel}/images`,
147
+ isHome,
148
+ ...pageMetadata,
149
+ pagePath: isHome ? '' : pageMetadata.pagePath,
150
+ parentPagePath: isHome ? null : pageMetadata.parentPagePath,
151
+ });
110
152
 
111
- const { routeId, routeOrder } = parseRouteDirectory(routeDirectory, `${siteRoutesLabel}/${routeDirectory}`);
153
+ if (isHome) {
154
+ const homePagesDir = path.join(pageDir, 'pages');
155
+ const homeChildEntries = await readdir(homePagesDir, { withFileTypes: true }).catch((error) => {
156
+ if (error?.code === 'ENOENT') return [];
157
+ throw error;
158
+ });
159
+ const homeChildDirectories = homeChildEntries.filter((childEntry) => childEntry.isDirectory());
160
+ if (homeChildDirectories.length > 0) {
161
+ throw new Error([
162
+ `${pageLabel} is the homepage and cannot contain child pages.`,
163
+ `Move these page directories beside ${homePageDirectory} under ${sitePagesLabel}/, or below another non-home page:`,
164
+ ...homeChildDirectories.map(({ name }) => `- ${pageLabel}/pages/${name}`),
165
+ ].join('\n'));
166
+ }
167
+ continue;
168
+ }
169
+
170
+ await collectPageContentFiles(
171
+ path.join(pageDir, 'pages'),
172
+ `${pageLabel}/pages`,
173
+ pageDirectory,
174
+ );
175
+ }
176
+ };
112
177
 
113
- contentFiles.push({
114
- contentLabel: `${siteRoutesLabel}/${routeDirectory}/content.md`,
115
- contentPath: routeContentPath,
116
- imagesDir: path.join(routeDir, 'images'),
117
- imagesLabel: `${siteRoutesLabel}/${routeDirectory}/images`,
118
- isHome: false,
119
- routeDirectory,
120
- routeId,
121
- routeOrder,
122
- });
178
+ await collectPageContentFiles(sitePagesDir, sitePagesLabel);
179
+ if (!contentFiles.some(({ isHome }) => isHome)) {
180
+ throw new Error(`Homepage content is missing. Create ${siteContentLabel}.`);
123
181
  }
124
182
 
125
183
  return contentFiles;
@@ -129,7 +187,15 @@ export const splitSiteFile = (source, label = siteContentLabel) => {
129
187
  const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
130
188
 
131
189
  if (!match) {
132
- throw new Error(`${label} is missing frontmatter delimited by ---.`);
190
+ if (/^---(?:\r?\n|$)/.test(source)) {
191
+ throw new Error(`${label} starts YAML frontmatter with ---, but no closing --- delimiter was found.`);
192
+ }
193
+
194
+ return {
195
+ frontmatter: '',
196
+ frontmatterBody: '',
197
+ body: source,
198
+ };
133
199
  }
134
200
 
135
201
  return {
@@ -250,10 +316,27 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
250
316
  if (knownTopLevelFrontmatterKeys.has(key)) continue;
251
317
 
252
318
  let fix;
253
- if (fileKind === 'content' && key === 'images') {
319
+ let message = `Frontmatter line ${lineNumber} defines "${key}" at the top level, but it is not a valid top-level ${fileKind} field.`;
320
+ if (fileKind === 'content' && key === 'title') {
321
+ fix = 'Remove "title:" and write the page title as the single Markdown H1 after frontmatter, for example "# About".';
322
+ } else if (fileKind === 'content' && key === 'description') {
323
+ fix = 'Indent "description:" under "page:". The optional meta description belongs in the page object.';
324
+ } else if (fileKind === 'content' && key === 'images') {
254
325
  fix = 'Put local image references in norna-image-stack or norna-image-carousel blocks in the Markdown body.';
255
326
  } else if (fileKind === 'content' && knownThemeTopLevelFrontmatterKeys.has(key)) {
256
327
  fix = `Move "${key}:" to theme.yaml. Visual settings do not belong in content frontmatter.`;
328
+ } else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'navigation') {
329
+ message = `Frontmatter line ${lineNumber}: navigation is technical, site-wide configuration and does not belong in ${fileKind}. Set it in config.yaml.`;
330
+ fix = 'Set the navigation mode under "navigation:" in config.yaml.';
331
+ } else if (fileKind === 'page theme' && ['preset', 'shape', 'palette', 'typography'].includes(key)) {
332
+ message = `Frontmatter line ${lineNumber}: page themes may not define site-wide visual identity through "${key}".`;
333
+ fix = `Move "${key}:" to the root theme.yaml. Page themes may set only layout.textWidth, layout.contentSpacing, images, and sections.backgroundPattern.`;
334
+ } else if ((fileKind === 'theme' || fileKind === 'page theme') && ['logo', 'site'].includes(key)) {
335
+ message = `Frontmatter line ${lineNumber}: ${fileKind} may not define navigation logo settings. Optional logo display settings belong under "logo:" in ${sitewideContentLabel}.`;
336
+ fix = `Move only the logo display settings under "logo:" in ${sitewideContentLabel}; the homepage Markdown H1 supplies navigation text and logo alternative text.`;
337
+ } else if (fileKind === 'sitewide content' && (key === 'navigation' || key === 'site')) {
338
+ message = `Frontmatter line ${lineNumber}: "${key}:" no longer defines site identity.`;
339
+ fix = 'Use the homepage Markdown H1 for navigation text and logo alternative text. Move only an optional logo height to top-level "logo:".';
257
340
  } else if (knownNestedFrontmatterKeys.has(key)) {
258
341
  fix = `Indent "${key}:" under the object it belongs to.`;
259
342
  } else {
@@ -262,7 +345,7 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
262
345
 
263
346
  addIssue({
264
347
  severity: 'error',
265
- message: `Frontmatter line ${lineNumber} defines "${key}" at the top level, but it is not a valid top-level ${fileKind} field.`,
348
+ message,
266
349
  fix,
267
350
  });
268
351
  }
@@ -286,10 +369,10 @@ export const validateThemeYamlStructure = (frontmatter, addIssue) =>
286
369
  fileKind: 'theme',
287
370
  });
288
371
 
289
- export const validateRouteThemeYamlStructure = (frontmatter, addIssue) =>
372
+ export const validatePageThemeYamlStructure = (frontmatter, addIssue) =>
290
373
  validateFrontmatterStructure(frontmatter, addIssue, {
291
- knownTopLevelFrontmatterKeys: knownRouteThemeTopLevelFrontmatterKeys,
292
- fileKind: 'route theme',
374
+ knownTopLevelFrontmatterKeys: knownPageThemeTopLevelFrontmatterKeys,
375
+ fileKind: 'page theme',
293
376
  });
294
377
 
295
378
  export const validateSitewideYamlStructure = (frontmatter, addIssue) =>
@@ -303,53 +386,32 @@ export const readThemeFile = async (sitePath) => {
303
386
  return { frontmatter: source, frontmatterBody: source, body: '' };
304
387
  };
305
388
 
306
- export const getFrontmatterSections = (frontmatter) => {
307
- const sections = [];
308
- const lines = frontmatter.split(/\r?\n/);
309
- let inSections = false;
310
-
311
- for (const [index, line] of lines.entries()) {
312
- if (/^sections:\s*$/.test(line)) {
313
- inSections = true;
314
- continue;
315
- }
316
-
317
- if (!inSections) continue;
318
- if (/^[a-zA-Z0-9_-]+:/.test(line)) break;
319
-
320
- const sectionMatch = line.match(/^\s{2}([a-z0-9-]+):(?:\s+.*)?\s*$/);
321
- if (sectionMatch) {
322
- sections.push({ id: sectionMatch[1], line: index + 1 });
323
- }
324
- }
325
-
326
- return sections;
327
- };
328
-
329
- export const getHeadingId = (heading) => heading.match(explicitHeadingIdRegex)?.[1];
330
-
331
389
  export const getDeprecatedInlineStyleReferences = (body) => Array.from(body.matchAll(deprecatedInlineStyleReferenceRegex))
332
390
  .map((match) => match[1]);
333
391
 
334
- export const getBodySections = (body) => {
335
- const matches = Array.from(body.matchAll(h2Regex));
336
- const prelude = matches.length > 0 ? body.slice(0, matches[0].index) : body;
337
- const sections = [];
338
-
339
- for (let index = 0; index < matches.length; index += 1) {
340
- const match = matches[index];
341
- const start = match.index ?? 0;
342
- const next = matches[index + 1];
343
- const end = next?.index ?? body.length;
344
- const text = body.slice(start, end).trimEnd();
345
- const heading = match[0];
346
- const id = getHeadingId(heading);
347
- const line = body.slice(0, start).split(/\r?\n/).length;
348
-
349
- sections.push({ id, heading, line, text });
350
- }
392
+ export const getBodySections = async (body) => {
393
+ const { headings, source } = await getMarkdownHeadings(body);
394
+ const structuralHeadings = headings.filter((heading) => heading.depth <= 2);
395
+ const prelude = structuralHeadings.length > 0 ? source.slice(0, structuralHeadings[0].index) : source;
396
+ const sections = structuralHeadings.map((heading, index) => {
397
+ const next = structuralHeadings[index + 1];
398
+ const end = next?.index ?? source.length;
399
+
400
+ return {
401
+ ...heading,
402
+ heading: heading.source,
403
+ isPageTitle: heading.depth === 1,
404
+ level: heading.depth,
405
+ text: source.slice(heading.index, end).trimEnd(),
406
+ };
407
+ });
351
408
 
352
- return { prelude, sections };
409
+ return {
410
+ headings,
411
+ pageHeadings: sections.filter((section) => section.isPageTitle),
412
+ prelude,
413
+ sections,
414
+ };
353
415
  };
354
416
 
355
417
  export const getImageFiles = async (directory) => {
@@ -0,0 +1 @@
1
+ export const homePageDirectory = '000-home';
@@ -1,10 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { homePageDirectory } from './site-conventions.mjs';
4
5
 
5
6
  const siteDirectoryEnvName = 'NORNA_SITE_DIR';
6
7
  const invocationRootEnvName = 'NORNA_INVOCATION_ROOT';
7
8
  const defaultSiteDirectory = 'site';
9
+ export { homePageDirectory };
8
10
 
9
11
  const currentFile = fileURLToPath(import.meta.url);
10
12
  const currentDirectory = path.dirname(currentFile);
@@ -23,7 +25,10 @@ export const invocationRoot = configuredInvocationRoot
23
25
  const hasSiteFilesInDirectory = (siteDir) => {
24
26
  return (
25
27
  existsSync(path.join(siteDir, 'config.yaml'))
26
- && existsSync(path.join(siteDir, 'content.md'))
28
+ && (
29
+ existsSync(path.join(siteDir, 'pages', homePageDirectory, 'content.md'))
30
+ || existsSync(path.join(siteDir, 'content.md'))
31
+ )
27
32
  );
28
33
  };
29
34
 
@@ -116,10 +121,11 @@ const getPathLabel = (filePath) => {
116
121
  export const siteDir = resolvedSitePaths.siteDir;
117
122
  export const siteConfigPath = path.join(siteDir, 'config.yaml');
118
123
  export const siteThemePath = path.join(siteDir, 'theme.yaml');
119
- export const siteContentPath = path.join(siteDir, 'content.md');
120
124
  export const sitewideContentPath = path.join(siteDir, 'sitewide-content.yaml');
121
- export const siteImagesDir = path.join(siteDir, 'images');
122
- export const siteRoutesDir = path.join(siteDir, 'routes');
125
+ export const sitePagesDir = path.join(siteDir, 'pages');
126
+ export const siteHomePageDir = path.join(sitePagesDir, homePageDirectory);
127
+ export const siteContentPath = path.join(siteHomePageDir, 'content.md');
128
+ export const siteImagesDir = path.join(siteHomePageDir, 'images');
123
129
  export const sitePublicDir = path.join(siteDir, 'public');
124
130
  export const siteStateDir = path.join(siteDir, '.norna');
125
131
  export const astroPublicDir = path.join(siteStateDir, 'public');
@@ -138,7 +144,7 @@ export const siteThemeLabel = getPathLabel(siteThemePath);
138
144
  export const siteContentLabel = getPathLabel(siteContentPath);
139
145
  export const sitewideContentLabel = getPathLabel(sitewideContentPath);
140
146
  export const siteImagesLabel = getPathLabel(siteImagesDir);
141
- export const siteRoutesLabel = getPathLabel(siteRoutesDir);
147
+ export const sitePagesLabel = getPathLabel(sitePagesDir);
142
148
  export const sitePublicLabel = getPathLabel(sitePublicDir);
143
149
  export const astroPublicLabel = getPathLabel(astroPublicDir);
144
150
  export const generatedImagesManifestLabel = getPathLabel(generatedImagesManifestPath);
@@ -1,18 +1,18 @@
1
1
  import { readFile, readdir } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {
4
- validateRouteThemeYamlStructure,
4
+ validatePageThemeYamlStructure,
5
5
  validateThemeYamlStructure,
6
6
  } from './site-content.mjs';
7
7
  import {
8
- siteRoutesDir,
9
- siteRoutesLabel,
8
+ sitePagesDir,
9
+ sitePagesLabel,
10
10
  siteThemeLabel,
11
11
  siteThemePath,
12
- sitewideContentLabel,
13
12
  } from './site-paths.mjs';
14
13
  import { resolveThemePresentation } from './presentation.mjs';
15
14
  import { resolveThemeConfig } from './theme-presets.mjs';
15
+ import { pageThemeSchema, themeVisualSchema } from './schema-definitions.mjs';
16
16
  import { parseYamlConfig } from './yaml-config.mjs';
17
17
 
18
18
  export const readThemeConfig = async () => {
@@ -24,18 +24,16 @@ export const readThemeConfig = async () => {
24
24
  throw error;
25
25
  });
26
26
  const config = parseYamlConfig(themeFile, siteThemeLabel, {
27
+ schema: themeVisualSchema,
27
28
  validateStructure: validateThemeYamlStructure,
28
29
  });
29
- if (Object.hasOwn(config, 'navigation')) {
30
- throw new Error(`${siteThemeLabel} may not define navigation. The site label and logo settings belong in ${sitewideContentLabel}.`);
31
- }
32
30
  resolveThemeConfig(config, siteThemeLabel);
33
31
  resolveThemePresentation(config, siteThemeLabel);
34
32
 
35
33
  return config;
36
34
  };
37
35
 
38
- const getRouteThemeFiles = async (directory, relativeDirectory = '') => {
36
+ const getPageThemeFiles = async (directory, relativeDirectory = '') => {
39
37
  const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
40
38
  if (error?.code === 'ENOENT') return [];
41
39
  throw error;
@@ -46,14 +44,14 @@ const getRouteThemeFiles = async (directory, relativeDirectory = '') => {
46
44
  const relativePath = path.join(relativeDirectory, entry.name);
47
45
  const absolutePath = path.join(directory, entry.name);
48
46
  if (entry.isDirectory()) {
49
- files.push(...await getRouteThemeFiles(absolutePath, relativePath));
47
+ files.push(...await getPageThemeFiles(absolutePath, relativePath));
50
48
  continue;
51
49
  }
52
50
 
53
51
  if (entry.name === 'theme.yaml') {
54
52
  files.push({
55
53
  path: absolutePath,
56
- label: `${siteRoutesLabel}/${relativePath.split(path.sep).join('/')}`,
54
+ label: `${sitePagesLabel}/${relativePath.split(path.sep).join('/')}`,
57
55
  });
58
56
  }
59
57
  }
@@ -61,20 +59,16 @@ const getRouteThemeFiles = async (directory, relativeDirectory = '') => {
61
59
  return files;
62
60
  };
63
61
 
64
- export const validateRouteThemeFiles = async () => {
65
- const files = await getRouteThemeFiles(siteRoutesDir);
62
+ export const validatePageThemeFiles = async () => {
63
+ const files = await getPageThemeFiles(sitePagesDir);
66
64
  const configs = [];
67
65
 
68
66
  for (const file of files) {
69
67
  const source = await readFile(file.path, 'utf8');
70
68
  const config = parseYamlConfig(source, file.label, {
71
- validateStructure: validateRouteThemeYamlStructure,
69
+ schema: pageThemeSchema,
70
+ validateStructure: validatePageThemeYamlStructure,
72
71
  });
73
- if (Object.hasOwn(config, 'navigation')) {
74
- throw new Error(`${file.label} may not define navigation. The site label and logo settings belong in ${sitewideContentLabel}.`);
75
- }
76
- resolveThemeConfig(config, file.label);
77
- resolveThemePresentation(config, file.label);
78
72
 
79
73
  configs.push({ ...file, config });
80
74
  }
@@ -2,9 +2,12 @@ import { mergeDeep } from './typography.mjs';
2
2
 
3
3
  const themePresetConfigurations = Object.freeze({
4
4
  portfolio: Object.freeze({
5
+ shape: 'square',
5
6
  layout: {
6
- density: 'normal',
7
+ contentSpacing: 'normal',
8
+ textWidth: 'wide',
7
9
  pageWidth: '1240px',
10
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
8
11
  gutter: {
9
12
  desktop: 'clamp(1.25rem, 4vw, 3rem)',
10
13
  mobile: '1rem',
@@ -21,12 +24,15 @@ const themePresetConfigurations = Object.freeze({
21
24
  rhythm: 'normal',
22
25
  },
23
26
  palette: 'dark',
24
- sectionSurfaces: ['base'],
27
+ sections: { backgroundPattern: 'uniform' },
25
28
  }),
26
29
  documentation: Object.freeze({
30
+ shape: 'soft',
27
31
  layout: {
28
- density: 'compact',
32
+ contentSpacing: 'compact',
33
+ textWidth: 'narrow',
29
34
  pageWidth: '1240px',
35
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
30
36
  gutter: {
31
37
  desktop: 'clamp(1.25rem, 4vw, 3rem)',
32
38
  mobile: '1rem',
@@ -43,12 +49,15 @@ const themePresetConfigurations = Object.freeze({
43
49
  rhythm: 'compact',
44
50
  },
45
51
  palette: 'paper',
46
- sectionSurfaces: ['base', 'soft'],
52
+ sections: { backgroundPattern: 'alternating' },
47
53
  }),
48
54
  project: Object.freeze({
55
+ shape: 'soft',
49
56
  layout: {
50
- density: 'compact',
57
+ contentSpacing: 'compact',
58
+ textWidth: 'normal',
51
59
  pageWidth: '1120px',
60
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
52
61
  gutter: {
53
62
  desktop: 'clamp(1.25rem, 4vw, 3rem)',
54
63
  mobile: '1rem',
@@ -65,12 +74,15 @@ const themePresetConfigurations = Object.freeze({
65
74
  rhythm: 'compact',
66
75
  },
67
76
  palette: 'light',
68
- sectionSurfaces: ['base', 'soft'],
77
+ sections: { backgroundPattern: 'alternating' },
69
78
  }),
70
79
  statement: Object.freeze({
80
+ shape: 'square',
71
81
  layout: {
72
- density: 'airy',
82
+ contentSpacing: 'spacious',
83
+ textWidth: 'normal',
73
84
  pageWidth: '1280px',
85
+ localNavigationGap: 'clamp(1.5rem, 3vw, 3rem)',
74
86
  gutter: {
75
87
  desktop: 'clamp(1.5rem, 5vw, 4rem)',
76
88
  mobile: '1rem',
@@ -87,7 +99,7 @@ const themePresetConfigurations = Object.freeze({
87
99
  rhythm: 'airy',
88
100
  },
89
101
  palette: 'paper',
90
- sectionSurfaces: ['base', 'emphasis'],
102
+ sections: { backgroundPattern: 'cycling' },
91
103
  }),
92
104
  });
93
105
 
@@ -154,6 +166,30 @@ export const resolveThemeConfig = (theme = {}, sourceLabel = 'theme.yaml') => {
154
166
  };
155
167
  };
156
168
 
169
+ const mergePageThemePart = (base, override, keys) => Object.fromEntries(keys
170
+ .filter((key) => override?.[key] !== undefined || base?.[key] !== undefined)
171
+ .map((key) => [key, override?.[key] ?? base?.[key]]));
172
+
173
+ export const mergePageThemeConfig = (base = {}, override = {}) => ({
174
+ ...base,
175
+ layout: {
176
+ ...(base.layout ?? {}),
177
+ ...mergePageThemePart(base.layout, override.layout, ['contentSpacing', 'textWidth']),
178
+ },
179
+ images: {
180
+ ...(base.images ?? {}),
181
+ ...mergePageThemePart(base.images, override.images, [
182
+ 'width',
183
+ 'maxAvailableWidthPercent',
184
+ 'maxAvailableHeightPercent',
185
+ ]),
186
+ },
187
+ sections: {
188
+ ...(base.sections ?? {}),
189
+ ...mergePageThemePart(base.sections, override.sections, ['backgroundPattern']),
190
+ },
191
+ });
192
+
157
193
  const quote = (value) => JSON.stringify(value);
158
194
  const responsiveValueLines = (label, value, indent = 2) => {
159
195
  const prefix = ' '.repeat(indent);
@@ -167,7 +203,7 @@ const responsiveValueLines = (label, value, indent = 2) => {
167
203
  export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml') => {
168
204
  const preset = getThemePreset(presetName, sourceLabel);
169
205
  const metadata = getThemePresetMetadata(presetName);
170
- const { layout, images, typography, palette, sectionSurfaces } = preset;
206
+ const { shape, layout, images, typography, palette, sections } = preset;
171
207
 
172
208
  return [
173
209
  `# Original values for Norna's "${presetName}" theme preset.`,
@@ -177,9 +213,14 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml
177
213
  `# Available theme presets: ${themePresetNames.join(', ')}.`,
178
214
  `preset: ${presetName}`,
179
215
  '',
216
+ '# Alternatives: square, soft.',
217
+ `shape: ${shape}`,
218
+ '',
180
219
  'layout:',
181
- ' # Alternatives: compact, normal, airy.',
182
- ` density: ${layout.density}`,
220
+ ' # Alternatives: compact, normal, spacious.',
221
+ ` contentSpacing: ${layout.contentSpacing}`,
222
+ ' # Alternatives: narrow, normal, wide.',
223
+ ` textWidth: ${layout.textWidth}`,
183
224
  ' # Any positive CSS length, including px, rem, em, %, or clamp(...).',
184
225
  ` pageWidth: ${layout.pageWidth}`,
185
226
  ' gutter:',
@@ -211,9 +252,9 @@ export const renderThemePresetReference = (presetName, sourceLabel = 'theme.yaml
211
252
  '',
212
253
  '# Alternatives: dark, light, paper.',
213
254
  `palette: ${palette}`,
214
- '# One value keeps every section on the same surface; multiple values cycle.',
215
- '# Use one to three unique values from: base, soft, emphasis.',
216
- `sectionSurfaces: [${sectionSurfaces.join(', ')}]`,
255
+ 'sections:',
256
+ ' # Alternatives: uniform, alternating, cycling.',
257
+ ` backgroundPattern: ${sections.backgroundPattern}`,
217
258
  '',
218
259
  ].join('\n');
219
260
  };
@@ -129,7 +129,7 @@ if (showHelp || !releaseType) {
129
129
  await run('git', ['add', ...releasePaths]);
130
130
  await run('git', ['commit', '-m', `Release ${releaseTag}`]);
131
131
  releaseCommitCreated = true;
132
- await run('git', ['tag', releaseTag]);
132
+ await run('git', ['tag', '-a', releaseTag, '-m', `Release ${releaseTag}`]);
133
133
  releaseTagCreated = true;
134
134
  await run(npmBin, ['run', 'release:publish']);
135
135
  await run('git', ['push', '--follow-tags']);