@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.
Files changed (136) hide show
  1. package/README.md +41 -29
  2. package/astro.config.mjs +33 -3
  3. package/bin/norna-cli.mjs +5 -3
  4. package/package.json +18 -12
  5. package/schemas/card-list.schema.json +1 -1
  6. package/schemas/config.schema.json +81 -66
  7. package/schemas/content-frontmatter.schema.json +23 -7
  8. package/schemas/manifest.json +49 -3
  9. package/schemas/page-theme.schema.json +1585 -41
  10. package/schemas/root-theme.schema.json +1767 -0
  11. package/schemas/sitewide-content.schema.json +18 -18
  12. package/schemas/theme.schema.json +243 -243
  13. package/scripts/add-site-node.mjs +31 -250
  14. package/scripts/build-site.mjs +11 -6
  15. package/scripts/check-config.mjs +7 -23
  16. package/scripts/dev-local.mjs +51 -17
  17. package/scripts/export-theme-preset.mjs +2 -2
  18. package/scripts/generate-images.mjs +89 -70
  19. package/scripts/generate-schemas.mjs +9 -6
  20. package/scripts/generate-search-index.mjs +3 -2
  21. package/scripts/lib/attachment-render.mjs +23 -0
  22. package/scripts/lib/category-destinations.mjs +13 -12
  23. package/scripts/lib/dev-server-identity.mjs +30 -0
  24. package/scripts/lib/documentation-links.mjs +1 -1
  25. package/scripts/lib/documentation-routes.json +70 -62
  26. package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
  27. package/scripts/lib/editor-attachments.mjs +69 -0
  28. package/scripts/lib/editor-file-policy.mjs +20 -0
  29. package/scripts/lib/editor-language-service.mjs +74 -73
  30. package/scripts/lib/editor-page-addresses.mjs +57 -0
  31. package/scripts/lib/editor-page-placement.mjs +102 -0
  32. package/scripts/lib/editor-resource-actions.mjs +216 -0
  33. package/scripts/lib/editor-site-editing-tree.mjs +181 -0
  34. package/scripts/lib/editor-site-files.mjs +154 -0
  35. package/scripts/lib/editor-site-links.mjs +81 -0
  36. package/scripts/lib/editor-site-preview.mjs +149 -0
  37. package/scripts/lib/editor-site-tree.mjs +158 -0
  38. package/scripts/lib/editor-source-files.mjs +87 -0
  39. package/scripts/lib/editor-theme-help.mjs +43 -0
  40. package/scripts/lib/locale-registry.mjs +1 -1
  41. package/scripts/lib/locales/en.mjs +1 -1
  42. package/scripts/lib/logo-assets.mjs +2 -1
  43. package/scripts/lib/markdown-images.mjs +59 -0
  44. package/scripts/lib/markdown-links.mjs +29 -9
  45. package/scripts/lib/navigation-model.mjs +2 -44
  46. package/scripts/lib/navigation-review.mjs +14 -50
  47. package/scripts/lib/norna-markdown-blocks.mjs +1 -58
  48. package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
  49. package/scripts/lib/page-aliases.mjs +8 -1
  50. package/scripts/lib/page-attachments.mjs +59 -0
  51. package/scripts/lib/page-markdown.mjs +11 -7
  52. package/scripts/lib/page-model.mjs +19 -36
  53. package/scripts/lib/page-move-apply.mjs +119 -0
  54. package/scripts/lib/page-move-plan.mjs +64 -78
  55. package/scripts/lib/page-order-source.mjs +22 -0
  56. package/scripts/lib/page-order.mjs +45 -0
  57. package/scripts/lib/page-placement-cli.mjs +50 -0
  58. package/scripts/lib/presentation.mjs +3 -3
  59. package/scripts/lib/project-config.mjs +10 -8
  60. package/scripts/lib/publication-paths.mjs +72 -0
  61. package/scripts/lib/schema-definitions.mjs +15 -38
  62. package/scripts/lib/schema-editor-metadata.mjs +16 -60
  63. package/scripts/lib/schema-value-definitions.mjs +1 -1
  64. package/scripts/lib/site-content.mjs +8 -25
  65. package/scripts/lib/site-conventions.mjs +19 -1
  66. package/scripts/lib/site-link-graph.mjs +88 -18
  67. package/scripts/lib/site-navigation-tree.mjs +2 -7
  68. package/scripts/lib/site-node-create.mjs +182 -0
  69. package/scripts/lib/site-paths.mjs +24 -12
  70. package/scripts/lib/site-source-format.mjs +12 -0
  71. package/scripts/lib/site-structure.mjs +86 -95
  72. package/scripts/lib/site-upgrade.mjs +32 -0
  73. package/scripts/lib/social-image-assets.mjs +2 -1
  74. package/scripts/lib/source-files.mjs +34 -0
  75. package/scripts/lib/structured-blocks.mjs +3 -1
  76. package/scripts/lib/theme-config.mjs +26 -68
  77. package/scripts/lib/theme-packages.mjs +47 -0
  78. package/scripts/lib/theme-presets.mjs +20 -53
  79. package/scripts/lib/typography.mjs +2 -2
  80. package/scripts/lib/yaml-config.mjs +1 -1
  81. package/scripts/lib/yaml-source-edit.mjs +94 -0
  82. package/scripts/move-site-page.mjs +46 -254
  83. package/scripts/show-typography.mjs +32 -17
  84. package/scripts/sync-content-sections.mjs +17 -17
  85. package/scripts/sync-site-public.mjs +44 -66
  86. package/scripts/upgrade-site.mjs +17 -0
  87. package/src/components/AreaNavigationMenu.astro +24 -0
  88. package/src/components/AreaNavigationPanel.astro +55 -0
  89. package/src/components/AreaNavigationSwitcher.astro +30 -0
  90. package/src/components/ContentBlocks.astro +2 -1
  91. package/src/components/ImageStack.astro +7 -5
  92. package/src/components/ImageStackEnhancement.astro +0 -7
  93. package/src/components/NavigationContinuity.astro +26 -0
  94. package/src/components/NavigationPageTree.astro +13 -4
  95. package/src/components/NavigationState.astro +11 -0
  96. package/src/components/NavigationTreeControls.astro +3 -2
  97. package/src/components/SearchPage.astro +11 -2
  98. package/src/components/SectionNavigationScript.astro +106 -26
  99. package/src/components/SiteNavigation.astro +45 -20
  100. package/src/components/SitePage.astro +38 -61
  101. package/src/components/SiteSection.astro +1 -0
  102. package/src/components/SiteTreeNavigation.astro +38 -18
  103. package/src/components/TopPageMenu.astro +4 -3
  104. package/src/components/TreeNavigationScript.astro +50 -57
  105. package/src/content.config.ts +11 -12
  106. package/src/layouts/BaseLayout.astro +15 -39
  107. package/src/lib/areaNavigation.ts +42 -0
  108. package/src/lib/imagePresentation.ts +3 -0
  109. package/src/lib/markdownImages.ts +101 -0
  110. package/src/lib/navigationContinuity.ts +212 -0
  111. package/src/lib/navigationFollowing.ts +28 -18
  112. package/src/lib/navigationState.mjs +88 -0
  113. package/src/lib/navigationStateScript.mjs +76 -0
  114. package/src/lib/navigationVisibility.ts +9 -0
  115. package/src/lib/pageThemes.ts +5 -42
  116. package/src/lib/sectionContent.ts +67 -21
  117. package/src/lib/siteNavigation.ts +6 -0
  118. package/src/lib/sitePages.ts +13 -36
  119. package/src/lib/sitePublicAssets.ts +2 -1
  120. package/src/pages/404.astro +1 -1
  121. package/src/pages/[...slug].astro +7 -16
  122. package/src/pages/index.astro +1 -3
  123. package/src/styles/area-navigation.css +408 -0
  124. package/src/styles/content.css +8 -29
  125. package/src/styles/media.css +24 -0
  126. package/src/styles/navigation-continuity.css +163 -0
  127. package/src/styles/page-layout.css +57 -68
  128. package/src/styles/responsive.css +0 -4
  129. package/starters/basic/README.md +9 -8
  130. package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
  131. package/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
  132. package/schemas/category.schema.json +0 -20
  133. package/src/components/PageContentsNavigation.astro +0 -20
  134. /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
  135. /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
  136. /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, (_match, attributes, headingHtml) => {
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?.id) throw new Error('Rendered Markdown H3 could not be matched to its heading id.');
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) => `pages/${page.pageDirectory}/images/${image}`;
155
+ const getImageSourceKey = (page: SitePage, image: string) => getPageImageSourceKey(page.pageDirectory, image);
115
156
 
116
- const renderHeadingTitleHtml = async (headingSource: string) => {
117
- const headingMarkdown = headingSource
118
- .replace(/^#{1,6}[ \t]+/, '')
119
- .replace(explicitHeadingIdRegex, '')
120
- .trim();
121
- const result = await markdownToHtml(headingMarkdown, {
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 paragraph = result.html.trim().match(/^<p>([\s\S]*)<\/p>$/i);
128
- if (!paragraph) throw new Error('Markdown heading could not be rendered as inline content.');
129
- return prepareContentHtml(paragraph[1] ?? '');
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({ type: 'html', html: prepareContentHtml(block.html) });
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. Navigation categories are not pages. Add a listed direct child page or remove the block.`);
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[];
@@ -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 { siteDirLabel, sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
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 contentLabel = `${sitePagesLabel}/${pageDirectory}/content.md`;
79
- const contentPath = path.join(sitePagesDir, pageDirectory, 'content.md');
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 : pageMetadata.pageOrder,
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(`${homePageDirectory} is the required homepage and cannot set navigation.listed to false.`);
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 page entry ${homePageDirectory} is missing.`);
175
+ throw new Error(`Homepage content ${siteDirLabel}/root/content.md is missing.`);
194
176
  }
195
177
 
196
- return pages.sort((left, right) => (
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
  }));
@@ -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 a root theme before building the site.');
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
- const categoryPaths = siteModel.categoryDestinations.destinations.map((destination) => ({
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, categoryPath } = Astro.props;
52
+ const { alias, entry, searchPage } = Astro.props;
62
53
 
63
- if (!searchPage && !alias && !categoryPath && (!entry || isHomePageEntry(entry))) {
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 && !categoryPath && !currentPage?.pathSegment) {
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} categoryPath={categoryPath} />}
69
+ : <SitePage entry={entry} />}
@@ -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 { homePageDirectory } from '../../scripts/lib/site-conventions.mjs';
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