@janga/norna 0.7.13 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -9
- package/bin/norna-cli.mjs +11 -4
- package/docs/README.md +71 -43
- package/docs/commands.md +34 -20
- package/docs/configuration.md +43 -62
- package/docs/content.md +34 -7
- package/docs/design/command-organization.md +15 -8
- package/docs/design/norna-diagram-design.md +6 -6
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +22 -9
- package/docs/getting-started.md +29 -16
- package/docs/how-to/embedded-site.md +1 -1
- package/docs/images-and-metadata.md +3 -3
- package/docs/local-development.md +11 -3
- package/docs/public-files.md +105 -0
- package/docs/publishing.md +4 -8
- package/docs/routes.md +6 -7
- package/docs/site-files.md +144 -0
- package/docs/sitewide-content.md +47 -33
- package/docs/theme.md +37 -49
- package/docs/typography.md +25 -31
- package/examples/complete-sites/dog-shelter-multi-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-single-page/site/config.md → dog-shelter-multi-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-multi-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-multi-page/site/config.md → dog-shelter-single-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-single-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
- package/examples/feature-demos/media-and-surfaces/README.md +1 -1
- package/examples/feature-demos/media-and-surfaces/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/site/content.md +2 -2
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/{route-content.md → content.md} +3 -7
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +3 -0
- package/examples/feature-demos/media-and-surfaces/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/media-and-surfaces/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/README.md +8 -0
- package/examples/feature-demos/sitewide-content/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/site/routes/010-identity/{route-content.md → content.md} +3 -3
- package/examples/feature-demos/sitewide-content/site/routes/020-notices/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/routes/030-footer/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/{sitewide-content.md → sitewide-content.yaml} +1 -11
- package/examples/feature-demos/sitewide-content/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/config.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/020-documentation/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/theme-presets/site/theme.yaml +1 -0
- package/fixtures/basic/site/{config.md → config.yaml} +0 -2
- package/fixtures/basic/site/content.md +3 -3
- package/fixtures/basic/site/{sitewide-content.md → sitewide-content.yaml} +0 -2
- package/fixtures/basic/site/{theme.md → theme.yaml} +3 -6
- package/fixtures/content-model-v2/site/{config.md → config.yaml} +0 -2
- package/fixtures/content-model-v2/site/theme.yaml +4 -0
- package/package.json +10 -3
- package/schemas/config.schema.json +77 -0
- package/schemas/content-frontmatter.schema.json +81 -0
- package/schemas/manifest.json +9 -0
- package/schemas/sitewide-content.schema.json +215 -0
- package/schemas/theme.schema.json +1208 -0
- package/scripts/check-config.mjs +18 -11
- package/scripts/export-theme-preset.mjs +1 -1
- package/scripts/generate-schemas.mjs +283 -0
- package/scripts/init-site.mjs +22 -27
- package/scripts/lib/documentation-links.mjs +8 -0
- package/scripts/lib/editor-language-service.mjs +609 -0
- package/scripts/lib/example-sites.mjs +2 -2
- package/scripts/lib/git-status.mjs +7 -0
- package/scripts/lib/logo-assets.mjs +15 -10
- package/scripts/lib/norna-markdown-blocks.mjs +90 -11
- package/scripts/lib/presentation.mjs +23 -12
- package/scripts/lib/project-config.mjs +17 -41
- package/scripts/lib/public-asset-conventions.mjs +112 -0
- package/scripts/lib/schema-definitions.mjs +213 -0
- package/scripts/lib/schema-value-definitions.mjs +86 -0
- package/scripts/lib/site-content.mjs +24 -18
- package/scripts/lib/site-paths.mjs +4 -8
- package/scripts/lib/sitewide-content.mjs +24 -0
- package/scripts/lib/theme-config.mjs +15 -35
- package/scripts/lib/theme-presets.mjs +76 -66
- package/scripts/lib/typography.mjs +15 -15
- package/scripts/lib/yaml-config.mjs +50 -0
- package/scripts/list-theme-presets.mjs +15 -0
- package/scripts/release.mjs +84 -8
- package/scripts/show-typography.mjs +22 -46
- package/scripts/sync-content-sections.mjs +2 -2
- package/scripts/test-client-javascript.mjs +28 -28
- package/scripts/test-content-check.mjs +21 -26
- package/scripts/test-content-model-v2.mjs +72 -40
- package/scripts/test-documentation.mjs +70 -4
- package/scripts/test-editor-language-service.mjs +158 -0
- package/scripts/test-engine-commands.mjs +65 -34
- package/scripts/test-package-check.mjs +37 -30
- package/scripts/test-project-config.mjs +15 -20
- package/scripts/test-schemas.mjs +102 -0
- package/scripts/test-temporary-visibility.mjs +5 -13
- package/scripts/test-theme-presets.mjs +64 -37
- package/src/components/CardList.astro +2 -5
- package/src/components/ImageCarousel.astro +21 -21
- package/src/components/{GalleryGrid.astro → ImageStack.astro} +12 -12
- package/src/components/SectionNavigationScript.astro +28 -0
- package/src/components/SiteNavigation.astro +6 -5
- package/src/components/SitePage.astro +8 -8
- package/src/components/SiteSection.astro +18 -21
- package/src/content.config.ts +17 -209
- package/src/layouts/BaseLayout.astro +9 -9
- package/src/lib/{galleryImages.ts → imagePresentation.ts} +7 -7
- package/src/lib/routeThemes.ts +9 -29
- package/src/lib/sectionContent.ts +3 -3
- package/src/lib/sitePublicAssets.ts +5 -21
- package/src/styles/global.css +73 -73
- package/starters/basic/.github/workflows/deploy.yml +1 -1
- package/starters/basic/README.md +16 -13
- package/starters/basic/package.json +3 -2
- package/starters/basic/site/{config.md → config.yaml} +0 -2
- package/starters/basic/site/content.md +1 -1
- package/starters/basic/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/basic/site/{theme.md → theme.yaml} +8 -15
- package/starters/project/.github/workflows/deploy.yml +1 -1
- package/starters/project/README.md +22 -15
- package/starters/project/package.json +3 -2
- package/starters/project/site/{config.md → config.yaml} +0 -2
- package/starters/project/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/project/site/{theme.md → theme.yaml} +4 -8
- package/docs/site-structure.md +0 -98
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +0 -16
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +0 -16
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.md +0 -8
- package/examples/feature-demos/theme-presets/site/config.md +0 -3
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.md +0 -3
- package/examples/feature-demos/theme-presets/site/theme.md +0 -3
- package/fixtures/content-model-v2/site/theme.md +0 -7
- package/scripts/lib/sitewide-config.mjs +0 -34
- package/src/components/MobileSectionNavigationScript.astro +0 -14
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/{route-content.md → content.md} +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/routes/010-media/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/020-documentation/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/030-project/{route-content.md → content.md} +0 -0
- /package/fixtures/content-model-v2/site/routes/010-guide/{route-content.md → content.md} +0 -0
- /package/starters/project/site/routes/010-guide/{route-content.md → content.md} +0 -0
package/src/content.config.ts
CHANGED
|
@@ -1,228 +1,36 @@
|
|
|
1
1
|
import { defineCollection } from 'astro:content';
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
|
+
import { basename } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import {
|
|
6
|
+
siteDir,
|
|
7
|
+
siteDirLabel,
|
|
8
|
+
siteThemePath,
|
|
9
|
+
sitewideContentPath,
|
|
10
|
+
} from '../scripts/lib/site-paths.mjs';
|
|
6
11
|
import { parseRouteDirectory } from '../scripts/lib/route-model.mjs';
|
|
7
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
siteSchema,
|
|
14
|
+
sitewideSchema,
|
|
15
|
+
themeVisualSchema,
|
|
16
|
+
} from '../scripts/lib/schema-definitions.mjs';
|
|
8
17
|
|
|
9
18
|
const siteEntryId = `${siteDirLabel
|
|
10
19
|
.replace(/^[./\\]+/, '')
|
|
11
20
|
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
12
21
|
.replace(/^-+|-+$/g, '') || 'site'}-content`;
|
|
13
|
-
const textAlign = z.enum(['left', 'center', 'right']);
|
|
14
|
-
const textSize = z.enum(['small', 'medium', 'large', 'xlarge']);
|
|
15
|
-
const textWidth = z.enum(['narrow', 'normal', 'wide']);
|
|
16
|
-
const headingWeight = z.union([z.literal(400), z.literal(500), z.literal(600), z.literal(700)]);
|
|
17
|
-
const typographyPreset = z.enum(['quiet-gallery', 'compact-gallery', 'text-forward', 'statement']);
|
|
18
|
-
const themePreset = z.enum(['portfolio', 'documentation', 'project', 'statement']);
|
|
19
|
-
const presentationPalette = z.enum(['dark', 'light', 'paper']);
|
|
20
|
-
const sectionSurfaceMode = z.enum(['none', 'cycle']);
|
|
21
|
-
const sectionSurface = z.enum(['base', 'soft', 'emphasis']);
|
|
22
|
-
const spacingDensity = z.enum(['compact', 'normal', 'airy']);
|
|
23
|
-
const lineHeight = z.number()
|
|
24
|
-
.min(1, 'Use a unitless line height of at least 1.')
|
|
25
|
-
.max(3, 'Use a unitless line height of at most 3.');
|
|
26
|
-
const cssLength = z.string().regex(
|
|
27
|
-
/^(?:0|(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em|ch|lh))$/,
|
|
28
|
-
'Use a CSS length such as "0", "0.8em", "1rem", or "12px".',
|
|
29
|
-
);
|
|
30
|
-
const visualCssLengthUnit = String.raw`(?:px|rem|em|vw|vh|vmin|vmax|ch|%)`;
|
|
31
|
-
const visualCssLengthValue = String.raw`(?:\d+(?:\.\d+)?|\.\d+)${visualCssLengthUnit}`;
|
|
32
|
-
const visualCssLength = z.string().regex(
|
|
33
|
-
new RegExp(`^(?:0|${visualCssLengthValue}|clamp\\(\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*,\\s*${visualCssLengthValue}\\s*\\))$`),
|
|
34
|
-
'Use a CSS length such as "0", "900px", "56rem", "90%", or "clamp(1rem, 4vw, 3rem)".',
|
|
35
|
-
);
|
|
36
|
-
const dateOnly = z.string()
|
|
37
|
-
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format.')
|
|
38
|
-
.refine(isDateOnly, 'Use a real calendar date.');
|
|
39
|
-
const visibilityWindow = z.object({
|
|
40
|
-
from: dateOnly.optional(),
|
|
41
|
-
until: dateOnly.optional(),
|
|
42
|
-
}).strict().refine(
|
|
43
|
-
(value) => value.from !== undefined || value.until !== undefined,
|
|
44
|
-
'Specify from, until, or both.',
|
|
45
|
-
).refine(
|
|
46
|
-
(value) => value.from === undefined || value.until === undefined || value.from < value.until,
|
|
47
|
-
'visible.until must be later than visible.from.',
|
|
48
|
-
);
|
|
49
|
-
const overrideResponsiveTextAlign = z.object({
|
|
50
|
-
desktop: textAlign.optional(),
|
|
51
|
-
mobile: textAlign.optional(),
|
|
52
|
-
}).strict().refine(
|
|
53
|
-
(value) => value.desktop !== undefined || value.mobile !== undefined,
|
|
54
|
-
'Specify desktop, mobile, or both.',
|
|
55
|
-
);
|
|
56
|
-
const commonTextPresentationOverride = {
|
|
57
|
-
align: overrideResponsiveTextAlign.optional(),
|
|
58
|
-
size: textSize.optional(),
|
|
59
|
-
lineHeight: lineHeight.optional(),
|
|
60
|
-
};
|
|
61
|
-
const headingPresentationOverride = z.object({
|
|
62
|
-
...commonTextPresentationOverride,
|
|
63
|
-
weight: headingWeight.optional(),
|
|
64
|
-
spacingBefore: cssLength.optional(),
|
|
65
|
-
spacingAfter: cssLength.optional(),
|
|
66
|
-
}).strict();
|
|
67
|
-
const headingLevelsPresentationOverride = z.object({
|
|
68
|
-
h1: headingPresentationOverride.optional(),
|
|
69
|
-
h2: headingPresentationOverride.optional(),
|
|
70
|
-
h3: headingPresentationOverride.optional(),
|
|
71
|
-
h4: headingPresentationOverride.optional(),
|
|
72
|
-
}).strict();
|
|
73
|
-
const bodyPresentationOverride = z.object({
|
|
74
|
-
...commonTextPresentationOverride,
|
|
75
|
-
width: textWidth.optional(),
|
|
76
|
-
paragraphSpacing: cssLength.optional(),
|
|
77
|
-
}).strict();
|
|
78
|
-
const captionPresentationOverride = z.object({
|
|
79
|
-
...commonTextPresentationOverride,
|
|
80
|
-
spacingBefore: cssLength.optional(),
|
|
81
|
-
}).strict();
|
|
82
|
-
const typographyOverrides = z.object({
|
|
83
|
-
headings: headingLevelsPresentationOverride.optional(),
|
|
84
|
-
body: bodyPresentationOverride.optional(),
|
|
85
|
-
caption: captionPresentationOverride.optional(),
|
|
86
|
-
}).strict();
|
|
87
|
-
const themeTypography = z.object({
|
|
88
|
-
fontFamily: z.string()
|
|
89
|
-
.min(1)
|
|
90
|
-
.refine((value) => !/[\n\r;{}]/.test(value), 'Do not use semicolons, braces, or line breaks.').optional(),
|
|
91
|
-
preset: typographyPreset.optional(),
|
|
92
|
-
rhythm: spacingDensity.optional(),
|
|
93
|
-
overrides: typographyOverrides.optional(),
|
|
94
|
-
}).strict().refine(
|
|
95
|
-
(value) => value.fontFamily !== undefined || value.preset !== undefined || value.rhythm !== undefined || value.overrides !== undefined,
|
|
96
|
-
'Specify fontFamily, preset, rhythm, overrides, or both.',
|
|
97
|
-
);
|
|
98
|
-
const responsiveCssLength = z.union([
|
|
99
|
-
visualCssLength,
|
|
100
|
-
z.object({
|
|
101
|
-
desktop: visualCssLength,
|
|
102
|
-
mobile: visualCssLength,
|
|
103
|
-
}).strict(),
|
|
104
|
-
]);
|
|
105
|
-
const responsivePercent = z.union([
|
|
106
|
-
z.number().positive().max(100),
|
|
107
|
-
z.object({
|
|
108
|
-
desktop: z.number().positive().max(100),
|
|
109
|
-
mobile: z.number().positive().max(100),
|
|
110
|
-
}).strict(),
|
|
111
|
-
]);
|
|
112
|
-
const themeLayoutSpacing = z.object({
|
|
113
|
-
blockGap: responsiveCssLength.optional(),
|
|
114
|
-
finalSectionBottom: responsiveCssLength.optional(),
|
|
115
|
-
firstSectionTop: responsiveCssLength.optional(),
|
|
116
|
-
headingToBlock: responsiveCssLength.optional(),
|
|
117
|
-
imageGap: responsiveCssLength.optional(),
|
|
118
|
-
sectionGap: responsiveCssLength.optional(),
|
|
119
|
-
}).strict();
|
|
120
|
-
const themeLayout = z.object({
|
|
121
|
-
density: spacingDensity.optional(),
|
|
122
|
-
pageWidth: visualCssLength.optional(),
|
|
123
|
-
gutter: responsiveCssLength.optional(),
|
|
124
|
-
spacing: themeLayoutSpacing.optional(),
|
|
125
|
-
}).strict();
|
|
126
|
-
const themeGallery = z.object({
|
|
127
|
-
width: visualCssLength.optional(),
|
|
128
|
-
maxAvailableWidthPercent: responsivePercent.optional(),
|
|
129
|
-
maxAvailableHeightPercent: responsivePercent.optional(),
|
|
130
|
-
}).strict();
|
|
131
|
-
const themePresentation = z.object({
|
|
132
|
-
palette: presentationPalette.optional(),
|
|
133
|
-
sectionSurfaces: z.object({
|
|
134
|
-
mode: sectionSurfaceMode,
|
|
135
|
-
sequence: z.array(sectionSurface).min(1).max(3).optional(),
|
|
136
|
-
}).strict().refine(
|
|
137
|
-
(value) => !value.sequence || new Set(value.sequence).size === value.sequence.length,
|
|
138
|
-
'Each section surface may appear only once in a sequence.',
|
|
139
|
-
).optional(),
|
|
140
|
-
}).strict();
|
|
141
|
-
const pageNavigation = z.object({
|
|
142
|
-
include: z.boolean().optional(),
|
|
143
|
-
label: z.string().optional(),
|
|
144
|
-
}).strict();
|
|
145
|
-
const themeNavigation = z.object({
|
|
146
|
-
brand: z.string().min(1).optional(),
|
|
147
|
-
logo: z.object({
|
|
148
|
-
alt: z.string().min(1).optional(),
|
|
149
|
-
height: visualCssLength.optional(),
|
|
150
|
-
}).strict().optional(),
|
|
151
|
-
}).strict();
|
|
152
|
-
|
|
153
|
-
const sectionMetadata = z.object({
|
|
154
|
-
visible: visibilityWindow.optional(),
|
|
155
|
-
}).strict();
|
|
156
|
-
const banner = z.object({
|
|
157
|
-
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
158
|
-
tone: z.enum(['warning']).default('warning'),
|
|
159
|
-
visible: visibilityWindow.optional(),
|
|
160
|
-
title: z.string().min(1),
|
|
161
|
-
text: z.string().min(1),
|
|
162
|
-
}).strict();
|
|
163
|
-
const banners = z.array(banner).optional().default([]).refine(
|
|
164
|
-
(values) => new Set(values.map((value) => value.id)).size === values.length,
|
|
165
|
-
'Banner ids must be unique.',
|
|
166
|
-
);
|
|
167
|
-
const dateTimeFormat = z.object({
|
|
168
|
-
locale: z.string().min(1),
|
|
169
|
-
timeZone: z.string().min(1),
|
|
170
|
-
dateStyle: z.string().min(1),
|
|
171
|
-
timeStyle: z.string().min(1),
|
|
172
|
-
}).strict().refine((value) => {
|
|
173
|
-
try {
|
|
174
|
-
new Intl.DateTimeFormat(value.locale, {
|
|
175
|
-
dateStyle: value.dateStyle as Intl.DateTimeFormatOptions['dateStyle'],
|
|
176
|
-
timeStyle: value.timeStyle as Intl.DateTimeFormatOptions['timeStyle'],
|
|
177
|
-
timeZone: value.timeZone,
|
|
178
|
-
});
|
|
179
|
-
return true;
|
|
180
|
-
} catch {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
}, 'Use a valid Intl.DateTimeFormat configuration.');
|
|
184
|
-
const sitewideFooter = z.object({
|
|
185
|
-
copyrightMessage: z.string().min(1).optional(),
|
|
186
|
-
buildInfo: z.object({
|
|
187
|
-
enabled: z.boolean().default(true),
|
|
188
|
-
text: z.string().min(1),
|
|
189
|
-
dateTimeFormat,
|
|
190
|
-
}).strict().optional(),
|
|
191
|
-
}).strict();
|
|
192
|
-
|
|
193
|
-
const siteSchema = z.object({
|
|
194
|
-
title: z.string(),
|
|
195
|
-
description: z.string(),
|
|
196
|
-
navigation: pageNavigation.optional(),
|
|
197
|
-
sections: z.record(z.string().regex(/^[a-z0-9-]+$/), sectionMetadata).optional().default({}),
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
export const themeVisualSchema = z.object({
|
|
201
|
-
preset: themePreset.optional(),
|
|
202
|
-
layout: themeLayout.optional(),
|
|
203
|
-
gallery: themeGallery.optional(),
|
|
204
|
-
typography: themeTypography.optional(),
|
|
205
|
-
presentation: themePresentation.optional(),
|
|
206
|
-
}).strict();
|
|
207
|
-
|
|
208
22
|
const siteThemeSchema = themeVisualSchema;
|
|
209
23
|
|
|
210
|
-
const sitewideSchema = z.object({
|
|
211
|
-
navigation: themeNavigation.optional(),
|
|
212
|
-
banners,
|
|
213
|
-
footer: sitewideFooter.optional(),
|
|
214
|
-
}).strict();
|
|
215
|
-
|
|
216
24
|
const site = defineCollection({
|
|
217
25
|
loader: glob({
|
|
218
|
-
pattern: ['content.md', 'routes/*/
|
|
26
|
+
pattern: ['content.md', 'routes/*/content.md'],
|
|
219
27
|
base: pathToFileURL(siteDir),
|
|
220
28
|
generateId: ({ entry }) => {
|
|
221
29
|
if (entry === 'content.md') {
|
|
222
30
|
return siteEntryId;
|
|
223
31
|
}
|
|
224
32
|
|
|
225
|
-
const routeDirectory = entry.match(/^routes\/([^/]+)\/
|
|
33
|
+
const routeDirectory = entry.match(/^routes\/([^/]+)\/content\.md$/)?.[1];
|
|
226
34
|
return routeDirectory
|
|
227
35
|
? `${siteEntryId.replace(/-content$/, '')}-route-${parseRouteDirectory(routeDirectory, `route directory routes/${routeDirectory}`).routeDirectory}`
|
|
228
36
|
: entry.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
@@ -233,16 +41,16 @@ const site = defineCollection({
|
|
|
233
41
|
|
|
234
42
|
const theme = defineCollection({
|
|
235
43
|
loader: glob({
|
|
236
|
-
pattern:
|
|
44
|
+
pattern: basename(siteThemePath),
|
|
237
45
|
base: pathToFileURL(siteDir),
|
|
238
|
-
generateId: (
|
|
46
|
+
generateId: () => `${siteEntryId.replace(/-content$/, '')}-theme`,
|
|
239
47
|
}),
|
|
240
48
|
schema: siteThemeSchema,
|
|
241
49
|
});
|
|
242
50
|
|
|
243
51
|
const sitewide = defineCollection({
|
|
244
52
|
loader: glob({
|
|
245
|
-
pattern:
|
|
53
|
+
pattern: basename(sitewideContentPath),
|
|
246
54
|
base: pathToFileURL(siteDir),
|
|
247
55
|
generateId: () => `${siteEntryId.replace(/-content$/, '')}-sitewide`,
|
|
248
56
|
}),
|
|
@@ -30,7 +30,7 @@ type VisualTheme = {
|
|
|
30
30
|
finalSectionBottom: { desktop: string; mobile: string };
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
|
-
|
|
33
|
+
images: {
|
|
34
34
|
width: string;
|
|
35
35
|
maxAvailableWidthPercent: { desktop: number; mobile: number };
|
|
36
36
|
maxAvailableHeightPercent: { desktop: number; mobile: number };
|
|
@@ -73,7 +73,7 @@ const {
|
|
|
73
73
|
const documentTitle = title;
|
|
74
74
|
const canonicalUrl = new URL(withBasePath(projectConfig.site.basePath, pathname), projectConfig.site.url).href;
|
|
75
75
|
const iconLinks = getIconLinks();
|
|
76
|
-
const scrollBehavior = projectConfig.navigation.scrollBehavior === '
|
|
76
|
+
const scrollBehavior = projectConfig.navigation.scrollBehavior === 'smooth' ? 'smooth' : 'auto';
|
|
77
77
|
const formatPercent = (value: number) => `${value}%`;
|
|
78
78
|
const formatViewportHeightPercent = (value: number) => `${value}svh`;
|
|
79
79
|
const layoutSpacing = visualTheme.layout.spacing;
|
|
@@ -90,17 +90,17 @@ const documentStyle = [
|
|
|
90
90
|
`--space-content-block-gap-mobile: ${layoutSpacing.blockGap.mobile}`,
|
|
91
91
|
`--space-heading-to-block-desktop: ${layoutSpacing.headingToBlock.desktop}`,
|
|
92
92
|
`--space-heading-to-block-mobile: ${layoutSpacing.headingToBlock.mobile}`,
|
|
93
|
-
`--space-
|
|
94
|
-
`--space-
|
|
93
|
+
`--space-image-item-desktop: ${layoutSpacing.imageGap.desktop}`,
|
|
94
|
+
`--space-image-item-mobile: ${layoutSpacing.imageGap.mobile}`,
|
|
95
95
|
`--space-section-to-section-desktop: ${layoutSpacing.sectionGap.desktop}`,
|
|
96
96
|
`--space-section-to-section-mobile: ${layoutSpacing.sectionGap.mobile}`,
|
|
97
97
|
`--space-final-section-bottom-desktop: ${layoutSpacing.finalSectionBottom.desktop}`,
|
|
98
98
|
`--space-final-section-bottom-mobile: ${layoutSpacing.finalSectionBottom.mobile}`,
|
|
99
|
-
`--
|
|
100
|
-
`--
|
|
101
|
-
`--
|
|
102
|
-
`--
|
|
103
|
-
`--
|
|
99
|
+
`--image-width: ${visualTheme.images.width}`,
|
|
100
|
+
`--image-max-available-width-desktop: ${formatPercent(visualTheme.images.maxAvailableWidthPercent.desktop)}`,
|
|
101
|
+
`--image-max-available-width-mobile: ${formatPercent(visualTheme.images.maxAvailableWidthPercent.mobile)}`,
|
|
102
|
+
`--image-max-height-desktop: ${formatViewportHeightPercent(visualTheme.images.maxAvailableHeightPercent.desktop)}`,
|
|
103
|
+
`--image-max-height-mobile: ${formatViewportHeightPercent(visualTheme.images.maxAvailableHeightPercent.mobile)}`,
|
|
104
104
|
].join('; ');
|
|
105
105
|
const { buildInfo, copyrightMessage } = footer ?? {};
|
|
106
106
|
const showBuildInfo = Boolean(buildInfo?.enabled);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getGeneratedImage } from './generatedImages';
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type ManagedImage = {
|
|
4
4
|
image: string;
|
|
5
5
|
src?: string;
|
|
6
6
|
alt?: string;
|
|
@@ -16,24 +16,24 @@ const hasDimensions = (image: ReturnType<typeof getGeneratedImage>): image is Ge
|
|
|
16
16
|
Boolean(image) && Number.isFinite(image.width) && Number.isFinite(image.height)
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
-
export const getCaptionId = (image:
|
|
19
|
+
export const getCaptionId = (image: ManagedImage, itemIndex: number, imageIndex = 0) => {
|
|
20
20
|
const source = (image.src ?? image.image)
|
|
21
21
|
.toLowerCase()
|
|
22
22
|
.replace(/[^a-z0-9]+/g, '-')
|
|
23
23
|
.replace(/^-|-$/g, '');
|
|
24
24
|
|
|
25
|
-
return `
|
|
25
|
+
return `image-caption-${source}-${itemIndex}-${imageIndex}`;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export const
|
|
28
|
+
export const getManagedImageSizes = (maxAvailableWidthPercent: { desktop: number; mobile: number }) => [
|
|
29
29
|
`(max-width: 700px) ${maxAvailableWidthPercent.mobile}vw`,
|
|
30
30
|
`${maxAvailableWidthPercent.desktop}vw`,
|
|
31
31
|
].join(', ');
|
|
32
32
|
|
|
33
|
-
export const getImageClass = (image:
|
|
33
|
+
export const getImageClass = (image: ManagedImage) => {
|
|
34
34
|
const generatedImage = getGeneratedImage(image.src ?? image.image);
|
|
35
35
|
return hasDimensions(generatedImage) && generatedImage.width > generatedImage.height
|
|
36
|
-
? '
|
|
36
|
+
? 'managed-image-landscape'
|
|
37
37
|
: undefined;
|
|
38
38
|
};
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ export const getImageLoadingProps = (isPriority: boolean) => ({
|
|
|
43
43
|
loading: isPriority ? 'eager' as const : 'lazy' as const,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
export const getCarouselAspectRatio = (images:
|
|
46
|
+
export const getCarouselAspectRatio = (images: ManagedImage[]) => {
|
|
47
47
|
const ratios = images
|
|
48
48
|
.map((image) => getGeneratedImage(image.src ?? image.image))
|
|
49
49
|
.filter(hasDimensions)
|
package/src/lib/routeThemes.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { parseYamlMapping } from '../../scripts/lib/frontmatter-yaml.mjs';
|
|
4
3
|
import {
|
|
5
|
-
|
|
6
|
-
validateFrontmatterIndentation,
|
|
7
|
-
validateRouteThemeFrontmatterStructure,
|
|
4
|
+
validateRouteThemeYamlStructure,
|
|
8
5
|
} from '../../scripts/lib/site-content.mjs';
|
|
9
6
|
import { siteRoutesDir, siteRoutesLabel, sitewideContentLabel } from '../../scripts/lib/site-paths.mjs';
|
|
10
|
-
import {
|
|
7
|
+
import { parseYamlConfig } from '../../scripts/lib/yaml-config.mjs';
|
|
11
8
|
|
|
12
9
|
type RouteTheme = {
|
|
13
10
|
id: string;
|
|
@@ -20,40 +17,23 @@ export const getRouteTheme = async (routeDirectory: string | null): Promise<Rout
|
|
|
20
17
|
const segments = routeDirectory.split(/[\\/]/).filter(Boolean);
|
|
21
18
|
for (let length = segments.length; length > 0; length -= 1) {
|
|
22
19
|
const themeSegments = segments.slice(0, length);
|
|
23
|
-
const themePath = path.join(siteRoutesDir, ...themeSegments, 'theme.
|
|
24
|
-
const themeLabel = `${siteRoutesLabel}/${themeSegments.join('/')}/theme.md`;
|
|
20
|
+
const themePath = path.join(siteRoutesDir, ...themeSegments, 'theme.yaml');
|
|
25
21
|
const source = await readFile(themePath, 'utf8').catch((error) => {
|
|
26
22
|
if (error?.code === 'ENOENT') return null;
|
|
27
23
|
throw error;
|
|
28
24
|
});
|
|
29
25
|
if (!source) continue;
|
|
26
|
+
const themeLabel = `${siteRoutesLabel}/${themeSegments.join('/')}/theme.yaml`;
|
|
30
27
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
validateRouteThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
35
|
-
if (issues.length > 0) {
|
|
36
|
-
throw new Error([
|
|
37
|
-
`${themeLabel} has invalid frontmatter.`,
|
|
38
|
-
...issues.map((issue) => `- ${issue.message}`),
|
|
39
|
-
].join('\n'));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const data = parseYamlMapping(frontmatterBody);
|
|
28
|
+
const data = parseYamlConfig(source, themeLabel, {
|
|
29
|
+
validateStructure: validateRouteThemeYamlStructure,
|
|
30
|
+
});
|
|
43
31
|
if (Object.hasOwn(data, 'navigation')) {
|
|
44
|
-
throw new Error(`${themeLabel} may not define navigation.
|
|
32
|
+
throw new Error(`${themeLabel} may not define navigation. The site label and logo settings belong in ${sitewideContentLabel}.`);
|
|
45
33
|
}
|
|
46
|
-
const parsed = themeVisualSchema.safeParse(data);
|
|
47
|
-
if (!parsed.success) {
|
|
48
|
-
throw new Error([
|
|
49
|
-
`${themeLabel} has invalid values.`,
|
|
50
|
-
...parsed.error.issues.map((issue) => `- ${issue.path.join('.') || '(root)'}: ${issue.message}`),
|
|
51
|
-
].join('\n'));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
34
|
return {
|
|
55
35
|
id: `routes/${themeSegments.join('/')}/theme`,
|
|
56
|
-
data
|
|
36
|
+
data,
|
|
57
37
|
};
|
|
58
38
|
}
|
|
59
39
|
|
|
@@ -12,7 +12,7 @@ import type { SitePage } from './sitePages';
|
|
|
12
12
|
|
|
13
13
|
type SiteSectionMetadataMap = CollectionEntry<'site'>['data']['sections'];
|
|
14
14
|
type SiteSectionMetadata = SiteSectionMetadataMap[string];
|
|
15
|
-
type
|
|
15
|
+
type ManagedImage = {
|
|
16
16
|
image: string;
|
|
17
17
|
src: string;
|
|
18
18
|
alt?: string;
|
|
@@ -38,8 +38,8 @@ type InlineNote = {
|
|
|
38
38
|
};
|
|
39
39
|
type SectionContentBlock =
|
|
40
40
|
| { type: 'html'; html: string }
|
|
41
|
-
| { type: 'image-stack'; images:
|
|
42
|
-
| { type: 'image-carousel'; images:
|
|
41
|
+
| { type: 'image-stack'; images: ManagedImage[] }
|
|
42
|
+
| { type: 'image-carousel'; images: ManagedImage[] }
|
|
43
43
|
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width: CardListWidth; cards: CardListItem[] }
|
|
44
44
|
| { type: 'note'; html: string; number?: number; id?: string; referenceId?: string };
|
|
45
45
|
export type ResolvedSection = SiteSectionMetadata & {
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { browserIconAssetDefinitions } from '../../scripts/lib/public-asset-conventions.mjs';
|
|
4
5
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
5
6
|
export { getLogoAsset } from '../../scripts/lib/logo-assets.mjs';
|
|
6
7
|
import { withBasePath } from './basePath';
|
|
@@ -12,27 +13,10 @@ type IconLink = {
|
|
|
12
13
|
sizes?: string;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
const faviconCandidates: IconLink[] =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type: 'image/svg+xml',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
rel: 'icon',
|
|
23
|
-
href: '/favicon.ico',
|
|
24
|
-
sizes: 'any',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
rel: 'icon',
|
|
28
|
-
href: '/favicon.png',
|
|
29
|
-
type: 'image/png',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
rel: 'apple-touch-icon',
|
|
33
|
-
href: '/apple-touch-icon.png',
|
|
34
|
-
},
|
|
35
|
-
];
|
|
16
|
+
const faviconCandidates: IconLink[] = browserIconAssetDefinitions.map(({ filename, ...definition }) => ({
|
|
17
|
+
...definition,
|
|
18
|
+
href: `/${filename}`,
|
|
19
|
+
}));
|
|
36
20
|
|
|
37
21
|
const publicAssetExists = (href: string) => {
|
|
38
22
|
const relativePath = href.replace(/^\//, '');
|