@janga/norna 0.7.5 → 0.7.7
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 +5 -5
- package/astro.config.mjs +76 -0
- package/bin/norna-cli.mjs +2 -2
- package/docs/README.md +3 -3
- package/docs/commands.md +6 -6
- package/docs/configuration.md +3 -2
- package/docs/content.md +154 -95
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +4 -4
- package/docs/getting-started.md +6 -4
- package/docs/images-and-metadata.md +112 -20
- package/docs/routes.md +52 -33
- package/docs/site-structure.md +7 -6
- package/docs/theme.md +8 -7
- package/docs/typography.md +5 -5
- package/examples/dog-gallery/site/content.md +41 -46
- package/examples/dog-gallery/site/routes/{dog-care → 010-dog-care}/route-content.md +1 -4
- package/fixtures/basic/site/content.md +2 -4
- package/fixtures/content-model-v2/.astro/collections/site.schema.json +776 -0
- package/fixtures/content-model-v2/.astro/collections/theme.schema.json +668 -0
- package/fixtures/content-model-v2/.astro/content-assets.mjs +4 -0
- package/fixtures/content-model-v2/.astro/content-modules.mjs +1 -0
- package/fixtures/content-model-v2/.astro/content.d.ts +175 -0
- package/fixtures/content-model-v2/.astro/data-store.json +1 -0
- package/fixtures/content-model-v2/.astro/dev-local.json +8 -0
- package/fixtures/content-model-v2/.astro/dev.json +13 -0
- package/fixtures/content-model-v2/.astro/dev.log +231 -0
- package/fixtures/content-model-v2/.astro/settings.json +5 -0
- package/fixtures/content-model-v2/.astro/types.d.ts +2 -0
- package/fixtures/content-model-v2/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
- package/fixtures/content-model-v2/dist/_astro/SitePage.BbfZub4g.css +1 -0
- package/fixtures/content-model-v2/dist/guide/index.html +3 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-960.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-768.webp +0 -0
- package/fixtures/content-model-v2/dist/index.html +8 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/_metadata.json +80 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js +1389 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/package.json +3 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/fixtures/content-model-v2/site/.norna/generated-images.json +210 -0
- package/fixtures/content-model-v2/site/config.mjs +11 -0
- package/fixtures/content-model-v2/site/content.md +84 -0
- package/fixtures/content-model-v2/site/images/card-section/adopt.svg +9 -0
- package/fixtures/content-model-v2/site/images/card-section/foster.svg +12 -0
- package/fixtures/content-model-v2/site/images/intro/detail.jpg +0 -0
- package/fixtures/content-model-v2/site/images/intro/duplicate.jpg +0 -0
- package/fixtures/content-model-v2/site/images/intro/hero.jpg +0 -0
- package/fixtures/content-model-v2/site/images/plain/image.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-one.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-three.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-two.jpg +0 -0
- package/fixtures/content-model-v2/site/routes/010-guide/images/intro/duplicate.jpg +0 -0
- package/fixtures/content-model-v2/site/routes/010-guide/route-content.md +17 -0
- package/fixtures/content-model-v2/site/theme.md +9 -0
- package/package.json +4 -2
- package/scripts/deploy-site.mjs +12 -5
- package/scripts/dev-local.mjs +6 -0
- package/scripts/generate-images.mjs +132 -56
- package/scripts/lib/image-dimensions.mjs +26 -0
- package/scripts/lib/norna-markdown-blocks.mjs +643 -0
- package/scripts/lib/route-model.mjs +17 -0
- package/scripts/lib/site-content.mjs +45 -37
- package/scripts/show-typography.mjs +18 -10
- package/scripts/sync-content-sections.mjs +280 -423
- package/scripts/test-content-check.mjs +59 -45
- package/scripts/test-content-model-v2.mjs +998 -0
- package/scripts/test-engine-commands.mjs +1 -1
- package/scripts/test-package-check.mjs +38 -11
- package/scripts/test-route-model.mjs +32 -0
- package/scripts/test-temporary-visibility.mjs +3 -6
- package/src/components/CardList.astro +77 -0
- package/src/components/GalleryGrid.astro +22 -12
- package/src/components/SiteNavigation.astro +5 -4
- package/src/components/SitePage.astro +12 -6
- package/src/components/SiteSection.astro +35 -13
- package/src/content.config.ts +8 -23
- package/src/lib/generatedImages.ts +50 -10
- package/src/lib/sectionContent.ts +130 -35
- package/src/lib/sitePages.ts +36 -15
- package/src/pages/[slug].astro +3 -3
- package/src/styles/global.css +187 -0
- package/starters/basic/site/content.md +10 -6
- package/starters/project/README.md +2 -2
- package/starters/project/site/content.md +0 -10
- package/starters/project/site/routes/{guide → 010-guide}/route-content.md +0 -6
|
@@ -27,7 +27,7 @@ try {
|
|
|
27
27
|
const directSiteRoot = path.join(tempRoot, 'current-directory-site');
|
|
28
28
|
await mkdir(directSiteRoot, { recursive: true });
|
|
29
29
|
await writeFile(path.join(directSiteRoot, 'config.mjs'), 'export default { site: { url: "https://example.com/" } };\n');
|
|
30
|
-
await writeFile(path.join(directSiteRoot, 'content.md'), '---\ntitle: Direct Site\
|
|
30
|
+
await writeFile(path.join(directSiteRoot, 'content.md'), '---\ntitle: Direct Site\ndescription: Direct site fixture.\n---\n\n## Intro {#intro}\n\nText.\n');
|
|
31
31
|
const directSiteProjectRoot = await realpath(tempRoot);
|
|
32
32
|
const directSiteDoctorResult = runCli(['doctor'], {
|
|
33
33
|
cwd: directSiteRoot,
|
|
@@ -190,15 +190,35 @@ try {
|
|
|
190
190
|
packageCheckThemePath,
|
|
191
191
|
packageCheckTheme.replace('brand: Example Site', 'brand: Package Check Brand'),
|
|
192
192
|
);
|
|
193
|
-
await
|
|
194
|
-
|
|
193
|
+
await writeFile(path.join(siteProjectRoot, 'site', 'content.md'), `---
|
|
194
|
+
title: Package Check Site
|
|
195
|
+
description: Site used by package checks.
|
|
196
|
+
sections:
|
|
197
|
+
intro: {}
|
|
198
|
+
work: {}
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Intro {#intro}
|
|
202
|
+
|
|
203
|
+
This page verifies that [packaged norna sites]{.highlight} can build with the current content model.
|
|
204
|
+
|
|
205
|
+
<!-- norna-image-provenance:
|
|
206
|
+
image: package-check.svg
|
|
207
|
+
source: package check fixture
|
|
208
|
+
-->
|
|
209
|
+
|
|
210
|
+
## Work {#work}
|
|
211
|
+
|
|
212
|
+
This section keeps the package check independent from user-facing starter copy.
|
|
213
|
+
`);
|
|
214
|
+
await mkdir(path.join(siteProjectRoot, 'site', 'routes', '010-about'), { recursive: true });
|
|
215
|
+
await writeFile(path.join(siteProjectRoot, 'site', 'routes', '010-about', 'route-content.md'), `---
|
|
195
216
|
title: About the site
|
|
196
217
|
description: Route used by package checks.
|
|
197
218
|
navigation:
|
|
198
219
|
label: About
|
|
199
|
-
order: 20
|
|
200
220
|
sections:
|
|
201
|
-
|
|
221
|
+
about: {}
|
|
202
222
|
---
|
|
203
223
|
|
|
204
224
|
## About {#about}
|
|
@@ -273,6 +293,10 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
273
293
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
274
294
|
'--section-body-line-height: 1.5',
|
|
275
295
|
);
|
|
296
|
+
await assertFileExcludes(
|
|
297
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
298
|
+
'norna-image-provenance',
|
|
299
|
+
);
|
|
276
300
|
await assertFileIncludes(
|
|
277
301
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
278
302
|
'--section-body-paragraph-spacing: 0.85em',
|
|
@@ -373,8 +397,8 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
373
397
|
await writeFile(
|
|
374
398
|
siteContentPath,
|
|
375
399
|
siteContent.replace(
|
|
376
|
-
'description:
|
|
377
|
-
'description:
|
|
400
|
+
'description: Site used by package checks.',
|
|
401
|
+
'description: Site used by package checks.\npresentation:\n typography:\n overrides:\n body:\n lineHeight: tight',
|
|
378
402
|
),
|
|
379
403
|
);
|
|
380
404
|
await runExpectFailure(
|
|
@@ -386,25 +410,28 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
386
410
|
await writeFile(
|
|
387
411
|
siteContentPath,
|
|
388
412
|
siteContent.replace(
|
|
389
|
-
'
|
|
390
|
-
' typography:\n overrides:\n caption:\n spacingBefore: wide',
|
|
413
|
+
' intro: {}',
|
|
414
|
+
' intro:\n presentation:\n typography:\n overrides:\n caption:\n spacingBefore: wide',
|
|
391
415
|
),
|
|
392
416
|
);
|
|
393
417
|
await runExpectFailure(
|
|
394
418
|
npxBin,
|
|
395
419
|
['norna', 'build'],
|
|
396
420
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
397
|
-
'sections.
|
|
421
|
+
'sections.intro.presentation.typography.overrides.caption.spacingBefore',
|
|
398
422
|
);
|
|
399
423
|
await writeFile(
|
|
400
424
|
siteContentPath,
|
|
401
|
-
siteContent.replace(
|
|
425
|
+
siteContent.replace(
|
|
426
|
+
' intro: {}',
|
|
427
|
+
' intro:\n presentation:\n typography:\n preset: dramatic',
|
|
428
|
+
),
|
|
402
429
|
);
|
|
403
430
|
await runExpectFailure(
|
|
404
431
|
npxBin,
|
|
405
432
|
['norna', 'build'],
|
|
406
433
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
407
|
-
'sections.
|
|
434
|
+
'sections.intro.presentation.typography.preset',
|
|
408
435
|
);
|
|
409
436
|
await writeFile(
|
|
410
437
|
siteThemePath,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { parseRouteDirectory } from './lib/route-model.mjs';
|
|
3
|
+
|
|
4
|
+
assert.deepEqual(parseRouteDirectory('010-getting-started'), {
|
|
5
|
+
routeDirectory: '010-getting-started',
|
|
6
|
+
routeOrder: 10,
|
|
7
|
+
routeId: 'getting-started',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
assert.deepEqual(parseRouteDirectory('120-api-reference'), {
|
|
11
|
+
routeDirectory: '120-api-reference',
|
|
12
|
+
routeOrder: 120,
|
|
13
|
+
routeId: 'api-reference',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
for (const routeDirectory of [
|
|
17
|
+
'000-home',
|
|
18
|
+
'10-about',
|
|
19
|
+
'010_About',
|
|
20
|
+
'010-About',
|
|
21
|
+
'010-about-',
|
|
22
|
+
'010-about--team',
|
|
23
|
+
'010-om-oss!',
|
|
24
|
+
]) {
|
|
25
|
+
assert.throws(
|
|
26
|
+
() => parseRouteDirectory(routeDirectory),
|
|
27
|
+
/Route directories must use the form NNN-route-id/,
|
|
28
|
+
`${routeDirectory} should be rejected`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log('Route model test passed.');
|
|
@@ -59,17 +59,14 @@ frame:
|
|
|
59
59
|
title: Temporary Visibility Test
|
|
60
60
|
description: Test site for temporary sections.
|
|
61
61
|
sections:
|
|
62
|
-
|
|
62
|
+
expired:
|
|
63
63
|
visible:
|
|
64
64
|
until: "2026-01-01"
|
|
65
|
-
|
|
66
|
-
- id: active
|
|
65
|
+
active:
|
|
67
66
|
visible:
|
|
68
67
|
from: "2026-01-01"
|
|
69
68
|
until: "2026-12-31"
|
|
70
|
-
|
|
71
|
-
- id: always
|
|
72
|
-
gallery: []
|
|
69
|
+
always: {}
|
|
73
70
|
|
|
74
71
|
---
|
|
75
72
|
## Expired {#expired}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import { withBasePath } from '../lib/basePath';
|
|
4
|
+
import { getImageAttributes } from '../lib/generatedImages';
|
|
5
|
+
|
|
6
|
+
type Card = {
|
|
7
|
+
title: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
image?: string;
|
|
10
|
+
src?: string;
|
|
11
|
+
link?: string;
|
|
12
|
+
'badge-text'?: string;
|
|
13
|
+
};
|
|
14
|
+
type CardListLayout = 'image-top' | 'image-left' | 'image-right';
|
|
15
|
+
type CardListFlow = 'grid' | 'stack';
|
|
16
|
+
type CardListSize = 's' | 'm' | 'l' | 'xl';
|
|
17
|
+
type CardListWidth = 'text' | 'narrow' | 'normal' | 'wide';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
cards: Card[];
|
|
21
|
+
layout?: CardListLayout;
|
|
22
|
+
flow?: CardListFlow;
|
|
23
|
+
size?: CardListSize;
|
|
24
|
+
width?: CardListWidth;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { cards, layout = 'image-top', flow = 'grid', size = 'm', width = 'normal' } = Astro.props;
|
|
28
|
+
const cardImageSizes = [
|
|
29
|
+
'(max-width: 700px) 100vw',
|
|
30
|
+
`min(20rem, ${projectConfig.gallery.maxAvailableWidthPercent.desktop}vw)`,
|
|
31
|
+
].join(', ');
|
|
32
|
+
const displayLink = (link: string) => withBasePath(projectConfig.site.basePath, link);
|
|
33
|
+
const cardListClasses = [
|
|
34
|
+
'card-list',
|
|
35
|
+
`card-list-layout-${layout}`,
|
|
36
|
+
`card-list-flow-${flow}`,
|
|
37
|
+
`card-list-size-${size}`,
|
|
38
|
+
`card-list-width-${width}`,
|
|
39
|
+
];
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<div class:list={cardListClasses}>
|
|
43
|
+
{cards.map((card) => {
|
|
44
|
+
const content = (
|
|
45
|
+
<>
|
|
46
|
+
{card.src && (
|
|
47
|
+
<div class="card-list-image-frame">
|
|
48
|
+
<img
|
|
49
|
+
{...getImageAttributes(card.src, cardImageSizes)}
|
|
50
|
+
class="card-list-image"
|
|
51
|
+
alt=""
|
|
52
|
+
decoding="async"
|
|
53
|
+
fetchpriority="low"
|
|
54
|
+
loading="lazy"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
<div class="card-list-content">
|
|
59
|
+
{card['badge-text'] && <p class="card-list-badge">{card['badge-text']}</p>}
|
|
60
|
+
<h3 class="card-list-title">{card.title}</h3>
|
|
61
|
+
{card.text && <p>{card.text}</p>}
|
|
62
|
+
</div>
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<article class:list={['card-list-item', {
|
|
68
|
+
'card-list-item-has-image': Boolean(card.src),
|
|
69
|
+
'card-list-item-linked': Boolean(card.link),
|
|
70
|
+
}]}>
|
|
71
|
+
{card.link
|
|
72
|
+
? <a class="card-list-link" href={displayLink(card.link)}>{content}</a>
|
|
73
|
+
: content}
|
|
74
|
+
</article>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type { CollectionEntry } from 'astro:content';
|
|
3
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
3
|
import { getGeneratedImage, getImageAttributes } from '../lib/generatedImages';
|
|
5
4
|
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
type GalleryImage = {
|
|
6
|
+
image: string;
|
|
7
|
+
src?: string;
|
|
8
|
+
alt?: string;
|
|
9
|
+
caption?: string;
|
|
10
|
+
};
|
|
11
|
+
type GalleryItem = GalleryImage | { carousel: GalleryImage[] };
|
|
12
|
+
type GeneratedImageWithDimensions = NonNullable<ReturnType<typeof getGeneratedImage>> & {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
9
16
|
|
|
10
17
|
interface Props {
|
|
11
18
|
title: string;
|
|
@@ -18,9 +25,12 @@ const galleryImageSizes = [
|
|
|
18
25
|
`(max-width: 700px) ${projectConfig.gallery.maxAvailableWidthPercent.mobile}vw`,
|
|
19
26
|
`${projectConfig.gallery.maxAvailableWidthPercent.desktop}vw`,
|
|
20
27
|
].join(', ');
|
|
28
|
+
const hasDimensions = (image: ReturnType<typeof getGeneratedImage>): image is GeneratedImageWithDimensions => (
|
|
29
|
+
Boolean(image) && Number.isFinite(image.width) && Number.isFinite(image.height)
|
|
30
|
+
);
|
|
21
31
|
const getImageClass = (image: GalleryImage) => {
|
|
22
|
-
const generatedImage = getGeneratedImage(image.image);
|
|
23
|
-
return generatedImage && generatedImage.width > generatedImage.height
|
|
32
|
+
const generatedImage = getGeneratedImage(image.src ?? image.image);
|
|
33
|
+
return hasDimensions(generatedImage) && generatedImage.width > generatedImage.height
|
|
24
34
|
? 'gallery-image-landscape'
|
|
25
35
|
: undefined;
|
|
26
36
|
};
|
|
@@ -31,8 +41,8 @@ const getImageLoadingProps = (isPriority: boolean) => ({
|
|
|
31
41
|
});
|
|
32
42
|
const getCarouselAspectRatio = (images: GalleryImage[]) => {
|
|
33
43
|
const ratios = images
|
|
34
|
-
.map((image) => getGeneratedImage(image.image))
|
|
35
|
-
.filter(
|
|
44
|
+
.map((image) => getGeneratedImage(image.src ?? image.image))
|
|
45
|
+
.filter(hasDimensions)
|
|
36
46
|
.map((image) => image.width / image.height);
|
|
37
47
|
|
|
38
48
|
return ratios.length > 0 ? Math.min(...ratios) : null;
|
|
@@ -48,9 +58,9 @@ const getCarouselAspectRatio = (images: GalleryImage[]) => {
|
|
|
48
58
|
<figure class="gallery-item">
|
|
49
59
|
<div class="gallery-image-frame">
|
|
50
60
|
<img
|
|
51
|
-
{...getImageAttributes(item.image, galleryImageSizes)}
|
|
61
|
+
{...getImageAttributes(item.src ?? item.image, galleryImageSizes)}
|
|
52
62
|
class:list={['gallery-image', getImageClass(item)]}
|
|
53
|
-
alt={item.alt}
|
|
63
|
+
alt={item.alt ?? ''}
|
|
54
64
|
{...getImageLoadingProps(isPriorityItem)}
|
|
55
65
|
/>
|
|
56
66
|
</div>
|
|
@@ -87,9 +97,9 @@ const getCarouselAspectRatio = (images: GalleryImage[]) => {
|
|
|
87
97
|
<figure class="gallery-carousel-slide" data-carousel-slide>
|
|
88
98
|
<div class="gallery-carousel-image-frame">
|
|
89
99
|
<img
|
|
90
|
-
{...getImageAttributes(image.image, galleryImageSizes)}
|
|
100
|
+
{...getImageAttributes(image.src ?? image.image, galleryImageSizes)}
|
|
91
101
|
class:list={['gallery-image', getImageClass(image)]}
|
|
92
|
-
alt={image.alt}
|
|
102
|
+
alt={image.alt ?? ''}
|
|
93
103
|
{...getImageLoadingProps(isPriorityItem && imageIndex === 0)}
|
|
94
104
|
/>
|
|
95
105
|
</div>
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type { CollectionEntry } from 'astro:content';
|
|
3
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
3
|
import { withBasePath } from '../lib/basePath';
|
|
4
|
+
import type { ResolvedSection } from '../lib/sectionContent';
|
|
5
5
|
import type { SitePage } from '../lib/sitePages';
|
|
6
6
|
|
|
7
|
-
type SiteSection = CollectionEntry<'site'>['data']['sections'][number];
|
|
8
|
-
type ResolvedSection = SiteSection & { title: string };
|
|
9
7
|
type FrameColors = {
|
|
10
8
|
backgroundColor: string;
|
|
11
9
|
textColor: string;
|
|
@@ -21,8 +19,11 @@ interface Props {
|
|
|
21
19
|
|
|
22
20
|
const { brandLabel, currentPage, pages, sections, frameColors } = Astro.props;
|
|
23
21
|
const smoothScrollConfig = projectConfig.navigation.smoothScroll;
|
|
22
|
+
const siteTopBackgroundColor = frameColors.backgroundColor === 'transparent'
|
|
23
|
+
? 'var(--color-page)'
|
|
24
|
+
: frameColors.backgroundColor;
|
|
24
25
|
const navigationStyle = [
|
|
25
|
-
`--site-top-background-color: ${
|
|
26
|
+
`--site-top-background-color: ${siteTopBackgroundColor}`,
|
|
26
27
|
`--site-top-text-color: ${frameColors.textColor}`,
|
|
27
28
|
].join('; ');
|
|
28
29
|
const showSiteNavigation = pages.length > 1;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
4
5
|
import SiteNavigation from './SiteNavigation.astro';
|
|
5
6
|
import SiteSection from './SiteSection.astro';
|
|
6
7
|
import { getSectionsContent } from '../lib/sectionContent';
|
|
7
8
|
import { getNavigationPages, getSitePage, getSitePages } from '../lib/sitePages';
|
|
8
9
|
import { getToday, isVisibleToday } from '../lib/visibility';
|
|
10
|
+
import { readSiteFile } from '../../scripts/lib/site-content.mjs';
|
|
11
|
+
import { siteContentPath, siteRoutesDir } from '../../scripts/lib/site-paths.mjs';
|
|
9
12
|
import { resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
|
|
10
13
|
|
|
11
14
|
type SiteEntry = CollectionEntry<'site'>;
|
|
@@ -16,13 +19,16 @@ interface Props {
|
|
|
16
19
|
|
|
17
20
|
const { entry } = Astro.props;
|
|
18
21
|
const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
22
|
+
const sitePages = getSitePages(await getCollection('site'));
|
|
23
|
+
const currentPage = getSitePage(entry);
|
|
19
24
|
const siteContentLabel = entry.id.includes('-route-')
|
|
20
|
-
? `${siteDirectory || 'site'}/routes/${
|
|
25
|
+
? `${siteDirectory || 'site'}/routes/${currentPage.routeDirectory}/route-content.md`
|
|
21
26
|
: `${siteDirectory || 'site'}/content.md`;
|
|
27
|
+
const pageContentPath = currentPage.routeDirectory
|
|
28
|
+
? path.join(siteRoutesDir, currentPage.routeDirectory, 'route-content.md')
|
|
29
|
+
: siteContentPath;
|
|
30
|
+
const rawMarkdown = (await readSiteFile(pageContentPath, siteContentLabel)).body;
|
|
22
31
|
const [theme] = await getCollection('theme');
|
|
23
|
-
const allEntries = await getCollection('site');
|
|
24
|
-
const sitePages = getSitePages(allEntries);
|
|
25
|
-
const currentPage = getSitePage(entry);
|
|
26
32
|
const navigationPages = getNavigationPages(sitePages);
|
|
27
33
|
const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
|
|
28
34
|
const themeData = theme?.data ?? {};
|
|
@@ -35,12 +41,12 @@ const frameColors = resolveFrameColors({
|
|
|
35
41
|
});
|
|
36
42
|
const sections = entry.data.sections;
|
|
37
43
|
const siteHtml = entry.rendered?.html ?? '';
|
|
38
|
-
const resolvedSections = getSectionsContent(siteHtml, sections, pagePresentation.inlineStyles);
|
|
44
|
+
const resolvedSections = getSectionsContent(siteHtml, rawMarkdown, sections, currentPage, pagePresentation.inlineStyles);
|
|
39
45
|
const today = getToday();
|
|
40
46
|
const visibleSections = resolvedSections.filter((section) => isVisibleToday(section.visible, today));
|
|
41
47
|
|
|
42
48
|
if (visibleSections.length === 0) {
|
|
43
|
-
throw new Error(`No sections are visible for ${today}. Check sections
|
|
49
|
+
throw new Error(`No sections are visible for ${today}. Check sections.<section-id>.visible in ${siteContentLabel}.`);
|
|
44
50
|
}
|
|
45
51
|
---
|
|
46
52
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { CollectionEntry } from 'astro:content';
|
|
3
3
|
import { resolveTypographyOverride } from '../../scripts/lib/typography.mjs';
|
|
4
|
+
import CardList from './CardList.astro';
|
|
4
5
|
import GalleryGrid from './GalleryGrid.astro';
|
|
6
|
+
import type { ResolvedSection } from '../lib/sectionContent';
|
|
5
7
|
|
|
6
|
-
type SiteSection = CollectionEntry<'site'>['data']['sections'][number];
|
|
7
|
-
type ResolvedSection = SiteSection & { title: string; contentHtml: string };
|
|
8
8
|
type ResponsiveValue<T> = { desktop: T; mobile: T };
|
|
9
9
|
type ResponsiveOverride<T> = { desktop?: T; mobile?: T };
|
|
10
10
|
type TextAlign = 'left' | 'center' | 'right';
|
|
@@ -12,7 +12,7 @@ type TextSize = 'small' | 'medium' | 'large' | 'xlarge';
|
|
|
12
12
|
type SectionPresentationOverride = {
|
|
13
13
|
backgroundColor?: string;
|
|
14
14
|
textColor?: string;
|
|
15
|
-
typography?: NonNullable<
|
|
15
|
+
typography?: NonNullable<ResolvedSection['presentation']>['typography'];
|
|
16
16
|
};
|
|
17
17
|
type ResolvedPagePresentation = {
|
|
18
18
|
backgroundColor: string;
|
|
@@ -197,7 +197,11 @@ const getAlignedTextWidth = (
|
|
|
197
197
|
});
|
|
198
198
|
const headingWidth = getAlignedTextWidth(headingAlign, '760px');
|
|
199
199
|
const bodyWidth = getAlignedTextWidth(bodyAlign, 'var(--text-width)');
|
|
200
|
-
const
|
|
200
|
+
const hasMedia = section.contentBlocks.some((block) => (
|
|
201
|
+
block.type === 'image-stack'
|
|
202
|
+
|| block.type === 'image-carousel'
|
|
203
|
+
|| (block.type === 'card-list' && block.cards.some((card) => card.image))
|
|
204
|
+
));
|
|
201
205
|
const markdownHeadingStyle = (level: 'h3' | 'h4') => {
|
|
202
206
|
const headingConfig = markdownHeadings[level];
|
|
203
207
|
const fontSize = getHeadingSizeValue(level, headingConfig.size);
|
|
@@ -255,16 +259,34 @@ const sectionStyle = [
|
|
|
255
259
|
<HeadingTag>{section.title}</HeadingTag>
|
|
256
260
|
</header>
|
|
257
261
|
|
|
258
|
-
<div class:list={['section-body', { 'section-body-has-gallery':
|
|
259
|
-
{section.
|
|
262
|
+
<div class:list={['section-body', { 'section-body-has-gallery': hasMedia, 'section-body-text-only': !hasMedia }]}>
|
|
263
|
+
{section.contentBlocks.map((block, blockIndex) => {
|
|
264
|
+
if (block.type === 'html') {
|
|
265
|
+
return block.html && <div class="section-markdown" set:html={block.html} />;
|
|
266
|
+
}
|
|
260
267
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
if (block.type === 'image-carousel') {
|
|
269
|
+
return (
|
|
270
|
+
<GalleryGrid
|
|
271
|
+
title={section.title}
|
|
272
|
+
items={[{ carousel: block.images }]}
|
|
273
|
+
priorityFirstImage={priorityGalleryImage && blockIndex === 0}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (block.type === 'card-list') {
|
|
279
|
+
return <CardList cards={block.cards} layout={block.layout} flow={block.flow} size={block.size} width={block.width} />;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<GalleryGrid
|
|
284
|
+
title={section.title}
|
|
285
|
+
items={block.images}
|
|
286
|
+
priorityFirstImage={priorityGalleryImage && blockIndex === 0}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
})}
|
|
268
290
|
</div>
|
|
269
291
|
</div>
|
|
270
292
|
</section>
|
package/src/content.config.ts
CHANGED
|
@@ -3,14 +3,13 @@ import { glob } from 'astro/loaders';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { z } from 'astro/zod';
|
|
5
5
|
import { siteDir, siteDirLabel } from '../scripts/lib/site-paths.mjs';
|
|
6
|
+
import { parseRouteDirectory } from '../scripts/lib/route-model.mjs';
|
|
6
7
|
import { isDateOnly } from './lib/visibility';
|
|
7
8
|
|
|
8
9
|
const siteEntryId = `${siteDirLabel
|
|
9
10
|
.replace(/^[./\\]+/, '')
|
|
10
11
|
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
11
12
|
.replace(/^-+|-+$/g, '') || 'site'}-content`;
|
|
12
|
-
const contentImageName = z.string().regex(/^[a-z0-9][a-z0-9.-]*\.(jpe?g|png)$/i);
|
|
13
|
-
const routeSlug = z.string().regex(/^[a-z0-9][a-z0-9-]*$/, 'Use lowercase letters, numbers, and hyphens.');
|
|
14
13
|
const colorValue = z.string().regex(
|
|
15
14
|
/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,
|
|
16
15
|
'Use a hex color such as "#000000".',
|
|
@@ -163,37 +162,23 @@ const frame = z.object({
|
|
|
163
162
|
const pageNavigation = z.object({
|
|
164
163
|
include: z.boolean().optional(),
|
|
165
164
|
label: z.string().optional(),
|
|
166
|
-
order: z.number().int().optional(),
|
|
167
165
|
}).strict();
|
|
168
166
|
const themeNavigation = z.object({
|
|
169
167
|
brand: z.string().min(1).optional(),
|
|
170
168
|
}).strict();
|
|
171
169
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
caption: z.string().optional(),
|
|
170
|
+
const sectionMetadata = z.object({
|
|
171
|
+
visible: visibilityWindow.optional(),
|
|
172
|
+
presentation: sectionPresentationOverride.optional(),
|
|
176
173
|
}).strict();
|
|
177
|
-
const galleryCarousel = z.object({
|
|
178
|
-
carousel: z.array(galleryImage).min(2, 'A carousel must contain at least two images.'),
|
|
179
|
-
}).strict();
|
|
180
|
-
const galleryItem = z.union([galleryImage, galleryCarousel]);
|
|
181
174
|
|
|
182
175
|
const siteSchema = z.object({
|
|
183
176
|
title: z.string(),
|
|
184
177
|
description: z.string(),
|
|
185
|
-
slug: routeSlug.optional(),
|
|
186
178
|
navigation: pageNavigation.optional(),
|
|
187
179
|
presentation: pagePresentation.optional(),
|
|
188
180
|
frame: frame.optional(),
|
|
189
|
-
sections: z.
|
|
190
|
-
z.object({
|
|
191
|
-
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
192
|
-
visible: visibilityWindow.optional(),
|
|
193
|
-
presentation: sectionPresentationOverride.optional(),
|
|
194
|
-
gallery: z.array(galleryItem).optional().default([]),
|
|
195
|
-
}).strict(),
|
|
196
|
-
).min(1),
|
|
181
|
+
sections: z.record(z.string().regex(/^[a-z0-9-]+$/), sectionMetadata).optional().default({}),
|
|
197
182
|
});
|
|
198
183
|
|
|
199
184
|
const themeSchema = z.object({
|
|
@@ -214,9 +199,9 @@ const site = defineCollection({
|
|
|
214
199
|
return siteEntryId;
|
|
215
200
|
}
|
|
216
201
|
|
|
217
|
-
const
|
|
218
|
-
return
|
|
219
|
-
? `${siteEntryId.replace(/-content$/, '')}-route-${
|
|
202
|
+
const routeDirectory = entry.match(/^routes\/([^/]+)\/route-content\.md$/)?.[1];
|
|
203
|
+
return routeDirectory
|
|
204
|
+
? `${siteEntryId.replace(/-content$/, '')}-route-${parseRouteDirectory(routeDirectory, `route directory routes/${routeDirectory}`).routeDirectory}`
|
|
220
205
|
: entry.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
221
206
|
},
|
|
222
207
|
}),
|
|
@@ -1,45 +1,65 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
3
|
import { generatedImagesManifestPath } from '../../scripts/lib/site-paths.mjs';
|
|
4
4
|
import { withBasePath } from './basePath';
|
|
5
5
|
|
|
6
6
|
type GeneratedImage = {
|
|
7
|
+
kind?: 'static';
|
|
7
8
|
outputVersion?: number;
|
|
8
9
|
sourceHash?: string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
src?: string;
|
|
11
|
+
width?: number;
|
|
12
|
+
height?: number;
|
|
13
|
+
variants?: Array<{
|
|
12
14
|
src: string;
|
|
13
15
|
width: number;
|
|
14
16
|
}>;
|
|
15
17
|
};
|
|
16
18
|
|
|
19
|
+
let cachedImagesSignature: string | null = null;
|
|
20
|
+
let cachedImages: Record<string, GeneratedImage | undefined> = {};
|
|
21
|
+
|
|
17
22
|
const readGeneratedImages = () => {
|
|
18
23
|
try {
|
|
19
|
-
|
|
24
|
+
const manifestStats = statSync(generatedImagesManifestPath);
|
|
25
|
+
const signature = `${manifestStats.mtimeMs}:${manifestStats.size}`;
|
|
26
|
+
|
|
27
|
+
if (signature === cachedImagesSignature) {
|
|
28
|
+
return cachedImages;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cachedImages = JSON.parse(readFileSync(generatedImagesManifestPath, 'utf8')) as Record<string, GeneratedImage | undefined>;
|
|
32
|
+
cachedImagesSignature = signature;
|
|
33
|
+
return cachedImages;
|
|
20
34
|
} catch {
|
|
35
|
+
cachedImagesSignature = null;
|
|
36
|
+
cachedImages = {};
|
|
21
37
|
return {};
|
|
22
38
|
}
|
|
23
39
|
};
|
|
24
40
|
|
|
25
|
-
const images = readGeneratedImages();
|
|
26
41
|
const maxDisplayImageWidth = 1920;
|
|
27
42
|
const fallbackDisplayImageWidth = 1440;
|
|
28
43
|
|
|
29
|
-
export const getGeneratedImage = (src: string) =>
|
|
44
|
+
export const getGeneratedImage = (src: string) => readGeneratedImages()[src];
|
|
30
45
|
|
|
31
46
|
const displaySrc = (src: string) => withBasePath(projectConfig.site.basePath, src);
|
|
32
47
|
|
|
33
|
-
export const getLinkedImageSrc = (src: string) =>
|
|
48
|
+
export const getLinkedImageSrc = (src: string) => {
|
|
49
|
+
const image = getGeneratedImage(src);
|
|
50
|
+
return displaySrc(image?.kind === 'static'
|
|
51
|
+
? image.src ?? src
|
|
52
|
+
: image?.variants?.at(-1)?.src ?? src);
|
|
53
|
+
};
|
|
34
54
|
|
|
35
|
-
const getDisplayVariants = (variants: GeneratedImage['variants']) => {
|
|
55
|
+
const getDisplayVariants = (variants: NonNullable<GeneratedImage['variants']>) => {
|
|
36
56
|
const sortedVariants = [...variants].sort((a, b) => a.width - b.width);
|
|
37
57
|
const displayVariants = sortedVariants.filter((variant) => variant.width <= maxDisplayImageWidth);
|
|
38
58
|
|
|
39
59
|
return displayVariants.length > 0 ? displayVariants : sortedVariants;
|
|
40
60
|
};
|
|
41
61
|
|
|
42
|
-
const getFallbackVariant = (variants: GeneratedImage['variants']) => (
|
|
62
|
+
const getFallbackVariant = (variants: NonNullable<GeneratedImage['variants']>) => (
|
|
43
63
|
variants.filter((variant) => variant.width <= fallbackDisplayImageWidth).at(-1) ?? variants[0]
|
|
44
64
|
);
|
|
45
65
|
|
|
@@ -53,6 +73,26 @@ export const getImageAttributes = (src: string, sizes: string) => {
|
|
|
53
73
|
};
|
|
54
74
|
}
|
|
55
75
|
|
|
76
|
+
if (image.kind === 'static') {
|
|
77
|
+
return {
|
|
78
|
+
src: displaySrc(image.src ?? src),
|
|
79
|
+
...(Number.isFinite(image.width) && Number.isFinite(image.height)
|
|
80
|
+
? {
|
|
81
|
+
style: `aspect-ratio: ${image.width} / ${image.height};`,
|
|
82
|
+
width: image.width,
|
|
83
|
+
height: image.height,
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!image.variants) {
|
|
90
|
+
return {
|
|
91
|
+
src: displaySrc(src),
|
|
92
|
+
sizes,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
56
96
|
const displayVariants = getDisplayVariants(image.variants);
|
|
57
97
|
const fallbackVariant = getFallbackVariant(displayVariants);
|
|
58
98
|
|