@janga/norna 0.7.23 → 0.7.25
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 +14 -18
- package/astro.config.mjs +14 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +25 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +80 -15
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +246 -246
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +16 -1
- package/scripts/dev-local.mjs +106 -11
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +3 -0
- package/scripts/lib/code-fence-metadata.mjs +228 -0
- package/scripts/lib/edit-source-link.mjs +102 -0
- package/scripts/lib/editor-language-service.mjs +32 -12
- package/scripts/lib/image-presentation.mjs +4 -0
- package/scripts/lib/navigation-model.mjs +34 -13
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +164 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +21 -1
- package/scripts/lib/page-markdown.mjs +32 -1
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +1 -10
- package/scripts/lib/project-config.mjs +141 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +30 -5
- package/scripts/lib/schema-editor-metadata.mjs +36 -4
- package/scripts/lib/schema-value-definitions.mjs +4 -1
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-link-graph.mjs +33 -2
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-paths.mjs +14 -2
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/table-render-plugin.mjs +33 -0
- package/scripts/lib/theme-presets.mjs +2 -2
- package/scripts/lib/theme-profiles.mjs +0 -4
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +67 -4
- package/scripts/sync-site-public.mjs +24 -5
- package/src/components/CardList.astro +1 -0
- package/src/components/CodeBlockCopyScript.astro +11 -5
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +9 -7
- package/src/components/ImageStack.astro +34 -9
- package/src/components/ImageStackEnhancement.astro +331 -0
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageContentsNavigation.astro +2 -3
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SectionNavigationScript.astro +45 -7
- package/src/components/SiteNavigation.astro +102 -51
- package/src/components/SitePage.astro +107 -39
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +11 -1
- package/src/components/TableOverflowScript.astro +251 -0
- package/src/components/TreeNavigationScript.astro +271 -34
- package/src/layouts/BaseLayout.astro +83 -2
- package/src/lib/generatedImages.ts +18 -0
- package/src/lib/sectionContent.ts +36 -30
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +6 -3
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +15 -5
- package/src/styles/content.css +538 -39
- package/src/styles/media.css +187 -8
- package/src/styles/navigation.css +199 -3
- package/src/styles/page-layout.css +481 -100
- package/src/styles/responsive.css +144 -28
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- package/starters/basic/site/pages/000-home/content.md +13 -50
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { HeadingNavigation, SectionNavigation } from './sectionContent';
|
|
2
2
|
import type { SiteNode, SitePage } from './sitePages';
|
|
3
|
+
import {
|
|
4
|
+
flattenSiteNavigationTree as flattenSiteNavigationTreeShared,
|
|
5
|
+
getDirectChildPages as getDirectChildPagesShared,
|
|
6
|
+
getFirstPageInNavigationNode as getFirstPageInNavigationNodeShared,
|
|
7
|
+
getListedSiteNavigationTree as getListedSiteNavigationTreeShared,
|
|
8
|
+
getSequentialPageNavigation as getSequentialPageNavigationShared,
|
|
9
|
+
} from '../../scripts/lib/site-navigation-tree.mjs';
|
|
3
10
|
|
|
4
11
|
export type SiteNavigationEntry = {
|
|
5
12
|
node: SiteNode;
|
|
@@ -11,57 +18,29 @@ export type SiteNavigationNode = SiteNavigationEntry & {
|
|
|
11
18
|
children: SiteNavigationNode[];
|
|
12
19
|
};
|
|
13
20
|
|
|
14
|
-
export const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new Error(`Site node "${entry.node.pagePath}" has no parent node "${entry.node.parentPagePath}".`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
parent.children.push(node);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return roots;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const getListedSiteNavigationTree = (entries: SiteNavigationEntry[]) => {
|
|
43
|
-
const filterListed = (nodes: SiteNavigationNode[]): SiteNavigationNode[] => nodes.flatMap((node) => {
|
|
44
|
-
if (!node.node.isHome && !node.node.navigation.listed) return [];
|
|
45
|
-
const children = filterListed(node.children);
|
|
46
|
-
if (node.node.kind === 'category' && children.length === 0) return [];
|
|
47
|
-
return [{ ...node, children }];
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return filterListed(getSiteNavigationTree(entries));
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const getFirstPageInNavigationNode = (node: SiteNavigationNode): SitePage | null => {
|
|
54
|
-
if (node.node.kind === 'page') return node.node;
|
|
55
|
-
|
|
56
|
-
for (const child of node.children) {
|
|
57
|
-
const page = getFirstPageInNavigationNode(child);
|
|
58
|
-
if (page) return page;
|
|
21
|
+
export const getListedSiteNavigationTree = (entries: SiteNavigationEntry[]) => (
|
|
22
|
+
getListedSiteNavigationTreeShared(entries) as SiteNavigationNode[]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const getFirstPageInNavigationNode = (node: SiteNavigationNode): SitePage | null => (
|
|
26
|
+
getFirstPageInNavigationNodeShared(node) as SitePage | null
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const flattenSiteNavigationTree = (nodes: SiteNavigationNode[]): SiteNavigationNode[] => (
|
|
30
|
+
flattenSiteNavigationTreeShared(nodes) as SiteNavigationNode[]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const getSequentialPageNavigation = (
|
|
34
|
+
nodes: SiteNavigationNode[],
|
|
35
|
+
currentPagePath: string,
|
|
36
|
+
): { previous: SitePage | null; next: SitePage | null } => (
|
|
37
|
+
getSequentialPageNavigationShared(nodes, currentPagePath) as {
|
|
38
|
+
previous: SitePage | null;
|
|
39
|
+
next: SitePage | null;
|
|
59
40
|
}
|
|
41
|
+
);
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
node,
|
|
66
|
-
...flattenSiteNavigationTree(node.children),
|
|
67
|
-
]);
|
|
43
|
+
export const getDirectChildPages = (
|
|
44
|
+
nodes: SiteNavigationNode[],
|
|
45
|
+
currentPagePath: string,
|
|
46
|
+
): SitePage[] => getDirectChildPagesShared(nodes, currentPagePath) as SitePage[];
|
package/src/lib/sitePages.ts
CHANGED
|
@@ -36,6 +36,7 @@ export type SitePage = SiteNodeBase & {
|
|
|
36
36
|
markdown: string;
|
|
37
37
|
markdownDocument: Awaited<ReturnType<typeof parsePageMarkdown>>;
|
|
38
38
|
contentLabel: string;
|
|
39
|
+
contentPath: string;
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
export type SiteCategory = SiteNodeBase & {
|
|
@@ -86,20 +87,21 @@ const compareNumberPaths = (left: number[], right: number[]) => {
|
|
|
86
87
|
const readPageMarkdownDocument = async (entry: SiteEntry) => {
|
|
87
88
|
const pageDirectory = getPageDirectory(entry);
|
|
88
89
|
const contentLabel = `${sitePagesLabel}/${pageDirectory}/content.md`;
|
|
89
|
-
const
|
|
90
|
+
const contentPath = path.join(sitePagesDir, pageDirectory, 'content.md');
|
|
91
|
+
const { body } = await readSiteFile(contentPath, contentLabel);
|
|
90
92
|
const markdownDocument = await parsePageMarkdown(body, { label: contentLabel });
|
|
91
93
|
|
|
92
94
|
if (markdownDocument.pageHeadings.length !== 1 || markdownDocument.regions[0]?.kind !== 'page-intro') {
|
|
93
95
|
throw new Error(`Page entry "${entry.id}" must contain exactly one Markdown H1 page title.`);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
return { body, contentLabel, markdownDocument };
|
|
98
|
+
return { body, contentLabel, contentPath, markdownDocument };
|
|
97
99
|
};
|
|
98
100
|
|
|
99
101
|
const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
|
|
100
102
|
const isHome = isHomePageEntry(entry);
|
|
101
103
|
const pageMetadata = getPageMetadata(entry);
|
|
102
|
-
const { body, contentLabel, markdownDocument } = await readPageMarkdownDocument(entry);
|
|
104
|
+
const { body, contentLabel, contentPath, markdownDocument } = await readPageMarkdownDocument(entry);
|
|
103
105
|
const pageDirectory = pageMetadata.pageDirectory;
|
|
104
106
|
const pageId = pageMetadata.pageId;
|
|
105
107
|
const pagePath = pageMetadata.pagePath;
|
|
@@ -126,6 +128,7 @@ const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
|
|
|
126
128
|
pagePath,
|
|
127
129
|
parentPagePath: isHome ? null : pageMetadata.parentPagePath,
|
|
128
130
|
contentLabel,
|
|
131
|
+
contentPath,
|
|
129
132
|
markdown: body,
|
|
130
133
|
markdownDocument,
|
|
131
134
|
title: markdownDocument.pageTitle.title,
|
|
@@ -4,6 +4,7 @@ import { sitePublicDir } from '../../scripts/lib/site-paths.mjs';
|
|
|
4
4
|
import { browserIconAssetDefinitions } from '../../scripts/lib/public-asset-conventions.mjs';
|
|
5
5
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
6
6
|
export { getLogoAsset } from '../../scripts/lib/logo-assets.mjs';
|
|
7
|
+
export { getSocialImageAsset } from '../../scripts/lib/social-image-assets.mjs';
|
|
7
8
|
import { withBasePath } from './basePath';
|
|
8
9
|
|
|
9
10
|
type IconLink = {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { resolveNavigationModel } from '../../scripts/lib/navigation-model.mjs';
|
|
4
|
+
import { getTextWidthCssValue, resolveFrameColors, resolvePagePresentation } from '../../scripts/lib/presentation.mjs';
|
|
5
|
+
import { projectConfig, resolveThemeVisualConfig } from '../../scripts/lib/project-config.mjs';
|
|
6
|
+
import SiteBanners from '../components/SiteBanners.astro';
|
|
7
|
+
import SiteNavigation from '../components/SiteNavigation.astro';
|
|
8
|
+
import SiteSection from '../components/SiteSection.astro';
|
|
9
|
+
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
10
|
+
import type { ResolvedSection } from '../lib/sectionContent';
|
|
11
|
+
import { flattenSiteNavigationTree, getListedSiteNavigationTree } from '../lib/siteNavigation';
|
|
12
|
+
import { getSiteModel, type SitePage } from '../lib/sitePages';
|
|
13
|
+
import { getLogoAsset } from '../lib/sitePublicAssets';
|
|
14
|
+
import { getToday, isVisibleToday } from '../lib/visibility';
|
|
15
|
+
|
|
16
|
+
const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
17
|
+
const siteModel = await getSiteModel(await getCollection('site'));
|
|
18
|
+
const { nodes: siteNodes, pages: sitePages } = siteModel;
|
|
19
|
+
const homePage = sitePages.find((page) => page.isHome);
|
|
20
|
+
if (!homePage) throw new Error('The required homepage is missing from the site page model.');
|
|
21
|
+
|
|
22
|
+
const [siteTheme] = await getCollection('theme');
|
|
23
|
+
if (!siteTheme) throw new Error('site/theme.yaml is required. Add a root theme before building the site.');
|
|
24
|
+
const [sitewide] = await getCollection('sitewide');
|
|
25
|
+
const sitewideData = sitewide?.data ?? {};
|
|
26
|
+
const siteThemeData = siteTheme.data;
|
|
27
|
+
const themeLabel = `${siteDirectory || 'site'}/${siteTheme.id}`;
|
|
28
|
+
const visualTheme = resolveThemeVisualConfig(siteThemeData, themeLabel);
|
|
29
|
+
const getPageContent = (page: SitePage) => page.markdownDocument.navigationHeadings.map((heading) => ({ ...heading }));
|
|
30
|
+
const siteNavigation = siteNodes.map((node) => {
|
|
31
|
+
const headings = node.kind === 'page' ? getPageContent(node) : [];
|
|
32
|
+
return {
|
|
33
|
+
node,
|
|
34
|
+
headings,
|
|
35
|
+
sections: headings
|
|
36
|
+
.filter((heading) => heading.depth === 2)
|
|
37
|
+
.map(({ id, title }) => ({ id, title })),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const renderedNavigationEntries = flattenSiteNavigationTree(getListedSiteNavigationTree(siteNavigation));
|
|
41
|
+
const navigationModel = resolveNavigationModel({
|
|
42
|
+
mode: projectConfig.navigation.mode,
|
|
43
|
+
nodes: renderedNavigationEntries.map(({ node, headings }) => ({
|
|
44
|
+
depth: Math.max(1, node.depth),
|
|
45
|
+
headings,
|
|
46
|
+
isHome: node.isHome,
|
|
47
|
+
kind: node.kind,
|
|
48
|
+
listed: node.navigation.listed,
|
|
49
|
+
pagePath: node.pagePath,
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
const pagePresentation = resolvePagePresentation(siteThemeData, themeLabel, {
|
|
53
|
+
navigationMode: navigationModel.mode,
|
|
54
|
+
});
|
|
55
|
+
const pageTextWidth = getTextWidthCssValue(pagePresentation.typography.values.body.width);
|
|
56
|
+
const frameColors = resolveFrameColors({ theme: siteThemeData, sourceLabel: themeLabel });
|
|
57
|
+
const logoAsset = getLogoAsset();
|
|
58
|
+
const logo = logoAsset
|
|
59
|
+
? {
|
|
60
|
+
src: logoAsset.href,
|
|
61
|
+
alt: homePage.title,
|
|
62
|
+
height: sitewideData.logo?.height,
|
|
63
|
+
}
|
|
64
|
+
: undefined;
|
|
65
|
+
const today = getToday();
|
|
66
|
+
const siteBanners = (sitewideData.banners ?? []).filter((banner) => isVisibleToday(banner.visible, today));
|
|
67
|
+
const notFoundSection: ResolvedSection = {
|
|
68
|
+
id: null,
|
|
69
|
+
title: projectConfig.locale.labels.notFound,
|
|
70
|
+
titleHtml: projectConfig.locale.labels.notFound,
|
|
71
|
+
headingLevel: 1,
|
|
72
|
+
contentBlocks: [{
|
|
73
|
+
type: 'html',
|
|
74
|
+
html: `<p>${projectConfig.locale.labels.notFoundText}</p><p><a href="${projectConfig.site.basePath}">${projectConfig.locale.labels.returnHome}</a></p>`,
|
|
75
|
+
}],
|
|
76
|
+
};
|
|
77
|
+
const hasNavigationRailFrame = navigationModel.requestedMode === 'tree'
|
|
78
|
+
|| (
|
|
79
|
+
navigationModel.requestedMode === 'automatic'
|
|
80
|
+
&& (navigationModel.hasNestedPages || navigationModel.hasCategories)
|
|
81
|
+
);
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
<BaseLayout
|
|
85
|
+
title={projectConfig.locale.labels.notFound}
|
|
86
|
+
presentation={pagePresentation}
|
|
87
|
+
visualTheme={visualTheme}
|
|
88
|
+
frameColors={frameColors}
|
|
89
|
+
footer={sitewideData.footer}
|
|
90
|
+
indexable={false}
|
|
91
|
+
>
|
|
92
|
+
<SiteNavigation
|
|
93
|
+
logo={logo}
|
|
94
|
+
navigationEntries={siteNavigation}
|
|
95
|
+
navigationMode={navigationModel.mode}
|
|
96
|
+
showTopNavigationSubmenus={navigationModel.requestedMode !== 'automatic' && navigationModel.mode !== 'tree'}
|
|
97
|
+
hasNavigationRailFrame={hasNavigationRailFrame}
|
|
98
|
+
frameColors={frameColors}
|
|
99
|
+
readerPreferences={pagePresentation.readerPreferences}
|
|
100
|
+
/>
|
|
101
|
+
{siteBanners.length > 0 && <SiteBanners banners={siteBanners} />}
|
|
102
|
+
<main class="site-content" id="main-content" style={`--page-text-width: ${pageTextWidth}`}>
|
|
103
|
+
<SiteSection
|
|
104
|
+
section={notFoundSection}
|
|
105
|
+
pagePresentation={pagePresentation}
|
|
106
|
+
visualTheme={visualTheme}
|
|
107
|
+
sectionIndex={0}
|
|
108
|
+
/>
|
|
109
|
+
</main>
|
|
110
|
+
</BaseLayout>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
3
|
import { assertPageAliasModel, createPageAliasModel } from '../../scripts/lib/page-aliases.mjs';
|
|
4
|
+
import { getGeneratedSiteRoutes } from '../../scripts/lib/page-aliases.mjs';
|
|
4
5
|
import { getSitePublicFiles } from '../../scripts/lib/site-link-graph.mjs';
|
|
6
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
5
7
|
import PageAliasRedirect from '../components/PageAliasRedirect.astro';
|
|
8
|
+
import SearchPage from '../components/SearchPage.astro';
|
|
6
9
|
import SitePage from '../components/SitePage.astro';
|
|
7
10
|
import { getSiteModel, getSitePage, isHomePageEntry } from '../lib/sitePages';
|
|
8
11
|
|
|
@@ -13,6 +16,7 @@ export async function getStaticPaths() {
|
|
|
13
16
|
categories: siteModel.categories,
|
|
14
17
|
pages: siteModel.pages,
|
|
15
18
|
publicFiles: await getSitePublicFiles(),
|
|
19
|
+
generatedRoutes: getGeneratedSiteRoutes({ searchEnabled: projectConfig.search.enabled }),
|
|
16
20
|
}));
|
|
17
21
|
const pagesByPathname = new Map(siteModel.pages.map((page) => [page.pathname, page]));
|
|
18
22
|
|
|
@@ -37,22 +41,28 @@ export async function getStaticPaths() {
|
|
|
37
41
|
};
|
|
38
42
|
});
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
const searchPath = projectConfig.search.enabled
|
|
45
|
+
? [{ params: { slug: 'search' }, props: { searchPage: true } }]
|
|
46
|
+
: [];
|
|
47
|
+
|
|
48
|
+
return [...pagePaths, ...aliasPaths, ...searchPath];
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
const { alias, entry } = Astro.props;
|
|
51
|
+
const { alias, entry, searchPage } = Astro.props;
|
|
44
52
|
|
|
45
|
-
if (!alias && (!entry || isHomePageEntry(entry))) {
|
|
53
|
+
if (!searchPage && !alias && (!entry || isHomePageEntry(entry))) {
|
|
46
54
|
throw new Error('Page entry is missing.');
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
const currentPage = entry ? await getSitePage(entry) : null;
|
|
50
58
|
|
|
51
|
-
if (!alias && !currentPage?.pathSegment) {
|
|
59
|
+
if (!searchPage && !alias && !currentPage?.pathSegment) {
|
|
52
60
|
throw new Error(`Page "${entry.id}" needs a page id.`);
|
|
53
61
|
}
|
|
54
62
|
---
|
|
55
63
|
|
|
56
|
-
{
|
|
64
|
+
{searchPage
|
|
65
|
+
? <SearchPage />
|
|
66
|
+
: alias
|
|
57
67
|
? <PageAliasRedirect targetPathname={alias.targetPathname} targetTitle={alias.targetTitle} />
|
|
58
68
|
: <SitePage entry={entry} />}
|