@janga/norna 0.7.5 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/astro.config.mjs +76 -0
- package/bin/norna-cli.mjs +2 -2
- package/docs/README.md +3 -3
- package/docs/commands.md +6 -6
- package/docs/configuration.md +3 -2
- package/docs/content.md +154 -95
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +4 -4
- package/docs/getting-started.md +6 -4
- package/docs/images-and-metadata.md +112 -20
- package/docs/routes.md +52 -33
- package/docs/site-structure.md +7 -6
- package/docs/theme.md +8 -7
- package/docs/typography.md +5 -5
- package/examples/dog-gallery/site/content.md +41 -46
- package/examples/dog-gallery/site/routes/{dog-care → 010-dog-care}/route-content.md +1 -4
- package/fixtures/basic/site/content.md +2 -4
- package/fixtures/content-model-v2/.astro/collections/site.schema.json +776 -0
- package/fixtures/content-model-v2/.astro/collections/theme.schema.json +668 -0
- package/fixtures/content-model-v2/.astro/content-assets.mjs +4 -0
- package/fixtures/content-model-v2/.astro/content-modules.mjs +1 -0
- package/fixtures/content-model-v2/.astro/content.d.ts +175 -0
- package/fixtures/content-model-v2/.astro/data-store.json +1 -0
- package/fixtures/content-model-v2/.astro/dev-local.json +8 -0
- package/fixtures/content-model-v2/.astro/dev.json +13 -0
- package/fixtures/content-model-v2/.astro/dev.log +231 -0
- package/fixtures/content-model-v2/.astro/settings.json +5 -0
- package/fixtures/content-model-v2/.astro/types.d.ts +2 -0
- package/fixtures/content-model-v2/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
- package/fixtures/content-model-v2/dist/_astro/SitePage.BbfZub4g.css +1 -0
- package/fixtures/content-model-v2/dist/guide/index.html +3 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-960.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-768.webp +0 -0
- package/fixtures/content-model-v2/dist/index.html +8 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/_metadata.json +80 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js +1389 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/package.json +3 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/fixtures/content-model-v2/site/.norna/generated-images.json +210 -0
- package/fixtures/content-model-v2/site/config.mjs +11 -0
- package/fixtures/content-model-v2/site/content.md +84 -0
- package/fixtures/content-model-v2/site/images/card-section/adopt.svg +9 -0
- package/fixtures/content-model-v2/site/images/card-section/foster.svg +12 -0
- package/fixtures/content-model-v2/site/images/intro/detail.jpg +0 -0
- package/fixtures/content-model-v2/site/images/intro/duplicate.jpg +0 -0
- package/fixtures/content-model-v2/site/images/intro/hero.jpg +0 -0
- package/fixtures/content-model-v2/site/images/plain/image.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-one.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-three.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-two.jpg +0 -0
- package/fixtures/content-model-v2/site/routes/010-guide/images/intro/duplicate.jpg +0 -0
- package/fixtures/content-model-v2/site/routes/010-guide/route-content.md +17 -0
- package/fixtures/content-model-v2/site/theme.md +9 -0
- package/package.json +4 -2
- package/scripts/deploy-site.mjs +12 -5
- package/scripts/dev-local.mjs +6 -0
- package/scripts/generate-images.mjs +132 -56
- package/scripts/lib/image-dimensions.mjs +26 -0
- package/scripts/lib/norna-markdown-blocks.mjs +643 -0
- package/scripts/lib/route-model.mjs +17 -0
- package/scripts/lib/site-content.mjs +45 -37
- package/scripts/show-typography.mjs +18 -10
- package/scripts/sync-content-sections.mjs +280 -423
- package/scripts/test-content-check.mjs +59 -45
- package/scripts/test-content-model-v2.mjs +998 -0
- package/scripts/test-engine-commands.mjs +1 -1
- package/scripts/test-package-check.mjs +38 -11
- package/scripts/test-route-model.mjs +32 -0
- package/scripts/test-temporary-visibility.mjs +3 -6
- package/src/components/CardList.astro +77 -0
- package/src/components/GalleryGrid.astro +22 -12
- package/src/components/SiteNavigation.astro +5 -4
- package/src/components/SitePage.astro +12 -6
- package/src/components/SiteSection.astro +35 -13
- package/src/content.config.ts +8 -23
- package/src/lib/generatedImages.ts +50 -10
- package/src/lib/sectionContent.ts +130 -35
- package/src/lib/sitePages.ts +36 -15
- package/src/pages/[slug].astro +3 -3
- package/src/styles/global.css +187 -0
- package/starters/basic/site/content.md +10 -6
- package/starters/project/README.md +2 -2
- package/starters/project/site/content.md +0 -10
- package/starters/project/site/routes/{guide → 010-guide}/route-content.md +0 -6
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
import type { CollectionEntry } from 'astro:content';
|
|
2
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import {
|
|
4
|
+
extractNornaMarkdownBlocks,
|
|
5
|
+
splitNornaMarkdownBlockMarkers,
|
|
6
|
+
splitNornaRenderedCodeBlocks,
|
|
7
|
+
} from '../../scripts/lib/norna-markdown-blocks.mjs';
|
|
3
8
|
import { applyBasePathToHtml } from './basePath';
|
|
9
|
+
import type { SitePage } from './sitePages';
|
|
4
10
|
|
|
5
|
-
type
|
|
11
|
+
type SiteSectionMetadataMap = CollectionEntry<'site'>['data']['sections'];
|
|
12
|
+
type SiteSectionMetadata = SiteSectionMetadataMap[string];
|
|
6
13
|
type ThemePresentation = CollectionEntry<'theme'>['data']['presentation'];
|
|
7
14
|
type InlineStyles = NonNullable<NonNullable<ThemePresentation>['inlineStyles']>;
|
|
15
|
+
type GalleryImage = {
|
|
16
|
+
image: string;
|
|
17
|
+
src: string;
|
|
18
|
+
alt?: string;
|
|
19
|
+
caption?: string;
|
|
20
|
+
};
|
|
21
|
+
type CardListItem = {
|
|
22
|
+
title: string;
|
|
23
|
+
text?: string;
|
|
24
|
+
image?: string;
|
|
25
|
+
src?: string;
|
|
26
|
+
link?: string;
|
|
27
|
+
'badge-text'?: string;
|
|
28
|
+
};
|
|
29
|
+
type CardListLayout = 'image-top' | 'image-left' | 'image-right';
|
|
30
|
+
type CardListFlow = 'grid' | 'stack';
|
|
31
|
+
type CardListSize = 's' | 'm' | 'l' | 'xl';
|
|
32
|
+
type CardListWidth = 'text' | 'narrow' | 'normal' | 'wide';
|
|
33
|
+
type SectionContentBlock =
|
|
34
|
+
| { type: 'html'; html: string }
|
|
35
|
+
| { type: 'image-stack'; images: GalleryImage[] }
|
|
36
|
+
| { type: 'image-carousel'; images: GalleryImage[] }
|
|
37
|
+
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width: CardListWidth; cards: CardListItem[] };
|
|
38
|
+
export type ResolvedSection = SiteSectionMetadata & {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
contentBlocks: SectionContentBlock[];
|
|
42
|
+
};
|
|
8
43
|
|
|
9
44
|
const headingRegex = /<h2\b([^>]*)>([\s\S]*?)<\/h2>/gi;
|
|
10
45
|
const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
11
46
|
const inlineStyleReferenceRegex = /\[([^\]<]+)\]\{\.([a-z][a-z0-9-]*)\}/g;
|
|
47
|
+
const markdownH2Regex = /^##\s+.*$/gm;
|
|
48
|
+
const imageProvenanceCommentRegex = /<!--\s*norna-image-provenance:[\s\S]*?-->/gi;
|
|
12
49
|
|
|
13
50
|
const stripTags = (html: string) => html.replace(/<[^>]*>/g, '');
|
|
14
51
|
|
|
@@ -58,18 +95,94 @@ const applyInlineStyles = (html: string, inlineStyles: InlineStyles | undefined)
|
|
|
58
95
|
return `<span class="inline-style inline-style-${styleName}" style="--inline-style-color: ${style.color}">${text}</span>`;
|
|
59
96
|
});
|
|
60
97
|
|
|
98
|
+
const stripImageProvenanceComments = (html: string) => html.replace(imageProvenanceCommentRegex, '');
|
|
99
|
+
|
|
61
100
|
const prepareContentHtml = (html: string, inlineStyles: InlineStyles | undefined) =>
|
|
62
|
-
applyBasePathToHtml(
|
|
101
|
+
applyBasePathToHtml(
|
|
102
|
+
projectConfig.site.basePath,
|
|
103
|
+
stripImageProvenanceComments(applyInlineStyles(html, inlineStyles)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const getRawMarkdownSections = (markdown: string) => {
|
|
107
|
+
const matches = Array.from(markdown.matchAll(markdownH2Regex));
|
|
108
|
+
const sections = new Map<string, string>();
|
|
109
|
+
|
|
110
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
111
|
+
const match = matches[index];
|
|
112
|
+
const heading = match[0] ?? '';
|
|
113
|
+
const id = heading.match(explicitHeadingIdRegex)?.[1];
|
|
114
|
+
if (!id) continue;
|
|
115
|
+
|
|
116
|
+
const start = match.index ?? 0;
|
|
117
|
+
const next = matches[index + 1];
|
|
118
|
+
const end = next?.index ?? markdown.length;
|
|
119
|
+
sections.set(id, markdown.slice(start, end));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return sections;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const getImageSourceKey = (page: SitePage, sectionId: string, image: string) => (
|
|
126
|
+
page.routeDirectory
|
|
127
|
+
? `routes/${page.routeDirectory}/images/${sectionId}/${image}`
|
|
128
|
+
: `images/${sectionId}/${image}`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const resolveContentBlocks = (
|
|
132
|
+
html: string,
|
|
133
|
+
rawMarkdown: string,
|
|
134
|
+
page: SitePage,
|
|
135
|
+
sectionId: string,
|
|
136
|
+
inlineStyles: InlineStyles | undefined,
|
|
137
|
+
) => {
|
|
138
|
+
const rawBlocks = extractNornaMarkdownBlocks(rawMarkdown);
|
|
139
|
+
const splitBlocks = rawBlocks.length > 0
|
|
140
|
+
? splitNornaRenderedCodeBlocks(html, rawBlocks)
|
|
141
|
+
: splitNornaMarkdownBlockMarkers(html);
|
|
142
|
+
|
|
143
|
+
return splitBlocks.map((block): SectionContentBlock => {
|
|
144
|
+
if (block.type === 'html') {
|
|
145
|
+
return {
|
|
146
|
+
type: 'html',
|
|
147
|
+
html: prepareContentHtml(block.html, inlineStyles),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
type: block.type,
|
|
153
|
+
...(block.type === 'card-list'
|
|
154
|
+
? {
|
|
155
|
+
layout: block.layout,
|
|
156
|
+
flow: block.flow,
|
|
157
|
+
size: block.size,
|
|
158
|
+
width: block.width,
|
|
159
|
+
cards: block.cards.map((card: CardListItem) => ({
|
|
160
|
+
...card,
|
|
161
|
+
...(card.image ? { src: getImageSourceKey(page, sectionId, card.image) } : {}),
|
|
162
|
+
})),
|
|
163
|
+
}
|
|
164
|
+
: {
|
|
165
|
+
images: block.images.map((image: { image: string; alt?: string; caption?: string }) => ({
|
|
166
|
+
...image,
|
|
167
|
+
src: getImageSourceKey(page, sectionId, image.image),
|
|
168
|
+
})),
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
};
|
|
63
173
|
|
|
64
174
|
export const getSectionsContent = (
|
|
65
175
|
html: string,
|
|
66
|
-
|
|
176
|
+
rawMarkdown: string,
|
|
177
|
+
sectionMetadata: SiteSectionMetadataMap,
|
|
178
|
+
page: SitePage,
|
|
67
179
|
inlineStyles?: InlineStyles,
|
|
68
180
|
) => {
|
|
69
181
|
const matches = Array.from(html.matchAll(headingRegex));
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
182
|
+
const rawSections = getRawMarkdownSections(rawMarkdown);
|
|
183
|
+
const metadataIds = new Set(Object.keys(sectionMetadata));
|
|
184
|
+
const sections: ResolvedSection[] = [];
|
|
185
|
+
const sectionIds = new Set<string>();
|
|
73
186
|
|
|
74
187
|
for (let index = 0; index < matches.length; index += 1) {
|
|
75
188
|
const match = matches[index];
|
|
@@ -84,47 +197,29 @@ export const getSectionsContent = (
|
|
|
84
197
|
const title = getHeadingTitle(headingHtml);
|
|
85
198
|
|
|
86
199
|
if (!explicitId) {
|
|
87
|
-
|
|
200
|
+
throw new Error(`Section heading "${title}" is missing an explicit id. Write it as: ## ${title} {#${id}}`);
|
|
88
201
|
}
|
|
89
202
|
|
|
90
|
-
if (
|
|
203
|
+
if (sectionIds.has(id)) {
|
|
91
204
|
throw new Error(`Duplicate Markdown section heading id: ${id}`);
|
|
92
205
|
}
|
|
93
206
|
|
|
94
|
-
|
|
207
|
+
sectionIds.add(id);
|
|
208
|
+
sections.push({
|
|
209
|
+
...(sectionMetadata[id] ?? {}),
|
|
210
|
+
id,
|
|
95
211
|
title,
|
|
96
|
-
|
|
212
|
+
contentBlocks: resolveContentBlocks(content, rawSections.get(id) ?? '', page, id, inlineStyles),
|
|
97
213
|
});
|
|
98
|
-
markdownSectionIds.push(id);
|
|
99
214
|
}
|
|
100
215
|
|
|
101
|
-
for (const id of
|
|
216
|
+
for (const id of metadataIds) {
|
|
102
217
|
if (!sectionIds.has(id)) {
|
|
103
|
-
console.warn(`Markdown section exists but is not used in frontmatter: ${id}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const orderedMarkdownSectionIds = markdownSectionIds.filter((id) => sectionIds.has(id));
|
|
108
|
-
const frontmatterSectionIds = sections.map((section) => section.id);
|
|
109
|
-
const hasOrderMismatch = frontmatterSectionIds.some((id, index) => id !== orderedMarkdownSectionIds[index]);
|
|
110
|
-
|
|
111
|
-
if (hasOrderMismatch) {
|
|
112
|
-
console.warn('Markdown section order differs from frontmatter. Run npm run content:sync to sort Markdown according to frontmatter.');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return sections.map((section) => {
|
|
116
|
-
const content = contentById.get(section.id);
|
|
117
|
-
|
|
118
|
-
if (!content) {
|
|
119
218
|
throw new Error(
|
|
120
|
-
`
|
|
219
|
+
`Section metadata "${id}" does not match any Markdown section. Add "## Heading {#${id}}" or remove sections.${id}.`,
|
|
121
220
|
);
|
|
122
221
|
}
|
|
222
|
+
}
|
|
123
223
|
|
|
124
|
-
|
|
125
|
-
...section,
|
|
126
|
-
title: content.title,
|
|
127
|
-
contentHtml: content.contentHtml,
|
|
128
|
-
};
|
|
129
|
-
});
|
|
224
|
+
return sections;
|
|
130
225
|
};
|
package/src/lib/sitePages.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import { parseRouteDirectory } from '../../scripts/lib/route-model.mjs';
|
|
2
3
|
|
|
3
4
|
type SiteEntry = CollectionEntry<'site'>;
|
|
4
5
|
|
|
@@ -10,9 +11,10 @@ export type SitePage = {
|
|
|
10
11
|
label: string;
|
|
11
12
|
order: number;
|
|
12
13
|
};
|
|
14
|
+
pathSegment: string;
|
|
13
15
|
pathname: string;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
routeDirectory: string | null;
|
|
17
|
+
routeId: string;
|
|
16
18
|
title: string;
|
|
17
19
|
};
|
|
18
20
|
|
|
@@ -25,21 +27,21 @@ const stripRouteIdPrefix = (id: string) => {
|
|
|
25
27
|
|
|
26
28
|
export const isHomePageEntry = (entry: SiteEntry) => stripRouteIdPrefix(entry.id) === null;
|
|
27
29
|
|
|
28
|
-
const
|
|
30
|
+
const getRouteDirectory = (entry: SiteEntry) => isHomePageEntry(entry) ? null : stripRouteIdPrefix(entry.id);
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const routeFolder = getRouteFolder(entry);
|
|
34
|
-
return entry.data.slug ?? routeFolder ?? '';
|
|
32
|
+
const getRouteMetadata = (entry: SiteEntry) => {
|
|
33
|
+
const routeDirectory = getRouteDirectory(entry);
|
|
34
|
+
return routeDirectory ? parseRouteDirectory(routeDirectory, `route directory ${routeDirectory}`) : null;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
const getPagePathname = (
|
|
37
|
+
const getPagePathname = (pathSegment: string) => pathSegment ? `/${pathSegment}/` : '/';
|
|
38
38
|
|
|
39
39
|
export const getSitePage = (entry: SiteEntry): SitePage => {
|
|
40
40
|
const isHome = isHomePageEntry(entry);
|
|
41
|
-
const
|
|
42
|
-
const
|
|
41
|
+
const routeMetadata = getRouteMetadata(entry);
|
|
42
|
+
const routeDirectory = routeMetadata?.routeDirectory ?? null;
|
|
43
|
+
const routeId = routeMetadata?.routeId ?? '';
|
|
44
|
+
const pathSegment = isHome ? '' : routeId;
|
|
43
45
|
const navigation = entry.data.navigation ?? {};
|
|
44
46
|
|
|
45
47
|
return {
|
|
@@ -48,11 +50,12 @@ export const getSitePage = (entry: SiteEntry): SitePage => {
|
|
|
48
50
|
navigation: {
|
|
49
51
|
include: navigation.include ?? true,
|
|
50
52
|
label: navigation.label ?? entry.data.title,
|
|
51
|
-
order:
|
|
53
|
+
order: isHome ? 0 : routeMetadata?.routeOrder ?? 100,
|
|
52
54
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
pathSegment,
|
|
56
|
+
pathname: getPagePathname(pathSegment),
|
|
57
|
+
routeDirectory,
|
|
58
|
+
routeId,
|
|
56
59
|
title: entry.data.title,
|
|
57
60
|
};
|
|
58
61
|
};
|
|
@@ -60,6 +63,8 @@ export const getSitePage = (entry: SiteEntry): SitePage => {
|
|
|
60
63
|
export const getSitePages = (entries: SiteEntry[]) => {
|
|
61
64
|
const pages = entries.map(getSitePage);
|
|
62
65
|
const pathnames = new Map<string, SitePage>();
|
|
66
|
+
const routeIds = new Map<string, SitePage>();
|
|
67
|
+
const routeOrders = new Map<number, SitePage>();
|
|
63
68
|
|
|
64
69
|
for (const page of pages) {
|
|
65
70
|
const existing = pathnames.get(page.pathname);
|
|
@@ -68,6 +73,22 @@ export const getSitePages = (entries: SiteEntry[]) => {
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
pathnames.set(page.pathname, page);
|
|
76
|
+
|
|
77
|
+
if (page.isHome) continue;
|
|
78
|
+
|
|
79
|
+
const existingRouteId = routeIds.get(page.routeId);
|
|
80
|
+
if (existingRouteId) {
|
|
81
|
+
throw new Error(`Duplicate route id "${page.routeId}" from "${existingRouteId.entry.id}" and "${page.entry.id}".`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
routeIds.set(page.routeId, page);
|
|
85
|
+
|
|
86
|
+
const existingRouteOrder = routeOrders.get(page.navigation.order);
|
|
87
|
+
if (existingRouteOrder) {
|
|
88
|
+
throw new Error(`Duplicate route order "${String(page.navigation.order).padStart(3, '0')}" from "${existingRouteOrder.entry.id}" and "${page.entry.id}".`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
routeOrders.set(page.navigation.order, page);
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
return pages.sort((left, right) => (
|
package/src/pages/[slug].astro
CHANGED
|
@@ -10,7 +10,7 @@ export async function getStaticPaths() {
|
|
|
10
10
|
return pages
|
|
11
11
|
.filter((page) => !page.isHome)
|
|
12
12
|
.map((page) => ({
|
|
13
|
-
params: { slug: page.
|
|
13
|
+
params: { slug: page.pathSegment },
|
|
14
14
|
props: { entry: page.entry },
|
|
15
15
|
}));
|
|
16
16
|
}
|
|
@@ -23,8 +23,8 @@ if (!entry || isHomePageEntry(entry)) {
|
|
|
23
23
|
|
|
24
24
|
const currentPage = getSitePage(entry);
|
|
25
25
|
|
|
26
|
-
if (!currentPage.
|
|
27
|
-
throw new Error(`Route "${entry.id}" needs a
|
|
26
|
+
if (!currentPage.pathSegment) {
|
|
27
|
+
throw new Error(`Route "${entry.id}" needs a route id.`);
|
|
28
28
|
}
|
|
29
29
|
---
|
|
30
30
|
|
package/src/styles/global.css
CHANGED
|
@@ -458,6 +458,182 @@ ul {
|
|
|
458
458
|
margin-top: 0.35rem;
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
.card-list {
|
|
462
|
+
display: grid;
|
|
463
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
|
|
464
|
+
gap: var(--card-list-gap, clamp(0.9rem, 2vw, 1.25rem));
|
|
465
|
+
width: min(100%, var(--card-list-width, var(--gallery-layout-width)));
|
|
466
|
+
margin: 0 auto var(--space-section-body-to-gallery);
|
|
467
|
+
text-align: left;
|
|
468
|
+
--card-list-width: var(--gallery-layout-width);
|
|
469
|
+
--card-list-gap: clamp(0.9rem, 2vw, 1.25rem);
|
|
470
|
+
--card-list-item-gap: 0.85rem;
|
|
471
|
+
--card-list-padding: clamp(0.9rem, 2vw, 1.15rem);
|
|
472
|
+
--card-list-media-width: 32%;
|
|
473
|
+
--card-list-media-min-width: 7rem;
|
|
474
|
+
--card-list-top-media-width: 20rem;
|
|
475
|
+
--card-list-title-scale: 1;
|
|
476
|
+
--card-list-text-scale: 1;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.card-list-width-text {
|
|
480
|
+
--card-list-width: var(--text-width);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.card-list-width-narrow {
|
|
484
|
+
--card-list-width: min(48rem, var(--gallery-layout-width));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.card-list-width-wide {
|
|
488
|
+
--card-list-width: var(--page-width);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.card-list-size-s {
|
|
492
|
+
--card-list-gap: 0.75rem;
|
|
493
|
+
--card-list-item-gap: 0.65rem;
|
|
494
|
+
--card-list-padding: 0.8rem;
|
|
495
|
+
--card-list-media-width: 26%;
|
|
496
|
+
--card-list-media-min-width: 5.5rem;
|
|
497
|
+
--card-list-top-media-width: 14rem;
|
|
498
|
+
--card-list-title-scale: 0.92;
|
|
499
|
+
--card-list-text-scale: 0.92;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.card-list-size-l {
|
|
503
|
+
--card-list-gap: clamp(1rem, 2.4vw, 1.5rem);
|
|
504
|
+
--card-list-item-gap: 1rem;
|
|
505
|
+
--card-list-padding: clamp(1.1rem, 2.4vw, 1.45rem);
|
|
506
|
+
--card-list-media-width: 36%;
|
|
507
|
+
--card-list-media-min-width: 8.5rem;
|
|
508
|
+
--card-list-top-media-width: 24rem;
|
|
509
|
+
--card-list-title-scale: 1.08;
|
|
510
|
+
--card-list-text-scale: 1.02;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.card-list-size-xl {
|
|
514
|
+
--card-list-gap: clamp(1.15rem, 2.8vw, 1.8rem);
|
|
515
|
+
--card-list-item-gap: 1.1rem;
|
|
516
|
+
--card-list-padding: clamp(1.25rem, 3vw, 1.75rem);
|
|
517
|
+
--card-list-media-width: 40%;
|
|
518
|
+
--card-list-media-min-width: 10rem;
|
|
519
|
+
--card-list-top-media-width: 28rem;
|
|
520
|
+
--card-list-title-scale: 1.18;
|
|
521
|
+
--card-list-text-scale: 1.06;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.card-list-flow-stack {
|
|
525
|
+
grid-template-columns: 1fr;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.card-list-item {
|
|
529
|
+
display: grid;
|
|
530
|
+
gap: var(--card-list-item-gap);
|
|
531
|
+
align-content: start;
|
|
532
|
+
margin: 0;
|
|
533
|
+
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
|
534
|
+
border-radius: 8px;
|
|
535
|
+
padding: var(--card-list-padding);
|
|
536
|
+
background: color-mix(in srgb, var(--section-background-color, var(--color-page)) 92%, currentColor 8%);
|
|
537
|
+
color: inherit;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.card-list-layout-image-left .card-list-item-has-image,
|
|
541
|
+
.card-list-layout-image-right .card-list-item-has-image {
|
|
542
|
+
grid-template-columns: minmax(var(--card-list-media-min-width), var(--card-list-media-width)) minmax(0, 1fr);
|
|
543
|
+
align-items: center;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.card-list-layout-image-right .card-list-item-has-image {
|
|
547
|
+
grid-template-columns: minmax(0, 1fr) minmax(var(--card-list-media-min-width), var(--card-list-media-width));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.card-list-layout-image-left .card-list-item-linked,
|
|
551
|
+
.card-list-layout-image-right .card-list-item-linked {
|
|
552
|
+
grid-template-columns: 1fr;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.card-list-layout-image-right .card-list-image-frame {
|
|
556
|
+
order: 2;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.card-list-link {
|
|
560
|
+
display: grid;
|
|
561
|
+
gap: var(--card-list-item-gap);
|
|
562
|
+
width: 100%;
|
|
563
|
+
color: inherit;
|
|
564
|
+
text-decoration: none;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.card-list-layout-image-left .card-list-item-has-image .card-list-link,
|
|
568
|
+
.card-list-layout-image-right .card-list-item-has-image .card-list-link {
|
|
569
|
+
grid-template-columns: minmax(var(--card-list-media-min-width), var(--card-list-media-width)) minmax(0, 1fr);
|
|
570
|
+
align-items: center;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.card-list-layout-image-right .card-list-item-has-image .card-list-link {
|
|
574
|
+
grid-template-columns: minmax(0, 1fr) minmax(var(--card-list-media-min-width), var(--card-list-media-width));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.card-list-link:hover .card-list-title,
|
|
578
|
+
.card-list-link:focus-visible .card-list-title {
|
|
579
|
+
text-decoration: underline;
|
|
580
|
+
text-underline-offset: 0.18em;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.card-list-image-frame {
|
|
584
|
+
display: grid;
|
|
585
|
+
width: 100%;
|
|
586
|
+
aspect-ratio: 16 / 10;
|
|
587
|
+
place-items: center;
|
|
588
|
+
overflow: hidden;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.card-list-flow-stack.card-list-layout-image-top .card-list-image-frame {
|
|
592
|
+
width: min(100%, var(--card-list-top-media-width));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.card-list-image {
|
|
596
|
+
display: block;
|
|
597
|
+
width: 100%;
|
|
598
|
+
height: 100%;
|
|
599
|
+
object-fit: contain;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.card-list-content {
|
|
603
|
+
display: grid;
|
|
604
|
+
gap: 0.4rem;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.card-list-badge {
|
|
608
|
+
justify-self: start;
|
|
609
|
+
margin: 0 0 0.15rem;
|
|
610
|
+
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
|
|
611
|
+
border-radius: 999px;
|
|
612
|
+
padding: 0.18rem 0.48rem;
|
|
613
|
+
background: color-mix(in srgb, currentColor 9%, transparent);
|
|
614
|
+
color: inherit;
|
|
615
|
+
font-size: calc(var(--section-body-font-size-desktop, 1rem) * 0.72);
|
|
616
|
+
font-weight: 700;
|
|
617
|
+
letter-spacing: 0;
|
|
618
|
+
line-height: 1.2;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.card-list-title {
|
|
622
|
+
margin: 0;
|
|
623
|
+
color: inherit;
|
|
624
|
+
font-size: calc(var(--section-markdown-h3-font-size-desktop, 1.1rem) * var(--card-list-title-scale));
|
|
625
|
+
font-weight: 700;
|
|
626
|
+
letter-spacing: 0;
|
|
627
|
+
line-height: var(--section-markdown-h3-line-height, 1.35);
|
|
628
|
+
text-transform: none;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.card-list-content p:not(.card-list-badge) {
|
|
632
|
+
margin: 0;
|
|
633
|
+
font-size: calc(var(--section-body-font-size-desktop, 1rem) * var(--card-list-text-scale));
|
|
634
|
+
line-height: var(--section-body-line-height, 1.5);
|
|
635
|
+
}
|
|
636
|
+
|
|
461
637
|
.gallery {
|
|
462
638
|
display: grid;
|
|
463
639
|
gap: var(--space-gallery-item);
|
|
@@ -858,6 +1034,17 @@ ul {
|
|
|
858
1034
|
text-align: var(--section-markdown-h4-align-mobile, var(--section-markdown-h4-align-desktop, left));
|
|
859
1035
|
}
|
|
860
1036
|
|
|
1037
|
+
.card-list-layout-image-left .card-list-item-has-image,
|
|
1038
|
+
.card-list-layout-image-right .card-list-item-has-image,
|
|
1039
|
+
.card-list-layout-image-left .card-list-item-has-image .card-list-link,
|
|
1040
|
+
.card-list-layout-image-right .card-list-item-has-image .card-list-link {
|
|
1041
|
+
grid-template-columns: 1fr;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.card-list-layout-image-right .card-list-image-frame {
|
|
1045
|
+
order: 0;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
861
1048
|
.section-markdown ul,
|
|
862
1049
|
.section-markdown ol {
|
|
863
1050
|
display: block;
|
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
title: Example Site
|
|
3
3
|
description: Minimal norna starter site.
|
|
4
4
|
sections:
|
|
5
|
-
|
|
5
|
+
intro:
|
|
6
6
|
presentation:
|
|
7
7
|
typography:
|
|
8
8
|
preset: statement
|
|
9
|
-
- id: work
|
|
10
|
-
- id: lorem1
|
|
11
|
-
- id: lorem2
|
|
12
|
-
- id: lorem3
|
|
13
9
|
|
|
14
10
|
---
|
|
15
11
|
|
|
@@ -19,7 +15,15 @@ routes defined, the navigation menu will be a sticky bar with anchor links for
|
|
|
19
15
|
ease of use on phones etc. [Highlighted inline text]{.highlight} when a short
|
|
20
16
|
phrase needs emphasis
|
|
21
17
|
## Work {#work}
|
|
22
|
-
Add image
|
|
18
|
+
Add Norna image blocks in Markdown and matching source images under
|
|
19
|
+
`images/work/`.
|
|
20
|
+
|
|
21
|
+
````
|
|
22
|
+
```norna-image-stack
|
|
23
|
+
- image: example.jpg
|
|
24
|
+
caption: Replace this with a real image and caption.
|
|
25
|
+
```
|
|
26
|
+
````
|
|
23
27
|
## What is Lorem Ipsum? {#lorem1}
|
|
24
28
|
Lorem Ipsum is simply dummy text of the printing and typesetting
|
|
25
29
|
industry. Lorem Ipsum has been the industry's standard dummy text ever
|
|
@@ -24,7 +24,7 @@ npm run dev -- --kill
|
|
|
24
24
|
density, typography rhythm, colors, frame colors, and inline text styles.
|
|
25
25
|
- `site/content.md`: homepage title, sections, placeholders, project summary,
|
|
26
26
|
links, install command, example usage, benefits, next steps, and license.
|
|
27
|
-
- `site/routes/guide/route-content.md`: short secondary page with realistic
|
|
27
|
+
- `site/routes/010-guide/route-content.md`: short secondary page with realistic
|
|
28
28
|
project guide content.
|
|
29
29
|
- `site/public/robots.txt`: static public file copied into the built site.
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ GitHub Pages workflow uses `npm ci`.
|
|
|
36
36
|
|
|
37
37
|
1. Replace the project name, tagline, links, install command, example usage,
|
|
38
38
|
benefits, use cases, and license in `site/content.md`.
|
|
39
|
-
2. Replace the guide examples in `site/routes/guide/route-content.md`, or
|
|
39
|
+
2. Replace the guide examples in `site/routes/010-guide/route-content.md`, or
|
|
40
40
|
delete the route if the homepage is enough.
|
|
41
41
|
3. Edit `site/theme.md` for brand name, layout density, typography rhythm,
|
|
42
42
|
colors, frame colors, and inline text styles.
|
|
@@ -3,16 +3,6 @@ title: Your project
|
|
|
3
3
|
description: A compact project site starter for an open source developer tool.
|
|
4
4
|
navigation:
|
|
5
5
|
label: Home
|
|
6
|
-
order: 0
|
|
7
|
-
sections:
|
|
8
|
-
- id: intro
|
|
9
|
-
- id: links
|
|
10
|
-
- id: install
|
|
11
|
-
- id: example
|
|
12
|
-
- id: why
|
|
13
|
-
- id: good-fit
|
|
14
|
-
- id: next
|
|
15
|
-
- id: license
|
|
16
6
|
---
|
|
17
7
|
|
|
18
8
|
## `<Your project> - <short value proposition>` {#intro}
|