@janga/norna 0.7.6 → 0.7.8
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 +8 -7
- package/astro.config.mjs +76 -0
- package/bin/norna-cli.mjs +2 -2
- package/docs/README.md +3 -3
- package/docs/commands.md +7 -6
- package/docs/configuration.md +3 -2
- package/docs/content.md +202 -91
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +3 -3
- package/docs/getting-started.md +50 -8
- package/docs/images-and-metadata.md +117 -20
- package/docs/routes.md +20 -12
- package/docs/site-structure.md +9 -5
- package/docs/theme.md +12 -8
- package/docs/typography.md +9 -10
- package/examples/dog-gallery/site/content.md +38 -77
- package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +0 -10
- 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 +3 -2
- package/scripts/deploy-site.mjs +11 -4
- 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/site-content.mjs +31 -29
- package/scripts/show-typography.mjs +17 -9
- package/scripts/sync-content-sections.mjs +414 -412
- package/scripts/test-content-check.mjs +59 -45
- package/scripts/test-content-model-v2.mjs +1230 -0
- package/scripts/test-engine-commands.mjs +1 -1
- package/scripts/test-package-check.mjs +36 -8
- 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 +9 -2
- package/src/components/SiteSection.astro +35 -13
- package/src/content.config.ts +4 -17
- package/src/lib/generatedImages.ts +50 -10
- package/src/lib/sectionContent.ts +130 -35
- package/src/styles/global.css +187 -0
- package/starters/basic/README.md +4 -2
- package/starters/basic/site/content.md +9 -11
- package/starters/project/README.md +7 -3
- package/starters/project/site/content.md +0 -9
- package/starters/project/site/routes/010-guide/route-content.md +0 -5
|
@@ -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,6 +190,27 @@ try {
|
|
|
190
190
|
packageCheckThemePath,
|
|
191
191
|
packageCheckTheme.replace('brand: Example Site', 'brand: Package Check Brand'),
|
|
192
192
|
);
|
|
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
|
+
`);
|
|
193
214
|
await mkdir(path.join(siteProjectRoot, 'site', 'routes', '010-about'), { recursive: true });
|
|
194
215
|
await writeFile(path.join(siteProjectRoot, 'site', 'routes', '010-about', 'route-content.md'), `---
|
|
195
216
|
title: About the site
|
|
@@ -197,7 +218,7 @@ description: Route used by package checks.
|
|
|
197
218
|
navigation:
|
|
198
219
|
label: About
|
|
199
220
|
sections:
|
|
200
|
-
|
|
221
|
+
about: {}
|
|
201
222
|
---
|
|
202
223
|
|
|
203
224
|
## About {#about}
|
|
@@ -272,6 +293,10 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
272
293
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
273
294
|
'--section-body-line-height: 1.5',
|
|
274
295
|
);
|
|
296
|
+
await assertFileExcludes(
|
|
297
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
298
|
+
'norna-image-provenance',
|
|
299
|
+
);
|
|
275
300
|
await assertFileIncludes(
|
|
276
301
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
277
302
|
'--section-body-paragraph-spacing: 0.85em',
|
|
@@ -372,8 +397,8 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
372
397
|
await writeFile(
|
|
373
398
|
siteContentPath,
|
|
374
399
|
siteContent.replace(
|
|
375
|
-
'description:
|
|
376
|
-
'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',
|
|
377
402
|
),
|
|
378
403
|
);
|
|
379
404
|
await runExpectFailure(
|
|
@@ -385,25 +410,28 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
385
410
|
await writeFile(
|
|
386
411
|
siteContentPath,
|
|
387
412
|
siteContent.replace(
|
|
388
|
-
'
|
|
389
|
-
' typography:\n overrides:\n caption:\n spacingBefore: wide',
|
|
413
|
+
' intro: {}',
|
|
414
|
+
' intro:\n presentation:\n typography:\n overrides:\n caption:\n spacingBefore: wide',
|
|
390
415
|
),
|
|
391
416
|
);
|
|
392
417
|
await runExpectFailure(
|
|
393
418
|
npxBin,
|
|
394
419
|
['norna', 'build'],
|
|
395
420
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
396
|
-
'sections.
|
|
421
|
+
'sections.intro.presentation.typography.overrides.caption.spacingBefore',
|
|
397
422
|
);
|
|
398
423
|
await writeFile(
|
|
399
424
|
siteContentPath,
|
|
400
|
-
siteContent.replace(
|
|
425
|
+
siteContent.replace(
|
|
426
|
+
' intro: {}',
|
|
427
|
+
' intro:\n presentation:\n typography:\n preset: dramatic',
|
|
428
|
+
),
|
|
401
429
|
);
|
|
402
430
|
await runExpectFailure(
|
|
403
431
|
npxBin,
|
|
404
432
|
['norna', 'build'],
|
|
405
433
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
406
|
-
'sections.
|
|
434
|
+
'sections.intro.presentation.typography.preset',
|
|
407
435
|
);
|
|
408
436
|
await writeFile(
|
|
409
437
|
siteThemePath,
|
|
@@ -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'>;
|
|
@@ -21,6 +24,10 @@ const currentPage = getSitePage(entry);
|
|
|
21
24
|
const siteContentLabel = entry.id.includes('-route-')
|
|
22
25
|
? `${siteDirectory || 'site'}/routes/${currentPage.routeDirectory}/route-content.md`
|
|
23
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;
|
|
24
31
|
const [theme] = await getCollection('theme');
|
|
25
32
|
const navigationPages = getNavigationPages(sitePages);
|
|
26
33
|
const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
|
|
@@ -34,12 +41,12 @@ const frameColors = resolveFrameColors({
|
|
|
34
41
|
});
|
|
35
42
|
const sections = entry.data.sections;
|
|
36
43
|
const siteHtml = entry.rendered?.html ?? '';
|
|
37
|
-
const resolvedSections = getSectionsContent(siteHtml, sections, pagePresentation.inlineStyles);
|
|
44
|
+
const resolvedSections = getSectionsContent(siteHtml, rawMarkdown, sections, currentPage, pagePresentation.inlineStyles);
|
|
38
45
|
const today = getToday();
|
|
39
46
|
const visibleSections = resolvedSections.filter((section) => isVisibleToday(section.visible, today));
|
|
40
47
|
|
|
41
48
|
if (visibleSections.length === 0) {
|
|
42
|
-
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}.`);
|
|
43
50
|
}
|
|
44
51
|
---
|
|
45
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
|
@@ -10,7 +10,6 @@ const siteEntryId = `${siteDirLabel
|
|
|
10
10
|
.replace(/^[./\\]+/, '')
|
|
11
11
|
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
12
12
|
.replace(/^-+|-+$/g, '') || 'site'}-content`;
|
|
13
|
-
const contentImageName = z.string().regex(/^[a-z0-9][a-z0-9.-]*\.(jpe?g|png)$/i);
|
|
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".',
|
|
@@ -168,15 +167,10 @@ const themeNavigation = z.object({
|
|
|
168
167
|
brand: z.string().min(1).optional(),
|
|
169
168
|
}).strict();
|
|
170
169
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
caption: z.string().optional(),
|
|
170
|
+
const sectionMetadata = z.object({
|
|
171
|
+
visible: visibilityWindow.optional(),
|
|
172
|
+
presentation: sectionPresentationOverride.optional(),
|
|
175
173
|
}).strict();
|
|
176
|
-
const galleryCarousel = z.object({
|
|
177
|
-
carousel: z.array(galleryImage).min(2, 'A carousel must contain at least two images.'),
|
|
178
|
-
}).strict();
|
|
179
|
-
const galleryItem = z.union([galleryImage, galleryCarousel]);
|
|
180
174
|
|
|
181
175
|
const siteSchema = z.object({
|
|
182
176
|
title: z.string(),
|
|
@@ -184,14 +178,7 @@ const siteSchema = z.object({
|
|
|
184
178
|
navigation: pageNavigation.optional(),
|
|
185
179
|
presentation: pagePresentation.optional(),
|
|
186
180
|
frame: frame.optional(),
|
|
187
|
-
sections: z.
|
|
188
|
-
z.object({
|
|
189
|
-
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
190
|
-
visible: visibilityWindow.optional(),
|
|
191
|
-
presentation: sectionPresentationOverride.optional(),
|
|
192
|
-
gallery: z.array(galleryItem).optional().default([]),
|
|
193
|
-
}).strict(),
|
|
194
|
-
).min(1),
|
|
181
|
+
sections: z.record(z.string().regex(/^[a-z0-9-]+$/), sectionMetadata).optional().default({}),
|
|
195
182
|
});
|
|
196
183
|
|
|
197
184
|
const themeSchema = z.object({
|
|
@@ -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
|
|