@janga/norna 0.7.27 → 0.7.29
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 +41 -29
- package/astro.config.mjs +33 -3
- package/bin/norna-cli.mjs +5 -3
- package/package.json +18 -12
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +81 -66
- package/schemas/content-frontmatter.schema.json +23 -7
- package/schemas/manifest.json +49 -3
- package/schemas/page-theme.schema.json +1585 -41
- package/schemas/root-theme.schema.json +1767 -0
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +243 -243
- package/scripts/add-site-node.mjs +31 -250
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +51 -17
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +89 -70
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +70 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +20 -0
- package/scripts/lib/editor-language-service.mjs +74 -73
- package/scripts/lib/editor-page-addresses.mjs +57 -0
- package/scripts/lib/editor-page-placement.mjs +102 -0
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +181 -0
- package/scripts/lib/editor-site-files.mjs +154 -0
- package/scripts/lib/editor-site-links.mjs +81 -0
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +158 -0
- package/scripts/lib/editor-source-files.mjs +87 -0
- package/scripts/lib/editor-theme-help.mjs +43 -0
- package/scripts/lib/locale-registry.mjs +1 -1
- package/scripts/lib/locales/en.mjs +1 -1
- package/scripts/lib/logo-assets.mjs +2 -1
- package/scripts/lib/markdown-images.mjs +59 -0
- package/scripts/lib/markdown-links.mjs +29 -9
- package/scripts/lib/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -58
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +8 -1
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +19 -36
- package/scripts/lib/page-move-apply.mjs +119 -0
- package/scripts/lib/page-move-plan.mjs +64 -78
- package/scripts/lib/page-order-source.mjs +22 -0
- package/scripts/lib/page-order.mjs +45 -0
- package/scripts/lib/page-placement-cli.mjs +50 -0
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +10 -8
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +15 -38
- package/scripts/lib/schema-editor-metadata.mjs +16 -60
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +8 -25
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +182 -0
- package/scripts/lib/site-paths.mjs +24 -12
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +86 -95
- package/scripts/lib/site-upgrade.mjs +32 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +34 -0
- package/scripts/lib/structured-blocks.mjs +3 -1
- package/scripts/lib/theme-config.mjs +26 -68
- package/scripts/lib/theme-packages.mjs +47 -0
- package/scripts/lib/theme-presets.mjs +20 -53
- package/scripts/lib/typography.mjs +2 -2
- package/scripts/lib/yaml-config.mjs +1 -1
- package/scripts/lib/yaml-source-edit.mjs +94 -0
- package/scripts/move-site-page.mjs +46 -254
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +17 -17
- package/scripts/sync-site-public.mjs +44 -66
- package/scripts/upgrade-site.mjs +17 -0
- package/src/components/AreaNavigationMenu.astro +24 -0
- package/src/components/AreaNavigationPanel.astro +55 -0
- package/src/components/AreaNavigationSwitcher.astro +30 -0
- package/src/components/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- package/src/components/ImageStackEnhancement.astro +0 -7
- package/src/components/NavigationContinuity.astro +26 -0
- package/src/components/NavigationPageTree.astro +13 -4
- package/src/components/NavigationState.astro +11 -0
- package/src/components/NavigationTreeControls.astro +3 -2
- package/src/components/SearchPage.astro +11 -2
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +38 -61
- package/src/components/SiteSection.astro +1 -0
- package/src/components/SiteTreeNavigation.astro +38 -18
- package/src/components/TopPageMenu.astro +4 -3
- package/src/components/TreeNavigationScript.astro +50 -57
- package/src/content.config.ts +11 -12
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -0
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -0
- package/src/lib/navigationContinuity.ts +212 -0
- package/src/lib/navigationFollowing.ts +28 -18
- package/src/lib/navigationState.mjs +88 -0
- package/src/lib/navigationStateScript.mjs +76 -0
- package/src/lib/navigationVisibility.ts +9 -0
- package/src/lib/pageThemes.ts +5 -42
- package/src/lib/sectionContent.ts +67 -21
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +13 -36
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +7 -16
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +408 -0
- package/src/styles/content.css +8 -29
- package/src/styles/media.css +24 -0
- package/src/styles/navigation-continuity.css +163 -0
- package/src/styles/page-layout.css +57 -68
- package/src/styles/responsive.css +0 -4
- package/starters/basic/README.md +9 -8
- package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
- package/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
- package/schemas/category.schema.json +0 -20
- package/src/components/PageContentsNavigation.astro +0 -20
- /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
- /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { getPageImageSourceKey } from '../../scripts/lib/site-conventions.mjs';
|
|
2
|
+
import { readPageAttachments, resolveAttachment, attachmentHref } from '../../scripts/lib/page-attachments.mjs';
|
|
1
3
|
import { markdownToHtml } from 'satteri';
|
|
4
|
+
import { parseFragment } from 'parse5';
|
|
2
5
|
import { groupRenderedTabs } from '../../scripts/lib/content-tabs.mjs';
|
|
3
6
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
4
7
|
import {
|
|
@@ -7,12 +10,20 @@ import {
|
|
|
7
10
|
import { splitNornaRenderedBlocks } from '../../scripts/lib/norna-markdown-blocks.mjs';
|
|
8
11
|
import { applyBasePathToHtml } from './basePath';
|
|
9
12
|
import type { SitePage } from './sitePages';
|
|
13
|
+
import { splitMarkdownImages, renderNestedMarkdownImages } from './markdownImages';
|
|
14
|
+
import { nornaMarkdownImagePlugin } from '../../scripts/lib/norna-markdown-render-plugin.mjs';
|
|
15
|
+
import { rewriteAttachmentLinks } from '../../scripts/lib/attachment-render.mjs';
|
|
16
|
+
import { siteDir } from '../../scripts/lib/site-paths.mjs';
|
|
17
|
+
import { pathToFileURL } from 'node:url';
|
|
10
18
|
|
|
11
19
|
type ManagedImage = {
|
|
12
20
|
image: string;
|
|
13
21
|
src: string;
|
|
14
22
|
alt?: string;
|
|
15
23
|
caption?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
href?: string;
|
|
26
|
+
linkTitle?: string;
|
|
16
27
|
};
|
|
17
28
|
type CardListItem = {
|
|
18
29
|
title: string;
|
|
@@ -93,40 +104,73 @@ const applyH3HeadingIds = (
|
|
|
93
104
|
html: string,
|
|
94
105
|
headings: Array<{ id: string | null }>,
|
|
95
106
|
) => {
|
|
107
|
+
const renderHeading = (attributes: string, headingHtml: string, heading: { id: string | null }) => {
|
|
108
|
+
if (!heading.id) throw new Error('Rendered Markdown H3 could not be matched to its heading id.');
|
|
109
|
+
const cleanAttributes = attributes
|
|
110
|
+
.replace(/\s+data-norna-page-heading="true"/g, '')
|
|
111
|
+
.replace(/\s+id=(["']).*?\1/gi, '');
|
|
112
|
+
const cleanHeadingHtml = headingHtml.replace(explicitHeadingIdRegex, '').trim();
|
|
113
|
+
return `<h3${cleanAttributes} id="${heading.id}">${cleanHeadingHtml}</h3>`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Content rendered before the heading marker was introduced may still be in
|
|
117
|
+
// Astro's content cache. Its top-level H3s can be identified from the HTML tree.
|
|
118
|
+
if (!html.includes('data-norna-page-heading=') && headings.length > 0) {
|
|
119
|
+
const fragment = parseFragment(html, { sourceCodeLocationInfo: true });
|
|
120
|
+
const renderedHeadings = fragment.childNodes.filter((node) => (
|
|
121
|
+
'tagName' in node && node.tagName === 'h3' && node.sourceCodeLocation?.startTag && node.sourceCodeLocation?.endTag
|
|
122
|
+
));
|
|
123
|
+
if (renderedHeadings.length !== headings.length) {
|
|
124
|
+
throw new Error('Markdown H3 headings could not be matched to the rendered page content.');
|
|
125
|
+
}
|
|
126
|
+
let result = html;
|
|
127
|
+
for (let index = renderedHeadings.length - 1; index >= 0; index -= 1) {
|
|
128
|
+
const location = renderedHeadings[index]?.sourceCodeLocation;
|
|
129
|
+
const heading = headings[index];
|
|
130
|
+
if (!location?.startTag || !location.endTag || !heading) {
|
|
131
|
+
throw new Error('Rendered Markdown H3 could not be matched to its heading id.');
|
|
132
|
+
}
|
|
133
|
+
const source = html.slice(location.startOffset, location.endOffset);
|
|
134
|
+
const replacement = source.replace(/<h3\b([^>]*)>([\s\S]*?)<\/h3>/i, (_match, attributes, headingHtml) =>
|
|
135
|
+
renderHeading(String(attributes), String(headingHtml), heading));
|
|
136
|
+
result = result.slice(0, location.startOffset) + replacement + result.slice(location.endOffset);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
96
141
|
let headingIndex = 0;
|
|
97
|
-
const result = html.replace(/<h3\b([^>]*)>([\s\S]*?)<\/h3>/gi, (
|
|
142
|
+
const result = html.replace(/<h3\b([^>]*)>([\s\S]*?)<\/h3>/gi, (source, attributes, headingHtml) => {
|
|
143
|
+
if (!/\sdata-norna-page-heading="true"/.test(attributes)) return source;
|
|
98
144
|
const heading = headings[headingIndex];
|
|
99
145
|
headingIndex += 1;
|
|
100
|
-
if (!heading
|
|
101
|
-
|
|
102
|
-
const cleanAttributes = String(attributes).replace(/\s+id=(["']).*?\1/gi, '');
|
|
103
|
-
const cleanHeadingHtml = String(headingHtml).replace(explicitHeadingIdRegex, '').trim();
|
|
104
|
-
return `<h3${cleanAttributes} id="${heading.id}">${cleanHeadingHtml}</h3>`;
|
|
146
|
+
if (!heading) throw new Error('Rendered Markdown H3 could not be matched to its heading id.');
|
|
147
|
+
return renderHeading(String(attributes), String(headingHtml), heading);
|
|
105
148
|
});
|
|
106
|
-
|
|
107
149
|
if (headingIndex !== headings.length) {
|
|
108
150
|
throw new Error('Markdown H3 headings could not be matched to the rendered page content.');
|
|
109
151
|
}
|
|
110
|
-
|
|
111
152
|
return result;
|
|
112
153
|
};
|
|
113
154
|
|
|
114
|
-
const getImageSourceKey = (page: SitePage, image: string) =>
|
|
155
|
+
const getImageSourceKey = (page: SitePage, image: string) => getPageImageSourceKey(page.pageDirectory, image);
|
|
115
156
|
|
|
116
|
-
const renderHeadingTitleHtml = async (headingSource: string) => {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const result = await markdownToHtml(
|
|
157
|
+
const renderHeadingTitleHtml = async (headingSource: string, page: SitePage) => {
|
|
158
|
+
const references = [...page.markdownDocument.markdownImages, ...page.markdownDocument.links];
|
|
159
|
+
const definitions = [...new Set(references.map((reference) => reference.definitionSource).filter(Boolean))].join('\n');
|
|
160
|
+
// Keep the heading syntax: text such as "1. Plan" becomes a list if parsed alone.
|
|
161
|
+
const source = await rewriteAttachmentLinks(`${headingSource}\n\n${definitions}`, pathToFileURL(page.contentPath), siteDir);
|
|
162
|
+
const result = await markdownToHtml(source, {
|
|
163
|
+
mdastPlugins: [nornaMarkdownImagePlugin],
|
|
122
164
|
features: {
|
|
123
165
|
gfm: true,
|
|
124
166
|
smartPunctuation: true,
|
|
125
167
|
},
|
|
126
168
|
});
|
|
127
|
-
const
|
|
128
|
-
if (!
|
|
129
|
-
|
|
169
|
+
const heading = result.html.trim().match(/^<h([12])\b[^>]*>([\s\S]*)<\/h\1>$/i);
|
|
170
|
+
if (!heading) throw new Error('Markdown heading could not be rendered as inline content.');
|
|
171
|
+
const html = prepareContentHtml((heading[2] ?? '').replace(explicitHeadingIdRegex, '').trim());
|
|
172
|
+
const marked = splitMarkdownImages(html, page.pageDirectory).map((block) => block.type === 'html' ? block.html : '').join('');
|
|
173
|
+
return renderNestedMarkdownImages(marked, page.title, { desktop: 100, mobile: 100 });
|
|
130
174
|
};
|
|
131
175
|
|
|
132
176
|
const splitRenderedRegions = (html: string, regionCount: number) => {
|
|
@@ -158,11 +202,12 @@ const resolveContentBlocks = async (
|
|
|
158
202
|
|
|
159
203
|
for (const block of splitBlocks) {
|
|
160
204
|
if (block.type === 'html') {
|
|
161
|
-
resolvedBlocks.push(
|
|
205
|
+
resolvedBlocks.push(...splitMarkdownImages(prepareContentHtml(block.html), page.pageDirectory));
|
|
162
206
|
continue;
|
|
163
207
|
}
|
|
164
208
|
|
|
165
209
|
if (block.type === 'card-list') {
|
|
210
|
+
const { files } = await readPageAttachments(page);
|
|
166
211
|
resolvedBlocks.push({
|
|
167
212
|
type: 'card-list',
|
|
168
213
|
layout: block.layout,
|
|
@@ -171,6 +216,7 @@ const resolveContentBlocks = async (
|
|
|
171
216
|
width: block.width,
|
|
172
217
|
cards: block.cards.map((card: CardListItem) => ({
|
|
173
218
|
...card,
|
|
219
|
+
...(card.link && resolveAttachment(card.link, page.pathname, files) ? { link: attachmentHref(resolveAttachment(card.link, page.pathname, files), card.link) } : {}),
|
|
174
220
|
...(card.image ? { src: getImageSourceKey(page, card.image) } : {}),
|
|
175
221
|
})),
|
|
176
222
|
});
|
|
@@ -178,7 +224,7 @@ const resolveContentBlocks = async (
|
|
|
178
224
|
}
|
|
179
225
|
if (block.type === 'page-list') {
|
|
180
226
|
if (childPages.length === 0) {
|
|
181
|
-
throw new Error(`${page.contentLabel} line ${block.line}: page-list has no listed direct child pages to display.
|
|
227
|
+
throw new Error(`${page.contentLabel} line ${block.line}: page-list has no listed direct child pages to display. Add a listed direct child page or remove the block.`);
|
|
182
228
|
}
|
|
183
229
|
resolvedBlocks.push({
|
|
184
230
|
type: 'page-list',
|
|
@@ -257,7 +303,7 @@ export const getSectionsContent = async (
|
|
|
257
303
|
sections.push({
|
|
258
304
|
id,
|
|
259
305
|
title,
|
|
260
|
-
titleHtml: await renderHeadingTitleHtml(bodySection.heading.source),
|
|
306
|
+
titleHtml: await renderHeadingTitleHtml(bodySection.heading.source, page),
|
|
261
307
|
headingLevel,
|
|
262
308
|
contentBlocks: groupRenderedTabs(
|
|
263
309
|
await resolveContentBlocks(content, bodySection.blocks, page, childPages),
|
|
@@ -7,6 +7,12 @@ import {
|
|
|
7
7
|
getSequentialPageNavigation as getSequentialPageNavigationShared,
|
|
8
8
|
} from '../../scripts/lib/site-navigation-tree.mjs';
|
|
9
9
|
|
|
10
|
+
export type CategoryDestinationMap = ReadonlyMap<string, { target: SitePage | null }>;
|
|
11
|
+
|
|
12
|
+
export const getNavigationDestination = (node: SiteNode, categoryDestinations?: CategoryDestinationMap): SiteNode => (
|
|
13
|
+
categoryDestinations?.get(node.pathname)?.target ?? node
|
|
14
|
+
);
|
|
15
|
+
|
|
10
16
|
export type SiteNavigationEntry = {
|
|
11
17
|
node: SiteNode;
|
|
12
18
|
headings: HeadingNavigation[];
|
package/src/lib/sitePages.ts
CHANGED
|
@@ -3,10 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import { decodePageEntryId, parsePageDirectoryPath } from '../../scripts/lib/page-model.mjs';
|
|
4
4
|
import { parsePageMarkdown } from '../../scripts/lib/page-markdown.mjs';
|
|
5
5
|
import { readSiteFile } from '../../scripts/lib/site-content.mjs';
|
|
6
|
-
import { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
|
|
6
|
+
import { homePageDirectory, getPageSourceDirectory } from '../../scripts/lib/site-conventions.mjs';
|
|
7
7
|
import { assertCategoryDestinationModel, createCategoryDestinationModel } from '../../scripts/lib/category-destinations.mjs';
|
|
8
8
|
import { getSiteStructure } from '../../scripts/lib/site-structure.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { siteDir, siteDirLabel } from '../../scripts/lib/site-paths.mjs';
|
|
10
10
|
|
|
11
11
|
type SiteEntry = CollectionEntry<'site'>;
|
|
12
12
|
|
|
@@ -16,7 +16,7 @@ type SiteNodeBase = {
|
|
|
16
16
|
isHome: boolean;
|
|
17
17
|
navigation: {
|
|
18
18
|
listed: boolean;
|
|
19
|
-
order: number;
|
|
19
|
+
order: number | null;
|
|
20
20
|
};
|
|
21
21
|
depth: number;
|
|
22
22
|
pageDirectories: string[];
|
|
@@ -25,7 +25,6 @@ type SiteNodeBase = {
|
|
|
25
25
|
pageDirectory: string;
|
|
26
26
|
pageId: string;
|
|
27
27
|
pageIds: string[];
|
|
28
|
-
pageOrders: number[];
|
|
29
28
|
pagePath: string;
|
|
30
29
|
parentPagePath: string | null;
|
|
31
30
|
title: string;
|
|
@@ -44,6 +43,7 @@ export type SitePage = SiteNodeBase & {
|
|
|
44
43
|
export type SiteCategory = SiteNodeBase & {
|
|
45
44
|
kind: 'category';
|
|
46
45
|
categorySourceLabel: string;
|
|
46
|
+
description?: string;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
export type SiteNode = SitePage | SiteCategory;
|
|
@@ -63,20 +63,11 @@ const getPageMetadata = (entry: SiteEntry) => {
|
|
|
63
63
|
|
|
64
64
|
const getPagePathname = (pathSegment: string) => pathSegment ? `/${pathSegment}/` : '/';
|
|
65
65
|
|
|
66
|
-
const compareNumberPaths = (left: number[], right: number[]) => {
|
|
67
|
-
const sharedLength = Math.min(left.length, right.length);
|
|
68
|
-
for (let index = 0; index < sharedLength; index += 1) {
|
|
69
|
-
const difference = left[index] - right[index];
|
|
70
|
-
if (difference !== 0) return difference;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return left.length - right.length;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
66
|
const readPageMarkdownDocument = async (entry: SiteEntry) => {
|
|
77
67
|
const pageDirectory = getPageDirectory(entry);
|
|
78
|
-
const
|
|
79
|
-
const
|
|
68
|
+
const contentRelativePath = [getPageSourceDirectory(pageDirectory), 'content.md'].filter(Boolean).join('/');
|
|
69
|
+
const contentLabel = `${siteDirLabel}/${contentRelativePath}`;
|
|
70
|
+
const contentPath = path.join(siteDir, contentRelativePath);
|
|
80
71
|
const { body, frontmatter } = await readSiteFile(contentPath, contentLabel);
|
|
81
72
|
const markdownDocument = await parsePageMarkdown(body, {
|
|
82
73
|
label: contentLabel,
|
|
@@ -110,7 +101,7 @@ const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
|
|
|
110
101
|
depth: pageMetadata.depth,
|
|
111
102
|
navigation: {
|
|
112
103
|
listed: navigation.listed ?? true,
|
|
113
|
-
order: isHome ? 0 :
|
|
104
|
+
order: isHome ? 0 : navigation.order ?? null,
|
|
114
105
|
},
|
|
115
106
|
pathSegment,
|
|
116
107
|
pathname: getPagePathname(pathSegment),
|
|
@@ -118,7 +109,6 @@ const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
|
|
|
118
109
|
pageDirectories: pageMetadata.pageDirectories,
|
|
119
110
|
pageId,
|
|
120
111
|
pageIds: pageMetadata.pageIds,
|
|
121
|
-
pageOrders: pageMetadata.pageOrders,
|
|
122
112
|
pagePath,
|
|
123
113
|
parentPagePath: isHome ? null : pageMetadata.parentPagePath,
|
|
124
114
|
contentLabel,
|
|
@@ -141,11 +131,11 @@ const createSiteCategory = (category: Awaited<ReturnType<typeof getSiteStructure
|
|
|
141
131
|
pathSegment: category.pagePath,
|
|
142
132
|
pathname: getPagePathname(category.pagePath),
|
|
143
133
|
categorySourceLabel: category.categorySourceLabel,
|
|
134
|
+
description: category.description,
|
|
144
135
|
pageDirectory: category.pageDirectory,
|
|
145
136
|
pageDirectories: category.pageDirectories,
|
|
146
137
|
pageId: category.pageId,
|
|
147
138
|
pageIds: category.pageIds,
|
|
148
|
-
pageOrders: category.pageOrders,
|
|
149
139
|
pagePath: category.pagePath,
|
|
150
140
|
parentPagePath: category.parentPagePath,
|
|
151
141
|
title: category.label,
|
|
@@ -157,11 +147,10 @@ const createSitePages = async (entries: SiteEntry[]) => {
|
|
|
157
147
|
const pages = await Promise.all(entries.map(createSitePage));
|
|
158
148
|
const pathnames = new Map<string, SitePage>();
|
|
159
149
|
const siblingPageIds = new Map<string, SitePage>();
|
|
160
|
-
const siblingPageOrders = new Map<string, SitePage>();
|
|
161
150
|
|
|
162
151
|
for (const page of pages) {
|
|
163
152
|
if (page.isHome && page.navigation.listed === false) {
|
|
164
|
-
throw new Error(
|
|
153
|
+
throw new Error(`The homepage at ${siteDirLabel}/root/content.md cannot set navigation.listed to false.`);
|
|
165
154
|
}
|
|
166
155
|
const existing = pathnames.get(page.pathname);
|
|
167
156
|
if (existing) {
|
|
@@ -181,24 +170,12 @@ const createSitePages = async (entries: SiteEntry[]) => {
|
|
|
181
170
|
|
|
182
171
|
siblingPageIds.set(pageIdKey, page);
|
|
183
172
|
|
|
184
|
-
const pageOrderKey = `${siblingScope}\0${page.navigation.order}`;
|
|
185
|
-
const existingPageOrder = siblingPageOrders.get(pageOrderKey);
|
|
186
|
-
if (existingPageOrder) {
|
|
187
|
-
throw new Error(`Duplicate sibling page order "${String(page.navigation.order).padStart(3, '0')}" below "${page.parentPagePath ?? '/'}" from "${existingPageOrder.entry.id}" and "${page.entry.id}".`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
siblingPageOrders.set(pageOrderKey, page);
|
|
191
173
|
}
|
|
192
174
|
if (!pages.some(({ isHome }) => isHome)) {
|
|
193
|
-
throw new Error(`Homepage
|
|
175
|
+
throw new Error(`Homepage content ${siteDirLabel}/root/content.md is missing.`);
|
|
194
176
|
}
|
|
195
177
|
|
|
196
|
-
return pages
|
|
197
|
-
left.isHome ? -1 : right.isHome ? 1 :
|
|
198
|
-
compareNumberPaths(left.pageOrders, right.pageOrders) ||
|
|
199
|
-
left.title.localeCompare(right.title, 'sv') ||
|
|
200
|
-
left.pathname.localeCompare(right.pathname, 'sv')
|
|
201
|
-
));
|
|
178
|
+
return pages;
|
|
202
179
|
};
|
|
203
180
|
|
|
204
181
|
export const getSiteModel = async (entries: SiteEntry[]) => {
|
|
@@ -225,7 +202,7 @@ export const getSiteModel = async (entries: SiteEntry[]) => {
|
|
|
225
202
|
categoryDestinations: assertCategoryDestinationModel(createCategoryDestinationModel(nodes)),
|
|
226
203
|
categories: nodes.filter((node): node is SiteCategory => node.kind === 'category'),
|
|
227
204
|
nodes,
|
|
228
|
-
pages,
|
|
205
|
+
pages: nodes.filter((node): node is SitePage => node.kind === 'page'),
|
|
229
206
|
warnings: structure.warnings,
|
|
230
207
|
};
|
|
231
208
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publicFilePublication } from '../../scripts/lib/publication-paths.mjs';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { sitePublicDir } from '../../scripts/lib/site-paths.mjs';
|
|
@@ -28,5 +29,5 @@ export const getIconLinks = () => faviconCandidates
|
|
|
28
29
|
.filter((candidate) => publicAssetExists(candidate.href))
|
|
29
30
|
.map((candidate) => ({
|
|
30
31
|
...candidate,
|
|
31
|
-
href: withBasePath(projectConfig.site.basePath, candidate.href),
|
|
32
|
+
href: withBasePath(projectConfig.site.basePath, publicFilePublication(candidate.href.slice(1)).pathname),
|
|
32
33
|
}));
|
package/src/pages/404.astro
CHANGED
|
@@ -20,7 +20,7 @@ const homePage = sitePages.find((page) => page.isHome);
|
|
|
20
20
|
if (!homePage) throw new Error('The required homepage is missing from the site page model.');
|
|
21
21
|
|
|
22
22
|
const [siteTheme] = await getCollection('theme');
|
|
23
|
-
if (!siteTheme) throw new Error('site/theme.yaml is required. Add
|
|
23
|
+
if (!siteTheme) throw new Error('site/root/tree-theme.yaml is required. Add the shared site theme before building the site.');
|
|
24
24
|
const [sitewide] = await getCollection('sitewide');
|
|
25
25
|
const sitewideData = sitewide?.data ?? {};
|
|
26
26
|
const siteThemeData = siteTheme.data;
|
|
@@ -3,6 +3,7 @@ import { getCollection } from 'astro:content';
|
|
|
3
3
|
import { assertPageAliasModel, createPageAliasModel } from '../../scripts/lib/page-aliases.mjs';
|
|
4
4
|
import { getGeneratedSiteRoutes } from '../../scripts/lib/page-aliases.mjs';
|
|
5
5
|
import { getSitePublicFiles } from '../../scripts/lib/site-link-graph.mjs';
|
|
6
|
+
import { sitePublicDir } from '../../scripts/lib/site-paths.mjs';
|
|
6
7
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
7
8
|
import PageAliasRedirect from '../components/PageAliasRedirect.astro';
|
|
8
9
|
import SearchPage from '../components/SearchPage.astro';
|
|
@@ -15,7 +16,7 @@ export async function getStaticPaths() {
|
|
|
15
16
|
const aliasModel = assertPageAliasModel(createPageAliasModel({
|
|
16
17
|
categories: siteModel.categories,
|
|
17
18
|
pages: siteModel.pages,
|
|
18
|
-
publicFiles: await getSitePublicFiles(),
|
|
19
|
+
publicFiles: await getSitePublicFiles(sitePublicDir, projectConfig),
|
|
19
20
|
generatedRoutes: getGeneratedSiteRoutes({ searchEnabled: projectConfig.search.enabled }),
|
|
20
21
|
}));
|
|
21
22
|
const pagesByPathname = new Map(siteModel.pages.map((page) => [page.pathname, page]));
|
|
@@ -45,28 +46,18 @@ export async function getStaticPaths() {
|
|
|
45
46
|
? [{ params: { slug: 'search' }, props: { searchPage: true } }]
|
|
46
47
|
: [];
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
params: { slug: destination.category.pathSegment },
|
|
50
|
-
props: destination.kind === 'redirect' ? {
|
|
51
|
-
alias: {
|
|
52
|
-
targetPathname: destination.target.pathname,
|
|
53
|
-
targetTitle: destination.target.title,
|
|
54
|
-
categoryTitle: destination.category.title,
|
|
55
|
-
},
|
|
56
|
-
} : { categoryPath: destination.pathname },
|
|
57
|
-
}));
|
|
58
|
-
return [...pagePaths, ...aliasPaths, ...searchPath, ...categoryPaths];
|
|
49
|
+
return [...pagePaths, ...aliasPaths, ...searchPath];
|
|
59
50
|
}
|
|
60
51
|
|
|
61
|
-
const { alias, entry, searchPage
|
|
52
|
+
const { alias, entry, searchPage } = Astro.props;
|
|
62
53
|
|
|
63
|
-
if (!searchPage && !alias &&
|
|
54
|
+
if (!searchPage && !alias && (!entry || isHomePageEntry(entry))) {
|
|
64
55
|
throw new Error('Page entry is missing.');
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
const currentPage = entry ? await getSitePage(entry) : null;
|
|
68
59
|
|
|
69
|
-
if (!searchPage && !alias && !
|
|
60
|
+
if (!searchPage && !alias && !currentPage?.pathSegment) {
|
|
70
61
|
throw new Error(`Page "${entry.id}" needs a page id.`);
|
|
71
62
|
}
|
|
72
63
|
---
|
|
@@ -75,4 +66,4 @@ if (!searchPage && !alias && !categoryPath && !currentPage?.pathSegment) {
|
|
|
75
66
|
? <SearchPage />
|
|
76
67
|
: alias
|
|
77
68
|
? <PageAliasRedirect targetPathname={alias.targetPathname} targetTitle={alias.targetTitle} categoryTitle={alias.categoryTitle} />
|
|
78
|
-
: <SitePage entry={entry}
|
|
69
|
+
: <SitePage entry={entry} />}
|
package/src/pages/index.astro
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
3
|
import SitePage from '../components/SitePage.astro';
|
|
4
4
|
import { isHomePageEntry } from '../lib/sitePages';
|
|
5
|
-
import {
|
|
5
|
+
import { siteContentLabel } from '../../scripts/lib/site-paths.mjs';
|
|
6
6
|
|
|
7
|
-
const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
8
|
-
const siteContentLabel = `${siteDirectory || 'site'}/pages/${homePageDirectory}/content.md`;
|
|
9
7
|
const entries = await getCollection('site');
|
|
10
8
|
const site = entries.find(isHomePageEntry);
|
|
11
9
|
|