@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.
Files changed (82) hide show
  1. package/README.md +14 -18
  2. package/astro.config.mjs +14 -0
  3. package/bin/norna-cli.mjs +6 -0
  4. package/package.json +25 -2
  5. package/schemas/category.schema.json +2 -2
  6. package/schemas/config.schema.json +80 -15
  7. package/schemas/content-frontmatter.schema.json +7 -7
  8. package/schemas/page-theme.schema.json +26 -26
  9. package/schemas/sitewide-content.schema.json +18 -18
  10. package/schemas/theme.schema.json +246 -246
  11. package/scripts/build-site.mjs +1 -0
  12. package/scripts/check-config.mjs +16 -1
  13. package/scripts/dev-local.mjs +106 -11
  14. package/scripts/generate-search-index.mjs +57 -0
  15. package/scripts/init-site.mjs +3 -0
  16. package/scripts/lib/code-fence-metadata.mjs +228 -0
  17. package/scripts/lib/edit-source-link.mjs +102 -0
  18. package/scripts/lib/editor-language-service.mjs +32 -12
  19. package/scripts/lib/image-presentation.mjs +4 -0
  20. package/scripts/lib/navigation-model.mjs +34 -13
  21. package/scripts/lib/navigation-review.mjs +396 -0
  22. package/scripts/lib/norna-markdown-blocks.mjs +164 -26
  23. package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
  24. package/scripts/lib/page-aliases.mjs +21 -1
  25. package/scripts/lib/page-markdown.mjs +32 -1
  26. package/scripts/lib/page-move-plan.mjs +659 -0
  27. package/scripts/lib/presentation-palette-metadata.mjs +1 -1
  28. package/scripts/lib/presentation.mjs +1 -10
  29. package/scripts/lib/project-config.mjs +141 -5
  30. package/scripts/lib/public-asset-conventions.mjs +36 -2
  31. package/scripts/lib/schema-definitions.mjs +30 -5
  32. package/scripts/lib/schema-editor-metadata.mjs +36 -4
  33. package/scripts/lib/schema-value-definitions.mjs +4 -1
  34. package/scripts/lib/semantic-callouts.mjs +128 -0
  35. package/scripts/lib/site-content.mjs +2 -1
  36. package/scripts/lib/site-link-graph.mjs +33 -2
  37. package/scripts/lib/site-navigation-tree.mjs +85 -0
  38. package/scripts/lib/site-paths.mjs +14 -2
  39. package/scripts/lib/social-image-assets.mjs +30 -0
  40. package/scripts/lib/table-render-plugin.mjs +33 -0
  41. package/scripts/lib/theme-presets.mjs +2 -2
  42. package/scripts/lib/theme-profiles.mjs +0 -4
  43. package/scripts/move-site-page.mjs +256 -0
  44. package/scripts/review-navigation.mjs +32 -0
  45. package/scripts/sync-content-sections.mjs +67 -4
  46. package/scripts/sync-site-public.mjs +24 -5
  47. package/src/components/CardList.astro +1 -0
  48. package/src/components/CodeBlockCopyScript.astro +11 -5
  49. package/src/components/EditSourceLink.astro +17 -0
  50. package/src/components/ImageCarousel.astro +9 -7
  51. package/src/components/ImageStack.astro +34 -9
  52. package/src/components/ImageStackEnhancement.astro +331 -0
  53. package/src/components/NavigationPageTree.astro +66 -55
  54. package/src/components/NavigationTreeControls.astro +77 -0
  55. package/src/components/PageAliasRedirect.astro +1 -1
  56. package/src/components/PageContentsNavigation.astro +2 -3
  57. package/src/components/PageList.astro +33 -0
  58. package/src/components/PageSequenceNavigation.astro +37 -0
  59. package/src/components/SearchPage.astro +151 -0
  60. package/src/components/SectionNavigationScript.astro +45 -7
  61. package/src/components/SiteNavigation.astro +102 -51
  62. package/src/components/SitePage.astro +107 -39
  63. package/src/components/SiteSection.astro +11 -3
  64. package/src/components/SiteTreeNavigation.astro +11 -1
  65. package/src/components/TableOverflowScript.astro +251 -0
  66. package/src/components/TreeNavigationScript.astro +271 -34
  67. package/src/layouts/BaseLayout.astro +83 -2
  68. package/src/lib/generatedImages.ts +18 -0
  69. package/src/lib/sectionContent.ts +36 -30
  70. package/src/lib/siteNavigation.ts +31 -52
  71. package/src/lib/sitePages.ts +6 -3
  72. package/src/lib/sitePublicAssets.ts +1 -0
  73. package/src/pages/404.astro +110 -0
  74. package/src/pages/[...slug].astro +15 -5
  75. package/src/styles/content.css +538 -39
  76. package/src/styles/media.css +187 -8
  77. package/src/styles/navigation.css +199 -3
  78. package/src/styles/page-layout.css +481 -100
  79. package/src/styles/responsive.css +144 -28
  80. package/starters/basic/README.md +6 -19
  81. package/starters/basic/package.json +1 -0
  82. 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 getSiteNavigationTree = (entries: SiteNavigationEntry[]) => {
15
- const nodes = new Map<string, SiteNavigationNode>();
16
- const roots: SiteNavigationNode[] = [];
17
-
18
- for (const entry of entries) {
19
- nodes.set(entry.node.pagePath, { ...entry, children: [] });
20
- }
21
-
22
- for (const entry of entries) {
23
- const node = nodes.get(entry.node.pagePath);
24
- if (!node) continue;
25
-
26
- if (entry.node.isHome || entry.node.parentPagePath === null) {
27
- roots.push(node);
28
- continue;
29
- }
30
-
31
- const parent = nodes.get(entry.node.parentPagePath);
32
- if (!parent) {
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
- return null;
62
- };
63
-
64
- export const flattenSiteNavigationTree = (nodes: SiteNavigationNode[]): SiteNavigationNode[] => nodes.flatMap((node) => [
65
- node,
66
- ...flattenSiteNavigationTree(node.children),
67
- ]);
43
+ export const getDirectChildPages = (
44
+ nodes: SiteNavigationNode[],
45
+ currentPagePath: string,
46
+ ): SitePage[] => getDirectChildPagesShared(nodes, currentPagePath) as SitePage[];
@@ -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 { body } = await readSiteFile(path.join(sitePagesDir, pageDirectory, 'content.md'), contentLabel);
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
- return [...pagePaths, ...aliasPaths];
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
- {alias
64
+ {searchPage
65
+ ? <SearchPage />
66
+ : alias
57
67
  ? <PageAliasRedirect targetPathname={alias.targetPathname} targetTitle={alias.targetTitle} />
58
68
  : <SitePage entry={entry} />}