@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
@@ -0,0 +1,221 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { spawnSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ const nornaBin = path.join(repoRoot, 'bin', 'norna.mjs');
9
+ const tempParent = path.join(repoRoot, 'node_modules', '.cache');
10
+ await mkdir(tempParent, { recursive: true });
11
+ const tempRoot = await mkdtemp(path.join(tempParent, 'norna-client-javascript-'));
12
+ const siteDir = path.join(tempRoot, 'site');
13
+ const routeDir = path.join(siteDir, 'routes', '010-details');
14
+ const configPath = path.join(siteDir, 'config.md');
15
+
16
+ const runBuild = () => {
17
+ const result = spawnSync(process.execPath, [nornaBin, 'build'], {
18
+ cwd: tempRoot,
19
+ encoding: 'utf8',
20
+ env: {
21
+ ...process.env,
22
+ NORNA_SITE_DIR: siteDir,
23
+ },
24
+ });
25
+
26
+ if (result.status !== 0) {
27
+ throw new Error([
28
+ `norna build exited with code ${result.status}.`,
29
+ result.stdout.trim(),
30
+ result.stderr.trim(),
31
+ ].filter(Boolean).join('\n'));
32
+ }
33
+ };
34
+
35
+ const readPage = (pathname = 'index.html') => readFile(path.join(tempRoot, 'dist', pathname), 'utf8');
36
+ const getScripts = (html) => html.match(/<script\b[^>]*>[\s\S]*?<\/script>/gi) ?? [];
37
+ const assertNoClientJavaScript = (html, label) => {
38
+ assert.deepEqual(getScripts(html), [], `${label} should not require client-side JavaScript.`);
39
+ };
40
+ const assertScrollBehavior = (html, behavior, label) => {
41
+ assert.match(
42
+ html,
43
+ new RegExp(`<html\\b[^>]*style="[^"]*scroll-behavior: ${behavior}(?:;|&quot;|\")`),
44
+ `${label} should render scroll-behavior: ${behavior}.`,
45
+ );
46
+ };
47
+ const writeConfig = (scrollBehavior) => writeFile(configPath, `---
48
+ url: https://example.com/
49
+ ${scrollBehavior ? `scrollBehavior: ${scrollBehavior}\n` : ''}---
50
+ `);
51
+ const writeSvg = (filePath, color) => writeFile(filePath, [
52
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 90">',
53
+ ` <rect width="160" height="90" fill="${color}"/>`,
54
+ '</svg>',
55
+ '',
56
+ ].join('\n'));
57
+
58
+ try {
59
+ await mkdir(path.join(siteDir, 'public'), { recursive: true });
60
+ await writeConfig();
61
+ await writeFile(path.join(siteDir, 'theme.md'), `---
62
+ typography:
63
+ preset: text-forward
64
+ presentation:
65
+ palette: paper
66
+ ---
67
+ `);
68
+ await writeFile(path.join(siteDir, 'sitewide-content.md'), `---
69
+ navigation:
70
+ brand: Test site
71
+ ---
72
+ `);
73
+ await writeFile(path.join(siteDir, 'content.md'), `---
74
+ title: Static single page
75
+ description: A single page without interactive features.
76
+ sections:
77
+ intro: {}
78
+ more: {}
79
+ ---
80
+ ## Intro {#intro}
81
+
82
+ Ordinary Markdown needs no client-side JavaScript.
83
+
84
+ ## More {#more}
85
+
86
+ Additional sections do not make the page interactive.
87
+ `);
88
+
89
+ runBuild();
90
+ const instantHtml = await readPage();
91
+ assertNoClientJavaScript(instantHtml, 'The default instant scroll mode');
92
+ assertScrollBehavior(instantHtml, 'auto', 'The default instant scroll mode');
93
+ assert.match(instantHtml, /<nav class="page-nav"/);
94
+ assert.match(instantHtml, /href="#intro"/);
95
+ assert.match(instantHtml, /href="#more"/);
96
+
97
+ await writeConfig('browser-smooth');
98
+ runBuild();
99
+ const browserSmoothHtml = await readPage();
100
+ assertNoClientJavaScript(browserSmoothHtml, 'The browser smooth scroll mode');
101
+ assertScrollBehavior(browserSmoothHtml, 'smooth', 'The browser smooth scroll mode');
102
+
103
+ await writeConfig();
104
+
105
+ await mkdir(path.join(routeDir, 'images', 'details'), { recursive: true });
106
+ await writeSvg(path.join(routeDir, 'images', 'details', 'first.svg'), '#7b8f78');
107
+ await writeSvg(path.join(routeDir, 'images', 'details', 'second.svg'), '#a7664b');
108
+ await writeFile(path.join(siteDir, 'content.md'), `---
109
+ title: Static baseline
110
+ description: A page without interactive features.
111
+ sections:
112
+ intro: {}
113
+ ---
114
+ ## Intro {#intro}
115
+
116
+ Ordinary Markdown needs no client-side JavaScript.
117
+ `);
118
+ await writeFile(path.join(routeDir, 'route-content.md'), `---
119
+ title: Details
120
+ description: Static managed images.
121
+ sections:
122
+ details: {}
123
+ ---
124
+ ## Details {#details}
125
+
126
+ Static image stacks remain static.
127
+
128
+ \`\`\`norna-image-stack
129
+ - image: first.svg
130
+ caption: First image.
131
+ - image: second.svg
132
+ caption: Second image.
133
+ \`\`\`
134
+
135
+ \`\`\`norna-card-list
136
+ - title: Static card
137
+ text: Cards do not require client-side behaviour.
138
+ \`\`\`
139
+ `);
140
+
141
+ runBuild();
142
+ assertNoClientJavaScript(await readPage(), 'A plain homepage in a multi-page site');
143
+ assertNoClientJavaScript(
144
+ await readPage(path.join('details', 'index.html')),
145
+ 'A page with managed image stacks and cards',
146
+ );
147
+
148
+ await writeFile(path.join(siteDir, 'content.md'), `---
149
+ title: Notes
150
+ description: A page with an enhanced sidenote.
151
+ sections:
152
+ intro: {}
153
+ ---
154
+ ## Intro {#intro}
155
+
156
+ This sentence has additional context.{note-ref}
157
+
158
+ {note: The note remains readable without JavaScript, while alignment is enhanced when JavaScript runs.}
159
+ `);
160
+ await writeFile(path.join(routeDir, 'route-content.md'), `---
161
+ title: Carousel
162
+ description: An interactive image carousel.
163
+ sections:
164
+ details: {}
165
+ ---
166
+ ## Details {#details}
167
+
168
+ \`\`\`norna-image-carousel
169
+ - image: first.svg
170
+ caption: First slide.
171
+ - image: second.svg
172
+ caption: Second slide.
173
+ \`\`\`
174
+ `);
175
+
176
+ runBuild();
177
+ const noteHtml = await readPage();
178
+ const carouselHtml = await readPage(path.join('details', 'index.html'));
179
+ assert.equal(getScripts(noteHtml).length, 1, 'A page with notes should load only the note enhancement.');
180
+ assert.match(noteHtml, /data-note-id=/);
181
+ assert.equal(getScripts(carouselHtml).length, 1, 'A carousel page should load only the carousel implementation.');
182
+ assert.match(carouselHtml, /data-carousel/);
183
+ assert.match(getScripts(carouselHtml)[0], /\ssrc=/, 'Carousel JavaScript should be emitted as a module asset.');
184
+
185
+ await writeFile(path.join(siteDir, 'content.md'), `---
186
+ title: Banner
187
+ description: A page with a dismissible banner.
188
+ sections:
189
+ intro: {}
190
+ ---
191
+ ## Intro {#intro}
192
+
193
+ The only interactive feature is the banner.
194
+ `);
195
+ await writeFile(path.join(routeDir, 'route-content.md'), `---
196
+ title: Details
197
+ description: A second plain page.
198
+ sections:
199
+ details: {}
200
+ ---
201
+ ## Details {#details}
202
+
203
+ Plain route content.
204
+ `);
205
+ await writeFile(path.join(siteDir, 'sitewide-content.md'), `---
206
+ banners:
207
+ - id: test-banner
208
+ title: Test banner
209
+ text: This banner can be dismissed.
210
+ ---
211
+ `);
212
+
213
+ runBuild();
214
+ const bannerHtml = await readPage();
215
+ assert.equal(getScripts(bannerHtml).length, 1, 'A dismissible banner should load only its dismissal script.');
216
+ assert.match(getScripts(bannerHtml)[0], /norna-banner:/);
217
+
218
+ console.log('Client JavaScript boundaries test passed.');
219
+ } finally {
220
+ await rm(tempRoot, { force: true, recursive: true });
221
+ }
@@ -239,6 +239,109 @@ test('content:check accepts valid inline notes and ignores note syntax in code f
239
239
  });
240
240
  });
241
241
 
242
+ test('content:check accepts wrapped and explicit multiline inline notes', async () => {
243
+ await withTempProject({
244
+ site: `---
245
+ title: Multiline inline notes
246
+ description: Fixture
247
+ ---
248
+ ## Wrapped {#wrapped}
249
+ This paragraph has a formatter-wrapped note.{note-ref}
250
+
251
+ {note: This note was wrapped by
252
+ an editor before its closing brace.}
253
+
254
+ ## Explicit {#explicit}
255
+ This paragraph has an explicit multiline note.{note-ref}
256
+
257
+ {note:
258
+ This note uses the explicit
259
+ multiline form.
260
+ }
261
+ `,
262
+ files: [],
263
+ }, async (root) => {
264
+ const result = runContentScript(root, ['--check']);
265
+ const output = getOutput(result);
266
+
267
+ assert.equal(result.status, 0, output);
268
+ assert.match(output, /Content check passed\./);
269
+ });
270
+ });
271
+
272
+ test('content:check reports an unclosed multiline inline note', async () => {
273
+ await withTempProject({
274
+ site: `---
275
+ title: Unclosed inline note
276
+ description: Fixture
277
+ ---
278
+ ## Intro {#intro}
279
+ This paragraph has a note reference.{note-ref}
280
+
281
+ {note: This note never closes
282
+ and continues to the end of the file.
283
+ `,
284
+ files: [],
285
+ }, async (root) => {
286
+ const result = runContentScript(root, ['--check']);
287
+ const output = getOutput(result);
288
+
289
+ assert.equal(result.status, 1, output);
290
+ assert.match(output, /The note starting on line \d+ is not closed\./);
291
+ assert.match(output, /End it with "\}" on its own line or at the end of the note text\./);
292
+ });
293
+ });
294
+
295
+ test('content:check rejects nested note syntax before a multiline note is closed', async () => {
296
+ await withTempProject({
297
+ site: `---
298
+ title: Nested inline note syntax
299
+ description: Fixture
300
+ ---
301
+ ## Nested note {#nested-note}
302
+ This paragraph has a note reference.{note-ref}
303
+
304
+ {note: The first note is still open
305
+ {note: A second note starts here.}
306
+
307
+ ## Nested reference {#nested-reference}
308
+ This paragraph has another note reference.{note-ref}
309
+
310
+ {note: This note is still open
311
+ and contains {note-ref} before closing.}
312
+ `,
313
+ files: [],
314
+ }, async (root) => {
315
+ const result = runContentScript(root, ['--check']);
316
+ const output = getOutput(result);
317
+
318
+ assert.equal(result.status, 1, output);
319
+ assert.match(output, /is not closed before another note starts on line \d+\./);
320
+ assert.match(output, /contains "\{note-ref\}" on line \d+\./);
321
+ });
322
+ });
323
+
324
+ test('content:check allows escaped and inline-code note syntax inside notes', async () => {
325
+ await withTempProject({
326
+ site: `---
327
+ title: Literal inline note syntax
328
+ description: Fixture
329
+ ---
330
+ ## Intro {#intro}
331
+ This paragraph has a note reference.{note-ref}
332
+
333
+ {note: Write \\{note: ... to show escaped text, or use \`{note-ref}\` as inline code.}
334
+ `,
335
+ files: [],
336
+ }, async (root) => {
337
+ const result = runContentScript(root, ['--check']);
338
+ const output = getOutput(result);
339
+
340
+ assert.equal(result.status, 0, output);
341
+ assert.match(output, /Content check passed\./);
342
+ });
343
+ });
344
+
242
345
  test('content:check rejects unpaired and repeated inline notes', async () => {
243
346
  await withTempProject({
244
347
  site: `---
@@ -51,17 +51,9 @@ const createTempSite = async ({ underRepoCache = false } = {}) => {
51
51
  const root = await mkdtemp(path.join(tempParent, 'norna-content-model-v2-'));
52
52
  const siteDir = path.join(root, 'site');
53
53
  await mkdir(siteDir, { recursive: true });
54
- await writeFile(path.join(siteDir, 'config.mjs'), `export default {
55
- site: {
56
- url: 'https://example.com',
57
- basePath: '/',
58
- },
59
- github: {
60
- branch: 'main',
61
- repo: 'owner/repo',
62
- pagesWorkflow: 'Deploy to GitHub Pages',
63
- },
64
- };
54
+ await writeFile(path.join(siteDir, 'config.md'), `---
55
+ url: https://example.com/
56
+ ---
65
57
  `);
66
58
  await writeFile(path.join(siteDir, 'theme.md'), `---
67
59
  typography:
@@ -294,11 +286,15 @@ description: Fixture
294
286
 
295
287
  The first paragraph points to an explanation.{note-ref}
296
288
 
297
- {note: This explanation names \`sitewide-content.md\` and stays beside the paragraph on wide screens.}
289
+ {note: This explanation names \`sitewide-content.md\` and stays beside the
290
+ paragraph on wide screens.}
298
291
 
299
292
  The second paragraph has its own explanation.{note-ref}
300
293
 
301
- {note: This is the second explanation.}
294
+ {note:
295
+ This is the second
296
+ explanation.
297
+ }
302
298
  `);
303
299
 
304
300
  await runNorna(['--site-dir', siteDir, 'build']);
@@ -0,0 +1,116 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const repoRoot = path.resolve(import.meta.dirname, '..');
8
+ const cliPath = path.join(repoRoot, 'bin', 'norna.mjs');
9
+ const tutorialPath = path.join(repoRoot, 'docs', 'getting-started.md');
10
+ const scratchRoot = path.join(repoRoot, 'todos');
11
+ await mkdir(scratchRoot, { recursive: true });
12
+ const tempRoot = await mkdtemp(path.join(scratchRoot, 'norna-documentation-'));
13
+ const tutorialSiteRoot = path.join(tempRoot, 'my-site');
14
+
15
+ const runCli = (args) => spawnSync(process.execPath, [cliPath, ...args], {
16
+ cwd: repoRoot,
17
+ encoding: 'utf8',
18
+ });
19
+
20
+ const assertSuccess = (result, label) => {
21
+ assert.equal(result.status, 0, `${label} failed:\n${result.stderr || result.stdout}`);
22
+ };
23
+
24
+ const extractTutorialFile = (source, label, language) => {
25
+ const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
+ const pattern = new RegExp(`${escapedLabel}\\n\\n\\x60{3}${language}\\n([\\s\\S]*?)\\n\\x60{3}`);
27
+ const match = source.match(pattern);
28
+ assert.ok(match, `Could not find the ${label} code block in docs/getting-started.md.`);
29
+ return `${match[1]}\n`;
30
+ };
31
+
32
+ const collectMarkdownFiles = async (directory) => {
33
+ const files = [];
34
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
35
+ const entryPath = path.join(directory, entry.name);
36
+ if (entry.isDirectory()) {
37
+ files.push(...await collectMarkdownFiles(entryPath));
38
+ } else if (entry.name.endsWith('.md')) {
39
+ files.push(entryPath);
40
+ }
41
+ }
42
+ return files;
43
+ };
44
+
45
+ const checkLocalMarkdownLinks = async () => {
46
+ const markdownFiles = [
47
+ path.join(repoRoot, 'README.md'),
48
+ ...await collectMarkdownFiles(path.join(repoRoot, 'docs')),
49
+ ...await collectMarkdownFiles(path.join(repoRoot, 'examples')),
50
+ ];
51
+ const missing = [];
52
+
53
+ for (const markdownPath of markdownFiles) {
54
+ const source = await readFile(markdownPath, 'utf8');
55
+ const prose = source
56
+ .replace(/(```+|~~~+)[\s\S]*?\1/g, '')
57
+ .replace(/`[^`\n]*`/g, '');
58
+ for (const match of prose.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
59
+ const target = match[1].trim().replace(/^<|>$/g, '');
60
+ if (/^(?:[a-z]+:|#|\/)/i.test(target)) continue;
61
+
62
+ const targetPath = decodeURIComponent(target.split('#', 1)[0].split('?', 1)[0]);
63
+ if (!targetPath) continue;
64
+ const resolved = path.resolve(path.dirname(markdownPath), targetPath);
65
+ if (!existsSync(resolved)) {
66
+ missing.push(`${path.relative(repoRoot, markdownPath)} -> ${target}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ assert.deepEqual(missing, [], `Broken local Markdown links:\n${missing.join('\n')}`);
72
+ };
73
+
74
+ try {
75
+ const tutorial = await readFile(tutorialPath, 'utf8');
76
+ const initResult = runCli(['init', tutorialSiteRoot]);
77
+ assertSuccess(initResult, 'Tutorial init');
78
+ await rm(path.join(tutorialSiteRoot, 'node_modules'), { force: true, recursive: true });
79
+ await symlink(path.join(repoRoot, 'node_modules'), path.join(tutorialSiteRoot, 'node_modules'), 'dir');
80
+
81
+ await Promise.all([
82
+ writeFile(
83
+ path.join(tutorialSiteRoot, 'site', 'content.md'),
84
+ extractTutorialFile(tutorial, 'Replace `site/content.md` with:', 'md'),
85
+ ),
86
+ writeFile(
87
+ path.join(tutorialSiteRoot, 'site', 'sitewide-content.md'),
88
+ extractTutorialFile(tutorial, 'Replace `site/sitewide-content.md` with:', 'yaml'),
89
+ ),
90
+ writeFile(
91
+ path.join(tutorialSiteRoot, 'site', 'theme.md'),
92
+ extractTutorialFile(tutorial, 'Replace `site/theme.md` with:', 'yaml'),
93
+ ),
94
+ ]);
95
+
96
+ const siteDirectory = path.join(tutorialSiteRoot, 'site');
97
+ assertSuccess(runCli(['--site-dir', siteDirectory, 'config:check']), 'Tutorial config:check');
98
+ assertSuccess(runCli(['--site-dir', siteDirectory, 'content:check']), 'Tutorial content:check');
99
+ assertSuccess(runCli(['--site-dir', siteDirectory, 'build']), 'Tutorial build');
100
+
101
+ const html = await readFile(path.join(tutorialSiteRoot, 'dist', 'index.html'), 'utf8');
102
+ assert.match(html, /My first Norna site/);
103
+ assert.match(html, /Welcome/);
104
+ assert.match(html, /Next/);
105
+
106
+ await checkLocalMarkdownLinks();
107
+
108
+ const llms = await readFile(path.join(repoRoot, 'site', 'public', 'llms.txt'), 'utf8');
109
+ for (const match of llms.matchAll(/https:\/\/raw\.githubusercontent\.com\/janga\/norna\/main\/([^\s)]+)/g)) {
110
+ assert.ok(existsSync(path.join(repoRoot, decodeURIComponent(match[1]))), `llms.txt target is missing: ${match[1]}`);
111
+ }
112
+
113
+ console.log('ok - tutorial source builds and documentation links resolve');
114
+ } finally {
115
+ await rm(tempRoot, { force: true, recursive: true });
116
+ }
@@ -24,9 +24,18 @@ try {
24
24
  assert.match(versionResult.stdout, new RegExp(`Installed norna: ${packageJson.version.replaceAll('.', '\\.')}`));
25
25
  assert.match(versionResult.stdout, /Installed Astro: /);
26
26
 
27
+ const deployHelpResult = runCli(['deploy', '--help']);
28
+ assert.equal(deployHelpResult.status, 0, deployHelpResult.stderr || deployHelpResult.stdout);
29
+ assert.match(deployHelpResult.stdout, /already committed default branch/);
30
+ const deployWatchHelpResult = runCli(['deploy:watch', '--help']);
31
+ assert.equal(deployWatchHelpResult.status, 0, deployWatchHelpResult.stderr || deployWatchHelpResult.stdout);
32
+ assert.match(deployWatchHelpResult.stdout, /Default: current repository/);
33
+ assert.match(deployWatchHelpResult.stdout, /Default: repository default branch/);
34
+ assert.match(deployWatchHelpResult.stdout, /Default: deploy\.yml/);
35
+
27
36
  const directSiteRoot = path.join(tempRoot, 'current-directory-site');
28
37
  await mkdir(directSiteRoot, { recursive: true });
29
- await writeFile(path.join(directSiteRoot, 'config.mjs'), 'export default { site: { url: "https://example.com/" } };\n');
38
+ await writeFile(path.join(directSiteRoot, 'config.md'), '---\nurl: https://example.com/\n---\n');
30
39
  await writeFile(path.join(directSiteRoot, 'content.md'), '---\ntitle: Direct Site\ndescription: Direct site fixture.\n---\n\n## Intro {#intro}\n\nText.\n');
31
40
  const directSiteProjectRoot = await realpath(tempRoot);
32
41
  const directSiteDoctorResult = runCli(['doctor'], {
@@ -95,7 +104,7 @@ try {
95
104
  assert.equal(customPurePackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
96
105
  assert.equal(customPurePackageJson.scripts['norna:build'], 'norna --site-dir presentation build');
97
106
  await readFile(path.join(customPureSiteRoot, 'presentation', 'content.md'));
98
- await readFile(path.join(customPureSiteRoot, 'presentation', 'config.mjs'));
107
+ await readFile(path.join(customPureSiteRoot, 'presentation', 'config.md'));
99
108
  await readFile(path.join(customPureSiteRoot, 'presentation', 'theme.md'));
100
109
 
101
110
  const mixedProjectRoot = path.join(tempRoot, 'mixed-project');
@@ -121,7 +130,7 @@ try {
121
130
  assert.equal(mixedPackageJson.scripts['norna:engine:update'], 'norna --site-dir presentation engine:update');
122
131
  assert.equal(mixedPackageJson.scripts['norna:engine:version'], 'norna --site-dir presentation engine:version');
123
132
  await readFile(path.join(mixedProjectRoot, 'presentation', 'content.md'));
124
- await readFile(path.join(mixedProjectRoot, 'presentation', 'config.mjs'));
133
+ await readFile(path.join(mixedProjectRoot, 'presentation', 'config.md'));
125
134
  await readFile(path.join(mixedProjectRoot, 'presentation', 'theme.md'));
126
135
 
127
136
  const conflictProjectRoot = path.join(tempRoot, 'conflict-project');
@@ -1,3 +1,5 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
1
3
  import path from 'node:path';
2
4
  import { fileURLToPath } from 'node:url';
3
5
  import { getExampleSites } from './lib/example-sites.mjs';
@@ -10,6 +12,13 @@ const examples = await getExampleSites(root);
10
12
  for (const example of examples) {
11
13
  console.log(`\nBuilding ${example.siteLabel}`);
12
14
  await runInherit(process.execPath, [cliPath, '--site-dir', example.siteLabel, 'build'], { cwd: root });
15
+
16
+ if (['svg', 'png', 'jpg', 'jpeg'].some((extension) => existsSync(path.join(example.siteDirectory, 'public', `logo.${extension}`)))) {
17
+ const homepageHtml = await readFile(path.join(root, 'dist', 'index.html'), 'utf8');
18
+ if (!homepageHtml.includes('class="site-brand-logo"')) {
19
+ throw new Error(`Example ${example.siteLabel} has a logo file that is not rendered on its homepage.`);
20
+ }
21
+ }
13
22
  }
14
23
 
15
24
  console.log(`\nok - built ${examples.length} example sites`);
@@ -158,12 +158,13 @@ try {
158
158
  assertFileExists(path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml')),
159
159
  assertFileExists(path.join(packagedStarterRoot, 'package.json')),
160
160
  assertFileExists(path.join(packagedStarterRoot, 'README.md')),
161
- assertFileExists(path.join(packagedStarterRoot, 'site', 'config.mjs')),
161
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'config.md')),
162
162
  assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.md')),
163
163
  assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.md')),
164
164
  assertFileExists(path.join(packagedStarterRoot, 'site', 'content.md')),
165
165
  assertFileExists(path.join(packagedStarterRoot, 'site', 'images', 'work', '.gitkeep')),
166
166
  assertFileExists(path.join(packagedStarterRoot, 'site', 'public', 'robots.txt')),
167
+ assertFileMissing(path.join(packagedStarterRoot, 'site', 'config.mjs')),
167
168
  ]);
168
169
  await assertFileIncludes(
169
170
  path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml'),
@@ -177,13 +178,11 @@ try {
177
178
  packageJson.name = 'norna-package-check-site';
178
179
  packageJson.dependencies['@janga/norna'] = tarballPath;
179
180
  await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
180
- const packageCheckConfigPath = path.join(siteProjectRoot, 'site', 'config.mjs');
181
+ const packageCheckConfigPath = path.join(siteProjectRoot, 'site', 'config.md');
181
182
  const packageCheckConfig = await readFile(packageCheckConfigPath, 'utf8');
182
183
  await writeFile(
183
184
  packageCheckConfigPath,
184
- packageCheckConfig
185
- .replace("url: 'https://example.com/'", "url: 'https://example.com/site/'")
186
- .replace("basePath: '/'", "basePath: '/site/'"),
185
+ packageCheckConfig.replace('url: https://example.com/', 'url: https://example.com/site/'),
187
186
  );
188
187
  const packageCheckSitewidePath = path.join(siteProjectRoot, 'site', 'sitewide-content.md');
189
188
  const packageCheckSitewide = await readFile(packageCheckSitewidePath, 'utf8');
@@ -241,10 +240,11 @@ This route verifies that packaged norna sites can build route pages.
241
240
  await runInherit(nornaBinPath, ['init', initializedSiteRoot], { cwd: tempRoot, env: npmEnv });
242
241
  await Promise.all([
243
242
  assertFileExists(path.join(initializedSiteRoot, 'package.json')),
244
- assertFileExists(path.join(initializedSiteRoot, 'site', 'config.mjs')),
243
+ assertFileExists(path.join(initializedSiteRoot, 'site', 'config.md')),
245
244
  assertFileExists(path.join(initializedSiteRoot, 'site', 'theme.md')),
246
245
  assertFileMissing(path.join(initializedSiteRoot, '.DS_Store')),
247
246
  assertFileMissing(path.join(initializedSiteRoot, 'site', '.DS_Store')),
247
+ assertFileMissing(path.join(initializedSiteRoot, 'site', 'config.mjs')),
248
248
  ]);
249
249
  await runInherit(npxBin, ['norna', 'engine:version'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
250
250
  await runInherit(npxBin, ['norna', 'doctor'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
@@ -287,6 +287,14 @@ This route verifies that packaged norna sites can build route pages.
287
287
  path.join(siteProjectRoot, 'dist', 'index.html'),
288
288
  '<meta name="format-detection" content="telephone=no">',
289
289
  );
290
+ await assertFileIncludes(
291
+ path.join(siteProjectRoot, 'dist', 'index.html'),
292
+ 'style="scroll-behavior: auto;',
293
+ );
294
+ await assertFileExcludes(
295
+ path.join(siteProjectRoot, 'dist', 'index.html'),
296
+ 'data-smooth-scroll=',
297
+ );
290
298
  await assertFileIncludes(
291
299
  path.join(siteProjectRoot, 'dist', 'about', 'index.html'),
292
300
  '<link rel="canonical" href="https://example.com/site/about/">',
@@ -506,17 +514,17 @@ This route verifies that packaged norna sites can build route pages.
506
514
  'typography.fontFamily',
507
515
  );
508
516
  await writeFile(siteThemePath, siteTheme);
509
- const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.mjs');
517
+ const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.md');
510
518
  const siteConfig = await readFile(siteConfigPath, 'utf8');
511
519
  await writeFile(
512
520
  siteConfigPath,
513
- siteConfig.replace("basePath: '/site/'", "basePath: 'site'"),
521
+ siteConfig.replace('url: https://example.com/site/', 'url: example.com/site/'),
514
522
  );
515
523
  await runExpectFailure(
516
524
  npxBin,
517
525
  ['norna', 'config:check'],
518
526
  { cwd: siteProjectRoot, env: npmEnv },
519
- 'site.basePath',
527
+ 'url must be an absolute URL',
520
528
  );
521
529
 
522
530
  console.log('Package check passed.');