@janga/norna 0.7.16 → 0.7.17

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 (200) hide show
  1. package/README.md +6 -6
  2. package/astro.config.mjs +35 -3
  3. package/docs/README.md +10 -10
  4. package/docs/commands.md +2 -2
  5. package/docs/content.md +82 -102
  6. package/docs/design/navigation-and-theme-plan.md +115 -0
  7. package/docs/design/norna-diagram-design.md +9 -9
  8. package/docs/design/site-examples-structure.md +9 -9
  9. package/docs/engine-development.md +9 -9
  10. package/docs/getting-started.md +20 -15
  11. package/docs/images-and-metadata.md +10 -9
  12. package/docs/pages.md +134 -0
  13. package/docs/public-files.md +49 -36
  14. package/docs/publishing.md +19 -3
  15. package/docs/site-files.md +29 -28
  16. package/docs/sitewide-content.md +20 -36
  17. package/docs/theme.md +20 -20
  18. package/docs/typography.md +10 -10
  19. package/examples/README.md +2 -2
  20. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
  21. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
  27. package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
  28. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
  29. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
  30. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +3 -4
  31. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
  32. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
  33. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
  34. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
  35. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
  36. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
  37. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
  38. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
  39. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
  40. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
  41. package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
  42. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
  43. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
  44. package/examples/complete-sites/dog-shelter-single-page/site/content.md +5 -2
  45. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
  46. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
  47. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
  48. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +185 -88
  49. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  50. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
  51. package/examples/feature-demos/media-and-surfaces/README.md +2 -2
  52. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
  53. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
  54. package/examples/feature-demos/media-and-surfaces/site/content.md +4 -7
  55. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
  56. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +7 -6
  57. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  58. package/examples/feature-demos/sitewide-content/README.md +3 -3
  59. package/examples/feature-demos/sitewide-content/site/content.md +8 -7
  60. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
  61. package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
  62. package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
  63. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
  64. package/examples/feature-demos/theme-presets/README.md +1 -1
  65. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +60 -60
  66. package/examples/feature-demos/theme-presets/site/content.md +4 -7
  67. package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/content.md +4 -7
  68. package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/content.md +4 -7
  69. package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/content.md +4 -7
  70. package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/content.md +4 -7
  71. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +1 -10
  72. package/fixtures/basic/site/content.md +5 -6
  73. package/fixtures/basic/site/sitewide-content.yaml +1 -8
  74. package/fixtures/content-model-v2/site/content.md +11 -13
  75. package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
  76. package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
  77. package/fixtures/nested-pages/site/config.yaml +1 -0
  78. package/fixtures/nested-pages/site/content.md +15 -0
  79. package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
  80. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
  81. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
  82. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +14 -0
  83. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
  84. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +3 -0
  85. package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
  86. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
  87. package/fixtures/nested-pages/site/theme.yaml +3 -0
  88. package/package.json +7 -4
  89. package/schemas/config.schema.json +5 -5
  90. package/schemas/content-frontmatter.schema.json +28 -63
  91. package/schemas/manifest.json +1 -0
  92. package/schemas/sitewide-content.schema.json +58 -132
  93. package/schemas/theme.schema.json +396 -86
  94. package/scripts/check-config.mjs +6 -5
  95. package/scripts/deploy-site.mjs +6 -8
  96. package/scripts/doctor.mjs +2 -2
  97. package/scripts/generate-images.mjs +11 -18
  98. package/scripts/generate-schemas.mjs +3 -217
  99. package/scripts/lib/base-path-redirect.mjs +9 -0
  100. package/scripts/lib/editor-language-service.mjs +126 -66
  101. package/scripts/lib/heading-ids.mjs +161 -0
  102. package/scripts/lib/navigation-model.mjs +51 -0
  103. package/scripts/lib/norna-markdown-blocks.mjs +3 -4
  104. package/scripts/lib/page-model.mjs +83 -0
  105. package/scripts/lib/project-config.mjs +28 -0
  106. package/scripts/lib/schema-definitions.mjs +41 -46
  107. package/scripts/lib/schema-editor-metadata.mjs +475 -0
  108. package/scripts/lib/schema-value-definitions.mjs +6 -6
  109. package/scripts/lib/site-content.mjs +124 -87
  110. package/scripts/lib/site-paths.mjs +2 -2
  111. package/scripts/lib/theme-config.mjs +9 -16
  112. package/scripts/lib/theme-presets.mjs +9 -1
  113. package/scripts/release.mjs +1 -1
  114. package/scripts/show-typography.mjs +26 -20
  115. package/scripts/sync-content-sections.mjs +106 -65
  116. package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +7 -24
  117. package/scripts/test-client-javascript.mjs +39 -42
  118. package/scripts/test-content-check.mjs +138 -54
  119. package/scripts/test-content-model-v2.mjs +445 -228
  120. package/scripts/test-documentation.mjs +6 -1
  121. package/scripts/test-editor-language-service.mjs +168 -24
  122. package/scripts/test-engine-commands.mjs +2 -1
  123. package/scripts/test-heading-ids.mjs +68 -0
  124. package/scripts/test-navigation-model.mjs +63 -0
  125. package/scripts/test-nested-pages.mjs +110 -0
  126. package/scripts/test-package-check.mjs +31 -40
  127. package/scripts/test-page-model.mjs +80 -0
  128. package/scripts/test-project-config.mjs +25 -1
  129. package/scripts/test-schemas.mjs +66 -14
  130. package/scripts/test-theme-presets.mjs +33 -24
  131. package/src/components/NavigationPageTree.astro +68 -0
  132. package/src/components/SectionNavigationScript.astro +110 -5
  133. package/src/components/SiteBreadcrumbs.astro +29 -0
  134. package/src/components/SiteNavigation.astro +65 -69
  135. package/src/components/SitePage.astro +70 -46
  136. package/src/components/SiteSection.astro +27 -20
  137. package/src/content.config.ts +9 -5
  138. package/src/layouts/BaseLayout.astro +11 -20
  139. package/src/lib/pageThemes.ts +42 -0
  140. package/src/lib/sectionContent.ts +109 -106
  141. package/src/lib/siteNavigation.ts +48 -0
  142. package/src/lib/sitePages.ts +85 -38
  143. package/src/pages/{[slug].astro → [...slug].astro} +2 -2
  144. package/src/styles/global.css +373 -55
  145. package/starters/basic/README.md +5 -7
  146. package/starters/basic/site/content.md +7 -4
  147. package/starters/basic/site/sitewide-content.yaml +1 -10
  148. package/starters/project/README.md +8 -8
  149. package/starters/project/site/content.md +5 -4
  150. package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
  151. package/starters/project/site/sitewide-content.yaml +1 -10
  152. package/docs/routes.md +0 -121
  153. package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
  154. package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
  155. package/scripts/lib/route-model.mjs +0 -17
  156. package/scripts/test-route-model.mjs +0 -32
  157. package/src/lib/routeThemes.ts +0 -41
  158. /package/examples/complete-sites/dog-shelter-multi-page/site/images/{intro/shelter-dog.jpg → shelter-dog.jpg} +0 -0
  159. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
  160. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
  161. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
  162. /package/examples/complete-sites/dog-shelter-single-page/site/images/{you-can-help/foster-dog.jpg → foster-dog.jpg} +0 -0
  163. /package/examples/complete-sites/dog-shelter-single-page/site/images/{what-we-do/rescue-dog.jpg → rescue-dog.jpg} +0 -0
  164. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
  165. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
  166. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
  167. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
  168. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
  169. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
  170. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
  171. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
  172. /package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/theme.yaml +0 -0
  173. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/puppy-portrait.jpg +0 -0
  174. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/shepherd-portrait.jpg +0 -0
  175. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/white-puppy.jpg +0 -0
  176. /package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/theme.yaml +0 -0
  177. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/black-schnauzer.jpg +0 -0
  178. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/retriever-portrait.jpg +0 -0
  179. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/schnauzer-portrait.jpg +0 -0
  180. /package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/theme.yaml +0 -0
  181. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/boxer-portrait.jpg +0 -0
  182. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grass-puppy.jpg +0 -0
  183. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grey-street-dog.jpg +0 -0
  184. /package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/theme.yaml +0 -0
  185. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/black-pedigree.jpg +0 -0
  186. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-grass.jpg +0 -0
  187. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-wide.jpg +0 -0
  188. /package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/theme.yaml +0 -0
  189. /package/fixtures/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
  190. /package/fixtures/content-model-v2/site/images/{card-section/adopt.svg → adopt.svg} +0 -0
  191. /package/fixtures/content-model-v2/site/images/{intro/detail.jpg → detail.jpg} +0 -0
  192. /package/fixtures/content-model-v2/site/images/{intro/duplicate.jpg → duplicate.jpg} +0 -0
  193. /package/fixtures/content-model-v2/site/images/{card-section/foster.svg → foster.svg} +0 -0
  194. /package/fixtures/content-model-v2/site/images/{intro/hero.jpg → hero.jpg} +0 -0
  195. /package/fixtures/content-model-v2/site/images/{plain/image.jpg → image.jpg} +0 -0
  196. /package/fixtures/content-model-v2/site/images/{timed/slide-one.jpg → slide-one.jpg} +0 -0
  197. /package/fixtures/content-model-v2/site/images/{timed/slide-three.jpg → slide-three.jpg} +0 -0
  198. /package/fixtures/content-model-v2/site/images/{timed/slide-two.jpg → slide-two.jpg} +0 -0
  199. /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
  200. /package/starters/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
@@ -168,7 +168,7 @@ try {
168
168
  assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.yaml')),
169
169
  assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.yaml')),
170
170
  assertFileExists(path.join(packagedStarterRoot, 'site', 'content.md')),
171
- assertFileExists(path.join(packagedStarterRoot, 'site', 'images', 'work', '.gitkeep')),
171
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'images', '.gitkeep')),
172
172
  assertFileExists(path.join(packagedStarterRoot, 'site', 'public', 'robots.txt')),
173
173
  ]);
174
174
  await assertFileIncludes(
@@ -198,21 +198,19 @@ try {
198
198
  const packageCheckSitewide = await readFile(packageCheckSitewidePath, 'utf8');
199
199
  await writeFile(
200
200
  packageCheckSitewidePath,
201
- packageCheckSitewide.replace('label: Example Site', 'label: Package Check Logo\n logo:\n height: 2.6rem'),
201
+ `logo:\n height: 2.6rem\n${packageCheckSitewide}`,
202
202
  );
203
203
  await writeFile(
204
204
  path.join(siteProjectRoot, 'site', 'public', 'logo.svg'),
205
205
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="black"/></svg>\n',
206
206
  );
207
207
  await writeFile(path.join(siteProjectRoot, 'site', 'content.md'), `---
208
- title: Package Check Site
209
- description: Site used by package checks.
210
- sections:
211
- intro: {}
212
- warning: {}
213
- work: {}
208
+ page:
209
+ description: Site used by package checks.
214
210
  ---
215
211
 
212
+ # Package Check Site
213
+
216
214
  ## Intro {#intro}
217
215
 
218
216
  This page verifies that **packaged norna sites** can build with the current content model.
@@ -230,19 +228,17 @@ This section verifies styled section headings.
230
228
 
231
229
  This section keeps the package check independent from user-facing starter copy.
232
230
  `);
233
- await mkdir(path.join(siteProjectRoot, 'site', 'routes', '010-about'), { recursive: true });
234
- await writeFile(path.join(siteProjectRoot, 'site', 'routes', '010-about', 'content.md'), `---
235
- title: About the site
236
- description: Route used by package checks.
237
- navigation:
238
- label: About
239
- sections:
240
- about: {}
231
+ await mkdir(path.join(siteProjectRoot, 'site', 'pages', '010-about'), { recursive: true });
232
+ await writeFile(path.join(siteProjectRoot, 'site', 'pages', '010-about', 'content.md'), `---
233
+ page:
234
+ description: Page used by package checks.
241
235
  ---
242
236
 
237
+ # About the site
238
+
243
239
  ## About {#about}
244
240
 
245
- This route verifies that packaged norna sites can build route pages.
241
+ This page verifies that packaged norna sites can build additional pages.
246
242
  `);
247
243
 
248
244
  await runInherit(npmBin, ['install', '--no-audit', '--no-fund', '--prefer-offline', '--fetch-retries=0'], { cwd: siteProjectRoot, env: npmEnv });
@@ -255,9 +251,9 @@ This route verifies that packaged norna sites can build route pages.
255
251
  assertFileMissing(path.join(initializedSiteRoot, '.DS_Store')),
256
252
  assertFileMissing(path.join(initializedSiteRoot, 'site', '.DS_Store')),
257
253
  ]);
258
- await runInherit(npxBin, ['norna', 'engine:version'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
259
- await runInherit(npxBin, ['norna', 'doctor'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
260
- await runInherit(npxBin, ['norna', 'config:check'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
254
+ await runInherit(npxBin, ['norna', 'engine:version'], { cwd: path.join(siteProjectRoot, 'site', 'images'), env: npmEnv });
255
+ await runInherit(npxBin, ['norna', 'doctor'], { cwd: path.join(siteProjectRoot, 'site', 'images'), env: npmEnv });
256
+ await runInherit(npxBin, ['norna', 'config:check'], { cwd: path.join(siteProjectRoot, 'site', 'images'), env: npmEnv });
261
257
  await runInherit(npxBin, ['norna', 'content:check'], { cwd: siteProjectRoot, env: npmEnv });
262
258
  await runInherit(npxBin, ['norna', 'check'], { cwd: siteProjectRoot, env: npmEnv });
263
259
  await runInherit(npxBin, ['norna', 'build'], { cwd: siteProjectRoot, env: npmEnv });
@@ -315,15 +311,23 @@ This route verifies that packaged norna sites can build route pages.
315
311
  );
316
312
  await assertFileIncludes(
317
313
  path.join(siteProjectRoot, 'dist', 'index.html'),
318
- '<a class="site-brand" href="/site/"><img class="site-brand-logo" src="/site/logo.svg" alt="Package Check Logo">',
314
+ '<a class="site-brand" href="/site/"><img class="site-brand-logo" src="/site/logo.svg" alt="Package Check Site">',
319
315
  );
320
- await assertFileIncludes(
316
+ await assertFileExcludes(
321
317
  path.join(siteProjectRoot, 'dist', 'index.html'),
322
318
  '<div class="site-nav-submenu">',
323
319
  );
324
320
  await assertFileIncludes(
325
321
  path.join(siteProjectRoot, 'dist', 'index.html'),
326
- 'href="/site/#intro">Intro</a>',
322
+ 'href="/site/about/">About the site</a>',
323
+ );
324
+ await assertFileIncludes(
325
+ path.join(siteProjectRoot, 'dist', 'index.html'),
326
+ '<nav class="page-nav" aria-label="On this page">',
327
+ );
328
+ await assertFileIncludes(
329
+ path.join(siteProjectRoot, 'dist', 'index.html'),
330
+ 'href="#intro">Intro</a>',
327
331
  );
328
332
  await assertFileIncludes(
329
333
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -367,7 +371,7 @@ This route verifies that packaged norna sites can build route pages.
367
371
  );
368
372
  await assertFileIncludes(
369
373
  path.join(siteProjectRoot, 'dist', 'index.html'),
370
- 'href="/site/#warning">Warning</a>',
374
+ 'href="#warning">Warning</a>',
371
375
  );
372
376
  await assertFileIncludes(
373
377
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -484,28 +488,15 @@ This route verifies that packaged norna sites can build route pages.
484
488
  await writeFile(
485
489
  siteContentPath,
486
490
  siteContent.replace(
487
- ' intro: {}',
488
- ' intro:\n typography:\n overrides:\n caption:\n spacingBefore: wide',
489
- ),
490
- );
491
- await runExpectFailure(
492
- npxBin,
493
- ['norna', 'build'],
494
- { cwd: siteProjectRoot, env: npmEnv },
495
- 'sections.intro: Unrecognized key: "typography"',
496
- );
497
- await writeFile(
498
- siteContentPath,
499
- siteContent.replace(
500
- ' intro: {}',
501
- ' intro:\n palette: dark',
491
+ 'description: Site used by package checks.',
492
+ 'description: Site used by package checks.\nsections:\n intro: {}',
502
493
  ),
503
494
  );
504
495
  await runExpectFailure(
505
496
  npxBin,
506
497
  ['norna', 'build'],
507
498
  { cwd: siteProjectRoot, env: npmEnv },
508
- 'sections.intro: Unrecognized key: "palette"',
499
+ 'defines "sections" at the top level, but it is not a valid top-level content field',
509
500
  );
510
501
  await writeFile(
511
502
  siteThemePath,
@@ -0,0 +1,80 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ decodePageDirectoryPath,
4
+ encodePageDirectoryPath,
5
+ getPageDirectoryAncestors,
6
+ parsePageDirectory,
7
+ parsePageDirectoryPath,
8
+ } from './lib/page-model.mjs';
9
+
10
+ assert.deepEqual(parsePageDirectory('010-getting-started'), {
11
+ pageDirectory: '010-getting-started',
12
+ pageOrder: 10,
13
+ pageId: 'getting-started',
14
+ });
15
+
16
+ assert.deepEqual(parsePageDirectory('120-api-reference'), {
17
+ pageDirectory: '120-api-reference',
18
+ pageOrder: 120,
19
+ pageId: 'api-reference',
20
+ });
21
+
22
+ assert.deepEqual(parsePageDirectoryPath('010-guides/pages/020-installation'), {
23
+ pageDirectory: '010-guides/pages/020-installation',
24
+ pageDirectories: ['010-guides', '020-installation'],
25
+ pageId: 'installation',
26
+ pageIds: ['guides', 'installation'],
27
+ pageOrder: 20,
28
+ pageOrders: [10, 20],
29
+ pagePath: 'guides/installation',
30
+ parentPagePath: 'guides',
31
+ depth: 2,
32
+ });
33
+
34
+ assert.equal(
35
+ encodePageDirectoryPath('010-guides/pages/020-installation'),
36
+ '010-guides--020-installation',
37
+ );
38
+ assert.equal(
39
+ decodePageDirectoryPath('010-guides--020-installation'),
40
+ '010-guides/pages/020-installation',
41
+ );
42
+ assert.deepEqual(
43
+ getPageDirectoryAncestors('010-guides/pages/020-installation/pages/030-macos'),
44
+ [
45
+ '010-guides',
46
+ '010-guides/pages/020-installation',
47
+ '010-guides/pages/020-installation/pages/030-macos',
48
+ ],
49
+ );
50
+
51
+ for (const pageDirectoryPath of [
52
+ '010-guides/020-installation',
53
+ '010-guides/children/020-installation',
54
+ 'pages/010-guides',
55
+ '010-guides/pages',
56
+ ]) {
57
+ assert.throws(
58
+ () => parsePageDirectoryPath(pageDirectoryPath),
59
+ /(Nested pages must use|Use a pages directory)/,
60
+ `${pageDirectoryPath} should be rejected`,
61
+ );
62
+ }
63
+
64
+ for (const pageDirectory of [
65
+ '000-home',
66
+ '10-about',
67
+ '010_About',
68
+ '010-About',
69
+ '010-about-',
70
+ '010-about--team',
71
+ '010-om-oss!',
72
+ ]) {
73
+ assert.throws(
74
+ () => parsePageDirectory(pageDirectory),
75
+ /Page directories must use the form NNN-page-id/,
76
+ `${pageDirectory} should be rejected`,
77
+ );
78
+ }
79
+
80
+ console.log('Page model test passed.');
@@ -4,6 +4,7 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { pathToFileURL } from 'node:url';
7
+ import { getBasePathRedirectLocation } from './lib/base-path-redirect.mjs';
7
8
 
8
9
  const repoRoot = path.resolve(import.meta.dirname, '..');
9
10
  const projectConfigUrl = pathToFileURL(path.join(repoRoot, 'scripts', 'lib', 'project-config.mjs')).href;
@@ -14,6 +15,7 @@ const importScript = `
14
15
  basePath: projectConfig.site.basePath,
15
16
  language: projectConfig.locale.lang,
16
17
  labels: projectConfig.locale.labels,
18
+ navigationMode: projectConfig.navigation.mode,
17
19
  scrollBehavior: projectConfig.navigation.scrollBehavior,
18
20
  url: projectConfig.site.url,
19
21
  }));
@@ -24,7 +26,7 @@ const createSite = async (name, config) => {
24
26
  const siteDir = path.join(projectRoot, 'site');
25
27
  await mkdir(siteDir, { recursive: true });
26
28
  await writeFile(path.join(siteDir, 'config.yaml'), config);
27
- await writeFile(path.join(siteDir, 'content.md'), '---\ntitle: Config test\ndescription: Config test.\n---\n\n## Intro {#intro}\n\nText.\n');
29
+ await writeFile(path.join(siteDir, 'content.md'), '# Config test\n\n## Intro {#intro}\n\nText.\n');
28
30
  await writeFile(path.join(siteDir, 'theme.yaml'), 'preset: documentation\n');
29
31
  return { projectRoot, siteDir };
30
32
  };
@@ -50,6 +52,12 @@ const assertFailure = (result, expectedPattern) => {
50
52
  };
51
53
 
52
54
  try {
55
+ assert.equal(getBasePathRedirectLocation('/norna/', '/norna'), '/norna/');
56
+ assert.equal(getBasePathRedirectLocation('/norna/', '/norna?source=test'), '/norna/?source=test');
57
+ assert.equal(getBasePathRedirectLocation('/norna/', '/norna/'), undefined);
58
+ assert.equal(getBasePathRedirectLocation('/norna/', '/norna/concepts/'), undefined);
59
+ assert.equal(getBasePathRedirectLocation('/', '/'), undefined);
60
+
53
61
  const minimalSite = await createSite('minimal', 'url: https://example.com/docs\n');
54
62
  const minimalResult = loadConfig(minimalSite);
55
63
  assert.equal(minimalResult.status, 0, minimalResult.stderr);
@@ -57,14 +65,20 @@ try {
57
65
  basePath: '/docs/',
58
66
  language: 'en',
59
67
  labels: {
68
+ breadcrumb: 'Breadcrumb',
60
69
  dismissBanner: 'Dismiss notice',
70
+ built: 'Built',
61
71
  images: 'Images',
72
+ navigationBack: 'Back to pages',
73
+ navigationMenu: 'Menu',
74
+ navigationOverview: 'Overview',
62
75
  note: 'Note',
63
76
  pageNavigation: 'On this page',
64
77
  siteBanners: 'Site notices',
65
78
  siteNavigation: 'Pages',
66
79
  skipToContent: 'Skip to content',
67
80
  },
81
+ navigationMode: 'automatic',
68
82
  scrollBehavior: 'instant',
69
83
  url: 'https://example.com/docs/',
70
84
  });
@@ -74,9 +88,19 @@ try {
74
88
  assert.equal(localizedResult.status, 0, localizedResult.stderr);
75
89
  const localizedConfig = JSON.parse(localizedResult.stdout);
76
90
  assert.equal(localizedConfig.language, 'sv-SE');
91
+ assert.equal(localizedConfig.labels.built, 'Byggd');
77
92
  assert.equal(localizedConfig.labels.skipToContent, 'Hoppa till innehållet');
78
93
  assert.equal(localizedConfig.scrollBehavior, 'smooth');
79
94
 
95
+ const treeNavigationSite = await createSite('tree-navigation', 'url: https://example.com/\n');
96
+ await writeFile(path.join(treeNavigationSite.siteDir, 'theme.yaml'), `preset: documentation
97
+ navigation:
98
+ mode: tree
99
+ `);
100
+ const treeNavigationResult = loadConfig(treeNavigationSite);
101
+ assert.equal(treeNavigationResult.status, 0, treeNavigationResult.stderr);
102
+ assert.equal(JSON.parse(treeNavigationResult.stdout).navigationMode, 'tree');
103
+
80
104
  const overrideResult = loadConfig(minimalSite, {
81
105
  NORNA_SITE_URL: 'http://127.0.0.1:4567/preview',
82
106
  });
@@ -15,10 +15,13 @@ const filenames = [
15
15
  ];
16
16
  const requiredRichHelp = {
17
17
  'config.schema.json': ['url', 'language', 'scrollBehavior'],
18
- 'theme.schema.json': ['preset', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces'],
19
- 'sitewide-content.schema.json': ['navigation', 'banners', 'footer'],
20
- 'content-frontmatter.schema.json': ['title', 'description', 'navigation', 'sections'],
18
+ 'theme.schema.json': ['preset', 'navigation', 'layout', 'images', 'typography', 'palette', 'sectionSurfaces'],
19
+ 'sitewide-content.schema.json': ['logo', 'banners', 'footer'],
20
+ 'content-frontmatter.schema.json': ['page', 'navigation'],
21
21
  };
22
+ const manifest = JSON.parse(await readFile(path.join(root, 'schemas', 'manifest.json'), 'utf8'));
23
+ assert.equal(manifest.editorApiVersion, 1);
24
+ assert.equal(manifest.schemaVersion, 1);
22
25
 
23
26
  const githubHeadingAnchor = (heading) => heading
24
27
  .toLowerCase()
@@ -49,22 +52,26 @@ const assertDocumentationLinks = async (markdownDescription, location) => {
49
52
  }
50
53
  };
51
54
 
52
- const visitSchema = (schema, location) => {
55
+ const visitSchema = (schema, location, richProperties) => {
53
56
  assert.equal(schema.enum, undefined, `${location} uses an undescribed enum.`);
54
57
  if (schema.properties) {
55
58
  for (const [name, property] of Object.entries(schema.properties)) {
56
59
  assert.ok(property.description, `${location}.${name} has no description.`);
57
- visitSchema(property, `${location}.${name}`);
60
+ richProperties.push([property, `${location}.${name}`]);
61
+ visitSchema(property, `${location}.${name}`, richProperties);
58
62
  }
59
63
  }
60
- if (schema.items) visitSchema(schema.items, `${location}[]`);
64
+ if (schema.items) visitSchema(schema.items, `${location}[]`, richProperties);
65
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
66
+ visitSchema(schema.additionalProperties, `${location}.*`, richProperties);
67
+ }
61
68
  for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
62
69
  for (const [index, candidate] of (schema[keyword] ?? []).entries()) {
63
70
  if (Object.hasOwn(candidate, 'const')) {
64
71
  assert.ok(candidate.title, `${location}.${keyword}[${index}] has no title.`);
65
72
  assert.ok(candidate.description, `${location}.${keyword}[${index}] has no description.`);
66
73
  }
67
- visitSchema(candidate, `${location}.${keyword}[${index}]`);
74
+ visitSchema(candidate, `${location}.${keyword}[${index}]`, richProperties);
68
75
  }
69
76
  }
70
77
  };
@@ -83,7 +90,16 @@ for (const filename of filenames) {
83
90
  );
84
91
  await assertDocumentationLinks(property.markdownDescription, `${filename}.${propertyName}`);
85
92
  }
86
- visitSchema(schema, filename);
93
+ const richProperties = [];
94
+ visitSchema(schema, filename, richProperties);
95
+ for (const [property, location] of richProperties) {
96
+ assert.match(
97
+ property.markdownDescription,
98
+ /^```yaml\n/,
99
+ `${location} rich help must start with YAML syntax.`,
100
+ );
101
+ await assertDocumentationLinks(property.markdownDescription, location);
102
+ }
87
103
  }
88
104
 
89
105
  const config = JSON.parse(await readFile(path.join(root, 'schemas', 'config.schema.json'), 'utf8'));
@@ -92,11 +108,47 @@ assert.deepEqual(config.properties.language.examples, ['en', 'sv', 'en-GB', 'sv-
92
108
  assert.equal(config.properties.scrollBehavior.default, 'instant');
93
109
 
94
110
  const sitewide = JSON.parse(await readFile(path.join(root, 'schemas', 'sitewide-content.schema.json'), 'utf8'));
95
- assert.deepEqual(Object.keys(sitewide.properties.navigation.properties), ['label', 'logo']);
96
- assert.deepEqual(Object.keys(sitewide.properties.navigation.properties.logo.properties), ['height']);
97
- assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /```yaml\nnavigation:\n label: Example Site\n logo:\n height: 2rem\n```/);
98
- assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /does not enable or select the file/);
99
- assert.match(sitewide.properties.navigation.properties.logo.markdownDescription, /docs\/public-files\.md#navigation-logo/);
100
- assert.equal(sitewide.properties.navigation.properties.logo.description.length < 100, true);
111
+ assert.deepEqual(Object.keys(sitewide.properties.logo.properties), ['height']);
112
+ assert.match(sitewide.properties.logo.markdownDescription, /```yaml\nlogo:\n height: 2rem\n```/);
113
+ assert.match(sitewide.properties.logo.markdownDescription, /does not enable or select the file/);
114
+ assert.match(sitewide.properties.logo.markdownDescription, /homepage Markdown H1/);
115
+ assert.match(sitewide.properties.logo.markdownDescription, /docs\/public-files\.md#navigation-logo/);
116
+ assert.equal(sitewide.properties.logo.description.length < 100, true);
117
+ const bannerItem = sitewide.properties.banners.items;
118
+ assert.equal(bannerItem.title, 'Warning banner');
119
+ assert.match(bannerItem.description, /important temporary information/);
120
+ assert.match(bannerItem.markdownDescription, /A `warning` is a dismissible site-wide notice/);
121
+ await assertDocumentationLinks(bannerItem.markdownDescription, 'sitewide-content.schema.json.banners[]');
122
+ assert.equal(bannerItem.defaultSnippets[0].label, 'Warning banner');
123
+ assert.deepEqual(bannerItem.defaultSnippets[0].body, {
124
+ id: '${1:project-status}',
125
+ tone: 'warning',
126
+ title: '${2:Important notice}',
127
+ text: '${3:Brief explanation.}',
128
+ });
129
+ await assertDocumentationLinks(
130
+ bannerItem.defaultSnippets[0].markdownDescription,
131
+ 'sitewide-content.schema.json.banners[].defaultSnippets[0]',
132
+ );
133
+ const buildInfo = sitewide.properties.footer.properties.buildInfo;
134
+ assert.equal(buildInfo.type, 'boolean');
135
+ assert.equal(buildInfo.default, false);
136
+ assert.match(buildInfo.markdownDescription, /footer:\n buildInfo: true/);
137
+
138
+ const theme = JSON.parse(await readFile(path.join(root, 'schemas', 'theme.schema.json'), 'utf8'));
139
+ assert.equal(theme.properties.layout.properties.gutter.defaultSnippets[0].label, 'Responsive page gutter');
140
+ assert.equal(theme.properties.layout.properties.spacing.defaultSnippets[0].label, 'Layout spacing overrides');
141
+ assert.equal(theme.properties.typography.properties.overrides.defaultSnippets[0].label, 'Typography overrides');
142
+ assert.equal(
143
+ theme.properties.images.properties.maxAvailableWidthPercent.defaultSnippets[0].label,
144
+ 'Responsive image limit',
145
+ );
146
+
147
+ const content = JSON.parse(await readFile(path.join(root, 'schemas', 'content-frontmatter.schema.json'), 'utf8'));
148
+ assert.deepEqual(Object.keys(content.properties), ['page', 'navigation']);
149
+ assert.deepEqual(Object.keys(content.properties.page.properties), ['description']);
150
+ assert.equal(content.properties.page.required, undefined);
151
+ assert.equal(content.required, undefined);
152
+ assert.deepEqual(Object.keys(content.properties.navigation.properties), ['listed']);
101
153
 
102
154
  console.log('Schema metadata tests passed.');
@@ -36,6 +36,7 @@ try {
36
36
  const resolved = resolveThemeConfig({ preset: presetName }, 'test theme');
37
37
  assert.equal(resolved.preset, presetName);
38
38
  assert.ok(resolved.layout?.density);
39
+ assert.equal(resolved.navigation?.mode, 'automatic');
39
40
  assert.ok(resolved.images?.width);
40
41
  assert.ok(resolved.typography?.fontFamily);
41
42
  assert.ok(resolved.typography?.profile);
@@ -49,6 +50,7 @@ try {
49
50
  palette: 'dark',
50
51
  }, 'test theme');
51
52
  assert.equal(overridden.layout.density, 'compact');
53
+ assert.equal(overridden.navigation.mode, 'automatic');
52
54
  assert.equal(overridden.layout.pageWidth, '1300px');
53
55
  assert.equal(overridden.images.width, '920px');
54
56
  assert.equal(overridden.palette, 'dark');
@@ -87,13 +89,15 @@ try {
87
89
  assert.equal(entry.description, metadata.description);
88
90
  }
89
91
 
90
- await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
92
+ await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
91
93
  await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
92
94
  await writeFile(path.join(siteDir, 'content.md'), `---
93
- title: Theme preset test
94
- description: Root page
95
+ page:
96
+ description: Root page
95
97
  ---
96
98
 
99
+ # Theme preset test
100
+
97
101
  ## Home {#home}
98
102
 
99
103
  Root content.
@@ -103,18 +107,15 @@ layout:
103
107
  pageWidth: 1300px
104
108
  palette: dark
105
109
  `);
106
- await writeFile(path.join(siteDir, 'routes', '010-guide', 'content.md'), `---
107
- title: Guide
108
- description: Route page
109
- navigation:
110
- label: Guide
110
+ await writeFile(path.join(siteDir, 'pages', '010-guide', 'content.md'), `---
111
+ page:
112
+ description: Additional page
111
113
  ---
112
114
 
113
- ## Guide {#guide}
114
-
115
- Route content.
115
+ # Guide
116
+ Page content.
116
117
  `);
117
- await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.yaml'), `preset: portfolio
118
+ await writeFile(path.join(siteDir, 'pages', '010-guide', 'theme.yaml'), `preset: portfolio
118
119
  layout:
119
120
  pageWidth: 1010px
120
121
  palette: light
@@ -123,26 +124,33 @@ palette: light
123
124
  const buildResult = runCli(['build']);
124
125
  assert.equal(buildResult.status, 0, buildResult.stderr || buildResult.stdout);
125
126
  const rootHtml = await readFile(path.join(tempRoot, 'dist', 'index.html'), 'utf8');
126
- const routeHtml = await readFile(path.join(tempRoot, 'dist', 'guide', 'index.html'), 'utf8');
127
+ const pageHtml = await readFile(path.join(tempRoot, 'dist', 'guide', 'index.html'), 'utf8');
127
128
  assert.match(rootHtml, /--page-width: 1300px/);
128
129
  assert.match(rootHtml, /--font-sans: Georgia, 'Times New Roman', serif/);
129
130
  assert.match(rootHtml, /--color-page: #000000/);
130
- assert.match(routeHtml, /--page-width: 1010px/);
131
- assert.match(routeHtml, /--font-sans: 'Helvetica Neue', Arial, sans-serif/);
132
- assert.match(routeHtml, /--color-page: #ffffff/);
131
+ assert.match(pageHtml, /--page-width: 1010px/);
132
+ assert.match(pageHtml, /--font-sans: 'Helvetica Neue', Arial, sans-serif/);
133
+ assert.match(pageHtml, /--color-page: #ffffff/);
134
+ assert.match(rootHtml, /data-navigation-mode="top"/);
135
+ assert.match(pageHtml, /data-navigation-mode="top"/);
133
136
 
134
137
  const typographyResult = runCli(['typography', 'show']);
135
138
  assert.equal(typographyResult.status, 0, typographyResult.stderr || typographyResult.stdout);
136
139
  assert.match(typographyResult.stdout, /value: reading/);
137
140
  assert.match(typographyResult.stdout, /value: restrained/);
138
141
 
139
- const routeThemePath = path.join(siteDir, 'routes', '010-guide', 'theme.yaml');
140
- const routeThemeSource = await readFile(routeThemePath, 'utf8');
141
- await writeFile(routeThemePath, 'preset: unknown\n');
142
- const invalidRoutePresetResult = runCli(['config:check']);
143
- assert.notEqual(invalidRoutePresetResult.status, 0);
144
- assert.match(invalidRoutePresetResult.stderr, /Unknown theme preset "unknown" in .*routes\/010-guide\/theme\.yaml/);
145
- await writeFile(routeThemePath, routeThemeSource);
142
+ const pageThemePath = path.join(siteDir, 'pages', '010-guide', 'theme.yaml');
143
+ const pageThemeSource = await readFile(pageThemePath, 'utf8');
144
+ await writeFile(pageThemePath, 'preset: unknown\n');
145
+ const invalidPagePresetResult = runCli(['config:check']);
146
+ assert.notEqual(invalidPagePresetResult.status, 0);
147
+ assert.match(invalidPagePresetResult.stderr, /Unknown theme preset "unknown" in .*pages\/010-guide\/theme\.yaml/);
148
+ await writeFile(pageThemePath, pageThemeSource);
149
+ await writeFile(pageThemePath, 'navigation:\n mode: sections\n');
150
+ const pageNavigationResult = runCli(['config:check']);
151
+ assert.notEqual(pageNavigationResult.status, 0);
152
+ assert.match(pageNavigationResult.stderr, /page themes may not define navigation/);
153
+ await writeFile(pageThemePath, pageThemeSource);
146
154
 
147
155
  const exportResult = runCli(['theme:export', 'documentation']);
148
156
  assert.equal(exportResult.status, 0, exportResult.stderr || exportResult.stdout);
@@ -154,6 +162,7 @@ palette: light
154
162
  assert.match(exportedSource, /# Alternatives: dark, light, paper\./);
155
163
  const exportedConfig = load(exportedSource);
156
164
  assert.equal(exportedConfig.preset, 'documentation');
165
+ assert.equal(exportedConfig.navigation.mode, 'automatic');
157
166
  assert.equal(exportedConfig.layout.pageWidth, themePresets.documentation.layout.pageWidth);
158
167
  assert.equal(exportedConfig.typography.fontFamily, themePresets.documentation.typography.fontFamily);
159
168
  assert.deepEqual(exportedConfig.sectionSurfaces, ['base', 'soft']);
@@ -167,7 +176,7 @@ palette: light
167
176
  assert.notEqual(unknownExportResult.status, 0);
168
177
  assert.match(unknownExportResult.stderr, /Unknown theme preset "unknown"/);
169
178
 
170
- console.log('ok - complete theme presets resolve for root and route themes and export as protected references');
179
+ console.log('ok - complete theme presets resolve for root and page themes and export as protected references');
171
180
  } finally {
172
181
  await rm(tempRoot, { force: true, recursive: true });
173
182
  }
@@ -0,0 +1,68 @@
1
+ ---
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { withBasePath } from '../lib/basePath';
4
+ import type { PageNavigationNode } from '../lib/siteNavigation';
5
+ import type { SitePage } from '../lib/sitePages';
6
+
7
+ interface Props {
8
+ nodes: PageNavigationNode[];
9
+ currentPage: SitePage;
10
+ variant: 'mobile' | 'submenu';
11
+ }
12
+
13
+ const { nodes, currentPage, variant } = Astro.props;
14
+ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
15
+ const hasBranch = (node: PageNavigationNode) => node.children.length > 0;
16
+ const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
17
+ const isCurrentBranch = (page: SitePage) => (
18
+ page.isHome
19
+ ? currentPage.isHome
20
+ : currentPage.pagePath === page.pagePath || currentPage.pagePath.startsWith(`${page.pagePath}/`)
21
+ );
22
+ ---
23
+
24
+ <ul class:list={['navigation-page-tree', `navigation-page-tree-${variant}`]}>
25
+ {nodes.map((node) => (
26
+ <li
27
+ class:list={['navigation-page-node', {
28
+ 'navigation-page-node-current': isCurrentPage(node.page),
29
+ 'navigation-page-node-current-branch': isCurrentBranch(node.page),
30
+ }]}
31
+ >
32
+ {variant === 'mobile' && hasBranch(node) ? (
33
+ <details class="mobile-navigation-branch" open={isCurrentBranch(node.page)}>
34
+ <summary><span>{node.page.title}</span></summary>
35
+ <div class="mobile-navigation-branch-content">
36
+ <ul class="mobile-navigation-page-links">
37
+ <li>
38
+ <a
39
+ class="mobile-nav-destination mobile-nav-overview"
40
+ href={pageHref(node.page)}
41
+ aria-current={isCurrentPage(node.page) ? 'page' : undefined}
42
+ >
43
+ {projectConfig.locale.labels.navigationOverview}
44
+ </a>
45
+ </li>
46
+ </ul>
47
+ {node.children.length > 0 && (
48
+ <Astro.self nodes={node.children} currentPage={currentPage} variant={variant} />
49
+ )}
50
+ </div>
51
+ </details>
52
+ ) : (
53
+ <>
54
+ <a
55
+ class:list={['navigation-page-link', { 'mobile-nav-destination': variant === 'mobile' }]}
56
+ href={pageHref(node.page)}
57
+ aria-current={isCurrentPage(node.page) ? 'page' : undefined}
58
+ >
59
+ {node.page.title}
60
+ </a>
61
+ {node.children.length > 0 && (
62
+ <Astro.self nodes={node.children} currentPage={currentPage} variant={variant} />
63
+ )}
64
+ </>
65
+ )}
66
+ </li>
67
+ ))}
68
+ </ul>