@janga/norna 0.7.11 → 0.7.13

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 (239) hide show
  1. package/README.md +120 -108
  2. package/bin/norna-cli.mjs +1 -1
  3. package/docs/README.md +107 -75
  4. package/docs/assets/norna-dog-shelter.jpg +0 -0
  5. package/docs/commands.md +7 -4
  6. package/docs/configuration.md +86 -185
  7. package/docs/design/command-organization.md +1 -1
  8. package/docs/design/norna-diagram-design.md +2 -1
  9. package/docs/design/site-examples-structure.md +4 -1
  10. package/docs/engine-development.md +15 -8
  11. package/docs/getting-started.md +126 -144
  12. package/docs/how-to/embedded-site.md +35 -0
  13. package/docs/publishing.md +47 -46
  14. package/docs/requirements.md +54 -0
  15. package/docs/routes.md +1 -1
  16. package/docs/site-structure.md +4 -4
  17. package/docs/sitewide-content.md +2 -1
  18. package/docs/theme.md +18 -0
  19. package/examples/README.md +26 -24
  20. package/examples/complete-sites/dog-shelter-multi-page/.astro/content-assets.mjs +4 -1
  21. package/examples/complete-sites/dog-shelter-multi-page/.astro/content.d.ts +1 -1
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +6 -8
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +26 -53
  25. package/examples/complete-sites/dog-shelter-multi-page/README.md +7 -0
  26. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +29 -35
  27. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{astro-D_GIpJIE.js → astro-B3JHJyiv.js} +4 -4
  28. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  29. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_aria-query.js +151 -151
  30. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -1
  31. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_axobject-query.js +131 -131
  32. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -1
  33. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_html-escaper.js +1 -1
  34. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -1
  35. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +10 -10
  36. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -1
  37. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{audit-DM9Hkl5B.js → audit-C3SVr7dT.js} +10 -10
  38. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/audit-C3SVr7dT.js.map +1 -0
  39. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{highlight-DnZ1ULOL.js → highlight-BLtvqtzO.js} +1 -1
  40. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  41. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{icons-RyebKi45.js → icons-C92i61-2.js} +1 -1
  42. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  43. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{settings-DaU-gNnK.js → settings-v5GgKks_.js} +2 -2
  44. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/settings-v5GgKks_.js.map +1 -0
  45. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{toolbar-63PA619H.js → toolbar-BFvJVp26.js} +2 -2
  46. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +1 -0
  47. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{ui-library--bkcNSYz.js → ui-library-Cd35oAvo.js} +10 -10
  48. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +1 -0
  49. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{window-jPkpKVcy.js → window-vo3A6TMn.js} +1 -1
  50. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  51. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/{xray-BXCHpEJP.js → xray-DwhzGckb.js} +3 -3
  52. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/xray-DwhzGckb.js.map +1 -0
  53. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +92 -12
  54. package/examples/complete-sites/dog-shelter-multi-page/site/config.md +3 -0
  55. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +12 -0
  56. package/examples/complete-sites/dog-shelter-multi-page/site/images/intro/shelter-dog.jpg +0 -0
  57. package/examples/complete-sites/dog-shelter-multi-page/site/public/logo.svg +9 -0
  58. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.jpg +0 -0
  59. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.jpg +0 -0
  60. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/route-content.md +11 -19
  61. package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/images/adopt/adoption-dog.jpg +0 -0
  62. package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/route-content.md +11 -0
  63. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +12 -2
  64. package/examples/complete-sites/dog-shelter-multi-page/site/theme.md +1 -23
  65. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +59 -0
  66. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +138 -0
  67. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +755 -0
  68. package/examples/complete-sites/dog-shelter-single-page/.astro/content-assets.mjs +4 -0
  69. package/examples/complete-sites/dog-shelter-single-page/.astro/content-modules.mjs +1 -0
  70. package/examples/complete-sites/dog-shelter-single-page/.astro/content.d.ts +183 -0
  71. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -0
  72. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +13 -0
  73. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +14 -0
  74. package/examples/complete-sites/dog-shelter-single-page/.astro/settings.json +5 -0
  75. package/examples/complete-sites/dog-shelter-single-page/.astro/types.d.ts +2 -0
  76. package/examples/complete-sites/dog-shelter-single-page/README.md +7 -0
  77. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +74 -0
  78. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro-B3JHJyiv.js +434 -0
  79. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  80. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  81. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  82. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  83. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  84. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  85. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  86. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  87. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  88. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/audit-C3SVr7dT.js +1534 -0
  89. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/audit-C3SVr7dT.js.map +1 -0
  90. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/highlight-BLtvqtzO.js +48 -0
  91. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  92. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/icons-C92i61-2.js +42 -0
  93. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  94. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/package.json +3 -0
  95. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  96. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/settings-v5GgKks_.js +190 -0
  97. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/settings-v5GgKks_.js.map +1 -0
  98. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/toolbar-BFvJVp26.js +458 -0
  99. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +1 -0
  100. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js +1016 -0
  101. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +1 -0
  102. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/window-vo3A6TMn.js +32 -0
  103. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  104. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/xray-DwhzGckb.js +111 -0
  105. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/xray-DwhzGckb.js.map +1 -0
  106. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +44 -12
  107. package/examples/complete-sites/dog-shelter-single-page/site/config.md +3 -0
  108. package/examples/complete-sites/dog-shelter-single-page/site/content.md +20 -24
  109. package/examples/complete-sites/dog-shelter-single-page/site/images/what-we-do/rescue-dog.jpg +0 -0
  110. package/examples/complete-sites/dog-shelter-single-page/site/images/you-can-help/foster-dog.jpg +0 -0
  111. package/examples/complete-sites/dog-shelter-single-page/site/public/logo.svg +9 -0
  112. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +12 -2
  113. package/examples/complete-sites/dog-shelter-single-page/site/theme.md +1 -23
  114. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  115. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +15 -2
  116. package/examples/feature-demos/media-and-surfaces/README.md +7 -1
  117. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +7 -1
  118. package/examples/{complete-sites/dog-shelter-multi-page → feature-demos/media-and-surfaces}/node_modules/.vite/deps/embla-carousel.js +1 -1
  119. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  120. package/examples/feature-demos/media-and-surfaces/site/config.md +3 -0
  121. package/examples/feature-demos/media-and-surfaces/site/content.md +5 -51
  122. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/route-content.md +10 -25
  123. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/route-content.md +1 -1
  124. package/examples/feature-demos/sitewide-content/.astro/collections/site.schema.json +59 -0
  125. package/examples/feature-demos/sitewide-content/.astro/collections/sitewide.schema.json +138 -0
  126. package/examples/feature-demos/sitewide-content/.astro/collections/theme.schema.json +755 -0
  127. package/examples/feature-demos/sitewide-content/.astro/content-assets.mjs +1 -0
  128. package/examples/feature-demos/sitewide-content/.astro/content-modules.mjs +1 -0
  129. package/examples/feature-demos/sitewide-content/.astro/content.d.ts +183 -0
  130. package/examples/feature-demos/sitewide-content/.astro/data-store.json +1 -0
  131. package/examples/feature-demos/sitewide-content/.astro/dev.json +13 -0
  132. package/examples/feature-demos/sitewide-content/.astro/dev.log +18 -0
  133. package/examples/feature-demos/sitewide-content/.astro/settings.json +5 -0
  134. package/examples/feature-demos/sitewide-content/.astro/types.d.ts +2 -0
  135. package/examples/feature-demos/sitewide-content/README.md +18 -0
  136. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/_metadata.json +74 -0
  137. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro-B3JHJyiv.js +434 -0
  138. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  139. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  140. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  141. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  142. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  143. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  144. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  145. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  146. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  147. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/audit-C3SVr7dT.js +1534 -0
  148. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/audit-C3SVr7dT.js.map +1 -0
  149. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/highlight-BLtvqtzO.js +48 -0
  150. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  151. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/icons-C92i61-2.js +42 -0
  152. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  153. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/package.json +3 -0
  154. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  155. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/settings-v5GgKks_.js +190 -0
  156. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/settings-v5GgKks_.js.map +1 -0
  157. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/toolbar-BFvJVp26.js +458 -0
  158. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +1 -0
  159. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/ui-library-Cd35oAvo.js +1016 -0
  160. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +1 -0
  161. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/window-vo3A6TMn.js +32 -0
  162. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  163. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/xray-DwhzGckb.js +111 -0
  164. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/xray-DwhzGckb.js.map +1 -0
  165. package/examples/feature-demos/sitewide-content/site/.norna/generated-images.json +1 -0
  166. package/examples/feature-demos/sitewide-content/site/config.md +3 -0
  167. package/examples/feature-demos/sitewide-content/site/content.md +16 -0
  168. package/examples/feature-demos/sitewide-content/site/public/logo.svg +6 -0
  169. package/examples/feature-demos/sitewide-content/site/public/robots.txt +2 -0
  170. package/examples/feature-demos/sitewide-content/site/routes/010-identity/route-content.md +20 -0
  171. package/examples/feature-demos/sitewide-content/site/routes/020-notices/route-content.md +22 -0
  172. package/examples/feature-demos/sitewide-content/site/routes/030-footer/route-content.md +17 -0
  173. package/examples/feature-demos/sitewide-content/site/sitewide-content.md +33 -0
  174. package/examples/feature-demos/sitewide-content/site/theme.md +3 -0
  175. package/examples/feature-demos/theme-presets/.astro/dev.log +37 -0
  176. package/examples/feature-demos/theme-presets/README.md +5 -1
  177. package/examples/feature-demos/theme-presets/site/config.md +3 -0
  178. package/fixtures/basic/site/config.md +3 -0
  179. package/fixtures/content-model-v2/site/config.md +3 -0
  180. package/package.json +12 -7
  181. package/scripts/build-pages.mjs +4 -6
  182. package/scripts/check-config.mjs +1 -10
  183. package/scripts/deploy-site.mjs +12 -8
  184. package/scripts/lib/example-sites.mjs +3 -0
  185. package/scripts/lib/github-repository.mjs +44 -0
  186. package/scripts/lib/norna-markdown-blocks.mjs +97 -10
  187. package/scripts/lib/project-config.mjs +112 -123
  188. package/scripts/lib/site-content.mjs +7 -0
  189. package/scripts/lib/site-paths.mjs +6 -2
  190. package/scripts/test-client-javascript.mjs +221 -0
  191. package/scripts/test-content-check.mjs +103 -0
  192. package/scripts/test-content-model-v2.mjs +9 -13
  193. package/scripts/test-documentation.mjs +116 -0
  194. package/scripts/test-engine-commands.mjs +12 -3
  195. package/scripts/test-examples.mjs +9 -0
  196. package/scripts/test-package-check.mjs +17 -9
  197. package/scripts/test-project-config.mjs +125 -0
  198. package/scripts/test-temporary-visibility.mjs +3 -10
  199. package/scripts/test-theme-presets.mjs +3 -8
  200. package/scripts/watch-pages-deploy.mjs +15 -11
  201. package/src/components/GalleryGrid.astro +30 -230
  202. package/src/components/ImageCarousel.astro +184 -0
  203. package/src/components/MobileSectionNavigationScript.astro +14 -0
  204. package/src/components/SiteNavigation.astro +54 -341
  205. package/src/components/SiteNotesScript.astro +43 -0
  206. package/src/components/SitePage.astro +4 -1
  207. package/src/components/SiteSection.astro +4 -47
  208. package/src/layouts/BaseLayout.astro +1 -1
  209. package/src/lib/galleryImages.ts +53 -0
  210. package/src/styles/global.css +81 -1
  211. package/starters/basic/.github/workflows/deploy.yml +1 -2
  212. package/starters/basic/README.md +10 -8
  213. package/starters/basic/site/config.md +3 -0
  214. package/starters/project/.github/workflows/deploy.yml +1 -2
  215. package/starters/project/README.md +3 -4
  216. package/starters/project/site/config.md +3 -0
  217. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -1
  218. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -1
  219. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/embla-carousel.js.map +0 -1
  220. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -1
  221. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -1
  222. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -1
  223. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -1
  224. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -1
  225. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -1
  226. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -1
  227. package/examples/complete-sites/dog-shelter-multi-page/site/config.mjs +0 -30
  228. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +0 -19
  229. package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +0 -19
  230. package/examples/complete-sites/dog-shelter-single-page/site/config.mjs +0 -30
  231. package/examples/complete-sites/dog-shelter-single-page/site/images/what-we-do/dog-house.svg +0 -25
  232. package/examples/complete-sites/dog-shelter-single-page/site/images/you-can-help/heart.svg +0 -13
  233. package/examples/feature-demos/media-and-surfaces/site/config.mjs +0 -30
  234. package/examples/feature-demos/media-and-surfaces/site/routes/010-media/theme.md +0 -3
  235. package/examples/feature-demos/theme-presets/site/config.mjs +0 -30
  236. package/fixtures/basic/site/config.mjs +0 -27
  237. package/fixtures/content-model-v2/site/config.mjs +0 -11
  238. package/starters/basic/site/config.mjs +0 -27
  239. package/starters/project/site/config.mjs +0 -30
@@ -12,7 +12,7 @@ import {
12
12
  extractNornaMarkdownBlocks,
13
13
  getNornaBlockImageReferences,
14
14
  } from './lib/norna-markdown-blocks.mjs';
15
- import { projectConfig } from './lib/project-config.mjs';
15
+ import { getGitHubRepositoryContext, githubPagesWorkflow } from './lib/github-repository.mjs';
16
16
  import {
17
17
  engineRoot,
18
18
  generatedImagesManifestLabel,
@@ -28,9 +28,9 @@ import {
28
28
  const execFileAsync = promisify(execFile);
29
29
 
30
30
  const root = siteProjectRoot;
31
- const branch = projectConfig.github.branch;
32
- const repo = projectConfig.github.repo;
33
- const pagesWorkflow = projectConfig.github.pagesWorkflow;
31
+ const pagesWorkflow = githubPagesWorkflow;
32
+ let branch;
33
+ let repo;
34
34
  const args = process.argv.slice(2);
35
35
  const mode = args[0] === 'commit' ? 'commit' : 'deploy';
36
36
  const modeArgs = mode === 'commit' ? args.slice(1) : args;
@@ -53,9 +53,9 @@ const failedConclusions = new Set(['action_required', 'cancelled', 'failure', 's
53
53
  const deployUsage = [
54
54
  'Usage: norna deploy',
55
55
  '',
56
- `Publishes an already committed ${branch} branch: builds, verifies a clean worktree,`,
57
- `pushes ${branch} when local ${branch} is ahead of origin/${branch}, and checks GitHub Pages.`,
58
- `If local ${branch} already matches origin/${branch}, it skips push and checks Pages.`,
56
+ 'Publishes an already committed default branch: builds, verifies a clean worktree,',
57
+ 'pushes it when local HEAD is ahead of origin, and checks GitHub Pages.',
58
+ 'If local HEAD already matches origin, it skips push and checks Pages.',
59
59
  '',
60
60
  'For the old build-and-commit convenience flow, use:',
61
61
  'norna deploy:commit "Commit message"',
@@ -63,7 +63,7 @@ const deployUsage = [
63
63
  const deployCommitUsage = [
64
64
  'Usage: norna deploy:commit "Commit message"',
65
65
  '',
66
- `Builds, stages only allowed site changes, commits, pushes ${branch},`,
66
+ 'Builds, stages only allowed site changes, commits, pushes the default branch,',
67
67
  'and checks GitHub Pages.',
68
68
  ].join('\n');
69
69
 
@@ -385,6 +385,10 @@ if (modeArgs.includes('--help') || modeArgs.includes('-h')) {
385
385
  }
386
386
 
387
387
  try {
388
+ const githubRepository = await getGitHubRepositoryContext({ cwd: root });
389
+ branch = githubRepository.branch;
390
+ repo = githubRepository.repo;
391
+
388
392
  if (mode === 'commit') {
389
393
  await deployWithCommit();
390
394
  } else {
@@ -13,6 +13,9 @@ export const getExampleSites = async (root) => {
13
13
 
14
14
  for (const entry of entries.filter((candidate) => candidate.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
15
15
  const siteDirectory = path.join(categoryDirectory, entry.name, 'site');
16
+ if (!existsSync(path.join(siteDirectory, 'config.md'))) {
17
+ throw new Error(`Example ${path.relative(root, siteDirectory)} is missing config.md.`);
18
+ }
16
19
  if (!existsSync(path.join(siteDirectory, 'content.md'))) {
17
20
  throw new Error(`Example ${path.relative(root, siteDirectory)} is missing content.md.`);
18
21
  }
@@ -0,0 +1,44 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ export const githubPagesWorkflow = 'deploy.yml';
7
+
8
+ export const getGitHubRepositoryContext = async ({ cwd, repo } = {}) => {
9
+ const args = [
10
+ 'repo',
11
+ 'view',
12
+ ...(repo ? [repo] : []),
13
+ '--json',
14
+ 'nameWithOwner,defaultBranchRef',
15
+ ];
16
+ let stdout;
17
+
18
+ try {
19
+ ({ stdout } = await execFileAsync('gh', args, {
20
+ cwd,
21
+ maxBuffer: 1024 * 1024,
22
+ }));
23
+ } catch (error) {
24
+ throw new Error([
25
+ 'Could not discover the GitHub repository and its default branch.',
26
+ 'Run this command in a Git repository with a GitHub remote and an authenticated gh CLI, or pass explicit deploy:watch options.',
27
+ error.stderr?.trim(),
28
+ error.message,
29
+ ].filter(Boolean).join('\n'));
30
+ }
31
+
32
+ const result = JSON.parse(stdout || '{}');
33
+ const defaultBranch = result.defaultBranchRef?.name;
34
+ const nameWithOwner = result.nameWithOwner;
35
+
36
+ if (!nameWithOwner || !defaultBranch) {
37
+ throw new Error('GitHub did not report a repository name and default branch.');
38
+ }
39
+
40
+ return Object.freeze({
41
+ branch: defaultBranch,
42
+ repo: nameWithOwner,
43
+ });
44
+ };
@@ -589,7 +589,8 @@ export const splitNornaMarkdownBlockMarkers = (html, options = {}) => {
589
589
  const normalizeBlockSource = (source) => source.replace(/\r\n?/g, '\n').trim();
590
590
 
591
591
  const inlineNoteReferenceRegex = /\{note-ref\}/g;
592
- const inlineNoteDeclarationRegex = /^\s*\{note:\s*(.*)\}\s*$/;
592
+ const inlineNoteDeclarationOpeningRegex = /^\s*\{note:\s*(.*)$/;
593
+ const inlineNoteDeclarationClosingRegex = /^(.*?)\}\s*$/;
593
594
  const inlineNoteDeclarationStartRegex = /\{note(?:\s*:|\s*\})/;
594
595
 
595
596
  const maskInlineCodeAndComments = (source) => source.replace(
@@ -605,7 +606,82 @@ const formatInlineNoteError = (message, options, line) => ({
605
606
  message: failMessage(message, { ...options, line }),
606
607
  });
607
608
 
608
- const getInlineNoteErrorMessage = () => 'Write the note on its own line as "{note: Your explanatory text.}".';
609
+ const getInlineNoteErrorMessage = () => 'Write the note on its own line as "{note: Short text.}", or end a multiline note with "}".';
610
+
611
+ const findUnescapedInlineNoteConstruct = (source) => {
612
+ const constructRegex = /\{note(?:-ref\}|\s*:|\s*\})/g;
613
+
614
+ for (const match of source.matchAll(constructRegex)) {
615
+ const index = match.index ?? 0;
616
+ let precedingBackslashes = 0;
617
+ for (let cursor = index - 1; cursor >= 0 && source[cursor] === '\\'; cursor -= 1) {
618
+ precedingBackslashes += 1;
619
+ }
620
+
621
+ if (precedingBackslashes % 2 === 0) {
622
+ return match[0].startsWith('{note-ref') ? 'reference' : 'note';
623
+ }
624
+ }
625
+
626
+ return null;
627
+ };
628
+
629
+ const collectInlineNoteDeclaration = (lines, maskedLines, startIndex) => {
630
+ const openingMatch = (lines[startIndex] ?? '').match(inlineNoteDeclarationOpeningRegex);
631
+ if (!openingMatch) return null;
632
+ const maskedOpeningMatch = (maskedLines[startIndex] ?? '').match(inlineNoteDeclarationOpeningRegex);
633
+
634
+ const textLines = [];
635
+ let currentLine = openingMatch[1] ?? '';
636
+ let currentMaskedLine = maskedOpeningMatch?.[1] ?? '';
637
+
638
+ for (let index = startIndex; index < lines.length; index += 1) {
639
+ const conflictType = findUnescapedInlineNoteConstruct(currentMaskedLine);
640
+ if (conflictType) {
641
+ return {
642
+ closed: false,
643
+ conflictIndex: index,
644
+ conflictType,
645
+ endIndex: index,
646
+ text: textLines.map((line) => line.trim()).filter(Boolean).join(' '),
647
+ };
648
+ }
649
+
650
+ const closingMatch = currentLine.match(inlineNoteDeclarationClosingRegex);
651
+ if (closingMatch) {
652
+ textLines.push(closingMatch[1]);
653
+ return {
654
+ closed: true,
655
+ endIndex: index,
656
+ text: textLines.map((line) => line.trim()).filter(Boolean).join(' '),
657
+ };
658
+ }
659
+
660
+ textLines.push(currentLine);
661
+ currentLine = lines[index + 1] ?? '';
662
+ currentMaskedLine = maskedLines[index + 1] ?? '';
663
+ }
664
+
665
+ return {
666
+ closed: false,
667
+ endIndex: lines.length - 1,
668
+ text: textLines.map((line) => line.trim()).filter(Boolean).join(' '),
669
+ };
670
+ };
671
+
672
+ const getUnclosedInlineNoteMessage = (noteDeclaration, startLine, lineOffset = 0) => {
673
+ if (noteDeclaration.conflictType === 'note') {
674
+ const conflictLine = lineOffset + noteDeclaration.conflictIndex + 1;
675
+ return `The note starting on line ${startLine} is not closed before another note starts on line ${conflictLine}. Close the first note with "}".`;
676
+ }
677
+
678
+ if (noteDeclaration.conflictType === 'reference') {
679
+ const conflictLine = lineOffset + noteDeclaration.conflictIndex + 1;
680
+ return `The note starting on line ${startLine} contains "{note-ref}" on line ${conflictLine}. Close the note before adding a note reference.`;
681
+ }
682
+
683
+ return `The note starting on line ${startLine} is not closed. End it with "}" on its own line or at the end of the note text.`;
684
+ };
609
685
 
610
686
  /**
611
687
  * Finds the deliberately small inline-note syntax while ignoring fenced and
@@ -640,8 +716,7 @@ export const extractInlineNoteDiagnostics = (markdown, options = {}) => {
640
716
  pendingParagraph = null;
641
717
  };
642
718
 
643
- const attachNote = (noteMatch, lineNumber) => {
644
- const text = noteMatch[1].trim();
719
+ const attachNote = (text, lineNumber) => {
645
720
  if (!text) {
646
721
  addError(`The note on line ${lineNumber} is empty. ${getInlineNoteErrorMessage()}`, lineNumber);
647
722
  pendingParagraph = null;
@@ -686,7 +761,10 @@ export const extractInlineNoteDiagnostics = (markdown, options = {}) => {
686
761
 
687
762
  const trimmed = line.trim();
688
763
  const maskedTrimmed = maskedLine.trim();
689
- const noteMatch = trimmed.match(inlineNoteDeclarationRegex);
764
+ const startsNoteDeclaration = inlineNoteDeclarationOpeningRegex.test(maskedLine);
765
+ const noteDeclaration = startsNoteDeclaration
766
+ ? collectInlineNoteDeclaration(lines, maskedLines, index)
767
+ : null;
690
768
  const looksLikeNoteDeclaration = inlineNoteDeclarationStartRegex.test(maskedTrimmed);
691
769
  const referenceCount = countInlineNoteReferences(maskedLine);
692
770
  const isHeading = /^#{1,6}(?:\s|$)/.test(trimmed);
@@ -698,8 +776,14 @@ export const extractInlineNoteDiagnostics = (markdown, options = {}) => {
698
776
  }
699
777
 
700
778
  if (pendingParagraph) {
701
- if (noteMatch) {
702
- attachNote(noteMatch, lineNumber);
779
+ if (noteDeclaration) {
780
+ if (!noteDeclaration.closed) {
781
+ addError(getUnclosedInlineNoteMessage(noteDeclaration, lineNumber, options.lineOffset), lineNumber);
782
+ pendingParagraph = null;
783
+ } else {
784
+ attachNote(noteDeclaration.text, lineNumber);
785
+ }
786
+ index = noteDeclaration.endIndex;
703
787
  continue;
704
788
  }
705
789
 
@@ -716,13 +800,16 @@ export const extractInlineNoteDiagnostics = (markdown, options = {}) => {
716
800
  noteJustSeen = false;
717
801
  }
718
802
 
719
- if (noteMatch || looksLikeNoteDeclaration) {
803
+ if (noteDeclaration || looksLikeNoteDeclaration) {
720
804
  addError(
721
- noteMatch
722
- ? `A "{note: ...}" requires a preceding paragraph containing "{note-ref}".`
805
+ noteDeclaration
806
+ ? noteDeclaration.closed
807
+ ? `A "{note: ...}" requires a preceding paragraph containing "{note-ref}".`
808
+ : getUnclosedInlineNoteMessage(noteDeclaration, lineNumber, options.lineOffset)
723
809
  : `Invalid note syntax on line ${lineNumber}. ${getInlineNoteErrorMessage()}`,
724
810
  lineNumber,
725
811
  );
812
+ if (noteDeclaration) index = noteDeclaration.endIndex;
726
813
  continue;
727
814
  }
728
815
 
@@ -1,9 +1,55 @@
1
- import { pathToFileURL } from 'node:url';
2
- import { siteConfigLabel, siteConfigPath, siteThemeLabel } from './site-paths.mjs';
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { parseYamlMapping } from './frontmatter-yaml.mjs';
3
+ import {
4
+ splitSiteFile,
5
+ validateConfigFrontmatterStructure,
6
+ validateFrontmatterIndentation,
7
+ } from './site-content.mjs';
8
+ import {
9
+ legacySiteConfigPath,
10
+ siteConfigLabel,
11
+ siteConfigPath,
12
+ siteThemeLabel,
13
+ } from './site-paths.mjs';
3
14
  import { readThemeConfig } from './theme-config.mjs';
4
15
  import { resolveThemeConfig } from './theme-presets.mjs';
5
16
 
6
- const { default: siteConfig } = await import(/* @vite-ignore */ pathToFileURL(siteConfigPath).href);
17
+ const fileExists = (filePath) => access(filePath).then(() => true, () => false);
18
+ const readSiteConfig = async () => {
19
+ const source = await readFile(siteConfigPath, 'utf8').catch(async (error) => {
20
+ if (error?.code === 'ENOENT' && await fileExists(legacySiteConfigPath)) {
21
+ throw new Error(`${siteConfigLabel} is required. Replace the obsolete config.mjs with a frontmatter-only config.md file.`);
22
+ }
23
+
24
+ if (error?.code === 'ENOENT') {
25
+ throw new Error(`${siteConfigLabel} is required. Create it before running Norna.`);
26
+ }
27
+
28
+ throw error;
29
+ });
30
+ const { body, frontmatter, frontmatterBody } = splitSiteFile(source, siteConfigLabel);
31
+ const issues = [];
32
+
33
+ validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
34
+ validateConfigFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
35
+
36
+ if (body.trim()) {
37
+ issues.push({
38
+ message: `${siteConfigLabel} may contain YAML frontmatter only. Remove the Markdown body.`,
39
+ });
40
+ }
41
+
42
+ if (issues.length > 0) {
43
+ throw new Error([
44
+ `${siteConfigLabel} has invalid frontmatter.`,
45
+ ...issues.map((issue) => `- ${issue.message}`),
46
+ ].join('\n'));
47
+ }
48
+
49
+ return parseYamlMapping(frontmatterBody);
50
+ };
51
+
52
+ const siteConfig = await readSiteConfig();
7
53
  const themeConfig = await readThemeConfig();
8
54
 
9
55
  const cssLengthPattern = String.raw`(?:\d+|\d*\.\d+)(?:px|rem|em|vw|vh|vmin|vmax|ch|%)`;
@@ -28,16 +74,6 @@ const readEnum = (object, key, path, allowedValues, fallback, sourceLabel = site
28
74
  return value;
29
75
  };
30
76
 
31
- const readString = (object, key, path, sourceLabel = siteConfigLabel) => {
32
- const value = object[key];
33
-
34
- if (typeof value !== 'string' || value.trim() === '') {
35
- throw new Error(`${path}.${key} must be a non-empty string in ${sourceLabel}.`);
36
- }
37
-
38
- return value.trim();
39
- };
40
-
41
77
  const readFontFamily = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
42
78
  const value = object[key] ?? fallback;
43
79
 
@@ -138,123 +174,82 @@ const readResponsivePercent = (object, key, path, fallback, sourceLabel = siteCo
138
174
  });
139
175
  };
140
176
 
141
- const readBoolean = (object, key, path, fallback) => {
142
- const value = object[key] ?? fallback;
143
-
144
- if (typeof value !== 'boolean') {
145
- throw new Error(`${path}.${key} must be a boolean in ${siteConfigLabel}.`);
146
- }
147
-
148
- return value;
149
- };
150
-
151
- const readPositiveInteger = (object, key, path, fallback) => {
152
- const value = object[key] ?? fallback;
177
+ const readSiteUrl = (config) => {
178
+ const configuredValue = process.env.NORNA_SITE_URL ?? config.url;
153
179
 
154
- if (!Number.isInteger(value) || value <= 0) {
155
- throw new Error(`${path}.${key} must be a positive integer in ${siteConfigLabel}.`);
180
+ if (typeof configuredValue !== 'string' || configuredValue.trim() === '') {
181
+ throw new Error(`url must be a non-empty absolute URL in ${siteConfigLabel}.`);
156
182
  }
157
183
 
158
- return value;
159
- };
160
-
161
- const readPositiveNumber = (object, key, path, fallback) => {
162
- const value = object[key] ?? fallback;
163
-
164
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
165
- throw new Error(`${path}.${key} must be a positive number in ${siteConfigLabel}.`);
166
- }
167
-
168
- return value;
169
- };
170
-
171
- const readUrl = (object, key, path) => {
172
- const value = readString(object, key, path);
173
-
184
+ let url;
174
185
  try {
175
- return new URL(value).href;
186
+ url = new URL(configuredValue.trim());
176
187
  } catch {
177
- throw new Error(`${path}.${key} must be an absolute URL in ${siteConfigLabel}.`);
188
+ throw new Error(`url must be an absolute URL such as "https://example.com/" in ${siteConfigLabel}.`);
178
189
  }
179
- };
180
190
 
181
- const readBasePath = (object, key, path) => {
182
- const value = object[key] ?? '/';
183
-
184
- if (typeof value !== 'string' || value.trim() === '') {
185
- throw new Error(`${path}.${key} must be a non-empty URL path in ${siteConfigLabel}.`);
191
+ if (!['http:', 'https:'].includes(url.protocol)) {
192
+ throw new Error(`url must use http or https in ${siteConfigLabel}.`);
186
193
  }
187
-
188
- const normalizedValue = value.trim();
189
-
190
- if (
191
- !normalizedValue.startsWith('/')
192
- || !normalizedValue.endsWith('/')
193
- || normalizedValue.includes('//')
194
- || /[\s?#]/.test(normalizedValue)
195
- ) {
196
- throw new Error(`${path}.${key} must start and end with "/" and must not contain whitespace, "?", "#", or "//" in ${siteConfigLabel}.`);
194
+ if (url.search || url.hash) {
195
+ throw new Error(`url must not contain a query string or fragment in ${siteConfigLabel}.`);
196
+ }
197
+ if (!url.pathname.endsWith('/')) {
198
+ url.pathname = `${url.pathname}/`;
199
+ }
200
+ if (url.pathname.includes('//')) {
201
+ throw new Error(`url must not contain repeated slashes in its path in ${siteConfigLabel}.`);
197
202
  }
198
203
 
199
- return normalizedValue;
204
+ return url;
200
205
  };
201
206
 
202
- const readSmoothScroll = (navigation) => {
203
- const rawSmoothScroll = assertObject(navigation.smoothScroll ?? {}, 'navigation.smoothScroll');
204
- const minimumDurationMs = readPositiveInteger(rawSmoothScroll, 'minimumDurationMs', 'navigation.smoothScroll', 2_000);
205
- const maximumDurationMs = readPositiveInteger(rawSmoothScroll, 'maximumDurationMs', 'navigation.smoothScroll', 4_000);
207
+ const localeLabels = Object.freeze({
208
+ en: Object.freeze({
209
+ dismissBanner: 'Dismiss notice',
210
+ gallery: 'Images',
211
+ note: 'Note',
212
+ pageNavigation: 'On this page',
213
+ siteBanners: 'Site notices',
214
+ siteNavigation: 'Pages',
215
+ skipToContent: 'Skip to content',
216
+ }),
217
+ sv: Object.freeze({
218
+ dismissBanner: 'Stäng meddelande',
219
+ gallery: 'Bilder',
220
+ note: 'Not',
221
+ pageNavigation: 'På den här sidan',
222
+ siteBanners: 'Meddelanden',
223
+ siteNavigation: 'Sidor',
224
+ skipToContent: 'Hoppa till innehållet',
225
+ }),
226
+ });
206
227
 
207
- if (maximumDurationMs < minimumDurationMs) {
208
- throw new Error(`navigation.smoothScroll.maximumDurationMs must be greater than or equal to minimumDurationMs in ${siteConfigLabel}.`);
209
- }
228
+ const readLocale = (config) => {
229
+ const lang = config.language ?? 'en';
210
230
 
211
- return Object.freeze({
212
- durationPerPixelMs: readPositiveNumber(rawSmoothScroll, 'durationPerPixelMs', 'navigation.smoothScroll', 0.22),
213
- enabled: readBoolean(rawSmoothScroll, 'enabled', 'navigation.smoothScroll', true),
214
- maximumDurationMs,
215
- minimumDurationMs,
216
- });
217
- };
231
+ if (typeof lang !== 'string' || !/^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]+)*$/.test(lang.trim())) {
232
+ throw new Error(`language must be a valid language tag such as "en" or "sv" in ${siteConfigLabel}.`);
233
+ }
218
234
 
219
- const readLocale = (rawLocale) => {
220
- const locale = assertObject(rawLocale ?? {}, 'locale');
221
- const labels = assertObject(locale.labels ?? {}, 'locale.labels');
222
- const lang = locale.lang ?? 'en';
235
+ const normalizedLang = lang.trim();
236
+ const languageKey = normalizedLang.toLowerCase().split('-')[0];
237
+ const labels = localeLabels[languageKey];
223
238
 
224
- if (typeof lang !== 'string' || !/^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]+)*$/.test(lang.trim())) {
225
- throw new Error(`locale.lang must be a valid language tag such as "en" or "sv" in ${siteConfigLabel}.`);
239
+ if (!labels) {
240
+ throw new Error(`language "${normalizedLang}" has no built-in Norna UI text. Supported languages: ${Object.keys(localeLabels).join(', ')}.`);
226
241
  }
227
242
 
228
243
  return Object.freeze({
229
- lang: lang.trim(),
230
- labels: Object.freeze({
231
- closeMenu: readString({ closeMenu: labels.closeMenu ?? 'Close menu' }, 'closeMenu', 'locale.labels'),
232
- dismissBanner: readString({ dismissBanner: labels.dismissBanner ?? 'Dismiss notice' }, 'dismissBanner', 'locale.labels'),
233
- skipToContent: readString({ skipToContent: labels.skipToContent ?? 'Skip to content' }, 'skipToContent', 'locale.labels'),
234
- sectionNavigation: readString({ sectionNavigation: labels.sectionNavigation ?? 'Sections' }, 'sectionNavigation', 'locale.labels'),
235
- gallery: readString({ gallery: labels.gallery ?? 'Images' }, 'gallery', 'locale.labels'),
236
- note: readString({ note: labels.note ?? 'Note' }, 'note', 'locale.labels'),
237
- menu: readString({ menu: labels.menu ?? 'Menu' }, 'menu', 'locale.labels'),
238
- pageNavigation: readString({ pageNavigation: labels.pageNavigation ?? 'On this page' }, 'pageNavigation', 'locale.labels'),
239
- siteNavigation: readString({ siteNavigation: labels.siteNavigation ?? 'Pages' }, 'siteNavigation', 'locale.labels'),
240
- siteBanners: readString({ siteBanners: labels.siteBanners ?? 'Site notices' }, 'siteBanners', 'locale.labels'),
241
- }),
244
+ lang: normalizedLang,
245
+ labels,
242
246
  });
243
247
  };
244
248
 
245
- const rawConfig = assertObject(siteConfig, 'default export');
246
- const misplacedThemeKeys = ['layout', 'gallery', 'typography'].filter((key) => key in rawConfig);
247
- if (misplacedThemeKeys.length > 0) {
248
- throw new Error(`${misplacedThemeKeys.join(', ')} belong in ${siteThemeLabel}, not ${siteConfigLabel}.`);
249
- }
250
-
249
+ const rawConfig = assertObject(siteConfig, 'config frontmatter');
251
250
  const rawTheme = assertObject(themeConfig, 'theme frontmatter', siteThemeLabel);
252
- const rawSite = assertObject(rawConfig.site, 'site');
253
- const rawNavigation = assertObject(rawConfig.navigation ?? {}, 'navigation');
254
- const rawLocale = rawConfig.locale ?? {};
255
- const rawGithub = assertObject(rawConfig.github, 'github');
256
- const rawDeploy = assertObject(rawConfig.deploy ?? {}, 'deploy');
257
- const rawDeployWatch = assertObject(rawDeploy.watch ?? {}, 'deploy.watch');
251
+ const siteUrl = readSiteUrl(rawConfig);
252
+ const scrollBehaviorNames = ['instant', 'browser-smooth'];
258
253
 
259
254
  const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
260
255
  const layoutDensityNames = ['compact', 'normal', 'airy'];
@@ -383,26 +378,20 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
383
378
 
384
379
  export const projectConfig = Object.freeze({
385
380
  site: Object.freeze({
386
- basePath: readBasePath(rawSite, 'basePath', 'site'),
387
- url: readUrl(rawSite, 'url', 'site'),
381
+ basePath: siteUrl.pathname,
382
+ url: siteUrl.href,
388
383
  }),
389
384
  ...resolveThemeVisualConfig(rawTheme, siteThemeLabel),
390
385
  navigation: Object.freeze({
391
- smoothScroll: readSmoothScroll(rawNavigation),
392
- }),
393
- locale: readLocale(rawLocale),
394
- github: Object.freeze({
395
- repo: readString(rawGithub, 'repo', 'github'),
396
- branch: readString(rawGithub, 'branch', 'github'),
397
- pagesWorkflow: readString(rawGithub, 'pagesWorkflow', 'github'),
398
- }),
399
- deploy: Object.freeze({
400
- watch: Object.freeze({
401
- intervalMs: readPositiveInteger(rawDeployWatch, 'intervalMs', 'deploy.watch', 10_000),
402
- timeoutMs: readPositiveInteger(rawDeployWatch, 'timeoutMs', 'deploy.watch', 15 * 60_000),
403
- runLimit: readPositiveInteger(rawDeployWatch, 'runLimit', 'deploy.watch', 10),
404
- }),
386
+ scrollBehavior: readEnum(
387
+ rawConfig,
388
+ 'scrollBehavior',
389
+ 'config frontmatter',
390
+ scrollBehaviorNames,
391
+ 'instant',
392
+ ),
405
393
  }),
394
+ locale: readLocale(rawConfig),
406
395
  });
407
396
 
408
397
  export default projectConfig;
@@ -20,6 +20,7 @@ const h2Regex = /^##\s+.*$/gm;
20
20
  const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
21
21
  const deprecatedInlineStyleReferenceRegex = /\[[^\]\n]+\]\{\.([a-z][a-z0-9-]*)\}/g;
22
22
  const frontmatterDelimiterRegex = /^---\s*$/;
23
+ const knownConfigTopLevelFrontmatterKeys = new Set(['url', 'language', 'scrollBehavior']);
23
24
  const knownContentTopLevelFrontmatterKeys = new Set(['title', 'description', 'navigation', 'sections']);
24
25
  const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', 'gallery', 'typography', 'presentation']);
25
26
  const knownRouteThemeTopLevelFrontmatterKeys = new Set(['navigation', 'preset', 'layout', 'gallery', 'typography', 'presentation']);
@@ -270,6 +271,12 @@ export const validateContentFrontmatterStructure = (frontmatter, addIssue) =>
270
271
  fileKind: 'content',
271
272
  });
272
273
 
274
+ export const validateConfigFrontmatterStructure = (frontmatter, addIssue) =>
275
+ validateFrontmatterStructure(frontmatter, addIssue, {
276
+ knownTopLevelFrontmatterKeys: knownConfigTopLevelFrontmatterKeys,
277
+ fileKind: 'config',
278
+ });
279
+
273
280
  export const validateThemeFrontmatterStructure = (frontmatter, addIssue) =>
274
281
  validateFrontmatterStructure(frontmatter, addIssue, {
275
282
  knownTopLevelFrontmatterKeys: knownThemeTopLevelFrontmatterKeys,
@@ -22,7 +22,10 @@ export const invocationRoot = configuredInvocationRoot
22
22
 
23
23
  const hasSiteFilesInDirectory = (siteDir) => {
24
24
  return (
25
- existsSync(path.join(siteDir, 'config.mjs'))
25
+ (
26
+ existsSync(path.join(siteDir, 'config.md'))
27
+ || existsSync(path.join(siteDir, 'config.mjs'))
28
+ )
26
29
  && existsSync(path.join(siteDir, 'content.md'))
27
30
  );
28
31
  };
@@ -114,7 +117,8 @@ const getPathLabel = (filePath) => {
114
117
  };
115
118
 
116
119
  export const siteDir = resolvedSitePaths.siteDir;
117
- export const siteConfigPath = path.join(siteDir, 'config.mjs');
120
+ export const siteConfigPath = path.join(siteDir, 'config.md');
121
+ export const legacySiteConfigPath = path.join(siteDir, 'config.mjs');
118
122
  export const siteThemePath = path.join(siteDir, 'theme.md');
119
123
  export const siteContentPath = path.join(siteDir, 'content.md');
120
124
  export const sitewideContentPath = path.join(siteDir, 'sitewide-content.md');