@terrafirmagreg-team/kit 1.2.10 → 1.2.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/kit",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "type": "module",
5
5
  "description": "Fumadocs content engine",
6
6
  "license": "LGPL-3.0-only",
@@ -24,11 +24,11 @@
24
24
  "dependencies": {
25
25
  "@base-ui/react": "^1.9.0",
26
26
  "@icons-pack/react-simple-icons": "13.15.1",
27
- "@terrafirmagreg-team/data": "^1.2.2",
28
- "@terrafirmagreg-team/mdx": "^1.2.4",
29
- "@terrafirmagreg-team/sites": "^1.2.1",
30
- "@terrafirmagreg-team/translate": "^1.2.6",
31
- "@terrafirmagreg-team/ui": "^1.2.7",
27
+ "@terrafirmagreg-team/data": "^1.2.3",
28
+ "@terrafirmagreg-team/mdx": "^1.2.6",
29
+ "@terrafirmagreg-team/sites": "^1.2.2",
30
+ "@terrafirmagreg-team/translate": "^1.2.8",
31
+ "@terrafirmagreg-team/ui": "^1.2.9",
32
32
  "fumadocs-core": "16.17.10",
33
33
  "lucide-react": "^1.63.0",
34
34
  "server-only": "^0.0.1",
@@ -21,6 +21,7 @@ import {
21
21
  } from '@terrafirmagreg-team/ui/components/blocks/nav/site-menu';
22
22
  import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
23
23
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
24
+ import { skeletonTree } from '@terrafirmagreg-team/ui/lib/tree';
24
25
  import { Globe } from 'lucide-react';
25
26
  import {
26
27
  SiCurseforge,
@@ -92,7 +93,7 @@ export function Layout({
92
93
  const pinned = current ? current.version : undefined;
93
94
  const meta = sectionDatasets.find((item) => item.id === baseId);
94
95
  const failedDatasets = useFailedDatasets();
95
- const featureHidden = baseId !== undefined && failedDatasets.includes(baseId);
96
+ const datasetFailed = baseId !== undefined && failedDatasets.includes(baseId);
96
97
  useDatasetAccent(meta?.accent);
97
98
  const nav = useNavOptions({ section, catalog });
98
99
  const pathname = usePathname();
@@ -110,12 +111,14 @@ export function Layout({
110
111
  $id: `${section}:empty`,
111
112
  };
112
113
  const tree = useTree(serverTree);
114
+ const effectiveTree =
115
+ datasetFailed && !tree.children.length ? skeletonTree() : tree;
113
116
  const pinnedTree = useMemo(
114
117
  () =>
115
118
  pinned && baseId
116
- ? pinTreeDataset(tree, section, baseId, pinned)
117
- : tree,
118
- [tree, section, baseId, pinned],
119
+ ? pinTreeDataset(effectiveTree, section, baseId, pinned)
120
+ : effectiveTree,
121
+ [effectiveTree, section, baseId, pinned],
119
122
  );
120
123
  const locales = meta?.languages ? localeItems(meta.languages) : undefined;
121
124
  const links = baseId
@@ -177,7 +180,7 @@ export function Layout({
177
180
  <LeftSidebarSearch />
178
181
  <Switcher {...nav.section} />
179
182
  <Switcher {...nav.dataset} />
180
- {Feature && !featureHidden ? (
183
+ {Feature ? (
181
184
  <>
182
185
  <LeftSidebarSeparator />
183
186
  <Feature />
@@ -20,10 +20,6 @@ import { ItemRow } from '@terrafirmagreg-team/ui/components/blocks/list/row';
20
20
  import { useUntranslated } from '@terrafirmagreg-team/ui/hooks/use-lang';
21
21
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
22
22
  import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
23
- import {
24
- openPageMarkdown,
25
- pageMarkdown,
26
- } from '@terrafirmagreg-team/mdx/lib/markdown';
27
23
  import type { TranslationState } from '@terrafirmagreg-team/translate/server';
28
24
 
29
25
  function ActionButton({
@@ -70,12 +66,10 @@ export function MarkdownCopyButton({
70
66
  );
71
67
  }
72
68
 
73
- export function CopyPageMarkdown({ title }: { title: string }) {
69
+ export function CopyPageMarkdown({ mdUrl }: { mdUrl: string }) {
74
70
  return (
75
71
  <MarkdownCopyButton
76
- getMarkdown={() =>
77
- pageMarkdown(document.querySelector('[data-md-root]'), title)
78
- }
72
+ getMarkdown={() => fetch(mdUrl).then((res) => res.text())}
79
73
  />
80
74
  );
81
75
  }
@@ -107,12 +101,10 @@ export function UntranslatedActions({
107
101
 
108
102
  export function ViewOptionsPopover({
109
103
  githubUrl,
110
- title,
111
104
  mdUrl,
112
105
  ...props
113
106
  }: ComponentProps<typeof PopoverTrigger> & {
114
107
  githubUrl?: string;
115
- title?: string;
116
108
  mdUrl?: string;
117
109
  }) {
118
110
  const t = useTranslations();
@@ -122,17 +114,9 @@ export function ViewOptionsPopover({
122
114
  href: githubUrl,
123
115
  icon: <SiGithub color="currentColor" className="size-4" />,
124
116
  },
125
- (mdUrl || title !== undefined) && {
117
+ mdUrl && {
126
118
  title: t('View as Markdown', { note: 'ui.view_markdown' }),
127
- ...(mdUrl
128
- ? { href: mdUrl }
129
- : {
130
- onClick: () =>
131
- void openPageMarkdown(
132
- document.querySelector('[data-md-root]'),
133
- title!,
134
- ),
135
- }),
119
+ href: mdUrl,
136
120
  icon: <TextIcon className="size-4" />,
137
121
  },
138
122
  ].filter((v) => !!v);
@@ -163,9 +147,9 @@ export function ViewOptionsPopover({
163
147
  {items.map((item) => (
164
148
  <ItemRow
165
149
  key={item.title}
166
- {...('href' in item
167
- ? { href: item.href, target: '_blank', rel: 'noreferrer noopener' }
168
- : { onClick: item.onClick })}
150
+ href={item.href}
151
+ target="_blank"
152
+ rel="noreferrer noopener"
169
153
  media={item.icon}
170
154
  title={item.title}
171
155
  trailing={
@@ -181,20 +165,18 @@ export function ViewOptionsPopover({
181
165
  export function PageActions({
182
166
  copied,
183
167
  crowdinUrl,
184
- title,
185
168
  mdUrl,
186
169
  githubUrl,
187
170
  }: {
188
171
  copied?: TranslationState;
189
172
  crowdinUrl?: string;
190
- title: string;
191
173
  mdUrl?: string;
192
174
  githubUrl?: string;
193
175
  }) {
194
176
  return (
195
177
  <>
196
- <CopyPageMarkdown title={title} />
197
- <ViewOptionsPopover title={title} mdUrl={mdUrl} githubUrl={githubUrl} />
178
+ {mdUrl ? <CopyPageMarkdown mdUrl={mdUrl} /> : null}
179
+ <ViewOptionsPopover mdUrl={mdUrl} githubUrl={githubUrl} />
198
180
  <UntranslatedActions copied={copied} crowdinUrl={crowdinUrl} />
199
181
  </>
200
182
  );
@@ -0,0 +1,30 @@
1
+ import type {
2
+ SectionFeatures,
3
+ SectionPageContext,
4
+ } from '../types/features.ts';
5
+ import type { ReactNode } from 'react';
6
+
7
+ export function pageFeatureNodes(
8
+ ctx: SectionPageContext,
9
+ features: SectionFeatures,
10
+ body: ReactNode,
11
+ ): {
12
+ actionsRight: ReactNode;
13
+ body: ReactNode;
14
+ footer: ReactNode;
15
+ comments: ReactNode;
16
+ } {
17
+ const wrapped = features.wrapBody ? (
18
+ features.wrapBody(body, ctx)
19
+ ) : (
20
+ <div data-md-root className="contents">
21
+ {body}
22
+ </div>
23
+ );
24
+ return {
25
+ actionsRight: features.actionsRight?.(ctx),
26
+ body: wrapped,
27
+ footer: features.footer?.(ctx),
28
+ comments: features.comments?.(ctx),
29
+ };
30
+ }
@@ -3,13 +3,11 @@ import { notFound } from 'next/navigation';
3
3
  import type { Metadata } from 'next';
4
4
  import { LangContentSwitch } from '@terrafirmagreg-team/ui/components/blocks/lang/content';
5
5
  import {
6
+ datasetLandingMetadata,
6
7
  generateDatasetLandingParams,
7
- getDatasetLanding,
8
8
  resolveDatasetLanding,
9
9
  } from '@terrafirmagreg-team/kit/lib/landing';
10
10
  import { sectionDatasets } from '@terrafirmagreg-team/kit/lib/datasets';
11
- import { sectionUrl } from '@terrafirmagreg-team/sites/location';
12
- import { canonicalUrl } from '@terrafirmagreg-team/kit/lib/urls';
13
11
  import type { DatasetLanding, SectionKey } from '@terrafirmagreg-team/kit/types';
14
12
  import {
15
13
  localeLoader,
@@ -58,22 +56,11 @@ export function createDatasetLandingPage<S extends SectionKey>(
58
56
  params,
59
57
  }: DatasetPageParams): Promise<Metadata> {
60
58
  const { slug } = await params;
61
- const page = getDatasetLanding(
59
+ return datasetLandingMetadata(
62
60
  section,
63
61
  slug,
64
- 'en_us',
65
62
  await sectionDatasets(section),
66
63
  );
67
- if (!page) notFound();
68
- return {
69
- title: page.title,
70
- description: page.description,
71
- alternates: {
72
- canonical: canonicalUrl(
73
- `${sectionUrl(section)}/${(slug ?? []).join('/')}`,
74
- ),
75
- },
76
- };
77
64
  }
78
65
 
79
66
  return { Page, generateStaticParams, generateMetadata };
@@ -1,8 +1,7 @@
1
1
  'use client';
2
2
  import { useEffect, useRef } from 'react';
3
- import { FileQuestion } from 'lucide-react';
4
- import { useRouter } from 'next/navigation';
5
- import { sectionUrl } from '@terrafirmagreg-team/sites/location';
3
+ import { usePathname, useRouter } from 'next/navigation';
4
+ import { isSectionKey, sectionUrl } from '@terrafirmagreg-team/sites/location';
6
5
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
7
6
  import { LangLink } from '@terrafirmagreg-team/ui/components/blocks/lang/link';
8
7
  import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
@@ -10,6 +9,7 @@ import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
10
9
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
11
10
  import { requestDumpRetry } from '@terrafirmagreg-team/data/lib/dump-status';
12
11
  import { useDumpStatus } from '@terrafirmagreg-team/kit/hooks/use-dump-status';
12
+ import { useSectionCatalog } from '@terrafirmagreg-team/kit/hooks/use-catalog';
13
13
  import type { SectionDataset, SectionKey } from '@terrafirmagreg-team/kit/types';
14
14
 
15
15
  export function SectionNotFound({
@@ -39,7 +39,7 @@ export function SectionNotFound({
39
39
 
40
40
  if (!datasets.length)
41
41
  return (
42
- <div className="flex flex-col items-center gap-3 py-16">
42
+ <div className="mx-2 flex min-w-0 flex-1 flex-col items-center justify-center gap-3 bg-background [grid-area:main] md:my-2 md:overflow-clip md:rounded-2xl md:border md:shadow-sm">
43
43
  <EmptyState variant="missing">
44
44
  {t('Section data is temporarily unavailable. Try again later.', {
45
45
  note: 'kit.section_unavailable',
@@ -55,13 +55,27 @@ export function SectionNotFound({
55
55
  </div>
56
56
  );
57
57
  return (
58
- <div className="flex flex-col items-center gap-3 py-16 text-muted-foreground">
59
- <FileQuestion className="size-10" />
60
- <p>{t('Page not found', { note: 'ui.not_found' })}</p>
58
+ <div className="mx-2 flex min-w-0 flex-1 flex-col items-center justify-center gap-6 bg-background [grid-area:main] md:my-2 md:overflow-clip md:rounded-2xl md:border md:shadow-sm">
59
+ <div className="flex flex-col items-center gap-2 text-center">
60
+ <span
61
+ aria-hidden
62
+ className="select-none text-6xl leading-none font-semibold tracking-tighter text-muted-foreground/25"
63
+ >
64
+ 404
65
+ </span>
66
+ <p className="font-medium text-foreground">
67
+ {t('Page not found', { note: 'ui.not_found' })}
68
+ </p>
69
+ <p className="max-w-sm text-sm text-balance text-muted-foreground">
70
+ {t("The page you're looking for doesn't exist or was moved.", {
71
+ note: 'ui.not_found_description',
72
+ })}
73
+ </p>
74
+ </div>
61
75
  <LangLink
62
76
  href={sectionUrl(section, datasets[0].id)}
63
77
  className={cn(
64
- buttonVariants({ variant: 'outline', size: 'xs' }),
78
+ buttonVariants({ variant: 'outline', size: 'sm' }),
65
79
  'no-underline',
66
80
  )}
67
81
  >
@@ -70,3 +84,55 @@ export function SectionNotFound({
70
84
  </div>
71
85
  );
72
86
  }
87
+
88
+ export function RootNotFound() {
89
+ const pathname = usePathname();
90
+ const catalog = useSectionCatalog();
91
+ const t = useTranslations();
92
+ const parts = pathname.split('/').filter(Boolean);
93
+ const section = isSectionKey(parts[0]) ? parts[0] : undefined;
94
+ const datasets = section ? catalog[section] : undefined;
95
+ const known = datasets?.some((dataset) => dataset.id === parts[1]);
96
+ const href =
97
+ section && datasets?.length
98
+ ? `/${section}/${known ? parts[1] : datasets[0].id}`
99
+ : '/';
100
+
101
+ return (
102
+ <main className="flex min-h-dvh flex-col items-center justify-center gap-8 p-8">
103
+ <div className="flex flex-col items-center gap-3 text-center">
104
+ <span
105
+ aria-hidden
106
+ className="select-none bg-gradient-to-b from-muted-foreground/40 to-muted-foreground/10 bg-clip-text text-[8rem] leading-none font-semibold tracking-tighter text-transparent"
107
+ >
108
+ 404
109
+ </span>
110
+ <h1 className="text-xl font-semibold text-foreground">
111
+ {t('Page not found', { note: 'ui.not_found' })}
112
+ </h1>
113
+ <p className="max-w-sm text-sm text-balance text-muted-foreground">
114
+ {t("The page you're looking for doesn't exist or was moved.", {
115
+ note: 'ui.not_found_description',
116
+ })}
117
+ </p>
118
+ </div>
119
+ <div className="flex items-center gap-2">
120
+ <LangLink
121
+ href={href}
122
+ className={cn(buttonVariants({ size: 'sm' }), 'no-underline')}
123
+ >
124
+ {t('Back', { note: 'ui.back' })}
125
+ </LangLink>
126
+ <LangLink
127
+ href="/"
128
+ className={cn(
129
+ buttonVariants({ variant: 'ghost', size: 'sm' }),
130
+ 'no-underline',
131
+ )}
132
+ >
133
+ {t('Go home', { note: 'ui.go_home' })}
134
+ </LangLink>
135
+ </div>
136
+ </main>
137
+ );
138
+ }
@@ -20,6 +20,7 @@ import {
20
20
  sectionDatasets,
21
21
  } from '@terrafirmagreg-team/kit/lib/datasets';
22
22
  import {
23
+ datasetLandingMetadata,
23
24
  generateDatasetLandingParams,
24
25
  getDatasetLanding,
25
26
  } from '@terrafirmagreg-team/kit/lib/landing';
@@ -33,7 +34,6 @@ import {
33
34
  localeLoader,
34
35
  registerLocaleRenderer,
35
36
  } from '@terrafirmagreg-team/kit/lib/locale/renderer';
36
- import { canonicalUrl } from '@terrafirmagreg-team/kit/lib/urls';
37
37
  import { sectionUrl } from '@terrafirmagreg-team/sites/location';
38
38
  import { langContent, loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
39
39
  import type {
@@ -170,6 +170,7 @@ export function createSectionRoutes<
170
170
  }
171
171
  if (!PageView) return null;
172
172
  return renderSectionWorkspace({
173
+ section,
173
174
  source: src as SectionSource,
174
175
  slug,
175
176
  contentDir: source.contentDir ?? `content/${section}`,
@@ -275,22 +276,12 @@ export function createSectionRoutes<
275
276
 
276
277
  async function generateMetadata({ params }: RouteParams): Promise<Metadata> {
277
278
  const { slug } = await params;
278
- if (slug?.length === 1 && landing) {
279
- const page = getDatasetLanding(
279
+ if (slug?.length === 1 && landing)
280
+ return datasetLandingMetadata(
280
281
  section,
281
282
  slug,
282
- 'en_us',
283
283
  await sectionDatasets(section),
284
284
  );
285
- if (!page) notFound();
286
- return {
287
- title: page.title,
288
- description: page.description,
289
- alternates: {
290
- canonical: canonicalUrl(`${sectionUrl(section)}/${slug.join('/')}`),
291
- },
292
- };
293
- }
294
285
  if (!PageView) return {};
295
286
  const src = (await source.getSource()) as SectionSource | undefined;
296
287
  if (src) {
@@ -10,11 +10,13 @@ import {
10
10
  import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
11
11
  import { readPageSource, translationState } from '@terrafirmagreg-team/translate/server';
12
12
  import { resolvePage, type SectionSource } from '@terrafirmagreg-team/kit/lib/page';
13
+ import { pageJsonLd } from '@terrafirmagreg-team/kit/lib/jsonld';
13
14
  import { findNeighbour } from 'fumadocs-core/page-tree';
14
15
  import { stripLangFromUrl } from '@terrafirmagreg-team/translate/lib/slug';
15
16
  import { sectionConfig } from '@terrafirmagreg-team/kit/lib/config';
16
17
  import type {
17
18
  DatasetCatalog,
19
+ SectionKey,
18
20
  SectionPage,
19
21
  SectionTrees,
20
22
  } from '@terrafirmagreg-team/kit/types';
@@ -53,6 +55,7 @@ export type SectionPageViewFn = (
53
55
  ) => SectionPageView | Promise<SectionPageView>;
54
56
 
55
57
  export async function renderSectionWorkspace({
58
+ section,
56
59
  source,
57
60
  slug,
58
61
  contentDir,
@@ -64,6 +67,7 @@ export async function renderSectionWorkspace({
64
67
  page,
65
68
  lang,
66
69
  }: {
70
+ section: SectionKey;
67
71
  source: SectionSource;
68
72
  slug: string[] | undefined;
69
73
  contentDir: string;
@@ -117,6 +121,18 @@ export async function renderSectionWorkspace({
117
121
  history,
118
122
  features,
119
123
  });
120
- return renderSectionView(view);
124
+ const jsonLd = JSON.stringify(pageJsonLd({ section, page, crumbs, lang }));
125
+ return renderSectionView({
126
+ ...view,
127
+ node: (
128
+ <>
129
+ <script
130
+ type="application/ld+json"
131
+ dangerouslySetInnerHTML={{ __html: jsonLd }}
132
+ />
133
+ {view.node}
134
+ </>
135
+ ),
136
+ });
121
137
  }
122
138
 
@@ -1,22 +1,23 @@
1
1
  'use client';
2
2
 
3
3
  import type { ReactNode } from 'react';
4
+ import { CloudOff } from 'lucide-react';
4
5
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
5
6
  import { requestDumpRetry } from '@terrafirmagreg-team/data/lib/dump-status';
6
- import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
7
7
  import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
8
8
  import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
9
9
 
10
10
  export function DumpUnavailable({ children }: { children?: ReactNode }) {
11
11
  const t = useTranslations();
12
12
  return (
13
- <div className="flex flex-col items-center gap-3 py-10">
14
- <EmptyState variant="missing">
13
+ <div className="pointer-events-auto flex max-w-xs flex-col items-center gap-2.5 rounded-lg border border-border bg-popover px-5 py-4 text-center shadow-lg">
14
+ <CloudOff className="size-5 text-muted-foreground" />
15
+ <p className="text-sm text-muted-foreground">
15
16
  {children ??
16
17
  t('Section data is temporarily unavailable. Try again later.', {
17
18
  note: 'kit.section_unavailable',
18
19
  })}
19
- </EmptyState>
20
+ </p>
20
21
  <button
21
22
  type="button"
22
23
  onClick={requestDumpRetry}
@@ -27,3 +28,11 @@ export function DumpUnavailable({ children }: { children?: ReactNode }) {
27
28
  </div>
28
29
  );
29
30
  }
31
+
32
+ export function DumpUnavailableOverlay({ children }: { children?: ReactNode }) {
33
+ return (
34
+ <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center p-6">
35
+ {children ?? <DumpUnavailable />}
36
+ </div>
37
+ );
38
+ }
package/src/lib/config.ts CHANGED
@@ -43,6 +43,7 @@ export type SectionConfigInput = {
43
43
  url: string;
44
44
  icon?: string;
45
45
  }[];
46
+ sections?: readonly SectionKey[];
46
47
  guide?: { bookOrder?: readonly string[] };
47
48
  }
48
49
  >;
@@ -4,6 +4,7 @@ import { sectionConfig } from './config';
4
4
  import { sections } from './sources';
5
5
  import { datasetId } from './tree';
6
6
  import { createAsyncCache } from '@terrafirmagreg-team/data/lib/async-cache';
7
+ import { dumpStatusSnapshot } from '@terrafirmagreg-team/data/lib/dump-status';
7
8
  import { loadLocaleRegistry, sortLangCodes } from '@terrafirmagreg-team/translate/server';
8
9
  import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
9
10
  import { sectionTabs } from '@terrafirmagreg-team/sites/sections';
@@ -79,6 +80,30 @@ async function datasetMeta(
79
80
  return meta ? metaFields(meta) : {};
80
81
  }
81
82
 
83
+ async function sectionDatasetMeta(
84
+ section: SectionKey,
85
+ id: string,
86
+ ): Promise<Omit<SectionDataset, 'id' | 'languages'>> {
87
+ const meta = await datasetMeta(section, id);
88
+ return Object.values(meta).some((value) => value !== undefined)
89
+ ? meta
90
+ : datasetMeta('docs', id);
91
+ }
92
+
93
+ const lastGoodDatasets = new Map<SectionKey, Map<string, SectionDataset>>();
94
+
95
+ function rememberDataset(section: SectionKey, row: SectionDataset) {
96
+ let good = lastGoodDatasets.get(section);
97
+ if (!good) lastGoodDatasets.set(section, (good = new Map()));
98
+ good.set(row.id, row);
99
+ }
100
+
101
+ function datasetMembership(section: SectionKey, id: string): boolean {
102
+ const declared = sectionConfig().datasetConfig[id]?.sections;
103
+ if (declared) return declared.includes(section);
104
+ return lastGoodDatasets.get(section)?.has(id) === true;
105
+ }
106
+
82
107
  function metaFields(
83
108
  meta: Record<string, unknown>,
84
109
  ): Omit<SectionDataset, 'id' | 'languages'> {
@@ -144,47 +169,59 @@ async function dumpDatasets(
144
169
  ): Promise<SectionDataset[]> {
145
170
  const dump = sectionConfig().dump;
146
171
  if (!dump) return [];
147
- const refs = await dump.discoverDatasets();
172
+ const refs = await dump.discoverDatasets().catch(() => [] as DatasetRef[]);
148
173
  const versions = new Map<string, string[]>();
149
- for (const ref of refs) {
150
- const { id, version } = ref;
151
- if (version) {
152
- const list = versions.get(id) ?? [];
153
- list.push(version);
154
- versions.set(id, list);
174
+ const ids: string[] = [];
175
+ if (refs.length) {
176
+ for (const ref of refs) {
177
+ const { id, version } = ref;
178
+ if (version) {
179
+ const list = versions.get(id) ?? [];
180
+ list.push(version);
181
+ versions.set(id, list);
182
+ } else {
183
+ ids.push(id);
184
+ }
185
+ }
186
+ } else {
187
+ for (const id of Object.keys(sectionConfig().datasetConfig)) {
188
+ if (datasetMembership(section, id)) ids.push(id);
155
189
  }
156
190
  }
157
- const ids = refs.filter((ref) => !ref.version);
158
- const usable = await Promise.all(
159
- ids.map(async (ref) => {
160
- const { id } = ref;
161
- const index = await dump.loadDataset(ref).catch(() => null);
162
- if (!index || !hasContent(index)) return null;
163
- return {
164
- id,
165
- languages: datasetLanguages(index.manifest.languages ?? []),
166
- };
167
- }),
168
- );
169
191
  return (
170
192
  await Promise.all(
171
- usable
172
- .filter((row): row is { id: string; languages: Lang[] } => row !== null)
173
- .map(async ({ id, languages }) => ({
193
+ ids.map(async (id): Promise<SectionDataset | null> => {
194
+ const index = await dump.loadDataset({ id }).catch(() => null);
195
+ if (!index) {
196
+ if (dumpStatusSnapshot().statuses[id]?.status !== 'failed')
197
+ return null;
198
+ const good = lastGoodDatasets.get(section)?.get(id);
199
+ if (good) return good;
200
+ if (!datasetMembership(section, id)) return null;
201
+ return {
202
+ id,
203
+ languages: datasetLanguages([]),
204
+ ...(await sectionDatasetMeta(section, id)),
205
+ ...datasetAppearance(id),
206
+ };
207
+ }
208
+ if (!hasContent(index)) return null;
209
+ const row: SectionDataset = {
174
210
  id,
175
- languages,
211
+ languages: datasetLanguages(index.manifest.languages ?? []),
176
212
  ...(versions.get(id)?.length
177
213
  ? { versions: versions.get(id)!.sort(compareVersions).reverse() }
178
214
  : {}),
179
- ...(await datasetMeta(section, id).then((meta) =>
180
- Object.values(meta).some((value) => value !== undefined)
181
- ? meta
182
- : datasetMeta('docs', id),
183
- )),
215
+ ...(await sectionDatasetMeta(section, id)),
184
216
  ...datasetAppearance(id),
185
- })),
186
- )
187
- ).sort(datasetOrder);
217
+ };
218
+ rememberDataset(section, row);
219
+ return row;
220
+ }),
221
+ )
222
+ )
223
+ .filter((row): row is SectionDataset => row !== null)
224
+ .sort(datasetOrder);
188
225
  }
189
226
 
190
227
  export function sectionDatasets(
@@ -0,0 +1,46 @@
1
+ import 'server-only';
2
+ import { stripLangFromUrl } from '@terrafirmagreg-team/translate/lib/slug';
3
+ import { sectionConfig } from './config';
4
+ import { canonicalUrl } from './urls';
5
+ import type { SectionCrumb } from './breadcrumb';
6
+ import type { SectionKey, SectionPage } from '@terrafirmagreg-team/kit/types';
7
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
8
+
9
+ export function pageJsonLd({
10
+ section,
11
+ page,
12
+ crumbs,
13
+ lang,
14
+ }: {
15
+ section: SectionKey;
16
+ page: SectionPage;
17
+ crumbs: readonly SectionCrumb[];
18
+ lang: Lang;
19
+ }): Record<string, unknown>[] {
20
+ const languages = [...sectionConfig().knownLanguages];
21
+ const url = canonicalUrl(stripLangFromUrl(page.url, languages));
22
+ const items = crumbs.length ? crumbs : [{ name: page.data.title }];
23
+ return [
24
+ {
25
+ '@context': 'https://schema.org',
26
+ '@type': 'Article',
27
+ headline: page.data.title,
28
+ description: page.data.description,
29
+ url,
30
+ inLanguage: lang.replace('_', '-'),
31
+ isPartOf: { '@type': 'WebSite', url: canonicalUrl(`/${section}`) },
32
+ },
33
+ {
34
+ '@context': 'https://schema.org',
35
+ '@type': 'BreadcrumbList',
36
+ itemListElement: items.map((crumb, index) => ({
37
+ '@type': 'ListItem',
38
+ position: index + 1,
39
+ name: crumb.name,
40
+ item: crumb.url
41
+ ? canonicalUrl(stripLangFromUrl(crumb.url, languages))
42
+ : url,
43
+ })),
44
+ },
45
+ ];
46
+ }
@@ -1,5 +1,9 @@
1
1
  import 'server-only';
2
+ import type { Metadata } from 'next';
3
+ import { notFound } from 'next/navigation';
2
4
  import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
5
+ import { sectionUrl } from '@terrafirmagreg-team/sites/location';
6
+ import { canonicalUrl, getDatasetImageUrl } from './urls';
3
7
  import { datasetSlugs, findDataset } from './datasets';
4
8
  import type {
5
9
  DatasetLanding,
@@ -30,6 +34,36 @@ export function getDatasetLanding(
30
34
  };
31
35
  }
32
36
 
37
+ export function datasetLandingMetadata(
38
+ section: SectionKey,
39
+ slug: string[] | undefined,
40
+ datasets: readonly SectionDataset[],
41
+ ): Metadata {
42
+ const page = getDatasetLanding(section, slug, 'en_us', datasets);
43
+ if (!page) notFound();
44
+ const meta = findDataset(datasets, { id: slug?.[0] ?? '' });
45
+ const description = page.description ?? meta?.description;
46
+ const title = page.datasetTitle ?? page.title;
47
+ const url = canonicalUrl(`${sectionUrl(section)}/${(slug ?? []).join('/')}`);
48
+ return {
49
+ title,
50
+ description,
51
+ alternates: { canonical: url },
52
+ openGraph: {
53
+ type: 'website',
54
+ url,
55
+ title,
56
+ description,
57
+ images: getDatasetImageUrl(section, slug?.[0] ?? '').url,
58
+ },
59
+ twitter: {
60
+ card: 'summary_large_image',
61
+ title,
62
+ description,
63
+ },
64
+ };
65
+ }
66
+
33
67
  export function resolveDatasetLanding(
34
68
  section: SectionKey,
35
69
  slug: string[] | undefined,
package/src/lib/page.ts CHANGED
@@ -1,30 +1,16 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { notFound } from 'next/navigation';
3
- import { createGetUrl } from 'fumadocs-core/source';
4
3
  import { firstLocalizedPage } from '@terrafirmagreg-team/translate/server';
5
4
  import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
6
5
  import { stripLangFromUrl, toInternalSlugs, toPublicSlug } from '@terrafirmagreg-team/translate/lib/slug';
7
6
  import { sectionConfig } from './config';
8
7
  import { sectionDatasets } from './datasets';
8
+ import { canonicalUrl, getPageImageUrl } from './urls';
9
9
  import type { SectionKey, SectionPage, SectionSource } from '@terrafirmagreg-team/kit/types';
10
10
  import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
11
11
 
12
12
  export type { SectionSource } from '@terrafirmagreg-team/kit/types';
13
-
14
- const canonicalUrl = (pathname: string): string =>
15
- new URL(
16
- pathname.startsWith('/') ? pathname : `/${pathname}`,
17
- sectionConfig().siteUrl,
18
- ).toString();
19
-
20
- let imageUrl: ReturnType<typeof createGetUrl> | undefined;
21
- const getImageUrl = () =>
22
- (imageUrl ??= createGetUrl(sectionConfig().previewRoute));
23
-
24
- function getDatasetImageUrl(section: string, dataset: string) {
25
- const segments = [section, dataset, 'image.png'];
26
- return { segments, url: getImageUrl()(segments) };
27
- }
13
+ export { getDatasetImageUrl } from './urls';
28
14
 
29
15
  export function getPage<S extends SectionSource>(
30
16
  source: S,
@@ -97,7 +83,7 @@ export async function sectionMetadata(
97
83
  url,
98
84
  title: doc.data.title,
99
85
  description: doc.data.description,
100
- images: getDatasetImageUrl(section, doc.slugs[0]).url,
86
+ images: getPageImageUrl(section, doc.slugs).url,
101
87
  },
102
88
  twitter: {
103
89
  card: 'summary_large_image',
package/src/lib/urls.ts CHANGED
@@ -15,3 +15,21 @@ export function getDatasetImageUrl(section: string, dataset: string) {
15
15
  };
16
16
  }
17
17
 
18
+ export function getPageImageUrl(section: string, slugs: string[]) {
19
+ const segments = [section, ...slugs, 'image.png'];
20
+
21
+ return {
22
+ segments,
23
+ url: createGetUrl(sectionConfig().previewRoute)(segments),
24
+ };
25
+ }
26
+
27
+ export function createGitFileUrl(git: {
28
+ user: string;
29
+ repo: string;
30
+ branch: string;
31
+ }) {
32
+ return (filePath: string) =>
33
+ `https://github.com/${git.user}/${git.repo}/blob/${git.branch}/${filePath}`;
34
+ }
35
+