@janga/norna 0.7.1 → 0.7.3

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 (168) hide show
  1. package/README.md +32 -18
  2. package/astro.config.mjs +2 -0
  3. package/bin/norna-cli.mjs +175 -0
  4. package/bin/norna.mjs +149 -150
  5. package/docs/README.md +41 -22
  6. package/docs/commands.md +25 -12
  7. package/docs/configuration.md +38 -135
  8. package/docs/content.md +108 -265
  9. package/docs/{command-organization.md → design/command-organization.md} +64 -60
  10. package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +19 -24
  11. package/docs/engine-development.md +35 -7
  12. package/docs/getting-started.md +55 -26
  13. package/docs/images-and-metadata.md +1 -1
  14. package/docs/local-development.md +14 -1
  15. package/docs/publishing.md +24 -1
  16. package/docs/routes.md +90 -0
  17. package/docs/site-structure.md +18 -10
  18. package/docs/theme.md +292 -0
  19. package/docs/typography.md +144 -0
  20. package/examples/dog-gallery/.astro/collections/site.schema.json +802 -0
  21. package/examples/dog-gallery/.astro/collections/theme.schema.json +714 -0
  22. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  23. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  24. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  25. package/examples/dog-gallery/.astro/data-store.json +1 -0
  26. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  27. package/examples/dog-gallery/.astro/dev.json +13 -0
  28. package/examples/dog-gallery/.astro/dev.log +49 -0
  29. package/examples/dog-gallery/.astro/settings.json +5 -0
  30. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  31. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  32. package/examples/dog-gallery/dist/_astro/SitePage.BFHZm5UG.css +1 -0
  33. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  34. package/examples/dog-gallery/dist/favicon.svg +7 -0
  35. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  36. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  37. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  38. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  39. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  40. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  41. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  42. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  43. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  44. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  45. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  46. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  47. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  48. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  75. package/examples/dog-gallery/dist/index.html +43 -0
  76. package/examples/dog-gallery/dist/robots.txt +2 -0
  77. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +74 -0
  78. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  79. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  80. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  81. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  82. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  83. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  84. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  85. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  86. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  87. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  88. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  89. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  91. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  92. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  93. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  94. package/examples/dog-gallery/node_modules/.vite/deps/package.json +3 -0
  95. package/examples/dog-gallery/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  96. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  97. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  98. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  99. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  100. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  101. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  102. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  103. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  104. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  105. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  106. package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
  107. package/examples/dog-gallery/site/config.mjs +67 -0
  108. package/examples/dog-gallery/site/content.md +168 -0
  109. package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
  110. package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
  111. package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
  112. package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
  113. package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
  114. package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
  115. package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
  116. package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
  117. package/examples/dog-gallery/site/public/favicon.svg +7 -0
  118. package/examples/dog-gallery/site/public/robots.txt +2 -0
  119. package/examples/dog-gallery/site/routes/dog-care/route-content.md +49 -0
  120. package/examples/dog-gallery/site/theme.md +114 -0
  121. package/fixtures/basic/site/config.mjs +3 -22
  122. package/fixtures/basic/site/content.md +1 -1
  123. package/fixtures/basic/site/theme.md +7 -3
  124. package/package.json +10 -9
  125. package/scripts/check-config.mjs +4 -3
  126. package/scripts/deploy-site.mjs +3 -3
  127. package/scripts/dev-local.mjs +64 -15
  128. package/scripts/init-site.mjs +11 -9
  129. package/scripts/lib/frontmatter-yaml.mjs +85 -0
  130. package/scripts/lib/presentation.mjs +10 -11
  131. package/scripts/lib/project-config.mjs +113 -45
  132. package/scripts/lib/site-content.mjs +21 -2
  133. package/scripts/lib/site-paths.mjs +18 -3
  134. package/scripts/lib/theme-config.mjs +34 -0
  135. package/scripts/lib/typography.mjs +103 -23
  136. package/scripts/show-typography.mjs +202 -75
  137. package/scripts/sync-content-sections.mjs +6 -6
  138. package/scripts/test-cli-discovery.mjs +124 -0
  139. package/scripts/test-content-check.mjs +3 -4
  140. package/scripts/test-engine-commands.mjs +20 -6
  141. package/scripts/test-navigation.mjs +10 -6
  142. package/scripts/test-package-check.mjs +40 -11
  143. package/scripts/test-temporary-visibility.mjs +2 -3
  144. package/src/components/SiteNavigation.astro +7 -5
  145. package/src/components/SitePage.astro +5 -3
  146. package/src/components/SiteSection.astro +63 -29
  147. package/src/content.config.ts +64 -2
  148. package/src/layouts/BaseLayout.astro +19 -1
  149. package/src/lib/basePath.ts +21 -0
  150. package/src/lib/generatedImages.ts +8 -4
  151. package/src/lib/sectionContent.ts +6 -1
  152. package/src/lib/sitePublicAssets.ts +8 -1
  153. package/src/styles/global.css +59 -55
  154. package/starters/basic/.github/workflows/deploy.yml +3 -3
  155. package/starters/basic/README.md +30 -14
  156. package/starters/basic/package.json +3 -3
  157. package/starters/basic/site/config.mjs +4 -24
  158. package/starters/basic/site/content.md +44 -9
  159. package/starters/basic/site/theme.md +113 -16
  160. package/starters/project/.github/workflows/deploy.yml +65 -0
  161. package/starters/project/README.md +72 -0
  162. package/starters/project/package-lock.json +4296 -0
  163. package/starters/project/package.json +35 -0
  164. package/starters/project/site/config.mjs +43 -0
  165. package/starters/project/site/content.md +88 -0
  166. package/starters/project/site/public/robots.txt +2 -0
  167. package/starters/project/site/routes/guide/route-content.md +58 -0
  168. package/starters/project/site/theme.md +15 -0
@@ -0,0 +1,124 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const launcherPath = path.join(repoRoot, 'bin', 'norna.mjs');
10
+ const tempRoot = await mkdtemp(path.join(tmpdir(), 'norna cli discovery-'));
11
+
12
+ const runLauncher = (args, cwd) => spawnSync(process.execPath, [launcherPath, ...args], {
13
+ cwd,
14
+ encoding: 'utf8',
15
+ });
16
+
17
+ const writeJson = (filePath, value) => writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
18
+
19
+ const createFakeNornaInstall = async (projectRoot, version = '1.7.0') => {
20
+ const packageRoot = path.join(projectRoot, 'node_modules', '@janga', 'norna');
21
+ const binDir = path.join(packageRoot, 'bin');
22
+ await mkdir(binDir, { recursive: true });
23
+ await writeJson(path.join(packageRoot, 'package.json'), {
24
+ name: '@janga/norna',
25
+ version,
26
+ type: 'module',
27
+ bin: {
28
+ norna: 'bin/norna.mjs',
29
+ },
30
+ });
31
+ await writeFile(path.join(binDir, 'norna.mjs'), `#!/usr/bin/env node
32
+ const payload = {
33
+ marker: 'fake-local-norna',
34
+ version: ${JSON.stringify(version)},
35
+ argv: process.argv.slice(2),
36
+ cwd: process.cwd()
37
+ };
38
+ console.log(JSON.stringify(payload));
39
+ if (process.argv.includes('--exit-7')) process.exit(7);
40
+ `);
41
+ };
42
+
43
+ const createProject = async (directoryName, {
44
+ declareDependency = true,
45
+ installNorna = true,
46
+ version = '1.7.0',
47
+ } = {}) => {
48
+ const projectRoot = path.join(tempRoot, directoryName);
49
+ await mkdir(projectRoot, { recursive: true });
50
+ await writeJson(path.join(projectRoot, 'package.json'), {
51
+ name: directoryName.toLowerCase().replaceAll(/\s+/g, '-'),
52
+ private: true,
53
+ type: 'module',
54
+ ...(declareDependency
55
+ ? { dependencies: { '@janga/norna': version } }
56
+ : {}),
57
+ });
58
+
59
+ if (installNorna) {
60
+ await createFakeNornaInstall(projectRoot, version);
61
+ }
62
+
63
+ return projectRoot;
64
+ };
65
+
66
+ try {
67
+ const noProjectResult = runLauncher(['--help'], tempRoot);
68
+ assert.equal(noProjectResult.status, 0, noProjectResult.stderr || noProjectResult.stdout);
69
+ assert.match(noProjectResult.stdout, /Usage: norna <command>/);
70
+ assert.doesNotMatch(noProjectResult.stdout, /fake-local-norna/);
71
+
72
+ const undeclaredProject = await createProject('undeclared project', {
73
+ declareDependency: false,
74
+ installNorna: true,
75
+ });
76
+ const undeclaredResult = runLauncher(['--help'], undeclaredProject);
77
+ assert.equal(undeclaredResult.status, 0, undeclaredResult.stderr || undeclaredResult.stdout);
78
+ assert.match(undeclaredResult.stdout, /Usage: norna <command>/);
79
+ assert.doesNotMatch(undeclaredResult.stdout, /fake-local-norna/);
80
+
81
+ const localProject = await createProject('local project', { version: '1.7.0' });
82
+ const localResult = runLauncher(['build', '--flag', 'value with spaces'], localProject);
83
+ assert.equal(localResult.status, 0, localResult.stderr || localResult.stdout);
84
+ const localPayload = JSON.parse(localResult.stdout.trim());
85
+ assert.equal(localPayload.marker, 'fake-local-norna');
86
+ assert.equal(localPayload.version, '1.7.0');
87
+ assert.deepEqual(localPayload.argv, ['build', '--flag', 'value with spaces']);
88
+ assert.equal(await realpath(localPayload.cwd), await realpath(localProject));
89
+
90
+ const subdirectory = path.join(localProject, 'site', 'images', 'work');
91
+ await mkdir(subdirectory, { recursive: true });
92
+ const subdirectoryResult = runLauncher(['doctor'], subdirectory);
93
+ assert.equal(subdirectoryResult.status, 0, subdirectoryResult.stderr || subdirectoryResult.stdout);
94
+ const subdirectoryPayload = JSON.parse(subdirectoryResult.stdout.trim());
95
+ assert.equal(subdirectoryPayload.marker, 'fake-local-norna');
96
+ assert.deepEqual(subdirectoryPayload.argv, ['doctor']);
97
+ assert.equal(await realpath(subdirectoryPayload.cwd), await realpath(subdirectory));
98
+
99
+ const spacedProject = await createProject('project with spaces', { version: '1.9.0' });
100
+ const spacedResult = runLauncher(['engine:version'], spacedProject);
101
+ assert.equal(spacedResult.status, 0, spacedResult.stderr || spacedResult.stdout);
102
+ const spacedPayload = JSON.parse(spacedResult.stdout.trim());
103
+ assert.equal(spacedPayload.version, '1.9.0');
104
+ assert.equal(await realpath(spacedPayload.cwd), await realpath(spacedProject));
105
+
106
+ const exitResult = runLauncher(['build', '--exit-7'], localProject);
107
+ assert.equal(exitResult.status, 7, exitResult.stderr || exitResult.stdout);
108
+ const exitPayload = JSON.parse(exitResult.stdout.trim());
109
+ assert.deepEqual(exitPayload.argv, ['build', '--exit-7']);
110
+
111
+ const engineRepoResult = runLauncher(['--help'], repoRoot);
112
+ assert.equal(engineRepoResult.status, 0, engineRepoResult.stderr || engineRepoResult.stdout);
113
+ assert.match(engineRepoResult.stdout, /Usage: norna <command>/);
114
+ assert.doesNotMatch(engineRepoResult.stdout, /fake-local-norna/);
115
+
116
+ const engineStarterResult = runLauncher(['--help'], path.join(repoRoot, 'starters', 'basic'));
117
+ assert.equal(engineStarterResult.status, 0, engineStarterResult.stderr || engineStarterResult.stdout);
118
+ assert.match(engineStarterResult.stdout, /Usage: norna <command>/);
119
+ assert.doesNotMatch(engineStarterResult.stdout, /fake-local-norna/);
120
+
121
+ console.log('ok - cli launcher discovers local norna installations and avoids self-delegation');
122
+ } finally {
123
+ await rm(tempRoot, { force: true, recursive: true });
124
+ }
@@ -43,9 +43,8 @@ const makePngHeader = ({ width, height }) => {
43
43
  };
44
44
 
45
45
  const defaultTheme = `---
46
- presentation:
47
- typography:
48
- preset: quiet-gallery
46
+ typography:
47
+ preset: quiet-gallery
49
48
  frame:
50
49
  colors: presentation
51
50
  ---
@@ -114,7 +113,7 @@ test('content:check groups section issues, global issues, and unreferenced image
114
113
 
115
114
  assert.equal(result.status, 1, output);
116
115
  assert.match(output, /^Content check failed\./m);
117
- assert.match(output, /Section and Gallery Issues\n\n\[min-konst\]\n Errors:/);
116
+ assert.match(output, /Section And Image Issues\n\n\[min-konst\]\n Errors:/);
118
117
  assert.match(output, /Image "vav\.jpeg" is used here but is located in site\/images\/mitt-hem\/\./);
119
118
  assert.match(output, /Image "missing\.jpeg" does not exist anywhere under site\/images\/\./);
120
119
  assert.match(output, /Image "duplicate\.jpg" is referenced more than once in this section\./);
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { spawnSync } from 'node:child_process';
3
- import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
@@ -23,6 +23,18 @@ try {
23
23
  assert.match(versionResult.stdout, new RegExp(`Installed norna: ${packageJson.version.replaceAll('.', '\\.')}`));
24
24
  assert.match(versionResult.stdout, /Installed Astro: /);
25
25
 
26
+ const directSiteRoot = path.join(tempRoot, 'current-directory-site');
27
+ await mkdir(directSiteRoot, { recursive: true });
28
+ await writeFile(path.join(directSiteRoot, 'config.mjs'), 'export default { site: { url: "https://example.com/" } };\n');
29
+ await writeFile(path.join(directSiteRoot, 'content.md'), '---\ntitle: Direct Site\nsections: []\n---\n');
30
+ const directSiteProjectRoot = await realpath(tempRoot);
31
+ const directSiteDoctorResult = runCli(['doctor'], {
32
+ cwd: directSiteRoot,
33
+ });
34
+ assert.equal(directSiteDoctorResult.status, 0, directSiteDoctorResult.stderr || directSiteDoctorResult.stdout);
35
+ assert.ok(directSiteDoctorResult.stdout.includes(`Site project root: ${directSiteProjectRoot}`));
36
+ assert.match(directSiteDoctorResult.stdout, /Site directory: current-directory-site/);
37
+
26
38
  const initializedSiteRoot = path.join(tempRoot, 'initialized-site');
27
39
  const initResult = runCli(['init', initializedSiteRoot]);
28
40
  assert.equal(initResult.status, 0, initResult.stderr || initResult.stdout);
@@ -35,16 +47,18 @@ try {
35
47
  assert.equal(initializedPackageJson.scripts['engine:update'], undefined);
36
48
  assert.equal(initializedPackageJson.scripts['engine:version'], undefined);
37
49
 
38
- const presetsResult = runCli(['typography:presets']);
50
+ const presetsResult = runCli(['typography', 'presets']);
39
51
  assert.equal(presetsResult.status, 0, presetsResult.stderr || presetsResult.stdout);
40
52
  assert.match(presetsResult.stdout, /quiet-gallery:/);
41
53
  assert.match(presetsResult.stdout, /text-forward:/);
42
54
 
43
- const showResult = runCli(['--site-dir', path.join(initializedSiteRoot, 'site'), 'typography:show']);
55
+ const showResult = runCli(['--site-dir', path.join(initializedSiteRoot, 'site'), 'typography', 'show']);
44
56
  assert.equal(showResult.status, 0, showResult.stderr || showResult.stdout);
45
57
  assert.match(showResult.stdout, /theme:/);
46
- assert.match(showResult.stdout, /page:/);
47
- assert.match(showResult.stdout, /preset: quiet-gallery/);
58
+ assert.match(showResult.stdout, /pages:/);
59
+ assert.match(showResult.stdout, /\s+\/:/);
60
+ assert.match(showResult.stdout, /value: quiet-gallery/);
61
+ assert.match(showResult.stdout, /inherited: true/);
48
62
  assert.match(showResult.stdout, /intro:/);
49
63
 
50
64
  const initAgainResult = runCli(['init', initializedSiteRoot]);
@@ -55,7 +69,7 @@ try {
55
69
  const customPureInitResult = runCli(['init', customPureSiteRoot, '--type', 'pure', '--site-dir', 'presentation']);
56
70
  assert.equal(customPureInitResult.status, 0, customPureInitResult.stderr || customPureInitResult.stdout);
57
71
  const customPurePackageJson = JSON.parse(await readFile(path.join(customPureSiteRoot, 'package.json'), 'utf8'));
58
- assert.equal(customPurePackageJson.scripts.dev, 'npm run norna:dev');
72
+ assert.equal(customPurePackageJson.scripts.dev, 'npm run norna:dev --');
59
73
  assert.equal(customPurePackageJson.scripts.build, 'npm run norna:build');
60
74
  assert.equal(customPurePackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
61
75
  assert.equal(customPurePackageJson.scripts['norna:build'], 'norna --site-dir presentation build');
@@ -6,6 +6,8 @@ import { promisify } from 'node:util';
6
6
  const execFileAsync = promisify(execFile);
7
7
 
8
8
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const cliPath = path.join(root, 'bin', 'norna.mjs');
10
+ const dogGallerySiteDir = path.join(root, 'examples', 'dog-gallery', 'site');
9
11
  const url = 'http://localhost:4321/';
10
12
  const probeUrls = [url, 'http://127.0.0.1:4321/', 'http://[::1]:4321/'];
11
13
  const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
@@ -53,9 +55,10 @@ const waitForServer = async () => {
53
55
  throw new Error(`Timed out waiting for ${url}`);
54
56
  };
55
57
 
56
- const runCapture = async (command, args) => execFileAsync(command, args, {
58
+ const runCapture = async (command, args, options = {}) => execFileAsync(command, args, {
57
59
  cwd: root,
58
60
  maxBuffer: 1024 * 1024 * 10,
61
+ ...options,
59
62
  });
60
63
 
61
64
  const runInherit = (command, args, options = {}) => new Promise((resolve, reject) => {
@@ -80,11 +83,10 @@ const runInherit = (command, args, options = {}) => new Promise((resolve, reject
80
83
  });
81
84
 
82
85
  const ensureServer = async () => {
83
- if (await isReachable()) {
84
- return false;
85
- }
86
+ await stopServer();
86
87
 
87
- await runInherit(npmBin, ['run', 'dev:local'], {
88
+ await runInherit(process.execPath, [cliPath, 'dev:local'], {
89
+ cwd: dogGallerySiteDir,
88
90
  env: {
89
91
  ...process.env,
90
92
  WALDE_NO_OPEN: '1',
@@ -96,7 +98,9 @@ const ensureServer = async () => {
96
98
  };
97
99
 
98
100
  const stopServer = async () => {
99
- await runCapture(npmBin, ['run', 'dev:stop']).catch(() => {});
101
+ await runCapture(process.execPath, [cliPath, 'dev:stop'], {
102
+ cwd: dogGallerySiteDir,
103
+ }).catch(() => {});
100
104
  };
101
105
 
102
106
  let startedServer = false;
@@ -176,9 +176,23 @@ try {
176
176
  packageJson.name = 'norna-package-check-site';
177
177
  packageJson.dependencies['@janga/norna'] = tarballPath;
178
178
  await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
179
+ const packageCheckConfigPath = path.join(siteProjectRoot, 'site', 'config.mjs');
180
+ const packageCheckConfig = await readFile(packageCheckConfigPath, 'utf8');
181
+ await writeFile(
182
+ packageCheckConfigPath,
183
+ packageCheckConfig
184
+ .replace("url: 'https://example.com/'", "url: 'https://example.com/site/'")
185
+ .replace("basePath: '/'", "basePath: '/site/'"),
186
+ );
187
+ const packageCheckThemePath = path.join(siteProjectRoot, 'site', 'theme.md');
188
+ const packageCheckTheme = await readFile(packageCheckThemePath, 'utf8');
189
+ await writeFile(
190
+ packageCheckThemePath,
191
+ packageCheckTheme.replace('brand: Example Site', 'brand: Package Check Brand'),
192
+ );
179
193
  await mkdir(path.join(siteProjectRoot, 'site', 'routes', 'about'), { recursive: true });
180
194
  await writeFile(path.join(siteProjectRoot, 'site', 'routes', 'about', 'route-content.md'), `---
181
- title: About the gallery
195
+ title: About the site
182
196
  description: Route used by package checks.
183
197
  navigation:
184
198
  label: About
@@ -221,7 +235,7 @@ This route verifies that packaged norna sites can build route pages.
221
235
  );
222
236
  await assertFileIncludes(
223
237
  path.join(siteProjectRoot, 'dist', 'index.html'),
224
- '--section-heading-font-size-desktop: clamp(1.65rem, 4.6vw, 5.65rem)',
238
+ '--section-heading-font-size-desktop: clamp(1.6rem, 1.22rem + 1.45vw, 2.75rem)',
225
239
  );
226
240
  await assertFileIncludes(
227
241
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -233,11 +247,15 @@ This route verifies that packaged norna sites can build route pages.
233
247
  );
234
248
  await assertFileIncludes(
235
249
  path.join(siteProjectRoot, 'dist', 'about', 'index.html'),
236
- '<link rel="canonical" href="https://example.com/about/">',
250
+ '<link rel="canonical" href="https://example.com/site/about/">',
237
251
  );
238
252
  await assertFileIncludes(
239
253
  path.join(siteProjectRoot, 'dist', 'about', 'index.html'),
240
- 'href="/about/" aria-current="page"',
254
+ 'href="/site/about/" aria-current="page"',
255
+ );
256
+ await assertFileIncludes(
257
+ path.join(siteProjectRoot, 'dist', 'index.html'),
258
+ '<a class="site-brand" href="/site/">Package Check Brand</a>',
241
259
  );
242
260
  await assertFileIncludes(
243
261
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -288,7 +306,7 @@ This route verifies that packaged norna sites can build route pages.
288
306
  await assertFileExists(path.join(siteProjectRoot, 'dist', 'favicon.ico'));
289
307
  await assertFileIncludes(
290
308
  path.join(siteProjectRoot, 'dist', 'index.html'),
291
- '<link rel="icon" sizes="any" href="/favicon.ico">',
309
+ '<link rel="icon" sizes="any" href="/site/favicon.ico">',
292
310
  );
293
311
  await assertFileExcludes(
294
312
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -298,12 +316,12 @@ This route verifies that packaged norna sites can build route pages.
298
316
  const siteContent = await readFile(siteContentPath, 'utf8');
299
317
  const siteThemePath = path.join(siteProjectRoot, 'site', 'theme.md');
300
318
  const siteTheme = await readFile(siteThemePath, 'utf8');
301
- await writeFile(siteThemePath, siteTheme.replace('\n preset: quiet-gallery', '\n preset: noisy-gallery'));
319
+ await writeFile(siteThemePath, siteTheme.replace('\n preset: quiet-gallery', '\n preset: noisy-gallery'));
302
320
  await runExpectFailure(
303
321
  npxBin,
304
322
  ['norna', 'build'],
305
323
  { cwd: siteProjectRoot, env: npmEnv },
306
- 'presentation.typography.preset',
324
+ 'typography.preset',
307
325
  );
308
326
  await writeFile(
309
327
  siteThemePath,
@@ -372,11 +390,9 @@ This route verifies that packaged norna sites can build route pages.
372
390
  { cwd: siteProjectRoot, env: npmEnv },
373
391
  'sections.0.presentation.typography.preset',
374
392
  );
375
- const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.mjs');
376
- const siteConfig = await readFile(siteConfigPath, 'utf8');
377
393
  await writeFile(
378
- siteConfigPath,
379
- siteConfig.replace(
394
+ siteThemePath,
395
+ siteTheme.replace(
380
396
  'fontFamily: "Arial, \'Helvetica Neue\', Helvetica, sans-serif"',
381
397
  'fontFamily: "Arial; color: red"',
382
398
  ),
@@ -387,6 +403,19 @@ This route verifies that packaged norna sites can build route pages.
387
403
  { cwd: siteProjectRoot, env: npmEnv },
388
404
  'typography.fontFamily',
389
405
  );
406
+ await writeFile(siteThemePath, siteTheme);
407
+ const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.mjs');
408
+ const siteConfig = await readFile(siteConfigPath, 'utf8');
409
+ await writeFile(
410
+ siteConfigPath,
411
+ siteConfig.replace("basePath: '/site/'", "basePath: 'site'"),
412
+ );
413
+ await runExpectFailure(
414
+ npxBin,
415
+ ['norna', 'config:check'],
416
+ { cwd: siteProjectRoot, env: npmEnv },
417
+ 'site.basePath',
418
+ );
390
419
 
391
420
  console.log('Package check passed.');
392
421
  } finally {
@@ -49,9 +49,8 @@ try {
49
49
  };
50
50
  `);
51
51
  await writeFile(path.join(siteDir, 'theme.md'), `---
52
- presentation:
53
- typography:
54
- preset: quiet-gallery
52
+ typography:
53
+ preset: quiet-gallery
55
54
  frame:
56
55
  colors: presentation
57
56
  ---
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import projectConfig from '../../scripts/lib/project-config.mjs';
4
+ import { withBasePath } from '../lib/basePath';
4
5
  import type { SitePage } from '../lib/sitePages';
5
6
 
6
7
  type SiteSection = CollectionEntry<'site'>['data']['sections'][number];
@@ -11,13 +12,14 @@ type FrameColors = {
11
12
  };
12
13
 
13
14
  interface Props {
15
+ brandLabel: string;
14
16
  currentPage: SitePage;
15
17
  pages: SitePage[];
16
18
  sections: ResolvedSection[];
17
19
  frameColors: FrameColors;
18
20
  }
19
21
 
20
- const { currentPage, pages, sections, frameColors } = Astro.props;
22
+ const { brandLabel, currentPage, pages, sections, frameColors } = Astro.props;
21
23
  const smoothScrollConfig = projectConfig.navigation.smoothScroll;
22
24
  const navigationStyle = [
23
25
  `--site-top-background-color: ${frameColors.backgroundColor}`,
@@ -26,8 +28,8 @@ const navigationStyle = [
26
28
  const showSiteNavigation = pages.length > 1;
27
29
  const showPageNavigation = sections.length > 1;
28
30
  const homePage = pages.find((page) => page.isHome) ?? currentPage;
29
- const brandLabel = homePage.title;
30
31
  const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname;
32
+ const pageHref = (page: SitePage) => withBasePath(projectConfig.site.basePath, page.pathname);
31
33
  ---
32
34
 
33
35
  <a class="skip-link" href="#main-content">{projectConfig.locale.labels.skipToContent}</a>
@@ -39,13 +41,13 @@ const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname
39
41
  >
40
42
  {showSiteNavigation && (
41
43
  <div class="site-nav-row">
42
- <a class="site-brand" href={homePage.pathname}>{brandLabel}</a>
44
+ <a class="site-brand" href={pageHref(homePage)}>{brandLabel}</a>
43
45
 
44
46
  <nav class="site-nav" aria-label={projectConfig.locale.labels.siteNavigation}>
45
47
  <ul>
46
48
  {pages.map((page) => (
47
49
  <li>
48
- <a href={page.pathname} aria-current={isCurrentPage(page) ? 'page' : undefined}>
50
+ <a href={pageHref(page)} aria-current={isCurrentPage(page) ? 'page' : undefined}>
49
51
  {page.navigation.label}
50
52
  </a>
51
53
  </li>
@@ -63,7 +65,7 @@ const isCurrentPage = (page: SitePage) => page.pathname === currentPage.pathname
63
65
  <ul>
64
66
  {pages.map((page) => (
65
67
  <li>
66
- <a href={page.pathname} aria-current={isCurrentPage(page) ? 'page' : undefined}>
68
+ <a href={pageHref(page)} aria-current={isCurrentPage(page) ? 'page' : undefined}>
67
69
  {page.navigation.label}
68
70
  </a>
69
71
  </li>
@@ -24,11 +24,12 @@ const allEntries = await getCollection('site');
24
24
  const sitePages = getSitePages(allEntries);
25
25
  const currentPage = getSitePage(entry);
26
26
  const navigationPages = getNavigationPages(sitePages);
27
+ const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
27
28
  const themeData = theme?.data ?? {};
28
- const pagePresentation = resolvePagePresentation(themeData.presentation, entry.data.presentation);
29
+ const brandLabel = themeData.navigation?.brand ?? homePage.title;
30
+ const pagePresentation = resolvePagePresentation(themeData, entry.data.presentation);
29
31
  const frameColors = resolveFrameColors({
30
- themePresentation: themeData.presentation,
31
- themeFrame: themeData.frame,
32
+ theme: themeData,
32
33
  pagePresentation,
33
34
  pageFrame: entry.data.frame,
34
35
  });
@@ -50,6 +51,7 @@ if (visibleSections.length === 0) {
50
51
  pathname={currentPage.pathname}
51
52
  >
52
53
  <SiteNavigation
54
+ brandLabel={brandLabel}
53
55
  currentPage={currentPage}
54
56
  pages={navigationPages}
55
57
  sections={visibleSections}
@@ -32,58 +32,61 @@ interface Props {
32
32
 
33
33
  const { section, pagePresentation, headingLevel, priorityGalleryImage = false } = Astro.props;
34
34
  const HeadingTag = headingLevel === 1 ? 'h1' : 'h2';
35
+ // Sections are authored with Markdown ## headings even when the first one renders as HTML h1.
36
+ const sectionHeadingVisualLevel = 'h2';
37
+ type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4';
35
38
  const headingSizeValues: Record<TextSize, ResponsiveValue<string>> = {
36
39
  small: {
37
- desktop: 'clamp(1.15rem, 2.2vw, 2rem)',
38
- mobile: 'clamp(1.15rem, 5vw, 1.8rem)',
40
+ desktop: 'clamp(1.1rem, 1.02rem + 0.35vw, 1.35rem)',
41
+ mobile: 'clamp(1.12rem, 1rem + 0.85vw, 1.42rem)',
39
42
  },
40
43
  medium: {
41
- desktop: 'clamp(1.4rem, 3.1vw, 3.2rem)',
42
- mobile: 'clamp(1.4rem, 3.1vw, 3.2rem)',
44
+ desktop: 'clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
45
+ mobile: 'clamp(1.3rem, 1.05rem + 1.35vw, 1.75rem)',
43
46
  },
44
47
  large: {
45
- desktop: 'clamp(1.65rem, 4.6vw, 5.65rem)',
46
- mobile: 'clamp(1.9rem, 8vw, 3.2rem)',
48
+ desktop: 'clamp(1.6rem, 1.22rem + 1.45vw, 2.75rem)',
49
+ mobile: 'clamp(1.5rem, 1.12rem + 2vw, 2.25rem)',
47
50
  },
48
51
  xlarge: {
49
- desktop: 'clamp(2rem, 5.6vw, 7rem)',
50
- mobile: 'clamp(2.15rem, 9.4vw, 3.8rem)',
52
+ desktop: 'clamp(2rem, 1.35rem + 2.4vw, 3.75rem)',
53
+ mobile: 'clamp(1.8rem, 1.25rem + 3vw, 2.85rem)',
51
54
  },
52
55
  };
53
56
  const bodySizeValues: Record<TextSize, ResponsiveValue<string>> = {
54
57
  small: {
55
- desktop: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
56
- mobile: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
58
+ desktop: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
59
+ mobile: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
57
60
  },
58
61
  medium: {
59
- desktop: 'clamp(1rem, 0.96rem + 0.18vw, 1.125rem)',
60
- mobile: 'clamp(1rem, 0.96rem + 0.18vw, 1.125rem)',
62
+ desktop: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
63
+ mobile: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
61
64
  },
62
65
  large: {
63
- desktop: 'clamp(1.1rem, 1.04rem + 0.24vw, 1.25rem)',
64
- mobile: 'clamp(1.1rem, 1.04rem + 0.24vw, 1.25rem)',
66
+ desktop: 'clamp(1.05rem, 1rem + 0.16vw, 1.12rem)',
67
+ mobile: 'clamp(1.05rem, 1rem + 0.16vw, 1.12rem)',
65
68
  },
66
69
  xlarge: {
67
- desktop: 'clamp(1.18rem, 1.08rem + 0.36vw, 1.42rem)',
68
- mobile: 'clamp(1.18rem, 1.08rem + 0.36vw, 1.42rem)',
70
+ desktop: 'clamp(1.14rem, 1.06rem + 0.26vw, 1.25rem)',
71
+ mobile: 'clamp(1.14rem, 1.06rem + 0.26vw, 1.25rem)',
69
72
  },
70
73
  };
71
74
  const captionSizeValues: Record<TextSize, ResponsiveValue<string>> = {
72
75
  small: {
73
- desktop: 'clamp(0.82rem, 0.78rem + 0.12vw, 0.92rem)',
74
- mobile: 'clamp(0.82rem, 0.78rem + 0.12vw, 0.92rem)',
76
+ desktop: 'clamp(0.78rem, 0.76rem + 0.08vw, 0.84rem)',
77
+ mobile: 'clamp(0.78rem, 0.76rem + 0.08vw, 0.84rem)',
75
78
  },
76
79
  medium: {
77
- desktop: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
78
- mobile: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
80
+ desktop: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
81
+ mobile: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
79
82
  },
80
83
  large: {
81
- desktop: 'clamp(1rem, 0.94rem + 0.2vw, 1.12rem)',
82
- mobile: 'clamp(1rem, 0.94rem + 0.2vw, 1.12rem)',
84
+ desktop: 'clamp(0.96rem, 0.92rem + 0.12vw, 1rem)',
85
+ mobile: 'clamp(0.96rem, 0.92rem + 0.12vw, 1rem)',
83
86
  },
84
87
  xlarge: {
85
- desktop: 'clamp(1.08rem, 1rem + 0.28vw, 1.25rem)',
86
- mobile: 'clamp(1.08rem, 1rem + 0.28vw, 1.25rem)',
88
+ desktop: 'clamp(1.04rem, 0.98rem + 0.18vw, 1.12rem)',
89
+ mobile: 'clamp(1.04rem, 0.98rem + 0.18vw, 1.12rem)',
87
90
  },
88
91
  };
89
92
  const sectionOverride = section.presentation as SectionPresentationOverride | undefined;
@@ -97,10 +100,26 @@ const textColor = sectionOverride?.textColor
97
100
  ?? pagePresentation.textColor;
98
101
  const resolvedTypography = resolveTypographyOverride(pagePresentation.typography, sectionOverride?.typography);
99
102
  const typography = resolvedTypography.values;
100
- const headingAlign = typography.heading.align as ResponsiveValue<TextAlign>;
103
+ const heading = typography.headings[sectionHeadingVisualLevel] as {
104
+ align: ResponsiveValue<TextAlign>;
105
+ size: TextSize;
106
+ lineHeight: number;
107
+ spacing: string;
108
+ };
109
+ const markdownHeadingLevels = ['h3', 'h4'] as const;
110
+ const markdownHeadings = Object.fromEntries(markdownHeadingLevels.map((level) => [
111
+ level,
112
+ typography.headings[level] as {
113
+ align: ResponsiveValue<TextAlign>;
114
+ size: TextSize;
115
+ lineHeight: number;
116
+ spacing: string;
117
+ },
118
+ ]));
119
+ const headingAlign = heading.align;
101
120
  const bodyAlign = typography.body.align as ResponsiveValue<TextAlign>;
102
121
  const captionAlign = typography.caption.align as ResponsiveValue<TextAlign>;
103
- const headingFontSize = getSizeValue(headingSizeValues, typography.heading.size as TextSize);
122
+ const headingFontSize = getSizeValue(headingSizeValues, heading.size);
104
123
  const bodyFontSize = getSizeValue(bodySizeValues, typography.body.size as TextSize);
105
124
  const captionFontSize = getSizeValue(captionSizeValues, typography.caption.size as TextSize);
106
125
  const justifyByAlign: Record<TextAlign, string> = {
@@ -118,6 +137,19 @@ const getAlignedTextWidth = (
118
137
  const headingWidth = getAlignedTextWidth(headingAlign, '760px');
119
138
  const bodyWidth = getAlignedTextWidth(bodyAlign, 'var(--text-width)');
120
139
  const hasGallery = section.gallery.length > 0;
140
+ const markdownHeadingStyle = (level: 'h3' | 'h4') => {
141
+ const headingConfig = markdownHeadings[level];
142
+ const fontSize = getSizeValue(headingSizeValues, headingConfig.size);
143
+
144
+ return [
145
+ `--section-markdown-${level}-align-desktop: ${headingConfig.align.desktop}`,
146
+ `--section-markdown-${level}-align-mobile: ${headingConfig.align.mobile}`,
147
+ `--section-markdown-${level}-font-size-desktop: ${fontSize.desktop}`,
148
+ `--section-markdown-${level}-font-size-mobile: ${fontSize.mobile}`,
149
+ `--section-markdown-${level}-line-height: ${headingConfig.lineHeight}`,
150
+ `--section-markdown-${level}-spacing: ${headingConfig.spacing}`,
151
+ ];
152
+ };
121
153
  const sectionStyle = [
122
154
  `--section-background-color: ${backgroundColor}`,
123
155
  `--section-text-color: ${textColor}`,
@@ -125,10 +157,12 @@ const sectionStyle = [
125
157
  `--section-heading-align-mobile: ${headingAlign.mobile}`,
126
158
  `--section-heading-font-size-desktop: ${headingFontSize.desktop}`,
127
159
  `--section-heading-font-size-mobile: ${headingFontSize.mobile}`,
128
- `--section-heading-line-height: ${typography.heading.lineHeight}`,
129
- `--section-heading-spacing: ${typography.heading.spacing}`,
160
+ `--section-heading-line-height: ${heading.lineHeight}`,
161
+ `--section-heading-spacing: ${heading.spacing}`,
130
162
  `--section-heading-width-desktop: ${headingWidth.desktop}`,
131
163
  `--section-heading-width-mobile: ${headingWidth.mobile}`,
164
+ ...markdownHeadingStyle('h3'),
165
+ ...markdownHeadingStyle('h4'),
132
166
  `--section-body-align-desktop: ${bodyAlign.desktop}`,
133
167
  `--section-body-align-mobile: ${bodyAlign.mobile}`,
134
168
  `--section-body-font-size-desktop: ${bodyFontSize.desktop}`,
@@ -150,7 +184,7 @@ const sectionStyle = [
150
184
 
151
185
  <section
152
186
  id={section.id}
153
- class:list={['site-section', { 'site-section-primary': headingLevel === 1 }]}
187
+ class="site-section"
154
188
  data-section-title={section.title}
155
189
  style={sectionStyle}
156
190
  >