@janga/norna 0.7.16 → 0.7.17
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 +6 -6
- package/astro.config.mjs +35 -3
- package/docs/README.md +10 -10
- package/docs/commands.md +2 -2
- package/docs/content.md +82 -102
- package/docs/design/navigation-and-theme-plan.md +115 -0
- package/docs/design/norna-diagram-design.md +9 -9
- package/docs/design/site-examples-structure.md +9 -9
- package/docs/engine-development.md +9 -9
- package/docs/getting-started.md +20 -15
- package/docs/images-and-metadata.md +10 -9
- package/docs/pages.md +134 -0
- package/docs/public-files.md +49 -36
- package/docs/publishing.md +19 -3
- package/docs/site-files.md +29 -28
- package/docs/sitewide-content.md +20 -36
- package/docs/theme.md +20 -20
- package/docs/typography.md +10 -10
- package/examples/README.md +2 -2
- package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
- package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
- package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
- package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
- package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
- package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
- package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
- package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
- package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
- package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
- package/examples/complete-sites/dog-shelter-multi-page/site/content.md +3 -4
- package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
- package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
- package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
- package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
- package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
- package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
- package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
- package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
- package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
- package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
- package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
- package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
- package/examples/complete-sites/dog-shelter-single-page/site/content.md +5 -2
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
- package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
- package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
- package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +185 -88
- package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
- package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
- package/examples/feature-demos/media-and-surfaces/README.md +2 -2
- package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
- package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
- package/examples/feature-demos/media-and-surfaces/site/content.md +4 -7
- package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
- package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +7 -6
- package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
- package/examples/feature-demos/sitewide-content/README.md +3 -3
- package/examples/feature-demos/sitewide-content/site/content.md +8 -7
- package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
- package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
- package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
- package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
- package/examples/feature-demos/theme-presets/README.md +1 -1
- package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +60 -60
- package/examples/feature-demos/theme-presets/site/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +1 -10
- package/fixtures/basic/site/content.md +5 -6
- package/fixtures/basic/site/sitewide-content.yaml +1 -8
- package/fixtures/content-model-v2/site/content.md +11 -13
- package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
- package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
- package/fixtures/nested-pages/site/config.yaml +1 -0
- package/fixtures/nested-pages/site/content.md +15 -0
- package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +14 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
- package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +3 -0
- package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
- package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
- package/fixtures/nested-pages/site/theme.yaml +3 -0
- package/package.json +7 -4
- package/schemas/config.schema.json +5 -5
- package/schemas/content-frontmatter.schema.json +28 -63
- package/schemas/manifest.json +1 -0
- package/schemas/sitewide-content.schema.json +58 -132
- package/schemas/theme.schema.json +396 -86
- package/scripts/check-config.mjs +6 -5
- package/scripts/deploy-site.mjs +6 -8
- package/scripts/doctor.mjs +2 -2
- package/scripts/generate-images.mjs +11 -18
- package/scripts/generate-schemas.mjs +3 -217
- package/scripts/lib/base-path-redirect.mjs +9 -0
- package/scripts/lib/editor-language-service.mjs +126 -66
- package/scripts/lib/heading-ids.mjs +161 -0
- package/scripts/lib/navigation-model.mjs +51 -0
- package/scripts/lib/norna-markdown-blocks.mjs +3 -4
- package/scripts/lib/page-model.mjs +83 -0
- package/scripts/lib/project-config.mjs +28 -0
- package/scripts/lib/schema-definitions.mjs +41 -46
- package/scripts/lib/schema-editor-metadata.mjs +475 -0
- package/scripts/lib/schema-value-definitions.mjs +6 -6
- package/scripts/lib/site-content.mjs +124 -87
- package/scripts/lib/site-paths.mjs +2 -2
- package/scripts/lib/theme-config.mjs +9 -16
- package/scripts/lib/theme-presets.mjs +9 -1
- package/scripts/release.mjs +1 -1
- package/scripts/show-typography.mjs +26 -20
- package/scripts/sync-content-sections.mjs +106 -65
- package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +7 -24
- package/scripts/test-client-javascript.mjs +39 -42
- package/scripts/test-content-check.mjs +138 -54
- package/scripts/test-content-model-v2.mjs +445 -228
- package/scripts/test-documentation.mjs +6 -1
- package/scripts/test-editor-language-service.mjs +168 -24
- package/scripts/test-engine-commands.mjs +2 -1
- package/scripts/test-heading-ids.mjs +68 -0
- package/scripts/test-navigation-model.mjs +63 -0
- package/scripts/test-nested-pages.mjs +110 -0
- package/scripts/test-package-check.mjs +31 -40
- package/scripts/test-page-model.mjs +80 -0
- package/scripts/test-project-config.mjs +25 -1
- package/scripts/test-schemas.mjs +66 -14
- package/scripts/test-theme-presets.mjs +33 -24
- package/src/components/NavigationPageTree.astro +68 -0
- package/src/components/SectionNavigationScript.astro +110 -5
- package/src/components/SiteBreadcrumbs.astro +29 -0
- package/src/components/SiteNavigation.astro +65 -69
- package/src/components/SitePage.astro +70 -46
- package/src/components/SiteSection.astro +27 -20
- package/src/content.config.ts +9 -5
- package/src/layouts/BaseLayout.astro +11 -20
- package/src/lib/pageThemes.ts +42 -0
- package/src/lib/sectionContent.ts +109 -106
- package/src/lib/siteNavigation.ts +48 -0
- package/src/lib/sitePages.ts +85 -38
- package/src/pages/{[slug].astro → [...slug].astro} +2 -2
- package/src/styles/global.css +373 -55
- package/starters/basic/README.md +5 -7
- package/starters/basic/site/content.md +7 -4
- package/starters/basic/site/sitewide-content.yaml +1 -10
- package/starters/project/README.md +8 -8
- package/starters/project/site/content.md +5 -4
- package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
- package/starters/project/site/sitewide-content.yaml +1 -10
- package/docs/routes.md +0 -121
- package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
- package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
- package/scripts/lib/route-model.mjs +0 -17
- package/scripts/test-route-model.mjs +0 -32
- package/src/lib/routeThemes.ts +0 -41
- /package/examples/complete-sites/dog-shelter-multi-page/site/images/{intro/shelter-dog.jpg → shelter-dog.jpg} +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
- /package/examples/complete-sites/dog-shelter-single-page/site/images/{you-can-help/foster-dog.jpg → foster-dog.jpg} +0 -0
- /package/examples/complete-sites/dog-shelter-single-page/site/images/{what-we-do/rescue-dog.jpg → rescue-dog.jpg} +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/puppy-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/shepherd-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/white-puppy.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/black-schnauzer.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/retriever-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/schnauzer-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/boxer-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grass-puppy.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grey-street-dog.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/black-pedigree.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-grass.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-wide.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/theme.yaml +0 -0
- /package/fixtures/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
- /package/fixtures/content-model-v2/site/images/{card-section/adopt.svg → adopt.svg} +0 -0
- /package/fixtures/content-model-v2/site/images/{intro/detail.jpg → detail.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{intro/duplicate.jpg → duplicate.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{card-section/foster.svg → foster.svg} +0 -0
- /package/fixtures/content-model-v2/site/images/{intro/hero.jpg → hero.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{plain/image.jpg → image.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{timed/slide-one.jpg → slide-one.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{timed/slide-three.jpg → slide-three.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{timed/slide-two.jpg → slide-two.jpg} +0 -0
- /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
- /package/starters/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
package/scripts/check-config.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { siteConfigLabel, sitePublicLabel } from './lib/site-paths.mjs';
|
|
|
2
2
|
import { getLogoAssets, getPublicAssetInspection } from './lib/logo-assets.mjs';
|
|
3
3
|
import { logoAssetFilenames } from './lib/public-asset-conventions.mjs';
|
|
4
4
|
import { readSitewideContent } from './lib/sitewide-content.mjs';
|
|
5
|
-
import { readThemeConfig,
|
|
5
|
+
import { readThemeConfig, validatePageThemeFiles } from './lib/theme-config.mjs';
|
|
6
6
|
|
|
7
7
|
const formatErrorMessage = (error) => {
|
|
8
8
|
if (error instanceof Error) {
|
|
@@ -16,7 +16,7 @@ try {
|
|
|
16
16
|
const { projectConfig } = await import('./lib/project-config.mjs');
|
|
17
17
|
const themeConfig = await readThemeConfig();
|
|
18
18
|
const sitewideContent = await readSitewideContent();
|
|
19
|
-
await
|
|
19
|
+
await validatePageThemeFiles();
|
|
20
20
|
const logoAssets = getLogoAssets();
|
|
21
21
|
const publicAssetInspection = getPublicAssetInspection();
|
|
22
22
|
const logoAssetPaths = logoAssetFilenames.map((filename) => `${sitePublicLabel}/${filename}`);
|
|
@@ -32,10 +32,10 @@ try {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (logoAssets.length === 0) {
|
|
35
|
-
if (sitewideContent.
|
|
36
|
-
throw new Error(`Site-wide
|
|
35
|
+
if (sitewideContent.logo) {
|
|
36
|
+
throw new Error(`Site-wide logo is configured, but no logo file was found. Add exactly one of ${logoAssetPaths.join(', ')}, or remove logo.`);
|
|
37
37
|
}
|
|
38
|
-
console.warn(`Warning: No logo file found in ${sitePublicLabel}. Norna will use
|
|
38
|
+
console.warn(`Warning: No logo file found in ${sitePublicLabel}. Norna will use page titles for navigation.`);
|
|
39
39
|
console.warn(`Add exactly one of ${logoAssetPaths.join(', ')} when the site should have a logo.`);
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -51,6 +51,7 @@ try {
|
|
|
51
51
|
console.log(`Image max available height: desktop ${projectConfig.images.maxAvailableHeightPercent.desktop}%, mobile ${projectConfig.images.maxAvailableHeightPercent.mobile}%`);
|
|
52
52
|
console.log(`Font family: ${projectConfig.typography.fontFamily}`);
|
|
53
53
|
console.log(`Language: ${projectConfig.locale.lang}`);
|
|
54
|
+
console.log(`Navigation mode: ${projectConfig.navigation.mode}`);
|
|
54
55
|
console.log(`Scroll behavior: ${projectConfig.navigation.scrollBehavior}`);
|
|
55
56
|
} catch (error) {
|
|
56
57
|
console.error('Config check failed.');
|
package/scripts/deploy-site.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
siteContentLabel,
|
|
22
22
|
siteImagesLabel,
|
|
23
23
|
sitePublicLabel,
|
|
24
|
-
|
|
24
|
+
sitePagesLabel,
|
|
25
25
|
siteThemeLabel,
|
|
26
26
|
} from './lib/site-paths.mjs';
|
|
27
27
|
|
|
@@ -150,15 +150,13 @@ const getExpectedImagePaths = async () => {
|
|
|
150
150
|
for (const contentFile of contentFiles) {
|
|
151
151
|
const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
|
|
152
152
|
|
|
153
|
-
for (const section of getBodySections(body).sections) {
|
|
154
|
-
if (!section.id) continue;
|
|
155
|
-
|
|
153
|
+
for (const section of (await getBodySections(body)).sections) {
|
|
156
154
|
const blocks = extractNornaMarkdownBlocks(section.text, { label: contentFile.contentLabel });
|
|
157
155
|
for (const { image } of getNornaBlockImageReferences(blocks)) {
|
|
158
156
|
if (image.includes('/') || image.includes('\\')) continue;
|
|
159
157
|
if (!supportedImageExtensions.has(path.extname(image).toLowerCase())) continue;
|
|
160
158
|
|
|
161
|
-
imagePaths.add(toPosixPath(path.join(contentFile.imagesLabel,
|
|
159
|
+
imagePaths.add(toPosixPath(path.join(contentFile.imagesLabel, image)));
|
|
162
160
|
}
|
|
163
161
|
}
|
|
164
162
|
}
|
|
@@ -174,7 +172,7 @@ const isExpectedUntracked = (entry, expectedImagePaths) => (
|
|
|
174
172
|
expectedImagePaths.has(entry.path)
|
|
175
173
|
|| entry.path === generatedImagesManifestLabel
|
|
176
174
|
|| entry.path.startsWith(`${sitePublicLabel}/`)
|
|
177
|
-
|| entry.path.startsWith(`${
|
|
175
|
+
|| entry.path.startsWith(`${sitePagesLabel}/`)
|
|
178
176
|
)
|
|
179
177
|
);
|
|
180
178
|
|
|
@@ -182,7 +180,7 @@ const isAllowedPath = (entry, filePath, expectedImagePaths) => (
|
|
|
182
180
|
filePath === siteContentLabel
|
|
183
181
|
|| filePath === siteThemeLabel
|
|
184
182
|
|| (!isUntracked(entry) && filePath.startsWith(`${siteImagesLabel}/`))
|
|
185
|
-
|| filePath.startsWith(`${
|
|
183
|
+
|| filePath.startsWith(`${sitePagesLabel}/`)
|
|
186
184
|
|| filePath.startsWith(`${sitePublicLabel}/`)
|
|
187
185
|
|| expectedImagePaths.has(filePath)
|
|
188
186
|
|| filePath.startsWith('src/')
|
|
@@ -213,7 +211,7 @@ const assertDeployableStatus = async (entries, expectedImagePaths) => {
|
|
|
213
211
|
if (unexpectedUntracked.length > 0) {
|
|
214
212
|
fail([
|
|
215
213
|
'Refusing to deploy: unexpected untracked files are present.',
|
|
216
|
-
`Only new referenced images under ${siteImagesLabel}
|
|
214
|
+
`Only new referenced images directly under ${siteImagesLabel}/ or ${sitePagesLabel}/<NNN-page-id>/images/ are staged automatically.`,
|
|
217
215
|
...unexpectedUntracked.map((entry) => `- ${formatEntry(entry)}`),
|
|
218
216
|
].join('\n'));
|
|
219
217
|
}
|
package/scripts/doctor.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
siteImagesDir,
|
|
13
13
|
siteProjectRoot,
|
|
14
14
|
sitePublicDir,
|
|
15
|
-
|
|
15
|
+
sitePagesDir,
|
|
16
16
|
siteThemePath,
|
|
17
17
|
} from './lib/site-paths.mjs';
|
|
18
18
|
|
|
@@ -26,7 +26,7 @@ const lines = [
|
|
|
26
26
|
`Site theme: ${siteThemePath}`,
|
|
27
27
|
`Site content: ${siteContentPath}`,
|
|
28
28
|
`Site images: ${siteImagesDir}`,
|
|
29
|
-
`Site
|
|
29
|
+
`Site pages: ${sitePagesDir}`,
|
|
30
30
|
`Site public source: ${sitePublicDir}`,
|
|
31
31
|
`Generated image manifest: ${generatedImagesManifestPath}`,
|
|
32
32
|
`Astro public output: ${astroPublicDir}`,
|
|
@@ -104,22 +104,18 @@ const fail = (message) => {
|
|
|
104
104
|
throw new Error(message);
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
const getImageSourceKey = (contentFile,
|
|
107
|
+
const getImageSourceKey = (contentFile, image) => (
|
|
108
108
|
contentFile.isHome
|
|
109
|
-
? `images/${
|
|
110
|
-
: `
|
|
109
|
+
? `images/${image}`
|
|
110
|
+
: `pages/${contentFile.pageDirectory}/images/${image}`
|
|
111
111
|
);
|
|
112
112
|
|
|
113
113
|
const getReferencedImages = async (contentFile) => {
|
|
114
114
|
const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
|
|
115
|
-
const { sections } = getBodySections(body);
|
|
115
|
+
const { sections } = await getBodySections(body);
|
|
116
116
|
const references = [];
|
|
117
117
|
|
|
118
118
|
for (const section of sections) {
|
|
119
|
-
if (!section.id) {
|
|
120
|
-
fail(`${contentFile.contentLabel}: section heading "${section.heading}" is missing an explicit id.`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
119
|
const blocks = extractNornaMarkdownBlocks(section.text, { label: contentFile.contentLabel });
|
|
124
120
|
for (const { image, line, blockDisplayType } of getNornaBlockImageReferences(blocks)) {
|
|
125
121
|
references.push({
|
|
@@ -127,8 +123,8 @@ const getReferencedImages = async (contentFile) => {
|
|
|
127
123
|
image,
|
|
128
124
|
line,
|
|
129
125
|
blockDisplayType,
|
|
130
|
-
|
|
131
|
-
sourceKey: getImageSourceKey(contentFile,
|
|
126
|
+
sectionLabel: section.id ?? 'page title',
|
|
127
|
+
sourceKey: getImageSourceKey(contentFile, image),
|
|
132
128
|
});
|
|
133
129
|
}
|
|
134
130
|
}
|
|
@@ -171,9 +167,8 @@ const getReferencedSources = async () => {
|
|
|
171
167
|
fail(`Image file does not exist: ${contentFile.imagesLabel}/${siteImagePath}`);
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
fail(`Image "${imageName}" is used in section "${reference.sectionId}" but is located in ${contentFile.imagesLabel}/${currentDirectory}/. Run norna content:sync, or npm run norna:sync in starter-style repositories, to move it.`);
|
|
170
|
+
if (path.dirname(sourcePath) !== contentFile.imagesDir) {
|
|
171
|
+
fail(`Image "${imageName}" is used in ${reference.sectionLabel} but is not stored directly in ${contentFile.imagesLabel}/. Run norna content:sync, or npm run norna:sync in starter-style repositories, to move it.`);
|
|
177
172
|
}
|
|
178
173
|
|
|
179
174
|
seen.add(reference.sourceKey);
|
|
@@ -194,16 +189,14 @@ const validateCarouselOrientations = async (manifest) => {
|
|
|
194
189
|
|
|
195
190
|
for (const contentFile of contentFiles) {
|
|
196
191
|
const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
|
|
197
|
-
const { sections } = getBodySections(body);
|
|
192
|
+
const { sections } = await getBodySections(body);
|
|
198
193
|
|
|
199
194
|
for (const section of sections) {
|
|
200
|
-
if (!section.id) continue;
|
|
201
|
-
|
|
202
195
|
const blocks = extractNornaMarkdownBlocks(section.text, { label: contentFile.contentLabel });
|
|
203
196
|
for (const block of blocks.filter((candidate) => candidate.type === 'image-carousel')) {
|
|
204
197
|
const orientations = new Set(
|
|
205
198
|
block.images
|
|
206
|
-
.map((image) => manifest[getImageSourceKey(contentFile,
|
|
199
|
+
.map((image) => manifest[getImageSourceKey(contentFile, image.image)])
|
|
207
200
|
.filter(Boolean)
|
|
208
201
|
.filter((entry) => Number.isFinite(entry.width) && Number.isFinite(entry.height))
|
|
209
202
|
.map(getOrientation)
|
|
@@ -212,7 +205,7 @@ const validateCarouselOrientations = async (manifest) => {
|
|
|
212
205
|
orientations.delete('square');
|
|
213
206
|
if (orientations.size > 1) {
|
|
214
207
|
throw new Error(
|
|
215
|
-
`Carousel in section "${section.id}" on line ${block.line} mixes landscape and portrait images. Use landscape images with optional square images, or portrait images with optional square images.`,
|
|
208
|
+
`Carousel in ${section.id ? `section "${section.id}"` : 'the page introduction'} on line ${block.line} mixes landscape and portrait images. Use landscape images with optional square images, or portrait images with optional square images.`,
|
|
216
209
|
);
|
|
217
210
|
}
|
|
218
211
|
}
|
|
@@ -8,8 +8,7 @@ import {
|
|
|
8
8
|
sitewideSchema,
|
|
9
9
|
themeVisualSchema,
|
|
10
10
|
} from './lib/schema-definitions.mjs';
|
|
11
|
-
import {
|
|
12
|
-
import { getSchemaValueDefinition } from './lib/schema-value-definitions.mjs';
|
|
11
|
+
import { applySchemaEditorMetadata } from './lib/schema-editor-metadata.mjs';
|
|
13
12
|
|
|
14
13
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
14
|
const schemaDirectory = path.join(root, 'schemas');
|
|
@@ -19,231 +18,18 @@ const definitions = [
|
|
|
19
18
|
['sitewide-content.schema.json', 'Norna site-wide content', sitewideSchema],
|
|
20
19
|
['content-frontmatter.schema.json', 'Norna page frontmatter', siteSchema],
|
|
21
20
|
];
|
|
22
|
-
const yamlExample = (source) => `\`\`\`yaml\n${source}\n\`\`\``;
|
|
23
|
-
|
|
24
|
-
const schemaProperty = (schema, propertyPath) => {
|
|
25
|
-
let current = schema;
|
|
26
|
-
for (const name of propertyPath.split('.')) {
|
|
27
|
-
current = current?.properties?.[name];
|
|
28
|
-
if (!current) throw new Error(`Generated schema has no property at ${propertyPath}.`);
|
|
29
|
-
}
|
|
30
|
-
return current;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const addHelp = (schema, propertyPath, paragraphs, examples = []) => {
|
|
34
|
-
const property = schemaProperty(schema, propertyPath);
|
|
35
|
-
property.markdownDescription = paragraphs.join('\n\n');
|
|
36
|
-
if (examples.length > 0) property.examples = examples;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const getLiteralValues = (schema) => {
|
|
40
|
-
if (Array.isArray(schema.enum)) return schema.enum;
|
|
41
|
-
if (
|
|
42
|
-
Array.isArray(schema.anyOf)
|
|
43
|
-
&& schema.anyOf.length > 0
|
|
44
|
-
&& schema.anyOf.every((candidate) => Object.hasOwn(candidate, 'const'))
|
|
45
|
-
) {
|
|
46
|
-
return schema.anyOf.map((candidate) => candidate.const);
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const addValueDescriptions = (schema) => {
|
|
52
|
-
if (!schema || typeof schema !== 'object') return;
|
|
53
|
-
if (Array.isArray(schema)) {
|
|
54
|
-
for (const value of schema) addValueDescriptions(value);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const values = getLiteralValues(schema);
|
|
59
|
-
const definition = values ? getSchemaValueDefinition(values) : null;
|
|
60
|
-
if (definition) {
|
|
61
|
-
delete schema.enum;
|
|
62
|
-
delete schema.anyOf;
|
|
63
|
-
schema.oneOf = values.map((value) => ({
|
|
64
|
-
const: value,
|
|
65
|
-
title: definition.options[value].title,
|
|
66
|
-
description: definition.options[value].description,
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
for (const value of Object.values(schema)) addValueDescriptions(value);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const addLanguageSuggestions = (jsonSchema) => {
|
|
74
|
-
const language = jsonSchema.properties?.language;
|
|
75
|
-
if (!language) throw new Error('Generated config schema has no language property.');
|
|
76
|
-
const definition = getSchemaValueDefinition(['en', 'sv']);
|
|
77
|
-
language.default = 'en';
|
|
78
|
-
language.examples = ['en', 'sv', 'en-GB', 'sv-SE'];
|
|
79
|
-
language.oneOf = [
|
|
80
|
-
...definition.values.map((value) => ({
|
|
81
|
-
const: value,
|
|
82
|
-
title: definition.options[value].title,
|
|
83
|
-
description: definition.options[value].description,
|
|
84
|
-
})),
|
|
85
|
-
{
|
|
86
|
-
type: 'string',
|
|
87
|
-
pattern: '^(?:en|sv)-[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$',
|
|
88
|
-
title: 'Regional language tag',
|
|
89
|
-
description: 'Use an English or Swedish regional language tag such as en-GB or sv-SE.',
|
|
90
|
-
},
|
|
91
|
-
];
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const addSitewideNavigationHelp = (jsonSchema) => {
|
|
95
|
-
const navigation = jsonSchema.properties?.navigation;
|
|
96
|
-
const label = navigation?.properties?.label;
|
|
97
|
-
const logo = navigation?.properties?.logo;
|
|
98
|
-
if (!navigation || !label || !logo) throw new Error('Generated site-wide schema has no navigation label/logo properties.');
|
|
99
|
-
|
|
100
|
-
navigation.markdownDescription = [
|
|
101
|
-
yamlExample('navigation:\n label: Example Site'),
|
|
102
|
-
'`navigation` defines the identity shared by the homepage and every route. Routes cannot replace it.',
|
|
103
|
-
documentationLink('Navigation identity reference', 'sitewide-content.md', 'navigation-identity'),
|
|
104
|
-
].join('\n\n');
|
|
105
|
-
label.markdownDescription = [
|
|
106
|
-
yamlExample('navigation:\n label: Example Site'),
|
|
107
|
-
'- Without a logo, this text is shown in the navigation.',
|
|
108
|
-
'- With a logo, this text becomes the image alternative text.',
|
|
109
|
-
'- When omitted, Norna uses the homepage title.',
|
|
110
|
-
documentationLink('Navigation identity reference', 'sitewide-content.md', 'navigation-identity'),
|
|
111
|
-
].join('\n\n');
|
|
112
|
-
label.examples = ['Example Site'];
|
|
113
|
-
logo.markdownDescription = [
|
|
114
|
-
yamlExample('navigation:\n label: Example Site\n logo:\n height: 2rem'),
|
|
115
|
-
'`logo` optionally overrides the displayed height of the logo file that Norna discovers automatically. It does not enable or select the file.',
|
|
116
|
-
documentationLink('Navigation logo filenames and placement', 'public-files.md', 'navigation-logo'),
|
|
117
|
-
].join('\n\n');
|
|
118
|
-
logo.properties.height.examples = ['2rem'];
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const addConfigHelp = (jsonSchema) => {
|
|
122
|
-
jsonSchema.markdownDescription = [
|
|
123
|
-
yamlExample('url: https://example.com/'),
|
|
124
|
-
'`config.yaml` contains the few technical settings shared by the complete site.',
|
|
125
|
-
documentationLink('Configuration reference', 'configuration.md'),
|
|
126
|
-
].join('\n\n');
|
|
127
|
-
addHelp(jsonSchema, 'url', [
|
|
128
|
-
yamlExample('url: https://example.com/'),
|
|
129
|
-
'The absolute public URL. Its path also determines the base path for generated links and assets.',
|
|
130
|
-
documentationLink('Public URL reference', 'configuration.md', 'url'),
|
|
131
|
-
], ['https://example.com/', 'https://owner.github.io/repository-name/']);
|
|
132
|
-
addHelp(jsonSchema, 'language', [
|
|
133
|
-
yamlExample('language: en-GB'),
|
|
134
|
-
'Sets the page language and selects Norna\'s built-in English or Swedish interface text. The default is `en`.',
|
|
135
|
-
documentationLink('Language reference', 'configuration.md', 'language'),
|
|
136
|
-
], ['en', 'sv', 'en-GB', 'sv-SE']);
|
|
137
|
-
addHelp(jsonSchema, 'scrollBehavior', [
|
|
138
|
-
yamlExample('scrollBehavior: smooth'),
|
|
139
|
-
'Controls same-page anchor movement. `instant` is the default; `smooth` uses the browser\'s native smooth scrolling.',
|
|
140
|
-
documentationLink('Scroll behavior reference', 'configuration.md', 'scrollbehavior'),
|
|
141
|
-
], ['instant', 'smooth']);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const addThemeHelp = (jsonSchema) => {
|
|
145
|
-
jsonSchema.markdownDescription = [
|
|
146
|
-
yamlExample('preset: documentation'),
|
|
147
|
-
'Choose one complete preset first. Add focused overrides only when the site needs to differ from it.',
|
|
148
|
-
documentationLink('Theme reference', 'theme.md'),
|
|
149
|
-
].join('\n\n');
|
|
150
|
-
addHelp(jsonSchema, 'preset', [
|
|
151
|
-
yamlExample('preset: documentation'),
|
|
152
|
-
'Selects coordinated layout, image sizing, typography, palette and section surfaces. This is the normal starting point for a theme.',
|
|
153
|
-
documentationLink('Compare theme presets', 'theme.md', 'theme-presets'),
|
|
154
|
-
]);
|
|
155
|
-
addHelp(jsonSchema, 'layout', [
|
|
156
|
-
yamlExample('layout:\n density: compact\n pageWidth: 72rem'),
|
|
157
|
-
'Overrides page width, gutters and structural spacing after the selected preset.',
|
|
158
|
-
documentationLink('Layout reference', 'theme.md', 'layout'),
|
|
159
|
-
]);
|
|
160
|
-
addHelp(jsonSchema, 'images', [
|
|
161
|
-
yamlExample('images:\n width: 900px\n maxAvailableHeightPercent: 74'),
|
|
162
|
-
'Overrides the available width and height used by Norna-managed images.',
|
|
163
|
-
documentationLink('Image sizing reference', 'theme.md', 'image-sizing'),
|
|
164
|
-
]);
|
|
165
|
-
addHelp(jsonSchema, 'typography', [
|
|
166
|
-
yamlExample('typography:\n profile: reading\n rhythm: normal'),
|
|
167
|
-
'Overrides the preset\'s font stack, typography profile, rhythm or individual text settings.',
|
|
168
|
-
documentationLink('Typography configuration reference', 'typography.md', 'configuration-shape'),
|
|
169
|
-
]);
|
|
170
|
-
addHelp(jsonSchema, 'palette', [
|
|
171
|
-
yamlExample('palette: paper'),
|
|
172
|
-
'Chooses a coordinated color system for the page frame, navigation, footer and section surfaces.',
|
|
173
|
-
documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
|
|
174
|
-
], ['dark', 'light', 'paper']);
|
|
175
|
-
addHelp(jsonSchema, 'sectionSurfaces', [
|
|
176
|
-
yamlExample('sectionSurfaces: [base, soft, emphasis]'),
|
|
177
|
-
'Lists one to three semantic surfaces. Norna cycles through them in section order; omit this field to keep the preset sequence.',
|
|
178
|
-
documentationLink('Palette and section surfaces', 'theme.md', 'palette-and-section-surfaces'),
|
|
179
|
-
], [['base'], ['base', 'soft'], ['base', 'soft', 'emphasis']]);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const addSitewideHelp = (jsonSchema) => {
|
|
183
|
-
jsonSchema.markdownDescription = [
|
|
184
|
-
yamlExample('navigation:\n label: Example Site'),
|
|
185
|
-
'`sitewide-content.yaml` optionally defines editorial content and identity shared by every page.',
|
|
186
|
-
documentationLink('Site-wide content reference', 'sitewide-content.md'),
|
|
187
|
-
].join('\n\n');
|
|
188
|
-
addSitewideNavigationHelp(jsonSchema);
|
|
189
|
-
addHelp(jsonSchema, 'banners', [
|
|
190
|
-
yamlExample('banners:\n - id: project-status\n tone: warning\n title: Experimental code\n text: Not for production use.'),
|
|
191
|
-
'Adds dismissible notices above page content. List order controls their presentation order; each `id` must be unique.',
|
|
192
|
-
documentationLink('Banner reference', 'sitewide-content.md', 'banners'),
|
|
193
|
-
]);
|
|
194
|
-
addHelp(jsonSchema, 'footer', [
|
|
195
|
-
yamlExample('footer:\n copyrightMessage: Copyright Example Owner.'),
|
|
196
|
-
'Adds shared footer content. Generated build information is optional.',
|
|
197
|
-
documentationLink('Footer reference', 'sitewide-content.md', 'footer'),
|
|
198
|
-
]);
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const addContentHelp = (jsonSchema) => {
|
|
202
|
-
jsonSchema.markdownDescription = [
|
|
203
|
-
yamlExample('---\ntitle: About\ndescription: About this project.\n---'),
|
|
204
|
-
'Every homepage and route `content.md` starts with page metadata, followed by Markdown sections.',
|
|
205
|
-
documentationLink('Page frontmatter reference', 'content.md', 'page-frontmatter'),
|
|
206
|
-
].join('\n\n');
|
|
207
|
-
addHelp(jsonSchema, 'title', [
|
|
208
|
-
yamlExample('title: About'),
|
|
209
|
-
'The page title used in metadata and, by default, route navigation.',
|
|
210
|
-
documentationLink('Page frontmatter reference', 'content.md', 'page-frontmatter'),
|
|
211
|
-
], ['About']);
|
|
212
|
-
addHelp(jsonSchema, 'description', [
|
|
213
|
-
yamlExample('description: About this project.'),
|
|
214
|
-
'A concise page description used in metadata.',
|
|
215
|
-
documentationLink('Page frontmatter reference', 'content.md', 'page-frontmatter'),
|
|
216
|
-
], ['About this project.']);
|
|
217
|
-
addHelp(jsonSchema, 'navigation', [
|
|
218
|
-
yamlExample('navigation:\n include: true\n label: About us'),
|
|
219
|
-
'Controls whether a route appears in site navigation and optionally replaces its visible title there. Homepage navigation is section-based.',
|
|
220
|
-
documentationLink('Route navigation reference', 'routes.md', 'navigation'),
|
|
221
|
-
]);
|
|
222
|
-
addHelp(jsonSchema, 'sections', [
|
|
223
|
-
yamlExample('sections:\n announcement:\n visible:\n until: "2026-12-01"'),
|
|
224
|
-
'Adds optional metadata keyed by explicit Markdown section id. Markdown headings remain the source of section content and order.',
|
|
225
|
-
documentationLink('Section metadata reference', 'content.md', 'section-metadata'),
|
|
226
|
-
]);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
21
|
const schemaFiles = new Map();
|
|
230
22
|
|
|
231
23
|
for (const [filename, title, schema] of definitions) {
|
|
232
24
|
const jsonSchema = z.toJSONSchema(schema, { target: 'draft-07', io: 'input' });
|
|
233
25
|
jsonSchema.$id = `https://janga.github.io/norna/schemas/${filename}`;
|
|
234
26
|
jsonSchema.title = title;
|
|
235
|
-
|
|
236
|
-
if (filename === 'config.schema.json') {
|
|
237
|
-
addLanguageSuggestions(jsonSchema);
|
|
238
|
-
addConfigHelp(jsonSchema);
|
|
239
|
-
}
|
|
240
|
-
if (filename === 'theme.schema.json') addThemeHelp(jsonSchema);
|
|
241
|
-
if (filename === 'sitewide-content.schema.json') addSitewideHelp(jsonSchema);
|
|
242
|
-
if (filename === 'content-frontmatter.schema.json') addContentHelp(jsonSchema);
|
|
27
|
+
applySchemaEditorMetadata(filename, jsonSchema);
|
|
243
28
|
schemaFiles.set(filename, `${JSON.stringify(jsonSchema, null, 2)}\n`);
|
|
244
29
|
}
|
|
245
30
|
|
|
246
31
|
schemaFiles.set('manifest.json', `${JSON.stringify({
|
|
32
|
+
editorApiVersion: 1,
|
|
247
33
|
schemaVersion: 1,
|
|
248
34
|
files: {
|
|
249
35
|
config: 'config.schema.json',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const getBasePathRedirectLocation = (basePath, requestUrl) => {
|
|
2
|
+
if (basePath === '/') return undefined;
|
|
3
|
+
|
|
4
|
+
const basePathWithoutTrailingSlash = basePath.replace(/\/$/, '');
|
|
5
|
+
const parsedRequestUrl = new URL(requestUrl, 'http://localhost');
|
|
6
|
+
if (parsedRequestUrl.pathname !== basePathWithoutTrailingSlash) return undefined;
|
|
7
|
+
|
|
8
|
+
return `${basePathWithoutTrailingSlash}/${parsedRequestUrl.search}`;
|
|
9
|
+
};
|