@janga/norna 0.7.22 → 0.7.24

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 (88) hide show
  1. package/README.md +14 -18
  2. package/astro.config.mjs +15 -0
  3. package/bin/norna-cli.mjs +8 -2
  4. package/package.json +28 -3
  5. package/schemas/category.schema.json +2 -2
  6. package/schemas/config.schema.json +52 -24
  7. package/schemas/content-frontmatter.schema.json +25 -5
  8. package/schemas/page-theme.schema.json +53 -25
  9. package/schemas/sitewide-content.schema.json +19 -18
  10. package/schemas/theme.schema.json +385 -252
  11. package/scripts/build-site.mjs +1 -0
  12. package/scripts/check-config.mjs +36 -5
  13. package/scripts/deploy-site.mjs +0 -1
  14. package/scripts/dev-local.mjs +175 -112
  15. package/scripts/generate-search-index.mjs +57 -0
  16. package/scripts/init-site.mjs +1 -0
  17. package/scripts/lib/code-fence-metadata.mjs +223 -0
  18. package/scripts/lib/content-sync-apply.mjs +34 -0
  19. package/scripts/lib/content-sync-plan.mjs +179 -0
  20. package/scripts/lib/edit-source-link.mjs +49 -0
  21. package/scripts/lib/editor-language-service.mjs +160 -12
  22. package/scripts/lib/heading-ids.mjs +1 -1
  23. package/scripts/lib/image-presentation.mjs +6 -0
  24. package/scripts/lib/markdown-links.mjs +182 -0
  25. package/scripts/lib/navigation-model.mjs +44 -12
  26. package/scripts/lib/navigation-review.mjs +396 -0
  27. package/scripts/lib/norna-markdown-blocks.mjs +74 -26
  28. package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
  29. package/scripts/lib/page-aliases.mjs +184 -0
  30. package/scripts/lib/page-markdown.mjs +88 -7
  31. package/scripts/lib/page-move-plan.mjs +659 -0
  32. package/scripts/lib/presentation-palette-metadata.mjs +1 -1
  33. package/scripts/lib/presentation.mjs +10 -37
  34. package/scripts/lib/project-config.mjs +146 -19
  35. package/scripts/lib/public-asset-conventions.mjs +36 -2
  36. package/scripts/lib/schema-definitions.mjs +43 -15
  37. package/scripts/lib/schema-editor-metadata.mjs +60 -32
  38. package/scripts/lib/schema-value-definitions.mjs +6 -2
  39. package/scripts/lib/semantic-callouts.mjs +128 -0
  40. package/scripts/lib/site-content.mjs +28 -5
  41. package/scripts/lib/site-link-graph.mjs +357 -0
  42. package/scripts/lib/site-navigation-tree.mjs +85 -0
  43. package/scripts/lib/site-page-urls.mjs +10 -0
  44. package/scripts/lib/sitemap.mjs +34 -0
  45. package/scripts/lib/social-image-assets.mjs +30 -0
  46. package/scripts/lib/theme-presets.mjs +53 -25
  47. package/scripts/lib/theme-profiles.mjs +6 -8
  48. package/scripts/lib/yaml-config.mjs +6 -2
  49. package/scripts/move-site-page.mjs +256 -0
  50. package/scripts/review-navigation.mjs +32 -0
  51. package/scripts/sync-content-sections.mjs +182 -216
  52. package/scripts/sync-site-public.mjs +43 -3
  53. package/src/components/CodeBlockCopyScript.astro +97 -0
  54. package/src/components/DisplaySettings.astro +4 -4
  55. package/src/components/EditSourceLink.astro +17 -0
  56. package/src/components/ImageCarousel.astro +6 -3
  57. package/src/components/NavigationPageTree.astro +81 -86
  58. package/src/components/NavigationTreeControls.astro +77 -0
  59. package/src/components/PageAliasRedirect.astro +49 -0
  60. package/src/components/PageContentsNavigation.astro +21 -0
  61. package/src/components/PageList.astro +33 -0
  62. package/src/components/PageSequenceNavigation.astro +37 -0
  63. package/src/components/SearchPage.astro +151 -0
  64. package/src/components/SectionNavigationScript.astro +62 -6
  65. package/src/components/SiteNavigation.astro +50 -11
  66. package/src/components/SitePage.astro +113 -20
  67. package/src/components/SiteSection.astro +16 -3
  68. package/src/components/SiteTreeNavigation.astro +8 -2
  69. package/src/components/TreeNavigationScript.astro +207 -26
  70. package/src/content.config.ts +5 -2
  71. package/src/layouts/BaseLayout.astro +71 -10
  72. package/src/lib/readerPreferencesScript.mjs +2 -2
  73. package/src/lib/sectionContent.ts +32 -3
  74. package/src/lib/siteNavigation.ts +31 -52
  75. package/src/lib/sitePages.ts +3 -2
  76. package/src/lib/sitePublicAssets.ts +1 -0
  77. package/src/pages/404.astro +110 -0
  78. package/src/pages/[...slug].astro +45 -8
  79. package/src/styles/content.css +718 -0
  80. package/src/styles/foundations.css +284 -0
  81. package/src/styles/global.css +6 -2510
  82. package/src/styles/media.css +518 -0
  83. package/src/styles/navigation.css +526 -0
  84. package/src/styles/page-layout.css +786 -0
  85. package/src/styles/responsive.css +594 -0
  86. package/starters/basic/README.md +6 -19
  87. package/starters/basic/package.json +1 -0
  88. package/starters/basic/site/pages/000-home/content.md +13 -50
@@ -10,6 +10,7 @@ import { sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
10
10
  type SiteEntry = CollectionEntry<'site'>;
11
11
 
12
12
  type SiteNodeBase = {
13
+ aliases: string[];
13
14
  kind: 'category' | 'page';
14
15
  isHome: boolean;
15
16
  navigation: {
@@ -106,6 +107,7 @@ const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
106
107
  const navigation = entry.data.navigation ?? {};
107
108
 
108
109
  return {
110
+ aliases: entry.data.page?.aliases ?? [],
109
111
  kind: 'page',
110
112
  entry,
111
113
  isHome,
@@ -131,6 +133,7 @@ const createSitePage = async (entry: SiteEntry): Promise<SitePage> => {
131
133
  };
132
134
 
133
135
  const createSiteCategory = (category: Awaited<ReturnType<typeof getSiteStructure>>['categories'][number]): SiteCategory => ({
136
+ aliases: [],
134
137
  kind: 'category',
135
138
  isHome: false,
136
139
  depth: category.depth,
@@ -226,5 +229,3 @@ export const getSiteModel = async (entries: SiteEntry[]) => {
226
229
  warnings: structure.warnings,
227
230
  };
228
231
  };
229
-
230
- export const getSitePages = async (entries: SiteEntry[]) => (await getSiteModel(entries)).pages;
@@ -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,31 +1,68 @@
1
1
  ---
2
2
  import { getCollection } from 'astro:content';
3
+ import { assertPageAliasModel, createPageAliasModel } from '../../scripts/lib/page-aliases.mjs';
4
+ import { getGeneratedSiteRoutes } from '../../scripts/lib/page-aliases.mjs';
5
+ import { getSitePublicFiles } from '../../scripts/lib/site-link-graph.mjs';
6
+ import projectConfig from '../../scripts/lib/project-config.mjs';
7
+ import PageAliasRedirect from '../components/PageAliasRedirect.astro';
8
+ import SearchPage from '../components/SearchPage.astro';
3
9
  import SitePage from '../components/SitePage.astro';
4
- import { getSitePage, getSitePages, isHomePageEntry } from '../lib/sitePages';
10
+ import { getSiteModel, getSitePage, isHomePageEntry } from '../lib/sitePages';
5
11
 
6
12
  export async function getStaticPaths() {
7
13
  const entries = await getCollection('site');
8
- const pages = await getSitePages(entries);
14
+ const siteModel = await getSiteModel(entries);
15
+ const aliasModel = assertPageAliasModel(createPageAliasModel({
16
+ categories: siteModel.categories,
17
+ pages: siteModel.pages,
18
+ publicFiles: await getSitePublicFiles(),
19
+ generatedRoutes: getGeneratedSiteRoutes({ searchEnabled: projectConfig.search.enabled }),
20
+ }));
21
+ const pagesByPathname = new Map(siteModel.pages.map((page) => [page.pathname, page]));
9
22
 
10
- return pages
23
+ const pagePaths = siteModel.pages
11
24
  .filter((page) => !page.isHome)
12
25
  .map((page) => ({
13
26
  params: { slug: page.pathSegment },
14
27
  props: { entry: page.entry },
15
28
  }));
29
+ const aliasPaths = aliasModel.aliases.map((alias) => {
30
+ const page = pagesByPathname.get(alias.targetPathname);
31
+ if (!page) throw new Error(`Page alias "${alias.pathname}" has no target page ${alias.targetPathname}.`);
32
+
33
+ return {
34
+ params: { slug: alias.pathname.slice(1, -1) },
35
+ props: {
36
+ alias: {
37
+ targetPathname: page.pathname,
38
+ targetTitle: page.title,
39
+ },
40
+ },
41
+ };
42
+ });
43
+
44
+ const searchPath = projectConfig.search.enabled
45
+ ? [{ params: { slug: 'search' }, props: { searchPage: true } }]
46
+ : [];
47
+
48
+ return [...pagePaths, ...aliasPaths, ...searchPath];
16
49
  }
17
50
 
18
- const { entry } = Astro.props;
51
+ const { alias, entry, searchPage } = Astro.props;
19
52
 
20
- if (!entry || isHomePageEntry(entry)) {
53
+ if (!searchPage && !alias && (!entry || isHomePageEntry(entry))) {
21
54
  throw new Error('Page entry is missing.');
22
55
  }
23
56
 
24
- const currentPage = await getSitePage(entry);
57
+ const currentPage = entry ? await getSitePage(entry) : null;
25
58
 
26
- if (!currentPage.pathSegment) {
59
+ if (!searchPage && !alias && !currentPage?.pathSegment) {
27
60
  throw new Error(`Page "${entry.id}" needs a page id.`);
28
61
  }
29
62
  ---
30
63
 
31
- <SitePage entry={entry} />
64
+ {searchPage
65
+ ? <SearchPage />
66
+ : alias
67
+ ? <PageAliasRedirect targetPathname={alias.targetPathname} targetTitle={alias.targetTitle} />
68
+ : <SitePage entry={entry} />}