@notionx/create-notionx-app 1.0.0 → 2.0.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/dist/cli-notionx.js +25 -1
- package/dist/cli-notionx.js.map +1 -1
- package/dist/locale-add/persist.js +113 -0
- package/dist/locale-add/persist.js.map +1 -0
- package/dist/locale-add/plan.js +202 -21
- package/dist/locale-add/plan.js.map +1 -1
- package/dist/metadata.js.map +1 -1
- package/dist/notion-translation-sources/apply.js +11 -26
- package/dist/notion-translation-sources/apply.js.map +1 -1
- package/dist/notion-translation-sources/plan.js +25 -0
- package/dist/notion-translation-sources/plan.js.map +1 -1
- package/dist/provision/__tests__/translation-properties.test.js +86 -0
- package/dist/provision/__tests__/translation-properties.test.js.map +1 -0
- package/dist/provision/credentials.js +67 -0
- package/dist/provision/credentials.js.map +1 -0
- package/dist/provision/index.js +188 -11
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/notion.js +422 -269
- package/dist/provision/notion.js.map +1 -1
- package/dist/provision/notion.test.js +143 -116
- package/dist/provision/notion.test.js.map +1 -1
- package/dist/provision/wire.js +16 -0
- package/dist/provision/wire.js.map +1 -1
- package/dist/registry/install.test.js +2 -0
- package/dist/registry/install.test.js.map +1 -1
- package/dist/registry/project-meta.js +4 -2
- package/dist/registry/project-meta.js.map +1 -1
- package/dist/registry/registry-types.js.map +1 -1
- package/dist/registry/render-content-source-files.js +1 -0
- package/dist/registry/render-content-source-files.js.map +1 -1
- package/dist/registry/render-multi-source.js +72 -28
- package/dist/registry/render-multi-source.js.map +1 -1
- package/dist/registry/update.test.js +2 -0
- package/dist/registry/update.test.js.map +1 -1
- package/dist/render.js +2 -0
- package/dist/render.js.map +1 -1
- package/dist/render.test.js +18 -12
- package/dist/render.test.js.map +1 -1
- package/dist/templates/.dev.vars.example.tmpl +4 -0
- package/dist/templates/__tests__/middleware-integration.test.ts +58 -0
- package/dist/templates/app/{{contentSourceListPath}}/[slug]/page.tsx.tmpl +8 -4
- package/dist/templates/app/{{contentSourceListPath}}/page.tsx.tmpl +8 -4
- package/dist/templates/components/page-blocks/feature-grid-block.tsx.tmpl +4 -56
- package/dist/templates/components/page-blocks/hero-block.tsx.tmpl +6 -67
- package/dist/templates/components/page-blocks/latest-posts-block.tsx.tmpl +11 -19
- package/dist/templates/components/page-blocks/story-block.tsx.tmpl +4 -62
- package/dist/templates/components/page-blocks.tsx.tmpl +5 -5
- package/dist/templates/components/site/site-header.tsx.tmpl +5 -3
- package/dist/templates/env.d.ts.tmpl +8 -0
- package/dist/templates/lib/blocks/translations.ts.tmpl +22 -1
- package/dist/templates/lib/blog/translations.ts.tmpl +18 -2
- package/dist/templates/lib/content/models.ts.tmpl +6 -0
- package/dist/templates/lib/pages/source.ts.tmpl +136 -334
- package/dist/templates/lib/pages/translations.ts.tmpl +23 -1
- package/dist/templates/lib/site/request-env.ts.tmpl +16 -0
- package/dist/templates/lib/site/settings.ts.tmpl +96 -179
- package/dist/templates/lib/site/translations.ts.tmpl +34 -11
- package/dist/templates/middleware.ts.tmpl +56 -0
- package/dist/templates/worker/index.ts.tmpl +14 -5
- package/dist/templates/wrangler.jsonc.tmpl +5 -1
- package/package.json +1 -1
|
@@ -7,8 +7,10 @@ import Link from "next/link";
|
|
|
7
7
|
import Image from "next/image";
|
|
8
8
|
import { notFound } from "next/navigation";
|
|
9
9
|
import type { Metadata } from "next";
|
|
10
|
-
import {
|
|
10
|
+
import { getGenericNotionContentBySlugForLocale } from "@notionx/core";
|
|
11
11
|
import { {{contentSourceVarName}} } from "@/lib/content/models";
|
|
12
|
+
import { getRequestLocale } from "@/lib/site/request-env";
|
|
13
|
+
import { i18n } from "@/lib/i18n";
|
|
12
14
|
import { Badge } from "@/components/ui/badge";
|
|
13
15
|
import { NotionBlocks } from "@/components/notion-blocks";
|
|
14
16
|
import { SiteShell } from "@/components/site/site-shell";
|
|
@@ -35,7 +37,8 @@ export async function generateMetadata({
|
|
|
35
37
|
params: Promise<Params>;
|
|
36
38
|
}): Promise<Metadata> {
|
|
37
39
|
const { slug } = await params;
|
|
38
|
-
const
|
|
40
|
+
const locale = getRequestLocale(i18n.defaultLocale);
|
|
41
|
+
const item = await getGenericNotionContentBySlugForLocale({{contentSourceVarName}}, slug, locale);
|
|
39
42
|
if (!item) return { title: "Not found" };
|
|
40
43
|
return {
|
|
41
44
|
title: item.title,
|
|
@@ -52,8 +55,9 @@ export default async function {{contentSourceConstName}}DetailPage({
|
|
|
52
55
|
params: Promise<Params>;
|
|
53
56
|
}) {
|
|
54
57
|
const { slug } = await params;
|
|
55
|
-
const
|
|
56
|
-
const
|
|
58
|
+
const locale = getRequestLocale(i18n.defaultLocale);
|
|
59
|
+
const item = await getGenericNotionContentBySlugForLocale({{contentSourceVarName}}, slug, locale);
|
|
60
|
+
const page = await getSitePageForContentSource("{{contentSourceId}}", locale);
|
|
57
61
|
if (!item) notFound();
|
|
58
62
|
|
|
59
63
|
return (
|
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
// the Notion-backed Pages model.
|
|
5
5
|
|
|
6
6
|
import type { Metadata } from "next";
|
|
7
|
-
import {
|
|
7
|
+
import { listGenericNotionContentForLocale } from "@notionx/core";
|
|
8
8
|
import { PostCard } from "@/components/content/post-card";
|
|
9
9
|
import { {{contentSourceVarName}} } from "@/lib/content/models";
|
|
10
10
|
import { SiteShell } from "@/components/site/site-shell";
|
|
11
11
|
import { getSitePageForContentSource } from "@/lib/pages/source";
|
|
12
|
+
import { getRequestLocale } from "@/lib/site/request-env";
|
|
13
|
+
import { i18n } from "@/lib/i18n";
|
|
12
14
|
|
|
13
15
|
export const revalidate = 300;
|
|
14
16
|
|
|
15
17
|
export async function generateMetadata(): Promise<Metadata> {
|
|
16
|
-
const
|
|
18
|
+
const locale = getRequestLocale(i18n.defaultLocale);
|
|
19
|
+
const page = await getSitePageForContentSource("{{contentSourceId}}", locale);
|
|
17
20
|
return {
|
|
18
21
|
title: page?.seoTitle || page?.title || "{{contentSourceListTitle}}",
|
|
19
22
|
description:
|
|
@@ -22,8 +25,9 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export default async function {{contentSourceConstName}}Page() {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
28
|
+
const locale = getRequestLocale(i18n.defaultLocale);
|
|
29
|
+
const items = await listGenericNotionContentForLocale({{contentSourceVarName}}, locale);
|
|
30
|
+
const page = await getSitePageForContentSource("{{contentSourceId}}", locale);
|
|
27
31
|
const title = page?.title || "{{contentSourceListTitle}}";
|
|
28
32
|
const description = page?.description || "{{contentSourceListDescription}}";
|
|
29
33
|
|
|
@@ -1,67 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardDescription,
|
|
7
|
-
CardHeader,
|
|
8
|
-
CardTitle,
|
|
9
|
-
} from "@/components/ui/card";
|
|
10
|
-
import type { StructuredFeatureGridBlock } from "@/lib/pages/source";
|
|
11
|
-
|
|
12
|
-
function gridClassName(columns: StructuredFeatureGridBlock["columns"]) {
|
|
13
|
-
if (columns === 2) return "grid gap-6 md:grid-cols-2";
|
|
14
|
-
if (columns === 4) return "grid gap-6 md:grid-cols-2 xl:grid-cols-4";
|
|
15
|
-
return "grid gap-6 md:grid-cols-3";
|
|
16
|
-
}
|
|
1
|
+
import { NotionBlocks } from "@/components/notion-blocks";
|
|
2
|
+
import type { StructuredPageBlock } from "@/lib/pages/source";
|
|
17
3
|
|
|
18
4
|
export function FeatureGridBlock({
|
|
19
5
|
block,
|
|
20
6
|
}: {
|
|
21
|
-
block:
|
|
7
|
+
block: StructuredPageBlock;
|
|
22
8
|
}) {
|
|
23
9
|
return (
|
|
24
10
|
<section className="border-t bg-background">
|
|
25
11
|
<div className="container mx-auto max-w-6xl px-4 py-14">
|
|
26
|
-
<
|
|
27
|
-
<Badge variant="secondary">Feature Grid</Badge>
|
|
28
|
-
<h2 className="mt-4 text-3xl font-semibold tracking-tight md:text-4xl">
|
|
29
|
-
{block.headline}
|
|
30
|
-
</h2>
|
|
31
|
-
{block.body ? (
|
|
32
|
-
<p className="mt-4 text-lg text-muted-foreground">{block.body}</p>
|
|
33
|
-
) : null}
|
|
34
|
-
</div>
|
|
35
|
-
<div className={["mt-10", gridClassName(block.columns)].join(" ")}>
|
|
36
|
-
{block.items.map((item) => {
|
|
37
|
-
const card = (
|
|
38
|
-
<Card className="h-full">
|
|
39
|
-
<CardHeader>
|
|
40
|
-
<Badge variant="outline" className="w-fit">
|
|
41
|
-
{item.icon}
|
|
42
|
-
</Badge>
|
|
43
|
-
<CardTitle className="text-xl">{item.title}</CardTitle>
|
|
44
|
-
<CardDescription>{item.description}</CardDescription>
|
|
45
|
-
</CardHeader>
|
|
46
|
-
<CardContent>
|
|
47
|
-
{item.href ? (
|
|
48
|
-
<span className="text-sm font-medium text-primary">
|
|
49
|
-
Explore more
|
|
50
|
-
</span>
|
|
51
|
-
) : null}
|
|
52
|
-
</CardContent>
|
|
53
|
-
</Card>
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
return item.href ? (
|
|
57
|
-
<Link key={item.title} href={item.href} className="block">
|
|
58
|
-
{card}
|
|
59
|
-
</Link>
|
|
60
|
-
) : (
|
|
61
|
-
<div key={item.title}>{card}</div>
|
|
62
|
-
);
|
|
63
|
-
})}
|
|
64
|
-
</div>
|
|
12
|
+
<NotionBlocks blocks={block.blocks} />
|
|
65
13
|
</div>
|
|
66
14
|
</section>
|
|
67
15
|
);
|
|
@@ -1,72 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
import type { StructuredHeroBlock } from "@/lib/pages/source";
|
|
5
|
-
|
|
6
|
-
function sectionClassName(theme: StructuredHeroBlock["theme"]) {
|
|
7
|
-
if (theme === "inverse") return "border-b bg-primary text-primary-foreground";
|
|
8
|
-
if (theme === "default") return "border-b bg-background";
|
|
9
|
-
return "border-b bg-muted/20";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function HeroBlock({ block }: { block: StructuredHeroBlock }) {
|
|
13
|
-
const isCentered = block.alignment === "center";
|
|
1
|
+
import { NotionBlocks } from "@/components/notion-blocks";
|
|
2
|
+
import type { StructuredPageBlock } from "@/lib/pages/source";
|
|
14
3
|
|
|
4
|
+
export function HeroBlock({ block }: { block: StructuredPageBlock }) {
|
|
15
5
|
return (
|
|
16
|
-
<section className=
|
|
17
|
-
<div
|
|
18
|
-
|
|
19
|
-
"container mx-auto max-w-5xl px-4 py-16 md:py-24",
|
|
20
|
-
isCentered ? "text-center" : "text-left",
|
|
21
|
-
].join(" ")}
|
|
22
|
-
>
|
|
23
|
-
{block.eyebrow ? (
|
|
24
|
-
<Badge variant={block.theme === "inverse" ? "secondary" : "outline"}>
|
|
25
|
-
{block.eyebrow}
|
|
26
|
-
</Badge>
|
|
27
|
-
) : null}
|
|
28
|
-
<h2 className="mt-5 text-4xl font-semibold tracking-tight md:text-6xl">
|
|
29
|
-
{block.headline}
|
|
30
|
-
</h2>
|
|
31
|
-
{block.subheadline ? (
|
|
32
|
-
<p
|
|
33
|
-
className={[
|
|
34
|
-
"mt-5 max-w-2xl text-lg text-muted-foreground",
|
|
35
|
-
isCentered ? "mx-auto" : "",
|
|
36
|
-
block.theme === "inverse" ? "text-primary-foreground/80" : "",
|
|
37
|
-
].join(" ")}
|
|
38
|
-
>
|
|
39
|
-
{block.subheadline}
|
|
40
|
-
</p>
|
|
41
|
-
) : null}
|
|
42
|
-
{block.description ? (
|
|
43
|
-
<p
|
|
44
|
-
className={[
|
|
45
|
-
"mt-4 max-w-2xl text-sm",
|
|
46
|
-
isCentered ? "mx-auto" : "",
|
|
47
|
-
block.theme === "inverse" ? "text-primary-foreground/70" : "text-muted-foreground",
|
|
48
|
-
].join(" ")}
|
|
49
|
-
>
|
|
50
|
-
{block.description}
|
|
51
|
-
</p>
|
|
52
|
-
) : null}
|
|
53
|
-
<div
|
|
54
|
-
className={[
|
|
55
|
-
"mt-8 flex flex-wrap gap-3",
|
|
56
|
-
isCentered ? "justify-center" : "justify-start",
|
|
57
|
-
].join(" ")}
|
|
58
|
-
>
|
|
59
|
-
{block.primaryCta ? (
|
|
60
|
-
<Button asChild size="lg">
|
|
61
|
-
<Link href={block.primaryCta.href}>{block.primaryCta.label}</Link>
|
|
62
|
-
</Button>
|
|
63
|
-
) : null}
|
|
64
|
-
{block.secondaryCta ? (
|
|
65
|
-
<Button asChild size="lg" variant="secondary">
|
|
66
|
-
<Link href={block.secondaryCta.href}>{block.secondaryCta.label}</Link>
|
|
67
|
-
</Button>
|
|
68
|
-
) : null}
|
|
69
|
-
</div>
|
|
6
|
+
<section className="border-b bg-muted/20">
|
|
7
|
+
<div className="container mx-auto max-w-5xl px-4 py-16 md:py-24">
|
|
8
|
+
<NotionBlocks blocks={block.blocks} />
|
|
70
9
|
</div>
|
|
71
10
|
</section>
|
|
72
11
|
);
|
|
@@ -3,39 +3,31 @@ import { ArrowRight } from "lucide-react";
|
|
|
3
3
|
import { listGenericNotionContent } from "@notionx/core/notion";
|
|
4
4
|
import { {{contentSourceVarName}} } from "@/lib/content/models";
|
|
5
5
|
import { PostCard } from "@/components/content/post-card";
|
|
6
|
-
import {
|
|
6
|
+
import { NotionBlocks } from "@/components/notion-blocks";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
|
-
import type {
|
|
8
|
+
import type { StructuredPageBlock } from "@/lib/pages/source";
|
|
9
9
|
|
|
10
10
|
export async function LatestPostsBlock({
|
|
11
11
|
block,
|
|
12
12
|
}: {
|
|
13
|
-
block:
|
|
13
|
+
block: StructuredPageBlock;
|
|
14
14
|
}) {
|
|
15
15
|
const items = await listGenericNotionContent({{contentSourceVarName}});
|
|
16
|
-
const visibleItems = items.slice(0,
|
|
16
|
+
const visibleItems = items.slice(0, 6);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<section className="border-t bg-background">
|
|
20
20
|
<div className="container mx-auto max-w-6xl px-4 py-14">
|
|
21
21
|
<div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
|
22
22
|
<div className="max-w-3xl">
|
|
23
|
-
<
|
|
24
|
-
<h2 className="mt-4 text-3xl font-semibold tracking-tight md:text-4xl">
|
|
25
|
-
{block.headline}
|
|
26
|
-
</h2>
|
|
27
|
-
{block.body ? (
|
|
28
|
-
<p className="mt-4 text-lg text-muted-foreground">{block.body}</p>
|
|
29
|
-
) : null}
|
|
23
|
+
<NotionBlocks blocks={block.blocks} />
|
|
30
24
|
</div>
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</Button>
|
|
38
|
-
) : null}
|
|
25
|
+
<Button asChild variant="outline">
|
|
26
|
+
<Link href="{{contentSourceListPath}}">
|
|
27
|
+
View all posts
|
|
28
|
+
<ArrowRight className="ml-2 h-4 w-4" />
|
|
29
|
+
</Link>
|
|
30
|
+
</Button>
|
|
39
31
|
</div>
|
|
40
32
|
|
|
41
33
|
{visibleItems.length ? (
|
|
@@ -1,69 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
Card,
|
|
4
|
-
CardContent,
|
|
5
|
-
CardDescription,
|
|
6
|
-
CardHeader,
|
|
7
|
-
CardTitle,
|
|
8
|
-
} from "@/components/ui/card";
|
|
9
|
-
import type { StructuredStoryBlock } from "@/lib/pages/source";
|
|
10
|
-
|
|
11
|
-
function layoutClassName(layout: StructuredStoryBlock["layout"]) {
|
|
12
|
-
if (layout === "text-left") return "grid gap-8 lg:grid-cols-[1.2fr_0.8fr]";
|
|
13
|
-
if (layout === "media-left") return "grid gap-8 lg:grid-cols-2";
|
|
14
|
-
return "grid gap-8 lg:grid-cols-2";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function StoryBlock({ block }: { block: StructuredStoryBlock }) {
|
|
18
|
-
const mediaFirst = block.layout === "media-left";
|
|
19
|
-
|
|
20
|
-
const media = block.mediaUrl ? (
|
|
21
|
-
<div className="overflow-hidden rounded-2xl border bg-muted">
|
|
22
|
-
<img
|
|
23
|
-
src={block.mediaUrl}
|
|
24
|
-
alt={block.title}
|
|
25
|
-
className="h-full min-h-72 w-full object-cover"
|
|
26
|
-
/>
|
|
27
|
-
</div>
|
|
28
|
-
) : (
|
|
29
|
-
<Card>
|
|
30
|
-
<CardHeader>
|
|
31
|
-
<CardTitle>{block.title}</CardTitle>
|
|
32
|
-
<CardDescription>{block.description}</CardDescription>
|
|
33
|
-
</CardHeader>
|
|
34
|
-
<CardContent className="text-sm text-muted-foreground">
|
|
35
|
-
Add a media URL in the reusable block row to replace this placeholder.
|
|
36
|
-
</CardContent>
|
|
37
|
-
</Card>
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const copy = (
|
|
41
|
-
<div className="space-y-5">
|
|
42
|
-
<Badge variant="outline">Story</Badge>
|
|
43
|
-
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl">
|
|
44
|
-
{block.headline}
|
|
45
|
-
</h2>
|
|
46
|
-
<p className="text-base leading-7 text-muted-foreground">{block.body}</p>
|
|
47
|
-
{block.quote ? (
|
|
48
|
-
<blockquote className="rounded-2xl border bg-muted/40 p-6">
|
|
49
|
-
<p className="text-lg font-medium leading-8">"{block.quote}"</p>
|
|
50
|
-
{block.quoteAttribution ? (
|
|
51
|
-
<footer className="mt-3 text-sm text-muted-foreground">
|
|
52
|
-
{block.quoteAttribution}
|
|
53
|
-
</footer>
|
|
54
|
-
) : null}
|
|
55
|
-
</blockquote>
|
|
56
|
-
) : null}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
1
|
+
import { NotionBlocks } from "@/components/notion-blocks";
|
|
2
|
+
import type { StructuredPageBlock } from "@/lib/pages/source";
|
|
59
3
|
|
|
4
|
+
export function StoryBlock({ block }: { block: StructuredPageBlock }) {
|
|
60
5
|
return (
|
|
61
6
|
<section className="border-t bg-muted/10">
|
|
62
7
|
<div className="container mx-auto max-w-6xl px-4 py-14">
|
|
63
|
-
<
|
|
64
|
-
{mediaFirst ? media : copy}
|
|
65
|
-
{mediaFirst ? copy : media}
|
|
66
|
-
</div>
|
|
8
|
+
<NotionBlocks blocks={block.blocks} />
|
|
67
9
|
</div>
|
|
68
10
|
</section>
|
|
69
11
|
);
|
|
@@ -14,11 +14,11 @@ export async function PageBlocks({ blocks }: { blocks: StructuredBlock[] }) {
|
|
|
14
14
|
<>
|
|
15
15
|
{blocks.map((block) => (
|
|
16
16
|
<div key={block.slug}>
|
|
17
|
-
{block.
|
|
18
|
-
{block.
|
|
19
|
-
{block.
|
|
20
|
-
{block.
|
|
21
|
-
{block.
|
|
17
|
+
{block.variant === "hero" ? <HeroBlock block={block} /> : null}
|
|
18
|
+
{block.variant === "feature-grid" ? <FeatureGridBlock block={block} /> : null}
|
|
19
|
+
{block.variant === "story" ? <StoryBlock block={block} /> : null}
|
|
20
|
+
{block.variant === "latest-posts" ? <LatestPostsBlock block={block} /> : null}
|
|
21
|
+
{block.variant === "legacy" || !["hero", "feature-grid", "story", "latest-posts"].includes(block.variant) ? (
|
|
22
22
|
<section className="border-t bg-muted/10">
|
|
23
23
|
<div className="container mx-auto max-w-5xl px-4 py-14">
|
|
24
24
|
<NotionBlocks blocks={block.blocks} />
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { ThemeToggle } from "@/components/theme-toggle";
|
|
11
11
|
import { LocaleSwitcher } from "./locale-switcher";
|
|
12
12
|
import { i18n } from "@/lib/i18n";
|
|
13
|
+
import { getRequestLocale } from "@/lib/site/request-env";
|
|
13
14
|
import { getSiteNavigation } from "@/lib/pages/source";
|
|
14
15
|
import { getSiteSettings } from "@/lib/site/settings";
|
|
15
16
|
|
|
@@ -20,9 +21,10 @@ type NavItem = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export async function SiteHeader() {
|
|
24
|
+
const locale = getRequestLocale(i18n.defaultLocale);
|
|
23
25
|
const [pageNav, settings] = await Promise.all([
|
|
24
|
-
getSiteNavigation() as Promise<NavItem[]>,
|
|
25
|
-
getSiteSettings(),
|
|
26
|
+
getSiteNavigation(locale) as Promise<NavItem[]>,
|
|
27
|
+
getSiteSettings(locale),
|
|
26
28
|
]);
|
|
27
29
|
const nav =
|
|
28
30
|
pageNav.length > 0 ? pageNav : (settings.navigation.main as unknown as NavItem[]);
|
|
@@ -64,7 +66,7 @@ export async function SiteHeader() {
|
|
|
64
66
|
</Button>
|
|
65
67
|
) : null}
|
|
66
68
|
{i18n.supportedLocales.length > 1 ? (
|
|
67
|
-
<LocaleSwitcher currentLocale={
|
|
69
|
+
<LocaleSwitcher currentLocale={locale} />
|
|
68
70
|
) : null}
|
|
69
71
|
<ThemeToggle />
|
|
70
72
|
<Button asChild variant="outline" size="sm">
|
|
@@ -18,6 +18,14 @@ interface Env {
|
|
|
18
18
|
// here as a sentinel for "Notion not configured" — the loader
|
|
19
19
|
// will fall back to the static values in `lib/site/config.ts`.
|
|
20
20
|
NOTION_SITE_SETTINGS_DATA_SOURCE_ID?: string;
|
|
21
|
+
// Translation data sources (populated by `notionx locale add` or
|
|
22
|
+
// by the scaffolder when bilingual mode is enabled). Each holds
|
|
23
|
+
// locale-specific rows that the runtime merges with the base
|
|
24
|
+
// source via the LocaleContract fallback rule.
|
|
25
|
+
NOTION_BLOG_TRANSLATIONS_DATA_SOURCE_ID?: string;
|
|
26
|
+
NOTION_PAGES_TRANSLATIONS_DATA_SOURCE_ID?: string;
|
|
27
|
+
NOTION_BLOCKS_TRANSLATIONS_DATA_SOURCE_ID?: string;
|
|
28
|
+
NOTION_SITE_SETTINGS_TRANSLATIONS_DATA_SOURCE_ID?: string;
|
|
21
29
|
SITE_URL?: string;
|
|
22
30
|
}
|
|
23
31
|
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
import {
|
|
6
6
|
pickTranslation,
|
|
7
7
|
pickTranslationOrDefault,
|
|
8
|
+
mapNotionPageToLocalizedContentTranslation,
|
|
9
|
+
listGenericNotionContentForLocale,
|
|
8
10
|
} from "@notionx/core";
|
|
9
11
|
import { i18n } from "@/lib/i18n";
|
|
10
12
|
import { blocksContract } from "@/lib/locale-contract";
|
|
13
|
+
import { blockTranslationsSource } from "@/lib/content/models";
|
|
14
|
+
import type { NotionPageLike } from "@notionx/core";
|
|
11
15
|
|
|
12
16
|
export type BlockTranslation = {
|
|
13
17
|
pageId: string;
|
|
@@ -18,7 +22,8 @@ export type BlockTranslation = {
|
|
|
18
22
|
eyebrow: string;
|
|
19
23
|
headline: string;
|
|
20
24
|
subheadline: string;
|
|
21
|
-
|
|
25
|
+
// Body content is read from the translation page's children blocks
|
|
26
|
+
// (via getPageBlocks(translationPageId)), not a Notion property.
|
|
22
27
|
quote: string;
|
|
23
28
|
quoteAttribution: string;
|
|
24
29
|
primaryCtaLabel: string;
|
|
@@ -42,3 +47,19 @@ export function pickBlockTranslation(
|
|
|
42
47
|
)
|
|
43
48
|
);
|
|
44
49
|
}
|
|
50
|
+
|
|
51
|
+
export async function listBlockTranslations(locale: string): Promise<BlockTranslation[]> {
|
|
52
|
+
if (!blockTranslationsSource) return [];
|
|
53
|
+
const pages = await listGenericNotionContentForLocale(blockTranslationsSource, locale);
|
|
54
|
+
return pages
|
|
55
|
+
.map((p) => mapNotionPageToLocalizedContentTranslation<BlockTranslation>(p as unknown as NotionPageLike, {
|
|
56
|
+
fields: {
|
|
57
|
+
title: "Title",
|
|
58
|
+
source: "Source",
|
|
59
|
+
locale: "Locale",
|
|
60
|
+
slug: "Slug",
|
|
61
|
+
published: "Published",
|
|
62
|
+
},
|
|
63
|
+
}))
|
|
64
|
+
.filter((row): row is BlockTranslation => row !== null);
|
|
65
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
// Locale-aware blog lookup. Falls back
|
|
2
|
-
//
|
|
1
|
+
// Locale-aware blog lookup. Falls back per the blog contract's
|
|
2
|
+
// `hide` rule: a base post without a translation in the target
|
|
3
|
+
// locale is hidden from the list.
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
pickTranslation,
|
|
6
7
|
hideWhenMissing,
|
|
7
8
|
mapNotionPageToLocalizedContentTranslation,
|
|
9
|
+
listGenericNotionContentForLocale,
|
|
8
10
|
} from "@notionx/core";
|
|
9
11
|
import { i18n } from "@/lib/i18n";
|
|
10
12
|
import { blogContract } from "@/lib/locale-contract";
|
|
13
|
+
import { blogTranslationsSource } from "@/lib/content/models";
|
|
11
14
|
import type { NotionPageLike } from "@notionx/core";
|
|
12
15
|
|
|
13
16
|
export type BlogTranslationExtra = {
|
|
@@ -50,3 +53,16 @@ export function blogListForLocale(
|
|
|
50
53
|
) {
|
|
51
54
|
return hideWhenMissing(rows, locale);
|
|
52
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load all blog translation rows for a given locale from the
|
|
59
|
+
* `blog-translations` Notion data source. Returns an empty array
|
|
60
|
+
* when the translation source is not configured.
|
|
61
|
+
*/
|
|
62
|
+
export async function listBlogTranslations(locale: string): Promise<BlogTranslation[]> {
|
|
63
|
+
if (!blogTranslationsSource) return [];
|
|
64
|
+
const pages = await listGenericNotionContentForLocale(blogTranslationsSource, locale);
|
|
65
|
+
return pages
|
|
66
|
+
.map((p) => mapBlogTranslation(p as unknown as NotionPageLike))
|
|
67
|
+
.filter((row): row is BlogTranslation => row !== null);
|
|
68
|
+
}
|
|
@@ -34,6 +34,12 @@ import {
|
|
|
34
34
|
{{internalSourceDeclarations}}
|
|
35
35
|
// END generated-internal-sources
|
|
36
36
|
|
|
37
|
+
// BEGIN generated-translation-sources
|
|
38
|
+
// (Regenerated by `notionx add` / `notionx remove` / `notionx
|
|
39
|
+
// update`. Do not edit between these markers.)
|
|
40
|
+
{{translationSourceDeclarations}}
|
|
41
|
+
// END generated-translation-sources
|
|
42
|
+
|
|
37
43
|
// `contentSources` is the public list shown in admin nav and
|
|
38
44
|
// iterated by search/revalidation. `managedContentSources`
|
|
39
45
|
// additionally includes any internal singleton sources
|