@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.
- package/README.md +18 -4
- package/astro.config.mjs +2 -0
- package/bin/norna-cli.mjs +170 -0
- package/bin/norna.mjs +149 -150
- package/docs/README.md +36 -16
- package/docs/commands.md +18 -5
- package/docs/configuration.md +37 -2
- package/docs/content.md +98 -257
- package/docs/{command-organization.md → design/command-organization.md} +9 -5
- package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +17 -22
- package/docs/engine-development.md +33 -5
- package/docs/getting-started.md +40 -4
- package/docs/local-development.md +13 -0
- package/docs/publishing.md +23 -0
- package/docs/routes.md +90 -0
- package/docs/site-structure.md +15 -8
- package/docs/theme.md +150 -0
- package/docs/typography.md +125 -0
- package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
- package/examples/dog-gallery/site/config.mjs +97 -0
- package/examples/dog-gallery/site/content.md +146 -0
- package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
- package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
- package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
- package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
- package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
- package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
- package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
- package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
- package/examples/dog-gallery/site/public/favicon.svg +7 -0
- package/examples/dog-gallery/site/public/robots.txt +2 -0
- package/examples/dog-gallery/site/routes/dog-care/route-content.md +35 -0
- package/examples/dog-gallery/site/theme.md +55 -0
- package/fixtures/basic/site/config.mjs +1 -0
- package/package.json +8 -7
- package/scripts/check-config.mjs +1 -0
- package/scripts/dev-local.mjs +64 -15
- package/scripts/init-site.mjs +1 -1
- package/scripts/lib/project-config.mjs +22 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-paths.mjs +18 -3
- package/scripts/lib/typography.mjs +5 -5
- package/scripts/show-typography.mjs +252 -29
- package/scripts/test-cli-discovery.mjs +124 -0
- package/scripts/test-engine-commands.mjs +18 -4
- package/scripts/test-navigation.mjs +10 -6
- package/scripts/test-package-check.mjs +32 -4
- package/src/components/SiteNavigation.astro +7 -5
- package/src/components/SitePage.astro +3 -0
- package/src/components/SiteSection.astro +24 -24
- package/src/content.config.ts +4 -0
- package/src/layouts/BaseLayout.astro +2 -1
- package/src/lib/basePath.ts +21 -0
- package/src/lib/generatedImages.ts +8 -4
- package/src/lib/sectionContent.ts +6 -1
- package/src/lib/sitePublicAssets.ts +8 -1
- package/src/styles/global.css +8 -8
- package/starters/basic/.github/workflows/deploy.yml +3 -3
- package/starters/basic/README.md +21 -0
- package/starters/basic/package.json +1 -1
- package/starters/basic/site/config.mjs +1 -0
- package/starters/basic/site/content.md +5 -3
- 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, /
|
|
47
|
-
assert.match(showResult.stdout,
|
|
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
|
-
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
+
await stopServer();
|
|
86
87
|
|
|
87
|
-
await runInherit(
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
38
|
-
mobile: 'clamp(1.
|
|
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.
|
|
42
|
-
mobile: 'clamp(1.
|
|
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.
|
|
46
|
-
mobile: 'clamp(1.
|
|
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,
|
|
50
|
-
mobile: 'clamp(
|
|
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.
|
|
56
|
-
mobile: 'clamp(0.
|
|
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(
|
|
60
|
-
mobile: 'clamp(
|
|
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.
|
|
64
|
-
mobile: 'clamp(1.
|
|
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.
|
|
68
|
-
mobile: 'clamp(1.
|
|
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.
|
|
74
|
-
mobile: 'clamp(0.
|
|
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.
|
|
78
|
-
mobile: 'clamp(0.
|
|
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(
|
|
82
|
-
mobile: 'clamp(
|
|
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.
|
|
86
|
-
mobile: 'clamp(1.
|
|
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;
|
package/src/content.config.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
+
}));
|