@janga/norna 0.7.0
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/LICENSE +674 -0
- package/README.md +109 -0
- package/astro.config.mjs +17 -0
- package/bin/norna.mjs +170 -0
- package/docs/README.md +48 -0
- package/docs/command-organization.md +402 -0
- package/docs/commands.md +152 -0
- package/docs/configuration.md +376 -0
- package/docs/content.md +384 -0
- package/docs/engine-development.md +164 -0
- package/docs/getting-started.md +96 -0
- package/docs/images-and-metadata.md +88 -0
- package/docs/local-development.md +61 -0
- package/docs/publishing.md +81 -0
- package/docs/site-examples-structure-note.md +105 -0
- package/docs/site-structure.md +81 -0
- package/fixtures/basic/site/.norna/generated-images.json +1 -0
- package/fixtures/basic/site/config.mjs +59 -0
- package/fixtures/basic/site/content.md +17 -0
- package/fixtures/basic/site/images/work/.gitkeep +1 -0
- package/fixtures/basic/site/public/robots.txt +2 -0
- package/fixtures/basic/site/theme.md +7 -0
- package/package.json +90 -0
- package/scripts/build-site.mjs +16 -0
- package/scripts/check-config.mjs +37 -0
- package/scripts/deploy-site.mjs +389 -0
- package/scripts/dev-local.mjs +313 -0
- package/scripts/doctor.mjs +38 -0
- package/scripts/engine-version.mjs +137 -0
- package/scripts/generate-images.mjs +369 -0
- package/scripts/init-site.mjs +249 -0
- package/scripts/lib/astro-command.mjs +34 -0
- package/scripts/lib/ci-lockfile.mjs +34 -0
- package/scripts/lib/image-dimensions.mjs +78 -0
- package/scripts/lib/presentation.mjs +72 -0
- package/scripts/lib/project-config.mjs +322 -0
- package/scripts/lib/run-command.mjs +21 -0
- package/scripts/lib/site-content.mjs +392 -0
- package/scripts/lib/site-paths.mjs +127 -0
- package/scripts/lib/typography.mjs +166 -0
- package/scripts/release.mjs +77 -0
- package/scripts/show-typography.mjs +210 -0
- package/scripts/sync-content-sections.mjs +610 -0
- package/scripts/sync-site-public.mjs +42 -0
- package/scripts/test-ci-lockfile.mjs +65 -0
- package/scripts/test-content-check.mjs +364 -0
- package/scripts/test-engine-commands.mjs +128 -0
- package/scripts/test-navigation-preview.mjs +108 -0
- package/scripts/test-navigation.mjs +116 -0
- package/scripts/test-package-check.mjs +394 -0
- package/scripts/test-site-public.mjs +85 -0
- package/scripts/test-temporary-visibility.mjs +99 -0
- package/scripts/update-engine.mjs +127 -0
- package/scripts/watch-pages-deploy.mjs +430 -0
- package/src/components/GalleryGrid.astro +221 -0
- package/src/components/SiteNavigation.astro +410 -0
- package/src/components/SitePage.astro +69 -0
- package/src/components/SiteSection.astro +174 -0
- package/src/content.config.ts +171 -0
- package/src/layouts/BaseLayout.astro +90 -0
- package/src/lib/generatedImages.ts +63 -0
- package/src/lib/sectionContent.ts +125 -0
- package/src/lib/sitePages.ts +80 -0
- package/src/lib/sitePublicAssets.ts +39 -0
- package/src/lib/visibility.ts +35 -0
- package/src/pages/[slug].astro +31 -0
- package/src/pages/index.astro +16 -0
- package/src/styles/global.css +872 -0
- package/starters/basic/.github/workflows/deploy.yml +65 -0
- package/starters/basic/README.md +55 -0
- package/starters/basic/package.json +35 -0
- package/starters/basic/site/config.mjs +60 -0
- package/starters/basic/site/content.md +21 -0
- package/starters/basic/site/images/work/.gitkeep +1 -0
- package/starters/basic/site/public/robots.txt +2 -0
- package/starters/basic/site/theme.md +53 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
|
|
3
|
+
type SiteEntry = CollectionEntry<'site'>;
|
|
4
|
+
|
|
5
|
+
export type SitePage = {
|
|
6
|
+
entry: SiteEntry;
|
|
7
|
+
isHome: boolean;
|
|
8
|
+
navigation: {
|
|
9
|
+
include: boolean;
|
|
10
|
+
label: string;
|
|
11
|
+
order: number;
|
|
12
|
+
};
|
|
13
|
+
pathname: string;
|
|
14
|
+
routeFolder: string | null;
|
|
15
|
+
slug: string;
|
|
16
|
+
title: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const routeIdMarker = '-route-';
|
|
20
|
+
|
|
21
|
+
const stripRouteIdPrefix = (id: string) => {
|
|
22
|
+
const markerIndex = id.indexOf(routeIdMarker);
|
|
23
|
+
return markerIndex === -1 ? null : id.slice(markerIndex + routeIdMarker.length);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const isHomePageEntry = (entry: SiteEntry) => stripRouteIdPrefix(entry.id) === null;
|
|
27
|
+
|
|
28
|
+
const getRouteFolder = (entry: SiteEntry) => isHomePageEntry(entry) ? null : stripRouteIdPrefix(entry.id);
|
|
29
|
+
|
|
30
|
+
const getPageSlug = (entry: SiteEntry) => {
|
|
31
|
+
if (isHomePageEntry(entry)) return '';
|
|
32
|
+
|
|
33
|
+
const routeFolder = getRouteFolder(entry);
|
|
34
|
+
return entry.data.slug ?? routeFolder ?? '';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getPagePathname = (slug: string) => slug ? `/${slug}/` : '/';
|
|
38
|
+
|
|
39
|
+
export const getSitePage = (entry: SiteEntry): SitePage => {
|
|
40
|
+
const isHome = isHomePageEntry(entry);
|
|
41
|
+
const routeFolder = getRouteFolder(entry);
|
|
42
|
+
const slug = getPageSlug(entry);
|
|
43
|
+
const navigation = entry.data.navigation ?? {};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
entry,
|
|
47
|
+
isHome,
|
|
48
|
+
navigation: {
|
|
49
|
+
include: navigation.include ?? true,
|
|
50
|
+
label: navigation.label ?? entry.data.title,
|
|
51
|
+
order: navigation.order ?? (isHome ? 0 : 100),
|
|
52
|
+
},
|
|
53
|
+
pathname: getPagePathname(slug),
|
|
54
|
+
routeFolder,
|
|
55
|
+
slug,
|
|
56
|
+
title: entry.data.title,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const getSitePages = (entries: SiteEntry[]) => {
|
|
61
|
+
const pages = entries.map(getSitePage);
|
|
62
|
+
const pathnames = new Map<string, SitePage>();
|
|
63
|
+
|
|
64
|
+
for (const page of pages) {
|
|
65
|
+
const existing = pathnames.get(page.pathname);
|
|
66
|
+
if (existing) {
|
|
67
|
+
throw new Error(`Duplicate page path "${page.pathname}" from "${existing.entry.id}" and "${page.entry.id}".`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pathnames.set(page.pathname, page);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return pages.sort((left, right) => (
|
|
74
|
+
left.navigation.order - right.navigation.order ||
|
|
75
|
+
left.navigation.label.localeCompare(right.navigation.label, 'sv') ||
|
|
76
|
+
left.pathname.localeCompare(right.pathname, 'sv')
|
|
77
|
+
));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const getNavigationPages = (pages: SitePage[]) => pages.filter((page) => page.navigation.include);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sitePublicDir } from '../../scripts/lib/site-paths.mjs';
|
|
4
|
+
|
|
5
|
+
type IconLink = {
|
|
6
|
+
rel: 'icon' | 'apple-touch-icon';
|
|
7
|
+
href: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
sizes?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const faviconCandidates: IconLink[] = [
|
|
13
|
+
{
|
|
14
|
+
rel: 'icon',
|
|
15
|
+
href: '/favicon.svg',
|
|
16
|
+
type: 'image/svg+xml',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
rel: 'icon',
|
|
20
|
+
href: '/favicon.ico',
|
|
21
|
+
sizes: 'any',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
rel: 'icon',
|
|
25
|
+
href: '/favicon.png',
|
|
26
|
+
type: 'image/png',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
rel: 'apple-touch-icon',
|
|
30
|
+
href: '/apple-touch-icon.png',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const publicAssetExists = (href: string) => {
|
|
35
|
+
const relativePath = href.replace(/^\//, '');
|
|
36
|
+
return existsSync(path.join(sitePublicDir, relativePath));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const getIconLinks = () => faviconCandidates.filter((candidate) => publicAssetExists(candidate.href));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface VisibilityWindow {
|
|
2
|
+
from?: string;
|
|
3
|
+
until?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const dateOnlyRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
|
|
8
|
+
export const isDateOnly = (value: string) => {
|
|
9
|
+
if (!dateOnlyRegex.test(value)) return false;
|
|
10
|
+
|
|
11
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
12
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getToday = () => {
|
|
16
|
+
const configuredToday = process.env.NORNA_TODAY?.trim();
|
|
17
|
+
if (configuredToday) {
|
|
18
|
+
if (!isDateOnly(configuredToday)) {
|
|
19
|
+
throw new Error('NORNA_TODAY must use YYYY-MM-DD format.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return configuredToday;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new Date().toISOString().slice(0, 10);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const isVisibleToday = (visible?: VisibilityWindow, today = getToday()) => {
|
|
29
|
+
if (!visible) return true;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
(visible.from === undefined || today >= visible.from)
|
|
33
|
+
&& (visible.until === undefined || today < visible.until)
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import SitePage from '../components/SitePage.astro';
|
|
4
|
+
import { getSitePage, getSitePages, isHomePageEntry } from '../lib/sitePages';
|
|
5
|
+
|
|
6
|
+
export async function getStaticPaths() {
|
|
7
|
+
const entries = await getCollection('site');
|
|
8
|
+
const pages = getSitePages(entries);
|
|
9
|
+
|
|
10
|
+
return pages
|
|
11
|
+
.filter((page) => !page.isHome)
|
|
12
|
+
.map((page) => ({
|
|
13
|
+
params: { slug: page.slug },
|
|
14
|
+
props: { entry: page.entry },
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { entry } = Astro.props;
|
|
19
|
+
|
|
20
|
+
if (!entry || isHomePageEntry(entry)) {
|
|
21
|
+
throw new Error('Route page entry is missing.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const currentPage = getSitePage(entry);
|
|
25
|
+
|
|
26
|
+
if (!currentPage.slug) {
|
|
27
|
+
throw new Error(`Route "${entry.id}" needs a slug or route folder name.`);
|
|
28
|
+
}
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<SitePage entry={entry} />
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import SitePage from '../components/SitePage.astro';
|
|
4
|
+
import { isHomePageEntry } from '../lib/sitePages';
|
|
5
|
+
|
|
6
|
+
const siteDirectory = (process.env.NORNA_SITE_DIR ?? 'site').trim();
|
|
7
|
+
const siteContentLabel = `${siteDirectory || 'site'}/content.md`;
|
|
8
|
+
const entries = await getCollection('site');
|
|
9
|
+
const site = entries.find(isHomePageEntry);
|
|
10
|
+
|
|
11
|
+
if (!site) {
|
|
12
|
+
throw new Error(`Content file ${siteContentLabel} is missing.`);
|
|
13
|
+
}
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<SitePage entry={site} />
|