@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/scripts/check-config.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { siteConfigLabel, sitePublicLabel } from './lib/site-paths.mjs';
|
|
2
|
-
import { getLogoAssets } from './lib/logo-assets.mjs';
|
|
3
|
-
import {
|
|
2
|
+
import { getLogoAssets, getPublicAssetInspection } from './lib/logo-assets.mjs';
|
|
3
|
+
import { logoAssetFilenames } from './lib/public-asset-conventions.mjs';
|
|
4
|
+
import { readSitewideContent } from './lib/sitewide-content.mjs';
|
|
4
5
|
import { readThemeConfig, validateRouteThemeFiles } from './lib/theme-config.mjs';
|
|
5
6
|
|
|
6
7
|
const formatErrorMessage = (error) => {
|
|
@@ -14,22 +15,28 @@ const formatErrorMessage = (error) => {
|
|
|
14
15
|
try {
|
|
15
16
|
const { projectConfig } = await import('./lib/project-config.mjs');
|
|
16
17
|
const themeConfig = await readThemeConfig();
|
|
17
|
-
const
|
|
18
|
+
const sitewideContent = await readSitewideContent();
|
|
18
19
|
await validateRouteThemeFiles();
|
|
19
20
|
const logoAssets = getLogoAssets();
|
|
21
|
+
const publicAssetInspection = getPublicAssetInspection();
|
|
22
|
+
const logoAssetPaths = logoAssetFilenames.map((filename) => `${sitePublicLabel}/${filename}`);
|
|
23
|
+
for (const issue of publicAssetInspection.suspicious) {
|
|
24
|
+
console.warn(`Warning: ${sitePublicLabel}/${issue.filename}: ${issue.message}`);
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
if (logoAssets.length > 1) {
|
|
22
28
|
throw new Error([
|
|
23
|
-
`Found multiple logo files in ${sitePublicLabel}. Keep exactly one of
|
|
29
|
+
`Found multiple logo files in ${sitePublicLabel}. Keep exactly one of ${logoAssetFilenames.join(', ')}.`,
|
|
24
30
|
...logoAssets.map(({ filename }) => `- ${sitePublicLabel}/${filename}`),
|
|
25
31
|
].join('\n'));
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
if (logoAssets.length === 0) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.warn(
|
|
35
|
+
if (sitewideContent.navigation?.logo) {
|
|
36
|
+
throw new Error(`Site-wide navigation.logo is configured, but no logo file was found. Add exactly one of ${logoAssetPaths.join(', ')}, or remove navigation.logo.`);
|
|
37
|
+
}
|
|
38
|
+
console.warn(`Warning: No logo file found in ${sitePublicLabel}. Norna will use sitewide navigation.label or the homepage title as the navigation label.`);
|
|
39
|
+
console.warn(`Add exactly one of ${logoAssetPaths.join(', ')} when the site should have a logo.`);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
console.log('Config check passed.');
|
|
@@ -39,9 +46,9 @@ try {
|
|
|
39
46
|
console.log(`Page width: ${projectConfig.layout.pageWidth}`);
|
|
40
47
|
console.log(`Gutter: desktop ${projectConfig.layout.gutter.desktop}, mobile ${projectConfig.layout.gutter.mobile}`);
|
|
41
48
|
console.log(`Layout density: ${projectConfig.layout.density}`);
|
|
42
|
-
console.log(`Image area width: ${projectConfig.
|
|
43
|
-
console.log(`Image max available width: desktop ${projectConfig.
|
|
44
|
-
console.log(`Image max available height: desktop ${projectConfig.
|
|
49
|
+
console.log(`Image area width: ${projectConfig.images.width}`);
|
|
50
|
+
console.log(`Image max available width: desktop ${projectConfig.images.maxAvailableWidthPercent.desktop}%, mobile ${projectConfig.images.maxAvailableWidthPercent.mobile}%`);
|
|
51
|
+
console.log(`Image max available height: desktop ${projectConfig.images.maxAvailableHeightPercent.desktop}%, mobile ${projectConfig.images.maxAvailableHeightPercent.mobile}%`);
|
|
45
52
|
console.log(`Font family: ${projectConfig.typography.fontFamily}`);
|
|
46
53
|
console.log(`Language: ${projectConfig.locale.lang}`);
|
|
47
54
|
console.log(`Scroll behavior: ${projectConfig.navigation.scrollBehavior}`);
|
|
@@ -12,7 +12,7 @@ if (!presetName || extraArgs.length > 0) {
|
|
|
12
12
|
throw new Error(`Usage: norna theme:export <preset>\nAvailable presets: ${themePresetNames.join(', ')}`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const filename = `orig-${presetName}-theme.
|
|
15
|
+
const filename = `orig-${presetName}-theme.yaml`;
|
|
16
16
|
const outputPath = path.join(siteDir, filename);
|
|
17
17
|
const outputLabel = path.posix.join(path.posix.dirname(siteThemeLabel), filename);
|
|
18
18
|
const source = renderThemePresetReference(presetName, siteThemeLabel);
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { z } from 'astro/zod';
|
|
5
|
+
import {
|
|
6
|
+
configSchema,
|
|
7
|
+
siteSchema,
|
|
8
|
+
sitewideSchema,
|
|
9
|
+
themeVisualSchema,
|
|
10
|
+
} from './lib/schema-definitions.mjs';
|
|
11
|
+
import { documentationLink } from './lib/documentation-links.mjs';
|
|
12
|
+
import { getSchemaValueDefinition } from './lib/schema-value-definitions.mjs';
|
|
13
|
+
|
|
14
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const schemaDirectory = path.join(root, 'schemas');
|
|
16
|
+
const definitions = [
|
|
17
|
+
['config.schema.json', 'Norna site configuration', configSchema],
|
|
18
|
+
['theme.schema.json', 'Norna visual theme', themeVisualSchema],
|
|
19
|
+
['sitewide-content.schema.json', 'Norna site-wide content', sitewideSchema],
|
|
20
|
+
['content-frontmatter.schema.json', 'Norna page frontmatter', siteSchema],
|
|
21
|
+
];
|
|
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
|
+
const schemaFiles = new Map();
|
|
230
|
+
|
|
231
|
+
for (const [filename, title, schema] of definitions) {
|
|
232
|
+
const jsonSchema = z.toJSONSchema(schema, { target: 'draft-07', io: 'input' });
|
|
233
|
+
jsonSchema.$id = `https://janga.github.io/norna/schemas/${filename}`;
|
|
234
|
+
jsonSchema.title = title;
|
|
235
|
+
addValueDescriptions(jsonSchema);
|
|
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);
|
|
243
|
+
schemaFiles.set(filename, `${JSON.stringify(jsonSchema, null, 2)}\n`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
schemaFiles.set('manifest.json', `${JSON.stringify({
|
|
247
|
+
schemaVersion: 1,
|
|
248
|
+
files: {
|
|
249
|
+
config: 'config.schema.json',
|
|
250
|
+
contentFrontmatter: 'content-frontmatter.schema.json',
|
|
251
|
+
sitewideContent: 'sitewide-content.schema.json',
|
|
252
|
+
theme: 'theme.schema.json',
|
|
253
|
+
},
|
|
254
|
+
}, null, 2)}\n`);
|
|
255
|
+
|
|
256
|
+
const args = process.argv.slice(2);
|
|
257
|
+
const checkOnly = args.length === 1 && args[0] === '--check';
|
|
258
|
+
if (args.length > 0 && !checkOnly) {
|
|
259
|
+
throw new Error('Usage: node scripts/generate-schemas.mjs [--check]');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (checkOnly) {
|
|
263
|
+
const stale = [];
|
|
264
|
+
for (const [filename, expected] of schemaFiles) {
|
|
265
|
+
const actual = await readFile(path.join(schemaDirectory, filename), 'utf8').catch((error) => (
|
|
266
|
+
error?.code === 'ENOENT' ? null : Promise.reject(error)
|
|
267
|
+
));
|
|
268
|
+
if (actual !== expected) stale.push(filename);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (stale.length > 0) {
|
|
272
|
+
throw new Error(`Generated Norna schemas are stale: ${stale.join(', ')}. Run "npm run schemas:generate".`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`Norna schemas are up to date (${definitions.length} schemas).`);
|
|
276
|
+
} else {
|
|
277
|
+
await mkdir(schemaDirectory, { recursive: true });
|
|
278
|
+
for (const [filename, source] of schemaFiles) {
|
|
279
|
+
await writeFile(path.join(schemaDirectory, filename), source);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
console.log(`Generated ${definitions.length} Norna schemas in ${path.relative(root, schemaDirectory)}.`);
|
|
283
|
+
}
|
package/scripts/init-site.mjs
CHANGED
|
@@ -11,14 +11,14 @@ const packageName = '@janga/norna';
|
|
|
11
11
|
const cliExecutableName = 'norna';
|
|
12
12
|
|
|
13
13
|
const usage = `
|
|
14
|
-
Usage: norna init <target-dir> [--type
|
|
14
|
+
Usage: norna init <target-dir> [--type standalone|embedded] [--site-dir <path>]
|
|
15
15
|
|
|
16
16
|
Creates a new site project or adds a Norna site directory to an existing
|
|
17
17
|
project.
|
|
18
18
|
|
|
19
19
|
Examples:
|
|
20
20
|
norna init my-site
|
|
21
|
-
norna init my-site --type
|
|
21
|
+
norna init my-site --type standalone
|
|
22
22
|
norna init . --type embedded --site-dir presentation
|
|
23
23
|
`.trim();
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ const parseArgs = (rawArgs) => {
|
|
|
27
27
|
const options = {
|
|
28
28
|
siteDirectory: process.env[siteDirectoryEnv] ?? 'site',
|
|
29
29
|
targetDirectory: null,
|
|
30
|
-
type: '
|
|
30
|
+
type: 'standalone',
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
@@ -36,7 +36,7 @@ const parseArgs = (rawArgs) => {
|
|
|
36
36
|
if (arg === '--type') {
|
|
37
37
|
const value = rawArgs[index + 1];
|
|
38
38
|
if (!value || value.startsWith('-')) {
|
|
39
|
-
throw new Error('--type requires "
|
|
39
|
+
throw new Error('--type requires "standalone" or "embedded".');
|
|
40
40
|
}
|
|
41
41
|
options.type = value;
|
|
42
42
|
index += 1;
|
|
@@ -87,8 +87,8 @@ if (!targetDirectory) {
|
|
|
87
87
|
throw new Error(usage);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
if (!['
|
|
91
|
-
throw new Error(`Invalid --type "${type}". Use "
|
|
90
|
+
if (!['standalone', 'embedded'].includes(type)) {
|
|
91
|
+
throw new Error(`Invalid --type "${type}". Use "standalone" or "embedded".`);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
if (!siteDirectory || path.isAbsolute(siteDirectory) || siteDirectory.split(/[\\/]/).includes('..')) {
|
|
@@ -122,19 +122,20 @@ const readJsonFile = async (filePath) => JSON.parse(await readFile(filePath, 'ut
|
|
|
122
122
|
|
|
123
123
|
const siteDirArg = siteDirectory === 'site' ? [] : ['--site-dir', siteDirectory];
|
|
124
124
|
const cliCommand = (command) => [cliExecutableName, ...siteDirArg, command].join(' ');
|
|
125
|
-
const
|
|
125
|
+
const nornaScripts = {
|
|
126
126
|
'norna:dev': cliCommand('dev:local'),
|
|
127
127
|
'norna:dev:lan': cliCommand('dev:lan'),
|
|
128
128
|
'norna:dev:restart': cliCommand('dev:restart'),
|
|
129
129
|
'norna:dev:status': cliCommand('dev:status'),
|
|
130
130
|
'norna:dev:logs': cliCommand('dev:logs'),
|
|
131
131
|
'norna:dev:stop': cliCommand('dev:stop'),
|
|
132
|
-
'norna:check': '
|
|
132
|
+
'norna:check': cliCommand('check'),
|
|
133
133
|
'norna:config:check': cliCommand('config:check'),
|
|
134
134
|
'norna:content:check': cliCommand('content:check'),
|
|
135
135
|
'norna:sync': cliCommand('content:sync'),
|
|
136
|
+
'norna:theme:presets': cliCommand('theme:presets'),
|
|
136
137
|
'norna:theme:export': cliCommand('theme:export'),
|
|
137
|
-
'norna:typography:
|
|
138
|
+
'norna:typography:profiles': cliCommand('typography profiles'),
|
|
138
139
|
'norna:typography:show': cliCommand('typography show'),
|
|
139
140
|
'norna:public': cliCommand('site:public'),
|
|
140
141
|
'norna:images': cliCommand('images'),
|
|
@@ -149,17 +150,17 @@ const galleryScripts = {
|
|
|
149
150
|
'norna:engine:version': cliCommand('engine:version'),
|
|
150
151
|
};
|
|
151
152
|
|
|
152
|
-
const
|
|
153
|
+
const addNornaDependency = (packageJson, version) => {
|
|
153
154
|
packageJson.dependencies ??= {};
|
|
154
155
|
packageJson.dependencies[packageName] ??= version;
|
|
155
156
|
};
|
|
156
157
|
|
|
157
|
-
const
|
|
158
|
+
const addNornaScripts = (packageJson, scripts, { includeStandaloneAliases }) => {
|
|
158
159
|
packageJson.scripts ??= {};
|
|
159
160
|
const wantedScripts = {
|
|
160
|
-
...(
|
|
161
|
+
...(includeStandaloneAliases ? { dev: 'npm run norna:dev --' } : {}),
|
|
161
162
|
...scripts,
|
|
162
|
-
...(
|
|
163
|
+
...(includeStandaloneAliases ? { build: 'npm run norna:build' } : {}),
|
|
163
164
|
};
|
|
164
165
|
const conflicts = Object.entries(wantedScripts).filter(([name, value]) => (
|
|
165
166
|
packageJson.scripts[name] !== undefined && packageJson.scripts[name] !== value
|
|
@@ -178,12 +179,12 @@ const addGalleryScripts = (packageJson, scripts, { includePureAliases }) => {
|
|
|
178
179
|
const enginePackageJson = await readJsonFile(enginePackageJsonPath);
|
|
179
180
|
const existingEntries = await readDirectoryEntries(targetRoot);
|
|
180
181
|
|
|
181
|
-
if (type === '
|
|
182
|
+
if (type === 'standalone') {
|
|
182
183
|
if (existingEntries && existingEntries.length > 0) {
|
|
183
184
|
const existingPackageJson = existingEntries.includes('package.json')
|
|
184
185
|
? ' The target already contains package.json; use --type embedded to add a Norna site to an existing project.'
|
|
185
186
|
: '';
|
|
186
|
-
throw new Error(`Target directory must be empty for --type
|
|
187
|
+
throw new Error(`Target directory must be empty for --type standalone: ${targetRoot}.${existingPackageJson}`);
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
await mkdir(path.dirname(targetRoot), { recursive: true });
|
|
@@ -199,10 +200,8 @@ if (type === 'pure') {
|
|
|
199
200
|
|
|
200
201
|
const targetPackageJson = await readJsonFile(targetPackageJsonPath);
|
|
201
202
|
targetPackageJson.dependencies[packageName] = enginePackageJson.version;
|
|
202
|
-
if (siteDirectory !== 'site') {
|
|
203
|
-
|
|
204
|
-
addGalleryScripts(targetPackageJson, galleryScripts, { includePureAliases: true });
|
|
205
|
-
}
|
|
203
|
+
if (siteDirectory !== 'site') targetPackageJson.scripts = {};
|
|
204
|
+
addNornaScripts(targetPackageJson, nornaScripts, { includeStandaloneAliases: true });
|
|
206
205
|
await writeFile(targetPackageJsonPath, `${JSON.stringify(targetPackageJson, null, 2)}\n`);
|
|
207
206
|
} else {
|
|
208
207
|
if (!existingEntries) {
|
|
@@ -222,8 +221,8 @@ if (type === 'pure') {
|
|
|
222
221
|
throw new Error(`Norna site directory must be empty for --type embedded: ${targetSiteDir}`);
|
|
223
222
|
}
|
|
224
223
|
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
addNornaDependency(targetPackageJson, enginePackageJson.version);
|
|
225
|
+
addNornaScripts(targetPackageJson, nornaScripts, { includeStandaloneAliases: false });
|
|
227
226
|
await mkdir(path.dirname(targetSiteDir), { recursive: true });
|
|
228
227
|
await cp(starterSiteRoot, targetSiteDir, {
|
|
229
228
|
filter: (source) => path.basename(source) !== '.DS_Store',
|
|
@@ -244,9 +243,5 @@ if (isInsideEngineRoot(targetRoot)) {
|
|
|
244
243
|
console.log('');
|
|
245
244
|
console.log('Next steps:');
|
|
246
245
|
console.log(` cd ${targetRoot}`);
|
|
247
|
-
console.log(
|
|
248
|
-
|
|
249
|
-
: ' npm install');
|
|
250
|
-
console.log(type === 'embedded'
|
|
251
|
-
? ' npm run norna:dev'
|
|
252
|
-
: ' npm run dev');
|
|
246
|
+
console.log(' npm install');
|
|
247
|
+
console.log(' npm run norna:dev');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import packageJson from '../../package.json' with { type: 'json' };
|
|
2
|
+
|
|
3
|
+
export const documentationRef = `v${packageJson.version}`;
|
|
4
|
+
export const documentationRoot = `https://github.com/janga/norna/blob/${documentationRef}/docs`;
|
|
5
|
+
|
|
6
|
+
export const documentationLink = (label, file, anchor) => (
|
|
7
|
+
`[${label}](${documentationRoot}/${file}${anchor ? `#${anchor}` : ''})`
|
|
8
|
+
);
|