@janga/norna 0.7.13 → 0.7.15

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 (146) hide show
  1. package/README.md +15 -9
  2. package/bin/norna-cli.mjs +11 -4
  3. package/docs/README.md +71 -43
  4. package/docs/commands.md +34 -20
  5. package/docs/configuration.md +43 -62
  6. package/docs/content.md +34 -7
  7. package/docs/design/command-organization.md +15 -8
  8. package/docs/design/norna-diagram-design.md +6 -6
  9. package/docs/design/site-examples-structure.md +1 -1
  10. package/docs/engine-development.md +22 -9
  11. package/docs/getting-started.md +29 -16
  12. package/docs/how-to/embedded-site.md +1 -1
  13. package/docs/images-and-metadata.md +3 -3
  14. package/docs/local-development.md +11 -3
  15. package/docs/public-files.md +105 -0
  16. package/docs/publishing.md +4 -8
  17. package/docs/routes.md +6 -7
  18. package/docs/site-files.md +144 -0
  19. package/docs/sitewide-content.md +47 -33
  20. package/docs/theme.md +37 -49
  21. package/docs/typography.md +25 -31
  22. package/examples/complete-sites/dog-shelter-multi-page/README.md +8 -0
  23. package/examples/complete-sites/{dog-shelter-single-page/site/config.md → dog-shelter-multi-page/site/config.yaml} +0 -2
  24. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +6 -0
  25. package/examples/complete-sites/dog-shelter-multi-page/site/{theme.md → theme.yaml} +0 -2
  26. package/examples/complete-sites/dog-shelter-single-page/README.md +8 -0
  27. package/examples/complete-sites/{dog-shelter-multi-page/site/config.md → dog-shelter-single-page/site/config.yaml} +0 -2
  28. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +6 -0
  29. package/examples/complete-sites/dog-shelter-single-page/site/{theme.md → theme.yaml} +0 -2
  30. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  31. package/examples/feature-demos/media-and-surfaces/README.md +1 -1
  32. package/examples/feature-demos/media-and-surfaces/site/{config.md → config.yaml} +0 -2
  33. package/examples/feature-demos/media-and-surfaces/site/content.md +2 -2
  34. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/{route-content.md → content.md} +3 -7
  35. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +3 -0
  36. package/examples/feature-demos/media-and-surfaces/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
  37. package/examples/feature-demos/media-and-surfaces/site/{theme.md → theme.yaml} +0 -2
  38. package/examples/feature-demos/sitewide-content/README.md +8 -0
  39. package/examples/feature-demos/sitewide-content/site/{config.md → config.yaml} +0 -2
  40. package/examples/feature-demos/sitewide-content/site/routes/010-identity/{route-content.md → content.md} +3 -3
  41. package/examples/feature-demos/sitewide-content/site/routes/020-notices/{route-content.md → content.md} +1 -1
  42. package/examples/feature-demos/sitewide-content/site/routes/030-footer/{route-content.md → content.md} +1 -1
  43. package/examples/feature-demos/sitewide-content/site/{sitewide-content.md → sitewide-content.yaml} +1 -11
  44. package/examples/feature-demos/sitewide-content/site/{theme.md → theme.yaml} +0 -2
  45. package/examples/feature-demos/theme-presets/site/config.yaml +1 -0
  46. package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{theme.md → theme.yaml} +0 -2
  47. package/examples/feature-demos/theme-presets/site/routes/020-documentation/{theme.md → theme.yaml} +0 -2
  48. package/examples/feature-demos/theme-presets/site/routes/030-project/theme.yaml +1 -0
  49. package/examples/feature-demos/theme-presets/site/routes/040-statement/{route-content.md → content.md} +1 -1
  50. package/examples/feature-demos/theme-presets/site/routes/040-statement/{theme.md → theme.yaml} +0 -2
  51. package/examples/feature-demos/theme-presets/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
  52. package/examples/feature-demos/theme-presets/site/theme.yaml +1 -0
  53. package/fixtures/basic/site/{config.md → config.yaml} +0 -2
  54. package/fixtures/basic/site/content.md +3 -3
  55. package/fixtures/basic/site/{sitewide-content.md → sitewide-content.yaml} +0 -2
  56. package/fixtures/basic/site/{theme.md → theme.yaml} +3 -6
  57. package/fixtures/content-model-v2/site/{config.md → config.yaml} +0 -2
  58. package/fixtures/content-model-v2/site/theme.yaml +4 -0
  59. package/package.json +10 -3
  60. package/schemas/config.schema.json +77 -0
  61. package/schemas/content-frontmatter.schema.json +81 -0
  62. package/schemas/manifest.json +9 -0
  63. package/schemas/sitewide-content.schema.json +215 -0
  64. package/schemas/theme.schema.json +1208 -0
  65. package/scripts/check-config.mjs +18 -11
  66. package/scripts/export-theme-preset.mjs +1 -1
  67. package/scripts/generate-schemas.mjs +283 -0
  68. package/scripts/init-site.mjs +22 -27
  69. package/scripts/lib/documentation-links.mjs +8 -0
  70. package/scripts/lib/editor-language-service.mjs +609 -0
  71. package/scripts/lib/example-sites.mjs +2 -2
  72. package/scripts/lib/git-status.mjs +7 -0
  73. package/scripts/lib/logo-assets.mjs +15 -10
  74. package/scripts/lib/norna-markdown-blocks.mjs +90 -11
  75. package/scripts/lib/presentation.mjs +23 -12
  76. package/scripts/lib/project-config.mjs +17 -41
  77. package/scripts/lib/public-asset-conventions.mjs +112 -0
  78. package/scripts/lib/schema-definitions.mjs +213 -0
  79. package/scripts/lib/schema-value-definitions.mjs +86 -0
  80. package/scripts/lib/site-content.mjs +24 -18
  81. package/scripts/lib/site-paths.mjs +4 -8
  82. package/scripts/lib/sitewide-content.mjs +24 -0
  83. package/scripts/lib/theme-config.mjs +15 -35
  84. package/scripts/lib/theme-presets.mjs +76 -66
  85. package/scripts/lib/typography.mjs +15 -15
  86. package/scripts/lib/yaml-config.mjs +50 -0
  87. package/scripts/list-theme-presets.mjs +15 -0
  88. package/scripts/release.mjs +84 -8
  89. package/scripts/show-typography.mjs +22 -46
  90. package/scripts/sync-content-sections.mjs +2 -2
  91. package/scripts/test-client-javascript.mjs +28 -28
  92. package/scripts/test-content-check.mjs +21 -26
  93. package/scripts/test-content-model-v2.mjs +72 -40
  94. package/scripts/test-documentation.mjs +70 -4
  95. package/scripts/test-editor-language-service.mjs +158 -0
  96. package/scripts/test-engine-commands.mjs +65 -34
  97. package/scripts/test-package-check.mjs +37 -30
  98. package/scripts/test-project-config.mjs +15 -20
  99. package/scripts/test-schemas.mjs +102 -0
  100. package/scripts/test-temporary-visibility.mjs +5 -13
  101. package/scripts/test-theme-presets.mjs +64 -37
  102. package/src/components/CardList.astro +2 -5
  103. package/src/components/ImageCarousel.astro +21 -21
  104. package/src/components/{GalleryGrid.astro → ImageStack.astro} +12 -12
  105. package/src/components/SectionNavigationScript.astro +28 -0
  106. package/src/components/SiteNavigation.astro +6 -5
  107. package/src/components/SitePage.astro +8 -8
  108. package/src/components/SiteSection.astro +18 -21
  109. package/src/content.config.ts +17 -209
  110. package/src/layouts/BaseLayout.astro +9 -9
  111. package/src/lib/{galleryImages.ts → imagePresentation.ts} +7 -7
  112. package/src/lib/routeThemes.ts +9 -29
  113. package/src/lib/sectionContent.ts +3 -3
  114. package/src/lib/sitePublicAssets.ts +5 -21
  115. package/src/styles/global.css +73 -73
  116. package/starters/basic/.github/workflows/deploy.yml +1 -1
  117. package/starters/basic/README.md +16 -13
  118. package/starters/basic/package.json +3 -2
  119. package/starters/basic/site/{config.md → config.yaml} +0 -2
  120. package/starters/basic/site/content.md +1 -1
  121. package/starters/basic/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
  122. package/starters/basic/site/{theme.md → theme.yaml} +8 -15
  123. package/starters/project/.github/workflows/deploy.yml +1 -1
  124. package/starters/project/README.md +22 -15
  125. package/starters/project/package.json +3 -2
  126. package/starters/project/site/{config.md → config.yaml} +0 -2
  127. package/starters/project/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
  128. package/starters/project/site/{theme.md → theme.yaml} +4 -8
  129. package/docs/site-structure.md +0 -98
  130. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +0 -16
  131. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +0 -16
  132. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.md +0 -8
  133. package/examples/feature-demos/theme-presets/site/config.md +0 -3
  134. package/examples/feature-demos/theme-presets/site/routes/030-project/theme.md +0 -3
  135. package/examples/feature-demos/theme-presets/site/theme.md +0 -3
  136. package/fixtures/content-model-v2/site/theme.md +0 -7
  137. package/scripts/lib/sitewide-config.mjs +0 -34
  138. package/src/components/MobileSectionNavigationScript.astro +0 -14
  139. /package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/{route-content.md → content.md} +0 -0
  140. /package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/{route-content.md → content.md} +0 -0
  141. /package/examples/feature-demos/media-and-surfaces/site/routes/010-media/{route-content.md → content.md} +0 -0
  142. /package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{route-content.md → content.md} +0 -0
  143. /package/examples/feature-demos/theme-presets/site/routes/020-documentation/{route-content.md → content.md} +0 -0
  144. /package/examples/feature-demos/theme-presets/site/routes/030-project/{route-content.md → content.md} +0 -0
  145. /package/fixtures/content-model-v2/site/routes/010-guide/{route-content.md → content.md} +0 -0
  146. /package/starters/project/site/routes/010-guide/{route-content.md → content.md} +0 -0
@@ -4,7 +4,8 @@ import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promi
4
4
  import { tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { typographyPresets, typographyRhythms } from './lib/typography.mjs';
7
+ import { getShortGitStatusPath, parseShortGitStatus } from './lib/git-status.mjs';
8
+ import { typographyProfiles, typographyRhythms } from './lib/typography.mjs';
8
9
 
9
10
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
11
  const cliPath = path.join(repoRoot, 'bin', 'norna.mjs');
@@ -18,6 +19,10 @@ const runCli = (args, options = {}) => spawnSync(process.execPath, [cliPath, ...
18
19
  });
19
20
 
20
21
  try {
22
+ const releaseStatus = parseShortGitStatus(' M package-lock.json\n M schemas/theme.schema.json\n');
23
+ assert.deepEqual(releaseStatus, [' M package-lock.json', ' M schemas/theme.schema.json']);
24
+ assert.equal(getShortGitStatusPath(releaseStatus[0]), 'package-lock.json');
25
+
21
26
  const versionResult = runCli(['engine:version']);
22
27
  assert.equal(versionResult.status, 0, versionResult.stderr || versionResult.stdout);
23
28
  assert.match(versionResult.stdout, /norna engine:version/);
@@ -35,7 +40,7 @@ try {
35
40
 
36
41
  const directSiteRoot = path.join(tempRoot, 'current-directory-site');
37
42
  await mkdir(directSiteRoot, { recursive: true });
38
- await writeFile(path.join(directSiteRoot, 'config.md'), '---\nurl: https://example.com/\n---\n');
43
+ await writeFile(path.join(directSiteRoot, 'config.yaml'), 'url: https://example.com/\n');
39
44
  await writeFile(path.join(directSiteRoot, 'content.md'), '---\ntitle: Direct Site\ndescription: Direct site fixture.\n---\n\n## Intro {#intro}\n\nText.\n');
40
45
  const directSiteProjectRoot = await realpath(tempRoot);
41
46
  const directSiteDoctorResult = runCli(['doctor'], {
@@ -49,29 +54,36 @@ try {
49
54
  const initResult = runCli(['init', initializedSiteRoot]);
50
55
  assert.equal(initResult.status, 0, initResult.stderr || initResult.stdout);
51
56
  assert.match(initResult.stdout, /Created norna site at /);
57
+ assert.match(initResult.stdout, /npm run norna:dev/);
52
58
 
53
59
  const initializedPackageJson = JSON.parse(await readFile(path.join(initializedSiteRoot, 'package.json'), 'utf8'));
54
60
  assert.equal(initializedPackageJson.dependencies['@janga/norna'], packageJson.version);
61
+ assert.equal(initializedPackageJson.scripts['norna:check'], 'norna check');
55
62
  assert.equal(initializedPackageJson.scripts['norna:engine:update'], 'norna engine:update');
56
63
  assert.equal(initializedPackageJson.scripts['norna:engine:version'], 'norna engine:version');
64
+ assert.equal(initializedPackageJson.scripts['norna:theme:presets'], 'norna theme:presets');
57
65
  assert.equal(initializedPackageJson.scripts['norna:theme:export'], 'norna theme:export');
58
66
  assert.equal(initializedPackageJson.scripts['engine:update'], undefined);
59
67
  assert.equal(initializedPackageJson.scripts['engine:version'], undefined);
60
-
61
- const presetsResult = runCli(['typography', 'presets']);
62
- assert.equal(presetsResult.status, 0, presetsResult.stderr || presetsResult.stdout);
63
- assert.match(presetsResult.stdout, /quiet-gallery:/);
64
- assert.match(presetsResult.stdout, /text-forward:/);
65
- assert.match(presetsResult.stdout, /rhythms:/);
66
- assert.match(presetsResult.stdout, /normal:/);
67
- for (const [presetName, preset] of Object.entries(typographyPresets)) {
68
- for (const [level, heading] of Object.entries(preset.headings)) {
69
- assert.equal(heading.size, 'medium', `${presetName} ${level} should use medium size`);
70
- assert.ok([400, 500, 600, 700].includes(heading.weight), `${presetName} ${level} should use a supported weight`);
68
+ const checkResult = runCli(['--site-dir', path.join(initializedSiteRoot, 'site'), 'check']);
69
+ assert.equal(checkResult.status, 0, checkResult.stderr || checkResult.stdout);
70
+ assert.match(checkResult.stdout, /Config check passed\./);
71
+ assert.match(checkResult.stdout, /Content check passed\./);
72
+
73
+ const profilesResult = runCli(['typography', 'profiles']);
74
+ assert.equal(profilesResult.status, 0, profilesResult.stderr || profilesResult.stdout);
75
+ assert.match(profilesResult.stdout, /restrained:/);
76
+ assert.match(profilesResult.stdout, /reading:/);
77
+ assert.match(profilesResult.stdout, /rhythms:/);
78
+ assert.match(profilesResult.stdout, /normal:/);
79
+ for (const [profileName, profile] of Object.entries(typographyProfiles)) {
80
+ for (const [level, heading] of Object.entries(profile.headings)) {
81
+ assert.equal(heading.size, 'medium', `${profileName} ${level} should use medium size`);
82
+ assert.ok([400, 500, 600, 700].includes(heading.weight), `${profileName} ${level} should use a supported weight`);
71
83
  }
72
- assert.equal(preset.body.size, 'medium', `${presetName} body should use medium size`);
73
- assert.ok(['narrow', 'normal', 'wide'].includes(preset.body.width), `${presetName} body should use a supported width`);
74
- assert.equal(preset.caption.size, 'medium', `${presetName} caption should use medium size`);
84
+ assert.equal(profile.body.size, 'medium', `${profileName} body should use medium size`);
85
+ assert.ok(['narrow', 'normal', 'wide'].includes(profile.body.width), `${profileName} body should use a supported width`);
86
+ assert.equal(profile.caption.size, 'medium', `${profileName} caption should use medium size`);
75
87
  }
76
88
  for (const [rhythmName, rhythm] of Object.entries(typographyRhythms)) {
77
89
  for (const [level, heading] of Object.entries(rhythm.headings)) {
@@ -87,25 +99,26 @@ try {
87
99
  assert.match(showResult.stdout, /theme:/);
88
100
  assert.match(showResult.stdout, /pages:/);
89
101
  assert.match(showResult.stdout, /\s+\/:/);
90
- assert.match(showResult.stdout, /value: quiet-gallery/);
91
- assert.match(showResult.stdout, /source: "site\/theme\.md"/);
102
+ assert.match(showResult.stdout, /value: restrained/);
103
+ assert.match(showResult.stdout, /source: "site\/theme\.yaml"/);
92
104
  assert.match(showResult.stdout, /intro:/);
93
105
 
94
106
  const initAgainResult = runCli(['init', initializedSiteRoot]);
95
107
  assert.notEqual(initAgainResult.status, 0);
96
108
  assert.match(initAgainResult.stderr, /Target directory must be empty/);
97
109
 
98
- const customPureSiteRoot = path.join(tempRoot, 'custom-pure-site');
99
- const customPureInitResult = runCli(['init', customPureSiteRoot, '--type', 'pure', '--site-dir', 'presentation']);
100
- assert.equal(customPureInitResult.status, 0, customPureInitResult.stderr || customPureInitResult.stdout);
101
- const customPurePackageJson = JSON.parse(await readFile(path.join(customPureSiteRoot, 'package.json'), 'utf8'));
102
- assert.equal(customPurePackageJson.scripts.dev, 'npm run norna:dev --');
103
- assert.equal(customPurePackageJson.scripts.build, 'npm run norna:build');
104
- assert.equal(customPurePackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
105
- assert.equal(customPurePackageJson.scripts['norna:build'], 'norna --site-dir presentation build');
106
- await readFile(path.join(customPureSiteRoot, 'presentation', 'content.md'));
107
- await readFile(path.join(customPureSiteRoot, 'presentation', 'config.md'));
108
- await readFile(path.join(customPureSiteRoot, 'presentation', 'theme.md'));
110
+ const customStandaloneSiteRoot = path.join(tempRoot, 'custom-standalone-site');
111
+ const customStandaloneInitResult = runCli(['init', customStandaloneSiteRoot, '--type', 'standalone', '--site-dir', 'presentation']);
112
+ assert.equal(customStandaloneInitResult.status, 0, customStandaloneInitResult.stderr || customStandaloneInitResult.stdout);
113
+ const customStandalonePackageJson = JSON.parse(await readFile(path.join(customStandaloneSiteRoot, 'package.json'), 'utf8'));
114
+ assert.equal(customStandalonePackageJson.scripts.dev, 'npm run norna:dev --');
115
+ assert.equal(customStandalonePackageJson.scripts.build, 'npm run norna:build');
116
+ assert.equal(customStandalonePackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
117
+ assert.equal(customStandalonePackageJson.scripts['norna:check'], 'norna --site-dir presentation check');
118
+ assert.equal(customStandalonePackageJson.scripts['norna:build'], 'norna --site-dir presentation build');
119
+ await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'content.md'));
120
+ await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'config.yaml'));
121
+ await readFile(path.join(customStandaloneSiteRoot, 'presentation', 'theme.yaml'));
109
122
 
110
123
  const mixedProjectRoot = path.join(tempRoot, 'mixed-project');
111
124
  await mkdir(mixedProjectRoot, { recursive: true });
@@ -127,11 +140,12 @@ try {
127
140
  assert.equal(mixedPackageJson.scripts.build, 'node build-app.mjs');
128
141
  assert.equal(mixedPackageJson.scripts.dev, undefined);
129
142
  assert.equal(mixedPackageJson.scripts['norna:dev'], 'norna --site-dir presentation dev:local');
143
+ assert.equal(mixedPackageJson.scripts['norna:check'], 'norna --site-dir presentation check');
130
144
  assert.equal(mixedPackageJson.scripts['norna:engine:update'], 'norna --site-dir presentation engine:update');
131
145
  assert.equal(mixedPackageJson.scripts['norna:engine:version'], 'norna --site-dir presentation engine:version');
132
146
  await readFile(path.join(mixedProjectRoot, 'presentation', 'content.md'));
133
- await readFile(path.join(mixedProjectRoot, 'presentation', 'config.md'));
134
- await readFile(path.join(mixedProjectRoot, 'presentation', 'theme.md'));
147
+ await readFile(path.join(mixedProjectRoot, 'presentation', 'config.yaml'));
148
+ await readFile(path.join(mixedProjectRoot, 'presentation', 'theme.yaml'));
135
149
 
136
150
  const conflictProjectRoot = path.join(tempRoot, 'conflict-project');
137
151
  await mkdir(conflictProjectRoot, { recursive: true });
@@ -149,11 +163,11 @@ try {
149
163
  assert.match(conflictInitResult.stderr, /Refusing to overwrite existing npm scripts/);
150
164
  assert.match(conflictInitResult.stderr, /norna:dev/);
151
165
 
152
- const pureIntoExistingProjectResult = runCli(['init', '.', '--type', 'pure'], {
166
+ const standaloneIntoExistingProjectResult = runCli(['init', '.', '--type', 'standalone'], {
153
167
  cwd: mixedProjectRoot,
154
168
  });
155
- assert.notEqual(pureIntoExistingProjectResult.status, 0);
156
- assert.match(pureIntoExistingProjectResult.stderr, /use --type embedded/);
169
+ assert.notEqual(standaloneIntoExistingProjectResult.status, 0);
170
+ assert.match(standaloneIntoExistingProjectResult.stderr, /use --type embedded/);
157
171
 
158
172
  const updateFromEngineResult = runCli(['engine:update', '--skip-checks']);
159
173
  assert.notEqual(updateFromEngineResult.status, 0);
@@ -165,6 +179,23 @@ try {
165
179
  });
166
180
  assert.equal(releaseHelpResult.status, 0, releaseHelpResult.stderr || releaseHelpResult.stdout);
167
181
  assert.match(releaseHelpResult.stdout, /Usage: node scripts\/release\.mjs/);
182
+ const releaseSource = await readFile(path.join(repoRoot, 'scripts', 'release.mjs'), 'utf8');
183
+ const releaseSteps = [
184
+ "['version', releaseType, '--no-git-tag-version']",
185
+ "['run', 'schemas:generate']",
186
+ "['test']",
187
+ "['commit', '-m', `Release ${releaseTag}`]",
188
+ "['run', 'release:publish']",
189
+ "['push', '--follow-tags']",
190
+ ];
191
+ for (const step of releaseSteps) assert.ok(releaseSource.includes(step), `Missing release step: ${step}`);
192
+ for (let index = 1; index < releaseSteps.length; index += 1) {
193
+ assert.ok(
194
+ releaseSource.indexOf(releaseSteps[index - 1]) < releaseSource.indexOf(releaseSteps[index]),
195
+ `Release step is out of order: ${releaseSteps[index]}`,
196
+ );
197
+ }
198
+ assert.match(releaseSource, /rollbackPreparedRelease/);
168
199
 
169
200
  console.log('ok - engine commands report versions, initialize sites, guard engine self-updates, and document release usage');
170
201
  } finally {
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert/strict';
1
2
  import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
3
  import { spawn } from 'node:child_process';
3
4
  import { tmpdir } from 'node:os';
@@ -155,40 +156,49 @@ try {
155
156
  assertFileExists(path.join(unpackDir, 'package', 'docs', 'README.md')),
156
157
  assertFileExists(path.join(unpackDir, 'package', 'docs', 'configuration.md')),
157
158
  assertFileExists(path.join(unpackDir, 'package', 'docs', 'images-and-metadata.md')),
159
+ assertFileExists(path.join(unpackDir, 'package', 'schemas', 'manifest.json')),
160
+ assertFileExists(path.join(unpackDir, 'package', 'schemas', 'config.schema.json')),
161
+ assertFileExists(path.join(unpackDir, 'package', 'schemas', 'content-frontmatter.schema.json')),
162
+ assertFileExists(path.join(unpackDir, 'package', 'schemas', 'sitewide-content.schema.json')),
163
+ assertFileExists(path.join(unpackDir, 'package', 'schemas', 'theme.schema.json')),
158
164
  assertFileExists(path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml')),
159
165
  assertFileExists(path.join(packagedStarterRoot, 'package.json')),
160
166
  assertFileExists(path.join(packagedStarterRoot, 'README.md')),
161
- assertFileExists(path.join(packagedStarterRoot, 'site', 'config.md')),
162
- assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.md')),
163
- assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.md')),
167
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'config.yaml')),
168
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'theme.yaml')),
169
+ assertFileExists(path.join(packagedStarterRoot, 'site', 'sitewide-content.yaml')),
164
170
  assertFileExists(path.join(packagedStarterRoot, 'site', 'content.md')),
165
171
  assertFileExists(path.join(packagedStarterRoot, 'site', 'images', 'work', '.gitkeep')),
166
172
  assertFileExists(path.join(packagedStarterRoot, 'site', 'public', 'robots.txt')),
167
- assertFileMissing(path.join(packagedStarterRoot, 'site', 'config.mjs')),
168
173
  ]);
169
174
  await assertFileIncludes(
170
175
  path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml'),
171
176
  'node-version: 24.18.0',
172
177
  );
178
+ await assertFileIncludes(
179
+ path.join(packagedStarterRoot, '.github', 'workflows', 'deploy.yml'),
180
+ 'run: npm run norna:build',
181
+ );
173
182
  await cp(packagedStarterRoot, siteProjectRoot, {
174
183
  recursive: true,
175
184
  });
176
185
  const packageJsonPath = path.join(siteProjectRoot, 'package.json');
177
186
  const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
187
+ assert.equal(packageJson.scripts['norna:check'], 'norna check');
178
188
  packageJson.name = 'norna-package-check-site';
179
189
  packageJson.dependencies['@janga/norna'] = tarballPath;
180
190
  await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
181
- const packageCheckConfigPath = path.join(siteProjectRoot, 'site', 'config.md');
191
+ const packageCheckConfigPath = path.join(siteProjectRoot, 'site', 'config.yaml');
182
192
  const packageCheckConfig = await readFile(packageCheckConfigPath, 'utf8');
183
193
  await writeFile(
184
194
  packageCheckConfigPath,
185
195
  packageCheckConfig.replace('url: https://example.com/', 'url: https://example.com/site/'),
186
196
  );
187
- const packageCheckSitewidePath = path.join(siteProjectRoot, 'site', 'sitewide-content.md');
197
+ const packageCheckSitewidePath = path.join(siteProjectRoot, 'site', 'sitewide-content.yaml');
188
198
  const packageCheckSitewide = await readFile(packageCheckSitewidePath, 'utf8');
189
199
  await writeFile(
190
200
  packageCheckSitewidePath,
191
- packageCheckSitewide.replace('brand: Example Site', 'brand: Package Check Brand\n logo:\n alt: Package Check Logo\n height: 2.6rem'),
201
+ packageCheckSitewide.replace('label: Example Site', 'label: Package Check Logo\n logo:\n height: 2.6rem'),
192
202
  );
193
203
  await writeFile(
194
204
  path.join(siteProjectRoot, 'site', 'public', 'logo.svg'),
@@ -221,7 +231,7 @@ This section verifies styled section headings.
221
231
  This section keeps the package check independent from user-facing starter copy.
222
232
  `);
223
233
  await mkdir(path.join(siteProjectRoot, 'site', 'routes', '010-about'), { recursive: true });
224
- await writeFile(path.join(siteProjectRoot, 'site', 'routes', '010-about', 'route-content.md'), `---
234
+ await writeFile(path.join(siteProjectRoot, 'site', 'routes', '010-about', 'content.md'), `---
225
235
  title: About the site
226
236
  description: Route used by package checks.
227
237
  navigation:
@@ -240,16 +250,16 @@ This route verifies that packaged norna sites can build route pages.
240
250
  await runInherit(nornaBinPath, ['init', initializedSiteRoot], { cwd: tempRoot, env: npmEnv });
241
251
  await Promise.all([
242
252
  assertFileExists(path.join(initializedSiteRoot, 'package.json')),
243
- assertFileExists(path.join(initializedSiteRoot, 'site', 'config.md')),
244
- assertFileExists(path.join(initializedSiteRoot, 'site', 'theme.md')),
253
+ assertFileExists(path.join(initializedSiteRoot, 'site', 'config.yaml')),
254
+ assertFileExists(path.join(initializedSiteRoot, 'site', 'theme.yaml')),
245
255
  assertFileMissing(path.join(initializedSiteRoot, '.DS_Store')),
246
256
  assertFileMissing(path.join(initializedSiteRoot, 'site', '.DS_Store')),
247
- assertFileMissing(path.join(initializedSiteRoot, 'site', 'config.mjs')),
248
257
  ]);
249
258
  await runInherit(npxBin, ['norna', 'engine:version'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
250
259
  await runInherit(npxBin, ['norna', 'doctor'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
251
260
  await runInherit(npxBin, ['norna', 'config:check'], { cwd: path.join(siteProjectRoot, 'site', 'images', 'work'), env: npmEnv });
252
261
  await runInherit(npxBin, ['norna', 'content:check'], { cwd: siteProjectRoot, env: npmEnv });
262
+ await runInherit(npxBin, ['norna', 'check'], { cwd: siteProjectRoot, env: npmEnv });
253
263
  await runInherit(npxBin, ['norna', 'build'], { cwd: siteProjectRoot, env: npmEnv });
254
264
  await assertFileExists(path.join(siteProjectRoot, 'site', '.norna', 'public', 'robots.txt'));
255
265
  await assertFileExists(path.join(siteProjectRoot, 'dist', 'robots.txt'));
@@ -404,47 +414,44 @@ This route verifies that packaged norna sites can build route pages.
404
414
  );
405
415
  const siteContentPath = path.join(siteProjectRoot, 'site', 'content.md');
406
416
  const siteContent = await readFile(siteContentPath, 'utf8');
407
- const siteThemePath = path.join(siteProjectRoot, 'site', 'theme.md');
417
+ const siteThemePath = path.join(siteProjectRoot, 'site', 'theme.yaml');
408
418
  const siteTheme = await readFile(siteThemePath, 'utf8');
409
- await writeFile(siteThemePath, siteTheme.replace('\n preset: quiet-gallery', '\n preset: noisy-gallery'));
419
+ await writeFile(siteThemePath, siteTheme.replace('\n profile: restrained', '\n profile: noisy'));
410
420
  await runExpectFailure(
411
421
  npxBin,
412
422
  ['norna', 'build'],
413
423
  { cwd: siteProjectRoot, env: npmEnv },
414
- 'typography.preset',
424
+ 'Unknown typography profile: noisy',
415
425
  );
416
426
  await writeFile(
417
427
  siteThemePath,
418
- siteTheme.replace('\n palette: dark', '\n palette: neon'),
428
+ siteTheme.replace('\npalette: dark', '\npalette: neon'),
419
429
  );
420
430
  await runExpectFailure(
421
431
  npxBin,
422
432
  ['norna', 'build'],
423
433
  { cwd: siteProjectRoot, env: npmEnv },
424
- 'presentation.palette',
434
+ 'palette',
425
435
  );
426
436
  await writeFile(
427
437
  siteThemePath,
428
- siteTheme.replace(' sectionSurfaces:\n mode: cycle', ' sectionSurfaces:\n mode: glowing'),
438
+ siteTheme.replace('\nsectionSurfaces: [base, soft, emphasis]', '\nsectionSurfaces: [base, glowing]'),
429
439
  );
430
440
  await runExpectFailure(
431
441
  npxBin,
432
442
  ['norna', 'build'],
433
443
  { cwd: siteProjectRoot, env: npmEnv },
434
- 'presentation.sectionSurfaces.mode',
444
+ 'Unknown section surface "glowing"',
435
445
  );
436
446
  await writeFile(
437
447
  siteThemePath,
438
- siteTheme.replace(
439
- ' sectionSurfaces:\n mode: cycle',
440
- ' sectionSurfaces:\n mode: cycle\n sequence: [base, base]',
441
- ),
448
+ siteTheme.replace('\nsectionSurfaces: [base, soft, emphasis]', '\nsectionSurfaces: [base, base]'),
442
449
  );
443
450
  await runExpectFailure(
444
451
  npxBin,
445
452
  ['norna', 'build'],
446
453
  { cwd: siteProjectRoot, env: npmEnv },
447
- 'presentation.sectionSurfaces',
454
+ 'Each section surface may appear only once',
448
455
  );
449
456
  await writeFile(siteThemePath, siteTheme);
450
457
  await writeFile(
@@ -465,40 +472,40 @@ This route verifies that packaged norna sites can build route pages.
465
472
  siteContentPath,
466
473
  siteContent.replace(
467
474
  'description: Site used by package checks.',
468
- 'description: Site used by package checks.\npresentation:\n typography:\n overrides:\n body:\n lineHeight: tight',
475
+ 'description: Site used by package checks.\ntypography:\n overrides:\n body:\n lineHeight: tight',
469
476
  ),
470
477
  );
471
478
  await runExpectFailure(
472
479
  npxBin,
473
480
  ['norna', 'build'],
474
481
  { cwd: siteProjectRoot, env: npmEnv },
475
- 'presentation',
482
+ 'typography',
476
483
  );
477
484
  await writeFile(
478
485
  siteContentPath,
479
486
  siteContent.replace(
480
487
  ' intro: {}',
481
- ' intro:\n presentation:\n typography:\n overrides:\n caption:\n spacingBefore: wide',
488
+ ' intro:\n typography:\n overrides:\n caption:\n spacingBefore: wide',
482
489
  ),
483
490
  );
484
491
  await runExpectFailure(
485
492
  npxBin,
486
493
  ['norna', 'build'],
487
494
  { cwd: siteProjectRoot, env: npmEnv },
488
- 'sections.intro: Unrecognized key: "presentation"',
495
+ 'sections.intro: Unrecognized key: "typography"',
489
496
  );
490
497
  await writeFile(
491
498
  siteContentPath,
492
499
  siteContent.replace(
493
500
  ' intro: {}',
494
- ' intro:\n presentation:\n typography:\n preset: dramatic',
501
+ ' intro:\n palette: dark',
495
502
  ),
496
503
  );
497
504
  await runExpectFailure(
498
505
  npxBin,
499
506
  ['norna', 'build'],
500
507
  { cwd: siteProjectRoot, env: npmEnv },
501
- 'sections.intro: Unrecognized key: "presentation"',
508
+ 'sections.intro: Unrecognized key: "palette"',
502
509
  );
503
510
  await writeFile(
504
511
  siteThemePath,
@@ -514,7 +521,7 @@ This route verifies that packaged norna sites can build route pages.
514
521
  'typography.fontFamily',
515
522
  );
516
523
  await writeFile(siteThemePath, siteTheme);
517
- const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.md');
524
+ const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.yaml');
518
525
  const siteConfig = await readFile(siteConfigPath, 'utf8');
519
526
  await writeFile(
520
527
  siteConfigPath,
@@ -19,13 +19,13 @@ const importScript = `
19
19
  }));
20
20
  `;
21
21
 
22
- const createSite = async (name, config, { legacy = false } = {}) => {
22
+ const createSite = async (name, config) => {
23
23
  const projectRoot = path.join(tempRoot, name);
24
24
  const siteDir = path.join(projectRoot, 'site');
25
25
  await mkdir(siteDir, { recursive: true });
26
- await writeFile(path.join(siteDir, legacy ? 'config.mjs' : 'config.md'), config);
26
+ await writeFile(path.join(siteDir, 'config.yaml'), config);
27
27
  await writeFile(path.join(siteDir, 'content.md'), '---\ntitle: Config test\ndescription: Config test.\n---\n\n## Intro {#intro}\n\nText.\n');
28
- await writeFile(path.join(siteDir, 'theme.md'), '---\npreset: documentation\n---\n');
28
+ await writeFile(path.join(siteDir, 'theme.yaml'), 'preset: documentation\n');
29
29
  return { projectRoot, siteDir };
30
30
  };
31
31
 
@@ -50,7 +50,7 @@ const assertFailure = (result, expectedPattern) => {
50
50
  };
51
51
 
52
52
  try {
53
- const minimalSite = await createSite('minimal', '---\nurl: https://example.com/docs\n---\n');
53
+ const minimalSite = await createSite('minimal', 'url: https://example.com/docs\n');
54
54
  const minimalResult = loadConfig(minimalSite);
55
55
  assert.equal(minimalResult.status, 0, minimalResult.stderr);
56
56
  assert.deepEqual(JSON.parse(minimalResult.stdout), {
@@ -58,7 +58,7 @@ try {
58
58
  language: 'en',
59
59
  labels: {
60
60
  dismissBanner: 'Dismiss notice',
61
- gallery: 'Images',
61
+ images: 'Images',
62
62
  note: 'Note',
63
63
  pageNavigation: 'On this page',
64
64
  siteBanners: 'Site notices',
@@ -69,13 +69,13 @@ try {
69
69
  url: 'https://example.com/docs/',
70
70
  });
71
71
 
72
- const localizedSite = await createSite('localized', '---\nurl: https://example.com/\nlanguage: sv-SE\nscrollBehavior: browser-smooth\n---\n');
72
+ const localizedSite = await createSite('localized', 'url: https://example.com/\nlanguage: sv-SE\nscrollBehavior: smooth\n');
73
73
  const localizedResult = loadConfig(localizedSite);
74
74
  assert.equal(localizedResult.status, 0, localizedResult.stderr);
75
75
  const localizedConfig = JSON.parse(localizedResult.stdout);
76
76
  assert.equal(localizedConfig.language, 'sv-SE');
77
77
  assert.equal(localizedConfig.labels.skipToContent, 'Hoppa till innehållet');
78
- assert.equal(localizedConfig.scrollBehavior, 'browser-smooth');
78
+ assert.equal(localizedConfig.scrollBehavior, 'smooth');
79
79
 
80
80
  const overrideResult = loadConfig(minimalSite, {
81
81
  NORNA_SITE_URL: 'http://127.0.0.1:4567/preview',
@@ -91,34 +91,29 @@ try {
91
91
  );
92
92
 
93
93
  assertFailure(
94
- loadConfig(await createSite('unknown-field', '---\nurl: https://example.com/\nbasePath: /docs/\n---\n')),
94
+ loadConfig(await createSite('unknown-field', 'url: https://example.com/\nbasePath: /docs/\n')),
95
95
  /not a valid top-level config field/,
96
96
  );
97
97
  assertFailure(
98
- loadConfig(await createSite('markdown-body', '---\nurl: https://example.com/\n---\n\nConfiguration prose.\n')),
99
- /may contain YAML frontmatter only/,
98
+ loadConfig(await createSite('invalid-yaml', 'url: https://example.com/\nConfiguration prose.\n')),
99
+ /contains invalid YAML/,
100
100
  );
101
101
  assertFailure(
102
- loadConfig(await createSite('unsupported-language', '---\nurl: https://example.com/\nlanguage: de\n---\n')),
102
+ loadConfig(await createSite('unsupported-language', 'url: https://example.com/\nlanguage: de\n')),
103
103
  /has no built-in Norna UI text/,
104
104
  );
105
105
  assertFailure(
106
- loadConfig(await createSite('invalid-url', '---\nurl: example.com\n---\n')),
106
+ loadConfig(await createSite('invalid-url', 'url: example.com\n')),
107
107
  /url must be an absolute URL/,
108
108
  );
109
109
  assertFailure(
110
- loadConfig(await createSite('invalid-scroll-behavior', '---\nurl: https://example.com/\nscrollBehavior: slow\n---\n')),
111
- /scrollBehavior must be one of instant, browser-smooth/,
110
+ loadConfig(await createSite('invalid-scroll-behavior', 'url: https://example.com/\nscrollBehavior: slow\n')),
111
+ /scrollBehavior must be one of instant, smooth/,
112
112
  );
113
113
  assertFailure(
114
- loadConfig(await createSite('obsolete-smooth-scroll', '---\nurl: https://example.com/\nsmoothScroll: true\n---\n')),
114
+ loadConfig(await createSite('obsolete-smooth-scroll', 'url: https://example.com/\nsmoothScroll: true\n')),
115
115
  /not a valid top-level config field/,
116
116
  );
117
- assertFailure(
118
- loadConfig(await createSite('legacy', 'export default {};\n', { legacy: true })),
119
- /Replace the obsolete config\.mjs with a frontmatter-only config\.md file/,
120
- );
121
-
122
117
  console.log('Project config test passed.');
123
118
  } finally {
124
119
  await rm(tempRoot, { force: true, recursive: true });
@@ -0,0 +1,102 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
9
+ const expectedDocumentationRef = `v${packageJson.version}`;
10
+ const filenames = [
11
+ 'config.schema.json',
12
+ 'theme.schema.json',
13
+ 'sitewide-content.schema.json',
14
+ 'content-frontmatter.schema.json',
15
+ ];
16
+ const requiredRichHelp = {
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'],
21
+ };
22
+
23
+ const githubHeadingAnchor = (heading) => heading
24
+ .toLowerCase()
25
+ .replace(/<[^>]+>/g, '')
26
+ .replace(/[`*_~]/g, '')
27
+ .replace(/[^\p{L}\p{N}\s-]/gu, '')
28
+ .trim()
29
+ .replace(/\s+/g, '-');
30
+
31
+ const assertDocumentationLinks = async (markdownDescription, location) => {
32
+ const links = [...markdownDescription.matchAll(
33
+ /https:\/\/github\.com\/janga\/norna\/blob\/([^/\s]+)\/docs\/([^\s)#]+)(?:#([^\s)]+))?/g,
34
+ )];
35
+ assert.ok(links.length > 0, `${location} has no documentation link.`);
36
+
37
+ for (const [, reference, filename, anchor] of links) {
38
+ assert.equal(reference, expectedDocumentationRef, `${location} uses documentation ref ${reference}.`);
39
+ const documentationPath = path.join(root, 'docs', decodeURIComponent(filename));
40
+ assert.ok(existsSync(documentationPath), `${location} links to missing docs/${filename}.`);
41
+ if (!anchor) continue;
42
+
43
+ const source = await readFile(documentationPath, 'utf8');
44
+ const anchors = source
45
+ .split('\n')
46
+ .filter((line) => /^#{1,6}\s/.test(line))
47
+ .map((line) => githubHeadingAnchor(line.replace(/^#{1,6}\s+/, '')));
48
+ assert.ok(anchors.includes(anchor), `${location} links to missing anchor docs/${filename}#${anchor}.`);
49
+ }
50
+ };
51
+
52
+ const visitSchema = (schema, location) => {
53
+ assert.equal(schema.enum, undefined, `${location} uses an undescribed enum.`);
54
+ if (schema.properties) {
55
+ for (const [name, property] of Object.entries(schema.properties)) {
56
+ assert.ok(property.description, `${location}.${name} has no description.`);
57
+ visitSchema(property, `${location}.${name}`);
58
+ }
59
+ }
60
+ if (schema.items) visitSchema(schema.items, `${location}[]`);
61
+ for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
62
+ for (const [index, candidate] of (schema[keyword] ?? []).entries()) {
63
+ if (Object.hasOwn(candidate, 'const')) {
64
+ assert.ok(candidate.title, `${location}.${keyword}[${index}] has no title.`);
65
+ assert.ok(candidate.description, `${location}.${keyword}[${index}] has no description.`);
66
+ }
67
+ visitSchema(candidate, `${location}.${keyword}[${index}]`);
68
+ }
69
+ }
70
+ };
71
+
72
+ for (const filename of filenames) {
73
+ const schema = JSON.parse(await readFile(path.join(root, 'schemas', filename), 'utf8'));
74
+ assert.ok(schema.description, `${filename} has no root description.`);
75
+ assert.match(schema.markdownDescription, /^```yaml\n/, `${filename} rich help must start with YAML syntax.`);
76
+ await assertDocumentationLinks(schema.markdownDescription, filename);
77
+ for (const propertyName of requiredRichHelp[filename]) {
78
+ const property = schema.properties[propertyName];
79
+ assert.match(
80
+ property.markdownDescription,
81
+ /^```yaml\n/,
82
+ `${filename}.${propertyName} rich help must start with YAML syntax.`,
83
+ );
84
+ await assertDocumentationLinks(property.markdownDescription, `${filename}.${propertyName}`);
85
+ }
86
+ visitSchema(schema, filename);
87
+ }
88
+
89
+ const config = JSON.parse(await readFile(path.join(root, 'schemas', 'config.schema.json'), 'utf8'));
90
+ assert.equal(config.properties.language.default, 'en');
91
+ assert.deepEqual(config.properties.language.examples, ['en', 'sv', 'en-GB', 'sv-SE']);
92
+ assert.equal(config.properties.scrollBehavior.default, 'instant');
93
+
94
+ 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);
101
+
102
+ console.log('Schema metadata tests passed.');
@@ -37,19 +37,12 @@ try {
37
37
  await mkdir(path.join(siteDir, 'images', 'work'), { recursive: true });
38
38
  await mkdir(path.join(siteDir, 'public'), { recursive: true });
39
39
  await writeFile(path.join(siteDir, 'public', 'robots.txt'), 'User-agent: *\nAllow: /\n');
40
- await writeFile(path.join(siteDir, 'config.md'), `---
41
- url: https://example.com/
42
- ---
43
- `);
44
- await writeFile(path.join(siteDir, 'theme.md'), `---
45
- typography:
46
- preset: quiet-gallery
47
- presentation:
48
- palette: dark
49
- ---
40
+ await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
41
+ await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
42
+ profile: restrained
43
+ palette: dark
50
44
  `);
51
- await writeFile(path.join(siteDir, 'sitewide-content.md'), `---
52
- banners:
45
+ await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `banners:
53
46
  - id: expired-banner
54
47
  visible:
55
48
  until: "2026-01-01"
@@ -61,7 +54,6 @@ banners:
61
54
  until: "2026-12-31"
62
55
  title: Active banner
63
56
  text: Active banner text.
64
- ---
65
57
  `);
66
58
  await writeFile(path.join(siteDir, 'content.md'), `---
67
59
  title: Temporary Visibility Test