@janga/norna 0.7.0 → 0.7.2

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 (63) hide show
  1. package/README.md +18 -4
  2. package/astro.config.mjs +2 -0
  3. package/bin/norna-cli.mjs +170 -0
  4. package/bin/norna.mjs +149 -150
  5. package/docs/README.md +36 -16
  6. package/docs/commands.md +18 -5
  7. package/docs/configuration.md +37 -2
  8. package/docs/content.md +98 -257
  9. package/docs/{command-organization.md → design/command-organization.md} +9 -5
  10. package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +17 -22
  11. package/docs/engine-development.md +33 -5
  12. package/docs/getting-started.md +40 -4
  13. package/docs/local-development.md +13 -0
  14. package/docs/publishing.md +23 -0
  15. package/docs/routes.md +90 -0
  16. package/docs/site-structure.md +15 -8
  17. package/docs/theme.md +150 -0
  18. package/docs/typography.md +125 -0
  19. package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
  20. package/examples/dog-gallery/site/config.mjs +97 -0
  21. package/examples/dog-gallery/site/content.md +146 -0
  22. package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
  23. package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
  24. package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
  25. package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
  26. package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
  27. package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
  28. package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
  29. package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
  30. package/examples/dog-gallery/site/public/favicon.svg +7 -0
  31. package/examples/dog-gallery/site/public/robots.txt +2 -0
  32. package/examples/dog-gallery/site/routes/dog-care/route-content.md +35 -0
  33. package/examples/dog-gallery/site/theme.md +55 -0
  34. package/fixtures/basic/site/config.mjs +1 -0
  35. package/package.json +8 -7
  36. package/scripts/check-config.mjs +1 -0
  37. package/scripts/dev-local.mjs +64 -15
  38. package/scripts/init-site.mjs +1 -1
  39. package/scripts/lib/project-config.mjs +22 -0
  40. package/scripts/lib/site-content.mjs +2 -1
  41. package/scripts/lib/site-paths.mjs +18 -3
  42. package/scripts/lib/typography.mjs +5 -5
  43. package/scripts/show-typography.mjs +252 -29
  44. package/scripts/test-cli-discovery.mjs +124 -0
  45. package/scripts/test-engine-commands.mjs +18 -4
  46. package/scripts/test-navigation.mjs +10 -6
  47. package/scripts/test-package-check.mjs +32 -4
  48. package/src/components/SiteNavigation.astro +7 -5
  49. package/src/components/SitePage.astro +3 -0
  50. package/src/components/SiteSection.astro +24 -24
  51. package/src/content.config.ts +4 -0
  52. package/src/layouts/BaseLayout.astro +2 -1
  53. package/src/lib/basePath.ts +21 -0
  54. package/src/lib/generatedImages.ts +8 -4
  55. package/src/lib/sectionContent.ts +6 -1
  56. package/src/lib/sitePublicAssets.ts +8 -1
  57. package/src/styles/global.css +8 -8
  58. package/starters/basic/.github/workflows/deploy.yml +3 -3
  59. package/starters/basic/README.md +21 -0
  60. package/starters/basic/package.json +1 -1
  61. package/starters/basic/site/config.mjs +1 -0
  62. package/starters/basic/site/content.md +5 -3
  63. package/starters/basic/site/theme.md +4 -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
+ }
@@ -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);
@@ -43,8 +55,10 @@ try {
43
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,6 +176,20 @@ 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/gallery/'")
185
+ .replace("basePath: '/'", "basePath: '/gallery/'"),
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 Gallery', '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
195
  title: About the gallery
@@ -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.35rem, 1.1rem + 0.9vw, 2rem)',
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/gallery/about/">',
237
251
  );
238
252
  await assertFileIncludes(
239
253
  path.join(siteProjectRoot, 'dist', 'about', 'index.html'),
240
- 'href="/about/" aria-current="page"',
254
+ 'href="/gallery/about/" aria-current="page"',
255
+ );
256
+ await assertFileIncludes(
257
+ path.join(siteProjectRoot, 'dist', 'index.html'),
258
+ '<a class="site-brand" href="/gallery/">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="/gallery/favicon.ico">',
292
310
  );
293
311
  await assertFileExcludes(
294
312
  path.join(siteProjectRoot, 'dist', 'index.html'),
@@ -387,6 +405,16 @@ This route verifies that packaged norna sites can build route pages.
387
405
  { cwd: siteProjectRoot, env: npmEnv },
388
406
  'typography.fontFamily',
389
407
  );
408
+ await writeFile(
409
+ siteConfigPath,
410
+ siteConfig.replace("basePath: '/gallery/'", "basePath: 'gallery'"),
411
+ );
412
+ await runExpectFailure(
413
+ npxBin,
414
+ ['norna', 'config:check'],
415
+ { cwd: siteProjectRoot, env: npmEnv },
416
+ 'site.basePath',
417
+ );
390
418
 
391
419
  console.log('Package check passed.');
392
420
  } finally {
@@ -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,7 +24,9 @@ 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 ?? {};
29
+ const brandLabel = themeData.navigation?.brand ?? homePage.title;
28
30
  const pagePresentation = resolvePagePresentation(themeData.presentation, entry.data.presentation);
29
31
  const frameColors = resolveFrameColors({
30
32
  themePresentation: themeData.presentation,
@@ -50,6 +52,7 @@ if (visibleSections.length === 0) {
50
52
  pathname={currentPage.pathname}
51
53
  >
52
54
  <SiteNavigation
55
+ brandLabel={brandLabel}
53
56
  currentPage={currentPage}
54
57
  pages={navigationPages}
55
58
  sections={visibleSections}
@@ -34,56 +34,56 @@ const { section, pagePresentation, headingLevel, priorityGalleryImage = false }
34
34
  const HeadingTag = headingLevel === 1 ? 'h1' : 'h2';
35
35
  const headingSizeValues: Record<TextSize, ResponsiveValue<string>> = {
36
36
  small: {
37
- desktop: 'clamp(1.15rem, 2.2vw, 2rem)',
38
- mobile: 'clamp(1.15rem, 5vw, 1.8rem)',
37
+ desktop: 'clamp(1.1rem, 1.02rem + 0.35vw, 1.35rem)',
38
+ mobile: 'clamp(1.12rem, 1rem + 0.85vw, 1.42rem)',
39
39
  },
40
40
  medium: {
41
- desktop: 'clamp(1.4rem, 3.1vw, 3.2rem)',
42
- mobile: 'clamp(1.4rem, 3.1vw, 3.2rem)',
41
+ desktop: 'clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
42
+ mobile: 'clamp(1.3rem, 1.05rem + 1.35vw, 1.75rem)',
43
43
  },
44
44
  large: {
45
- desktop: 'clamp(1.65rem, 4.6vw, 5.65rem)',
46
- mobile: 'clamp(1.9rem, 8vw, 3.2rem)',
45
+ desktop: 'clamp(1.6rem, 1.22rem + 1.45vw, 2.75rem)',
46
+ mobile: 'clamp(1.5rem, 1.12rem + 2vw, 2.25rem)',
47
47
  },
48
48
  xlarge: {
49
- desktop: 'clamp(2rem, 5.6vw, 7rem)',
50
- mobile: 'clamp(2.15rem, 9.4vw, 3.8rem)',
49
+ desktop: 'clamp(2rem, 1.35rem + 2.4vw, 3.75rem)',
50
+ mobile: 'clamp(1.8rem, 1.25rem + 3vw, 2.85rem)',
51
51
  },
52
52
  };
53
53
  const bodySizeValues: Record<TextSize, ResponsiveValue<string>> = {
54
54
  small: {
55
- desktop: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
56
- mobile: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
55
+ desktop: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
56
+ mobile: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
57
57
  },
58
58
  medium: {
59
- desktop: 'clamp(1rem, 0.96rem + 0.18vw, 1.125rem)',
60
- mobile: 'clamp(1rem, 0.96rem + 0.18vw, 1.125rem)',
59
+ desktop: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
60
+ mobile: 'clamp(0.96rem, 0.94rem + 0.08vw, 1rem)',
61
61
  },
62
62
  large: {
63
- desktop: 'clamp(1.1rem, 1.04rem + 0.24vw, 1.25rem)',
64
- mobile: 'clamp(1.1rem, 1.04rem + 0.24vw, 1.25rem)',
63
+ desktop: 'clamp(1.05rem, 1rem + 0.16vw, 1.12rem)',
64
+ mobile: 'clamp(1.05rem, 1rem + 0.16vw, 1.12rem)',
65
65
  },
66
66
  xlarge: {
67
- desktop: 'clamp(1.18rem, 1.08rem + 0.36vw, 1.42rem)',
68
- mobile: 'clamp(1.18rem, 1.08rem + 0.36vw, 1.42rem)',
67
+ desktop: 'clamp(1.14rem, 1.06rem + 0.26vw, 1.25rem)',
68
+ mobile: 'clamp(1.14rem, 1.06rem + 0.26vw, 1.25rem)',
69
69
  },
70
70
  };
71
71
  const captionSizeValues: Record<TextSize, ResponsiveValue<string>> = {
72
72
  small: {
73
- desktop: 'clamp(0.82rem, 0.78rem + 0.12vw, 0.92rem)',
74
- mobile: 'clamp(0.82rem, 0.78rem + 0.12vw, 0.92rem)',
73
+ desktop: 'clamp(0.78rem, 0.76rem + 0.08vw, 0.84rem)',
74
+ mobile: 'clamp(0.78rem, 0.76rem + 0.08vw, 0.84rem)',
75
75
  },
76
76
  medium: {
77
- desktop: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
78
- mobile: 'clamp(0.9rem, 0.86rem + 0.16vw, 1rem)',
77
+ desktop: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
78
+ mobile: 'clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
79
79
  },
80
80
  large: {
81
- desktop: 'clamp(1rem, 0.94rem + 0.2vw, 1.12rem)',
82
- mobile: 'clamp(1rem, 0.94rem + 0.2vw, 1.12rem)',
81
+ desktop: 'clamp(0.96rem, 0.92rem + 0.12vw, 1rem)',
82
+ mobile: 'clamp(0.96rem, 0.92rem + 0.12vw, 1rem)',
83
83
  },
84
84
  xlarge: {
85
- desktop: 'clamp(1.08rem, 1rem + 0.28vw, 1.25rem)',
86
- mobile: 'clamp(1.08rem, 1rem + 0.28vw, 1.25rem)',
85
+ desktop: 'clamp(1.04rem, 0.98rem + 0.18vw, 1.12rem)',
86
+ mobile: 'clamp(1.04rem, 0.98rem + 0.18vw, 1.12rem)',
87
87
  },
88
88
  };
89
89
  const sectionOverride = section.presentation as SectionPresentationOverride | undefined;
@@ -108,6 +108,9 @@ const pageNavigation = z.object({
108
108
  label: z.string().optional(),
109
109
  order: z.number().int().optional(),
110
110
  }).strict();
111
+ const themeNavigation = z.object({
112
+ brand: z.string().min(1).optional(),
113
+ }).strict();
111
114
 
112
115
  const galleryImage = z.object({
113
116
  image: contentImageName,
@@ -137,6 +140,7 @@ const siteSchema = z.object({
137
140
  });
138
141
 
139
142
  const themeSchema = z.object({
143
+ navigation: themeNavigation.optional(),
140
144
  presentation: themePresentation.optional(),
141
145
  frame: frame.optional(),
142
146
  }).strict();
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import projectConfig from '../../scripts/lib/project-config.mjs';
3
3
  import { getIconLinks } from '../lib/sitePublicAssets';
4
+ import { withBasePath } from '../lib/basePath';
4
5
  import '../styles/global.css';
5
6
 
6
7
  type FrameColors = {
@@ -23,7 +24,7 @@ const {
23
24
  } = Astro.props;
24
25
 
25
26
  const documentTitle = title;
26
- const canonicalUrl = new URL(pathname, projectConfig.site.url).href;
27
+ const canonicalUrl = new URL(withBasePath(projectConfig.site.basePath, pathname), projectConfig.site.url).href;
27
28
  const iconLinks = getIconLinks();
28
29
  const scrollBehavior = projectConfig.navigation.smoothScroll.enabled ? 'smooth' : 'auto';
29
30
  const formatPercent = (value: number) => `${value}%`;
@@ -0,0 +1,21 @@
1
+ export const withBasePath = (basePath: string, value: string) => {
2
+ if (
3
+ basePath === '/'
4
+ || value.startsWith('#')
5
+ || value.startsWith('//')
6
+ || /^[a-z][a-z0-9+.-]*:/i.test(value)
7
+ ) {
8
+ return value;
9
+ }
10
+
11
+ if (!value.startsWith('/')) {
12
+ return value;
13
+ }
14
+
15
+ return `${basePath.replace(/\/$/, '')}${value}`;
16
+ };
17
+
18
+ export const applyBasePathToHtml = (basePath: string, html: string) =>
19
+ html.replace(/\b(href|src)=(["'])\/(?!\/)([^"']*)\2/g, (_match, attribute: string, quote: string, path: string) => (
20
+ `${attribute}=${quote}${withBasePath(basePath, `/${path}`)}${quote}`
21
+ ));
@@ -1,5 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
2
3
  import { generatedImagesManifestPath } from '../../scripts/lib/site-paths.mjs';
4
+ import { withBasePath } from './basePath';
3
5
 
4
6
  type GeneratedImage = {
5
7
  outputVersion?: number;
@@ -26,7 +28,9 @@ const fallbackDisplayImageWidth = 1440;
26
28
 
27
29
  export const getGeneratedImage = (src: string) => images[src];
28
30
 
29
- export const getLinkedImageSrc = (src: string) => getGeneratedImage(src)?.variants.at(-1)?.src ?? src;
31
+ const displaySrc = (src: string) => withBasePath(projectConfig.site.basePath, src);
32
+
33
+ export const getLinkedImageSrc = (src: string) => displaySrc(getGeneratedImage(src)?.variants.at(-1)?.src ?? src);
30
34
 
31
35
  const getDisplayVariants = (variants: GeneratedImage['variants']) => {
32
36
  const sortedVariants = [...variants].sort((a, b) => a.width - b.width);
@@ -44,7 +48,7 @@ export const getImageAttributes = (src: string, sizes: string) => {
44
48
 
45
49
  if (!image) {
46
50
  return {
47
- src,
51
+ src: displaySrc(src),
48
52
  sizes,
49
53
  };
50
54
  }
@@ -53,8 +57,8 @@ export const getImageAttributes = (src: string, sizes: string) => {
53
57
  const fallbackVariant = getFallbackVariant(displayVariants);
54
58
 
55
59
  return {
56
- src: fallbackVariant?.src ?? src,
57
- srcset: displayVariants.map((variant) => `${variant.src} ${variant.width}w`).join(', '),
60
+ src: displaySrc(fallbackVariant?.src ?? src),
61
+ srcset: displayVariants.map((variant) => `${displaySrc(variant.src)} ${variant.width}w`).join(', '),
58
62
  sizes,
59
63
  style: `aspect-ratio: ${image.width} / ${image.height};`,
60
64
  width: image.width,
@@ -1,4 +1,6 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
+ import projectConfig from '../../scripts/lib/project-config.mjs';
3
+ import { applyBasePathToHtml } from './basePath';
2
4
 
3
5
  type SiteSection = CollectionEntry<'site'>['data']['sections'][number];
4
6
  type ThemePresentation = CollectionEntry<'theme'>['data']['presentation'];
@@ -56,6 +58,9 @@ const applyInlineStyles = (html: string, inlineStyles: InlineStyles | undefined)
56
58
  return `<span class="inline-style inline-style-${styleName}" style="--inline-style-color: ${style.color}">${text}</span>`;
57
59
  });
58
60
 
61
+ const prepareContentHtml = (html: string, inlineStyles: InlineStyles | undefined) =>
62
+ applyBasePathToHtml(projectConfig.site.basePath, applyInlineStyles(html, inlineStyles));
63
+
59
64
  export const getSectionsContent = (
60
65
  html: string,
61
66
  sections: SiteSection[],
@@ -88,7 +93,7 @@ export const getSectionsContent = (
88
93
 
89
94
  contentById.set(id, {
90
95
  title,
91
- contentHtml: applyInlineStyles(content, inlineStyles),
96
+ contentHtml: prepareContentHtml(content, inlineStyles),
92
97
  });
93
98
  markdownSectionIds.push(id);
94
99
  }
@@ -1,6 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { sitePublicDir } from '../../scripts/lib/site-paths.mjs';
4
+ import projectConfig from '../../scripts/lib/project-config.mjs';
5
+ import { withBasePath } from './basePath';
4
6
 
5
7
  type IconLink = {
6
8
  rel: 'icon' | 'apple-touch-icon';
@@ -36,4 +38,9 @@ const publicAssetExists = (href: string) => {
36
38
  return existsSync(path.join(sitePublicDir, relativePath));
37
39
  };
38
40
 
39
- export const getIconLinks = () => faviconCandidates.filter((candidate) => publicAssetExists(candidate.href));
41
+ export const getIconLinks = () => faviconCandidates
42
+ .filter((candidate) => publicAssetExists(candidate.href))
43
+ .map((candidate) => ({
44
+ ...candidate,
45
+ href: withBasePath(projectConfig.site.basePath, candidate.href),
46
+ }));