@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,7 +2,7 @@ import { siteConfigLabel, sitePublicLabel } from './lib/site-paths.mjs';
2
2
  import { getLogoAssets, getPublicAssetInspection } from './lib/logo-assets.mjs';
3
3
  import { logoAssetFilenames } from './lib/public-asset-conventions.mjs';
4
4
  import { readSitewideContent } from './lib/sitewide-content.mjs';
5
- import { readThemeConfig, validateRouteThemeFiles } from './lib/theme-config.mjs';
5
+ import { readThemeConfig, validatePageThemeFiles } from './lib/theme-config.mjs';
6
6
 
7
7
  const formatErrorMessage = (error) => {
8
8
  if (error instanceof Error) {
@@ -16,7 +16,7 @@ try {
16
16
  const { projectConfig } = await import('./lib/project-config.mjs');
17
17
  const themeConfig = await readThemeConfig();
18
18
  const sitewideContent = await readSitewideContent();
19
- await validateRouteThemeFiles();
19
+ await validatePageThemeFiles();
20
20
  const logoAssets = getLogoAssets();
21
21
  const publicAssetInspection = getPublicAssetInspection();
22
22
  const logoAssetPaths = logoAssetFilenames.map((filename) => `${sitePublicLabel}/${filename}`);
@@ -32,10 +32,10 @@ try {
32
32
  }
33
33
 
34
34
  if (logoAssets.length === 0) {
35
- if (sitewideContent.navigation?.logo) {
36
- throw new Error(`Site-wide navigation.logo is configured, but no logo file was found. Add exactly one of ${logoAssetPaths.join(', ')}, or remove navigation.logo.`);
35
+ if (sitewideContent.logo) {
36
+ throw new Error(`Site-wide logo is configured, but no logo file was found. Add exactly one of ${logoAssetPaths.join(', ')}, or remove logo.`);
37
37
  }
38
- console.warn(`Warning: No logo file found in ${sitePublicLabel}. Norna will use sitewide navigation.label or the homepage title as the navigation label.`);
38
+ console.warn(`Warning: No logo file found in ${sitePublicLabel}. Norna will use page titles for navigation.`);
39
39
  console.warn(`Add exactly one of ${logoAssetPaths.join(', ')} when the site should have a logo.`);
40
40
  }
41
41
 
@@ -45,12 +45,14 @@ try {
45
45
  console.log(`Theme preset: ${themeConfig.preset ?? '(none)'}`);
46
46
  console.log(`Page width: ${projectConfig.layout.pageWidth}`);
47
47
  console.log(`Gutter: desktop ${projectConfig.layout.gutter.desktop}, mobile ${projectConfig.layout.gutter.mobile}`);
48
- console.log(`Layout density: ${projectConfig.layout.density}`);
48
+ console.log(`Content spacing: ${projectConfig.layout.contentSpacing}`);
49
+ console.log(`Text width: ${projectConfig.layout.textWidth}`);
49
50
  console.log(`Image area width: ${projectConfig.images.width}`);
50
51
  console.log(`Image max available width: desktop ${projectConfig.images.maxAvailableWidthPercent.desktop}%, mobile ${projectConfig.images.maxAvailableWidthPercent.mobile}%`);
51
52
  console.log(`Image max available height: desktop ${projectConfig.images.maxAvailableHeightPercent.desktop}%, mobile ${projectConfig.images.maxAvailableHeightPercent.mobile}%`);
52
53
  console.log(`Font family: ${projectConfig.typography.fontFamily}`);
53
54
  console.log(`Language: ${projectConfig.locale.lang}`);
55
+ console.log(`Navigation mode: ${projectConfig.navigation.mode}`);
54
56
  console.log(`Scroll behavior: ${projectConfig.navigation.scrollBehavior}`);
55
57
  } catch (error) {
56
58
  console.error('Config check failed.');
@@ -21,7 +21,7 @@ import {
21
21
  siteContentLabel,
22
22
  siteImagesLabel,
23
23
  sitePublicLabel,
24
- siteRoutesLabel,
24
+ sitePagesLabel,
25
25
  siteThemeLabel,
26
26
  } from './lib/site-paths.mjs';
27
27
 
@@ -150,15 +150,13 @@ const getExpectedImagePaths = async () => {
150
150
  for (const contentFile of contentFiles) {
151
151
  const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
152
152
 
153
- for (const section of getBodySections(body).sections) {
154
- if (!section.id) continue;
155
-
153
+ for (const section of (await getBodySections(body)).sections) {
156
154
  const blocks = extractNornaMarkdownBlocks(section.text, { label: contentFile.contentLabel });
157
155
  for (const { image } of getNornaBlockImageReferences(blocks)) {
158
156
  if (image.includes('/') || image.includes('\\')) continue;
159
157
  if (!supportedImageExtensions.has(path.extname(image).toLowerCase())) continue;
160
158
 
161
- imagePaths.add(toPosixPath(path.join(contentFile.imagesLabel, section.id, image)));
159
+ imagePaths.add(toPosixPath(path.join(contentFile.imagesLabel, image)));
162
160
  }
163
161
  }
164
162
  }
@@ -174,7 +172,7 @@ const isExpectedUntracked = (entry, expectedImagePaths) => (
174
172
  expectedImagePaths.has(entry.path)
175
173
  || entry.path === generatedImagesManifestLabel
176
174
  || entry.path.startsWith(`${sitePublicLabel}/`)
177
- || entry.path.startsWith(`${siteRoutesLabel}/`)
175
+ || entry.path.startsWith(`${sitePagesLabel}/`)
178
176
  )
179
177
  );
180
178
 
@@ -182,7 +180,7 @@ const isAllowedPath = (entry, filePath, expectedImagePaths) => (
182
180
  filePath === siteContentLabel
183
181
  || filePath === siteThemeLabel
184
182
  || (!isUntracked(entry) && filePath.startsWith(`${siteImagesLabel}/`))
185
- || filePath.startsWith(`${siteRoutesLabel}/`)
183
+ || filePath.startsWith(`${sitePagesLabel}/`)
186
184
  || filePath.startsWith(`${sitePublicLabel}/`)
187
185
  || expectedImagePaths.has(filePath)
188
186
  || filePath.startsWith('src/')
@@ -213,7 +211,7 @@ const assertDeployableStatus = async (entries, expectedImagePaths) => {
213
211
  if (unexpectedUntracked.length > 0) {
214
212
  fail([
215
213
  'Refusing to deploy: unexpected untracked files are present.',
216
- `Only new referenced images under ${siteImagesLabel}/<section-id>/ or ${siteRoutesLabel}/<NNN-route-id>/images/<section-id>/ are staged automatically.`,
214
+ `Only new referenced images directly under a page's images/ directory in ${sitePagesLabel}/ are staged automatically.`,
217
215
  ...unexpectedUntracked.map((entry) => `- ${formatEntry(entry)}`),
218
216
  ].join('\n'));
219
217
  }
@@ -12,7 +12,7 @@ import {
12
12
  siteImagesDir,
13
13
  siteProjectRoot,
14
14
  sitePublicDir,
15
- siteRoutesDir,
15
+ sitePagesDir,
16
16
  siteThemePath,
17
17
  } from './lib/site-paths.mjs';
18
18
 
@@ -26,7 +26,7 @@ const lines = [
26
26
  `Site theme: ${siteThemePath}`,
27
27
  `Site content: ${siteContentPath}`,
28
28
  `Site images: ${siteImagesDir}`,
29
- `Site routes: ${siteRoutesDir}`,
29
+ `Site pages: ${sitePagesDir}`,
30
30
  `Site public source: ${sitePublicDir}`,
31
31
  `Generated image manifest: ${generatedImagesManifestPath}`,
32
32
  `Astro public output: ${astroPublicDir}`,
@@ -23,7 +23,6 @@ import {
23
23
  generatedImagesManifestPath,
24
24
  originalImagesDir,
25
25
  siteDir,
26
- siteImagesDir,
27
26
  } from './lib/site-paths.mjs';
28
27
 
29
28
  const execFileAsync = promisify(execFile);
@@ -88,10 +87,8 @@ const getFilePathFromPublicPath = (publicPath) => path.join(astroPublicDir, publ
88
87
 
89
88
  const getSourceHashSlug = (sourceHash) => sourceHash.slice(0, sourceHashSlugLength);
90
89
 
91
- const getGeneratedPath = (sourcePath, sourceHash, width) => {
92
- const parsed = path.parse(path.relative(siteImagesDir, sourcePath).startsWith('..')
93
- ? path.relative(path.dirname(siteImagesDir), sourcePath)
94
- : path.relative(siteImagesDir, sourcePath));
90
+ const getGeneratedPath = (sourceKey, sourceHash, width) => {
91
+ const parsed = path.parse(sourceKey);
95
92
  return path.join(generatedImagesDir, parsed.dir, `${parsed.name}-${getSourceHashSlug(sourceHash)}-${width}.webp`);
96
93
  };
97
94
 
@@ -104,22 +101,14 @@ const fail = (message) => {
104
101
  throw new Error(message);
105
102
  };
106
103
 
107
- const getImageSourceKey = (contentFile, sectionId, image) => (
108
- contentFile.isHome
109
- ? `images/${sectionId}/${image}`
110
- : `routes/${contentFile.routeDirectory}/images/${sectionId}/${image}`
111
- );
104
+ const getImageSourceKey = (contentFile, image) => `pages/${contentFile.pageDirectory}/images/${image}`;
112
105
 
113
106
  const getReferencedImages = async (contentFile) => {
114
107
  const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
115
- const { sections } = getBodySections(body);
108
+ const { sections } = await getBodySections(body);
116
109
  const references = [];
117
110
 
118
111
  for (const section of sections) {
119
- if (!section.id) {
120
- fail(`${contentFile.contentLabel}: section heading "${section.heading}" is missing an explicit id.`);
121
- }
122
-
123
112
  const blocks = extractNornaMarkdownBlocks(section.text, { label: contentFile.contentLabel });
124
113
  for (const { image, line, blockDisplayType } of getNornaBlockImageReferences(blocks)) {
125
114
  references.push({
@@ -127,8 +116,8 @@ const getReferencedImages = async (contentFile) => {
127
116
  image,
128
117
  line,
129
118
  blockDisplayType,
130
- sectionId: section.id,
131
- sourceKey: getImageSourceKey(contentFile, section.id, image),
119
+ sectionLabel: section.id ?? 'page title',
120
+ sourceKey: getImageSourceKey(contentFile, image),
132
121
  });
133
122
  }
134
123
  }
@@ -171,9 +160,8 @@ const getReferencedSources = async () => {
171
160
  fail(`Image file does not exist: ${contentFile.imagesLabel}/${siteImagePath}`);
172
161
  }
173
162
 
174
- const currentDirectory = path.basename(path.dirname(sourcePath));
175
- if (reference.sectionId && currentDirectory !== reference.sectionId) {
176
- fail(`Image "${imageName}" is used in section "${reference.sectionId}" but is located in ${contentFile.imagesLabel}/${currentDirectory}/. Run norna content:sync, or npm run norna:sync in starter-style repositories, to move it.`);
163
+ if (path.dirname(sourcePath) !== contentFile.imagesDir) {
164
+ fail(`Image "${imageName}" is used in ${reference.sectionLabel} but is not stored directly in ${contentFile.imagesLabel}/. Run norna content:sync, or npm run norna:sync in starter-style repositories, to move it.`);
177
165
  }
178
166
 
179
167
  seen.add(reference.sourceKey);
@@ -194,16 +182,14 @@ const validateCarouselOrientations = async (manifest) => {
194
182
 
195
183
  for (const contentFile of contentFiles) {
196
184
  const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
197
- const { sections } = getBodySections(body);
185
+ const { sections } = await getBodySections(body);
198
186
 
199
187
  for (const section of sections) {
200
- if (!section.id) continue;
201
-
202
188
  const blocks = extractNornaMarkdownBlocks(section.text, { label: contentFile.contentLabel });
203
189
  for (const block of blocks.filter((candidate) => candidate.type === 'image-carousel')) {
204
190
  const orientations = new Set(
205
191
  block.images
206
- .map((image) => manifest[getImageSourceKey(contentFile, section.id, image.image)])
192
+ .map((image) => manifest[getImageSourceKey(contentFile, image.image)])
207
193
  .filter(Boolean)
208
194
  .filter((entry) => Number.isFinite(entry.width) && Number.isFinite(entry.height))
209
195
  .map(getOrientation)
@@ -212,7 +198,7 @@ const validateCarouselOrientations = async (manifest) => {
212
198
  orientations.delete('square');
213
199
  if (orientations.size > 1) {
214
200
  throw new Error(
215
- `Carousel in section "${section.id}" on line ${block.line} mixes landscape and portrait images. Use landscape images with optional square images, or portrait images with optional square images.`,
201
+ `Carousel in ${section.id ? `section "${section.id}"` : 'the page introduction'} on line ${block.line} mixes landscape and portrait images. Use landscape images with optional square images, or portrait images with optional square images.`,
216
202
  );
217
203
  }
218
204
  }
@@ -248,8 +234,8 @@ const getVariantWidths = ({ width }) => {
248
234
  return variantWidths;
249
235
  };
250
236
 
251
- const getVariants = (sourcePath, sourceHash, variantWidths) => variantWidths.map((width) => ({
252
- src: getPublicPath(getGeneratedPath(sourcePath, sourceHash, width)),
237
+ const getVariants = (sourceKey, sourceHash, variantWidths) => variantWidths.map((width) => ({
238
+ src: getPublicPath(getGeneratedPath(sourceKey, sourceHash, width)),
253
239
  width,
254
240
  }));
255
241
 
@@ -424,9 +410,9 @@ for (const { sourcePath, sourceKey } of sources) {
424
410
 
425
411
  const dimensions = await identify(sourcePath);
426
412
  const variantWidths = getVariantWidths(dimensions);
427
- const variants = getVariants(sourcePath, sourceHash, variantWidths);
413
+ const variants = getVariants(sourceKey, sourceHash, variantWidths);
428
414
  for (const width of variantWidths) {
429
- const outputPath = getGeneratedPath(sourcePath, sourceHash, width);
415
+ const outputPath = getGeneratedPath(sourceKey, sourceHash, width);
430
416
  await convert(sourcePath, outputPath, width);
431
417
  }
432
418
 
@@ -4,252 +4,41 @@ import { fileURLToPath } from 'node:url';
4
4
  import { z } from 'astro/zod';
5
5
  import {
6
6
  configSchema,
7
+ pageThemeSchema,
7
8
  siteSchema,
8
9
  sitewideSchema,
9
10
  themeVisualSchema,
10
11
  } from './lib/schema-definitions.mjs';
11
- import { documentationLink } from './lib/documentation-links.mjs';
12
- import { getSchemaValueDefinition } from './lib/schema-value-definitions.mjs';
12
+ import { applySchemaEditorMetadata } from './lib/schema-editor-metadata.mjs';
13
13
 
14
14
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
15
  const schemaDirectory = path.join(root, 'schemas');
16
16
  const definitions = [
17
17
  ['config.schema.json', 'Norna site configuration', configSchema],
18
18
  ['theme.schema.json', 'Norna visual theme', themeVisualSchema],
19
+ ['page-theme.schema.json', 'Norna page theme', pageThemeSchema],
19
20
  ['sitewide-content.schema.json', 'Norna site-wide content', sitewideSchema],
20
21
  ['content-frontmatter.schema.json', 'Norna page frontmatter', siteSchema],
21
22
  ];
22
- const yamlExample = (source) => `\`\`\`yaml\n${source}\n\`\`\``;
23
-
24
- const schemaProperty = (schema, propertyPath) => {
25
- let current = schema;
26
- for (const name of propertyPath.split('.')) {
27
- current = current?.properties?.[name];
28
- if (!current) throw new Error(`Generated schema has no property at ${propertyPath}.`);
29
- }
30
- return current;
31
- };
32
-
33
- const addHelp = (schema, propertyPath, paragraphs, examples = []) => {
34
- const property = schemaProperty(schema, propertyPath);
35
- property.markdownDescription = paragraphs.join('\n\n');
36
- if (examples.length > 0) property.examples = examples;
37
- };
38
-
39
- const getLiteralValues = (schema) => {
40
- if (Array.isArray(schema.enum)) return schema.enum;
41
- if (
42
- Array.isArray(schema.anyOf)
43
- && schema.anyOf.length > 0
44
- && schema.anyOf.every((candidate) => Object.hasOwn(candidate, 'const'))
45
- ) {
46
- return schema.anyOf.map((candidate) => candidate.const);
47
- }
48
- return null;
49
- };
50
-
51
- const addValueDescriptions = (schema) => {
52
- if (!schema || typeof schema !== 'object') return;
53
- if (Array.isArray(schema)) {
54
- for (const value of schema) addValueDescriptions(value);
55
- return;
56
- }
57
-
58
- const values = getLiteralValues(schema);
59
- const definition = values ? getSchemaValueDefinition(values) : null;
60
- if (definition) {
61
- delete schema.enum;
62
- delete schema.anyOf;
63
- schema.oneOf = values.map((value) => ({
64
- const: value,
65
- title: definition.options[value].title,
66
- description: definition.options[value].description,
67
- }));
68
- }
69
-
70
- for (const value of Object.values(schema)) addValueDescriptions(value);
71
- };
72
-
73
- const addLanguageSuggestions = (jsonSchema) => {
74
- const language = jsonSchema.properties?.language;
75
- if (!language) throw new Error('Generated config schema has no language property.');
76
- const definition = getSchemaValueDefinition(['en', 'sv']);
77
- language.default = 'en';
78
- language.examples = ['en', 'sv', 'en-GB', 'sv-SE'];
79
- language.oneOf = [
80
- ...definition.values.map((value) => ({
81
- const: value,
82
- title: definition.options[value].title,
83
- description: definition.options[value].description,
84
- })),
85
- {
86
- type: 'string',
87
- pattern: '^(?:en|sv)-[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$',
88
- title: 'Regional language tag',
89
- description: 'Use an English or Swedish regional language tag such as en-GB or sv-SE.',
90
- },
91
- ];
92
- };
93
-
94
- const addSitewideNavigationHelp = (jsonSchema) => {
95
- const navigation = jsonSchema.properties?.navigation;
96
- const label = navigation?.properties?.label;
97
- const logo = navigation?.properties?.logo;
98
- if (!navigation || !label || !logo) throw new Error('Generated site-wide schema has no navigation label/logo properties.');
99
-
100
- navigation.markdownDescription = [
101
- yamlExample('navigation:\n label: Example Site'),
102
- '`navigation` defines the identity shared by the homepage and every route. Routes cannot replace it.',
103
- documentationLink('Navigation identity reference', 'sitewide-content.md', 'navigation-identity'),
104
- ].join('\n\n');
105
- label.markdownDescription = [
106
- yamlExample('navigation:\n label: Example Site'),
107
- '- Without a logo, this text is shown in the navigation.',
108
- '- With a logo, this text becomes the image alternative text.',
109
- '- When omitted, Norna uses the homepage title.',
110
- documentationLink('Navigation identity reference', 'sitewide-content.md', 'navigation-identity'),
111
- ].join('\n\n');
112
- label.examples = ['Example Site'];
113
- logo.markdownDescription = [
114
- yamlExample('navigation:\n label: Example Site\n logo:\n height: 2rem'),
115
- '`logo` optionally overrides the displayed height of the logo file that Norna discovers automatically. It does not enable or select the file.',
116
- documentationLink('Navigation logo filenames and placement', 'public-files.md', 'navigation-logo'),
117
- ].join('\n\n');
118
- logo.properties.height.examples = ['2rem'];
119
- };
120
-
121
- const addConfigHelp = (jsonSchema) => {
122
- jsonSchema.markdownDescription = [
123
- yamlExample('url: https://example.com/'),
124
- '`config.yaml` contains the few technical settings shared by the complete site.',
125
- documentationLink('Configuration reference', 'configuration.md'),
126
- ].join('\n\n');
127
- addHelp(jsonSchema, 'url', [
128
- yamlExample('url: https://example.com/'),
129
- 'The absolute public URL. Its path also determines the base path for generated links and assets.',
130
- documentationLink('Public URL reference', 'configuration.md', 'url'),
131
- ], ['https://example.com/', 'https://owner.github.io/repository-name/']);
132
- addHelp(jsonSchema, 'language', [
133
- yamlExample('language: en-GB'),
134
- 'Sets the page language and selects Norna\'s built-in English or Swedish interface text. The default is `en`.',
135
- documentationLink('Language reference', 'configuration.md', 'language'),
136
- ], ['en', 'sv', 'en-GB', 'sv-SE']);
137
- addHelp(jsonSchema, 'scrollBehavior', [
138
- yamlExample('scrollBehavior: smooth'),
139
- 'Controls same-page anchor movement. `instant` is the default; `smooth` uses the browser\'s native smooth scrolling.',
140
- documentationLink('Scroll behavior reference', 'configuration.md', 'scrollbehavior'),
141
- ], ['instant', 'smooth']);
142
- };
143
-
144
- const addThemeHelp = (jsonSchema) => {
145
- jsonSchema.markdownDescription = [
146
- yamlExample('preset: documentation'),
147
- 'Choose one complete preset first. Add focused overrides only when the site needs to differ from it.',
148
- documentationLink('Theme reference', 'theme.md'),
149
- ].join('\n\n');
150
- addHelp(jsonSchema, 'preset', [
151
- yamlExample('preset: documentation'),
152
- 'Selects coordinated layout, image sizing, typography, palette and section surfaces. This is the normal starting point for a theme.',
153
- documentationLink('Compare theme presets', 'theme.md', 'theme-presets'),
154
- ]);
155
- addHelp(jsonSchema, 'layout', [
156
- yamlExample('layout:\n density: compact\n pageWidth: 72rem'),
157
- 'Overrides page width, gutters and structural spacing after the selected preset.',
158
- documentationLink('Layout reference', 'theme.md', 'layout'),
159
- ]);
160
- addHelp(jsonSchema, 'images', [
161
- yamlExample('images:\n width: 900px\n maxAvailableHeightPercent: 74'),
162
- 'Overrides the available width and height used by Norna-managed images.',
163
- documentationLink('Image sizing reference', 'theme.md', 'image-sizing'),
164
- ]);
165
- addHelp(jsonSchema, 'typography', [
166
- yamlExample('typography:\n profile: reading\n rhythm: normal'),
167
- 'Overrides the preset\'s font stack, typography profile, rhythm or individual text settings.',
168
- documentationLink('Typography configuration reference', 'typography.md', 'configuration-shape'),
169
- ]);
170
- addHelp(jsonSchema, 'palette', [
171
- yamlExample('palette: paper'),
172
- 'Chooses a coordinated color system for the page frame, navigation, footer and section surfaces.',
173
- documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
174
- ], ['dark', 'light', 'paper']);
175
- addHelp(jsonSchema, 'sectionSurfaces', [
176
- yamlExample('sectionSurfaces: [base, soft, emphasis]'),
177
- 'Lists one to three semantic surfaces. Norna cycles through them in section order; omit this field to keep the preset sequence.',
178
- documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
179
- ], [['base'], ['base', 'soft'], ['base', 'soft', 'emphasis']]);
180
- };
181
-
182
- const addSitewideHelp = (jsonSchema) => {
183
- jsonSchema.markdownDescription = [
184
- yamlExample('navigation:\n label: Example Site'),
185
- '`sitewide-content.yaml` optionally defines editorial content and identity shared by every page.',
186
- documentationLink('Site-wide content reference', 'sitewide-content.md'),
187
- ].join('\n\n');
188
- addSitewideNavigationHelp(jsonSchema);
189
- addHelp(jsonSchema, 'banners', [
190
- yamlExample('banners:\n - id: project-status\n tone: warning\n title: Experimental code\n text: Not for production use.'),
191
- 'Adds dismissible notices above page content. List order controls their presentation order; each `id` must be unique.',
192
- documentationLink('Banner reference', 'sitewide-content.md', 'banners'),
193
- ]);
194
- addHelp(jsonSchema, 'footer', [
195
- yamlExample('footer:\n copyrightMessage: Copyright Example Owner.'),
196
- 'Adds shared footer content. Generated build information is optional.',
197
- documentationLink('Footer reference', 'sitewide-content.md', 'footer'),
198
- ]);
199
- };
200
-
201
- const addContentHelp = (jsonSchema) => {
202
- jsonSchema.markdownDescription = [
203
- yamlExample('---\ntitle: About\ndescription: About this project.\n---'),
204
- 'Every homepage and route `content.md` starts with page metadata, followed by Markdown sections.',
205
- documentationLink('Page frontmatter reference', 'content.md', 'page-frontmatter'),
206
- ].join('\n\n');
207
- addHelp(jsonSchema, 'title', [
208
- yamlExample('title: About'),
209
- 'The page title used in metadata and, by default, route navigation.',
210
- documentationLink('Page frontmatter reference', 'content.md', 'page-frontmatter'),
211
- ], ['About']);
212
- addHelp(jsonSchema, 'description', [
213
- yamlExample('description: About this project.'),
214
- 'A concise page description used in metadata.',
215
- documentationLink('Page frontmatter reference', 'content.md', 'page-frontmatter'),
216
- ], ['About this project.']);
217
- addHelp(jsonSchema, 'navigation', [
218
- yamlExample('navigation:\n include: true\n label: About us'),
219
- 'Controls whether a route appears in site navigation and optionally replaces its visible title there. Homepage navigation is section-based.',
220
- documentationLink('Route navigation reference', 'routes.md', 'navigation'),
221
- ]);
222
- addHelp(jsonSchema, 'sections', [
223
- yamlExample('sections:\n announcement:\n visible:\n until: "2026-12-01"'),
224
- 'Adds optional metadata keyed by explicit Markdown section id. Markdown headings remain the source of section content and order.',
225
- documentationLink('Section metadata reference', 'content.md', 'section-metadata'),
226
- ]);
227
- };
228
-
229
23
  const schemaFiles = new Map();
230
24
 
231
25
  for (const [filename, title, schema] of definitions) {
232
26
  const jsonSchema = z.toJSONSchema(schema, { target: 'draft-07', io: 'input' });
233
27
  jsonSchema.$id = `https://janga.github.io/norna/schemas/${filename}`;
234
28
  jsonSchema.title = title;
235
- addValueDescriptions(jsonSchema);
236
- if (filename === 'config.schema.json') {
237
- addLanguageSuggestions(jsonSchema);
238
- addConfigHelp(jsonSchema);
239
- }
240
- if (filename === 'theme.schema.json') addThemeHelp(jsonSchema);
241
- if (filename === 'sitewide-content.schema.json') addSitewideHelp(jsonSchema);
242
- if (filename === 'content-frontmatter.schema.json') addContentHelp(jsonSchema);
29
+ applySchemaEditorMetadata(filename, jsonSchema);
243
30
  schemaFiles.set(filename, `${JSON.stringify(jsonSchema, null, 2)}\n`);
244
31
  }
245
32
 
246
33
  schemaFiles.set('manifest.json', `${JSON.stringify({
247
- schemaVersion: 1,
34
+ editorApiVersion: 1,
35
+ schemaVersion: 2,
248
36
  files: {
249
37
  config: 'config.schema.json',
250
38
  contentFrontmatter: 'content-frontmatter.schema.json',
251
39
  sitewideContent: 'sitewide-content.schema.json',
252
40
  theme: 'theme.schema.json',
41
+ pageTheme: 'page-theme.schema.json',
253
42
  },
254
43
  }, null, 2)}\n`);
255
44
 
@@ -0,0 +1,9 @@
1
+ export const getBasePathRedirectLocation = (basePath, requestUrl) => {
2
+ if (basePath === '/') return undefined;
3
+
4
+ const basePathWithoutTrailingSlash = basePath.replace(/\/$/, '');
5
+ const parsedRequestUrl = new URL(requestUrl, 'http://localhost');
6
+ if (parsedRequestUrl.pathname !== basePathWithoutTrailingSlash) return undefined;
7
+
8
+ return `${basePathWithoutTrailingSlash}/${parsedRequestUrl.search}`;
9
+ };