@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.
Files changed (61) hide show
  1. package/dist/cli-notionx.js +25 -1
  2. package/dist/cli-notionx.js.map +1 -1
  3. package/dist/locale-add/persist.js +113 -0
  4. package/dist/locale-add/persist.js.map +1 -0
  5. package/dist/locale-add/plan.js +202 -21
  6. package/dist/locale-add/plan.js.map +1 -1
  7. package/dist/metadata.js.map +1 -1
  8. package/dist/notion-translation-sources/apply.js +11 -26
  9. package/dist/notion-translation-sources/apply.js.map +1 -1
  10. package/dist/notion-translation-sources/plan.js +25 -0
  11. package/dist/notion-translation-sources/plan.js.map +1 -1
  12. package/dist/provision/__tests__/translation-properties.test.js +86 -0
  13. package/dist/provision/__tests__/translation-properties.test.js.map +1 -0
  14. package/dist/provision/credentials.js +67 -0
  15. package/dist/provision/credentials.js.map +1 -0
  16. package/dist/provision/index.js +188 -11
  17. package/dist/provision/index.js.map +1 -1
  18. package/dist/provision/notion.js +422 -269
  19. package/dist/provision/notion.js.map +1 -1
  20. package/dist/provision/notion.test.js +143 -116
  21. package/dist/provision/notion.test.js.map +1 -1
  22. package/dist/provision/wire.js +16 -0
  23. package/dist/provision/wire.js.map +1 -1
  24. package/dist/registry/install.test.js +2 -0
  25. package/dist/registry/install.test.js.map +1 -1
  26. package/dist/registry/project-meta.js +4 -2
  27. package/dist/registry/project-meta.js.map +1 -1
  28. package/dist/registry/registry-types.js.map +1 -1
  29. package/dist/registry/render-content-source-files.js +1 -0
  30. package/dist/registry/render-content-source-files.js.map +1 -1
  31. package/dist/registry/render-multi-source.js +72 -28
  32. package/dist/registry/render-multi-source.js.map +1 -1
  33. package/dist/registry/update.test.js +2 -0
  34. package/dist/registry/update.test.js.map +1 -1
  35. package/dist/render.js +2 -0
  36. package/dist/render.js.map +1 -1
  37. package/dist/render.test.js +18 -12
  38. package/dist/render.test.js.map +1 -1
  39. package/dist/templates/.dev.vars.example.tmpl +4 -0
  40. package/dist/templates/__tests__/middleware-integration.test.ts +58 -0
  41. package/dist/templates/app/{{contentSourceListPath}}/[slug]/page.tsx.tmpl +8 -4
  42. package/dist/templates/app/{{contentSourceListPath}}/page.tsx.tmpl +8 -4
  43. package/dist/templates/components/page-blocks/feature-grid-block.tsx.tmpl +4 -56
  44. package/dist/templates/components/page-blocks/hero-block.tsx.tmpl +6 -67
  45. package/dist/templates/components/page-blocks/latest-posts-block.tsx.tmpl +11 -19
  46. package/dist/templates/components/page-blocks/story-block.tsx.tmpl +4 -62
  47. package/dist/templates/components/page-blocks.tsx.tmpl +5 -5
  48. package/dist/templates/components/site/site-header.tsx.tmpl +5 -3
  49. package/dist/templates/env.d.ts.tmpl +8 -0
  50. package/dist/templates/lib/blocks/translations.ts.tmpl +22 -1
  51. package/dist/templates/lib/blog/translations.ts.tmpl +18 -2
  52. package/dist/templates/lib/content/models.ts.tmpl +6 -0
  53. package/dist/templates/lib/pages/source.ts.tmpl +136 -334
  54. package/dist/templates/lib/pages/translations.ts.tmpl +23 -1
  55. package/dist/templates/lib/site/request-env.ts.tmpl +16 -0
  56. package/dist/templates/lib/site/settings.ts.tmpl +96 -179
  57. package/dist/templates/lib/site/translations.ts.tmpl +34 -11
  58. package/dist/templates/middleware.ts.tmpl +56 -0
  59. package/dist/templates/worker/index.ts.tmpl +14 -5
  60. package/dist/templates/wrangler.jsonc.tmpl +5 -1
  61. 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 { getGenericNotionContentBySlug } from "@notionx/core/notion";
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 item = await getGenericNotionContentBySlug({{contentSourceVarName}}, slug);
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 item = await getGenericNotionContentBySlug({{contentSourceVarName}}, slug);
56
- const page = await getSitePageForContentSource("{{contentSourceId}}");
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 { listGenericNotionContent } from "@notionx/core/notion";
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 page = await getSitePageForContentSource("{{contentSourceId}}");
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 items = await listGenericNotionContent({{contentSourceVarName}});
26
- const page = await getSitePageForContentSource("{{contentSourceId}}");
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 Link from "next/link";
2
- import { Badge } from "@/components/ui/badge";
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: StructuredFeatureGridBlock;
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
- <div className="mx-auto max-w-3xl text-center">
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 Link from "next/link";
2
- import { Badge } from "@/components/ui/badge";
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={sectionClassName(block.theme)}>
17
- <div
18
- className={[
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 { Badge } from "@/components/ui/badge";
6
+ import { NotionBlocks } from "@/components/notion-blocks";
7
7
  import { Button } from "@/components/ui/button";
8
- import type { StructuredLatestPostsBlock } from "@/lib/pages/source";
8
+ import type { StructuredPageBlock } from "@/lib/pages/source";
9
9
 
10
10
  export async function LatestPostsBlock({
11
11
  block,
12
12
  }: {
13
- block: StructuredLatestPostsBlock;
13
+ block: StructuredPageBlock;
14
14
  }) {
15
15
  const items = await listGenericNotionContent({{contentSourceVarName}});
16
- const visibleItems = items.slice(0, block.count);
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
- <Badge variant="secondary">Latest Posts</Badge>
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
- {block.primaryCta ? (
32
- <Button asChild variant="outline">
33
- <Link href={block.primaryCta.href}>
34
- {block.primaryCta.label}
35
- <ArrowRight className="ml-2 h-4 w-4" />
36
- </Link>
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 { Badge } from "@/components/ui/badge";
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
- <div className={layoutClassName(block.layout)}>
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.type === "hero" ? <HeroBlock block={block} /> : null}
18
- {block.type === "feature-grid" ? <FeatureGridBlock block={block} /> : null}
19
- {block.type === "story" ? <StoryBlock block={block} /> : null}
20
- {block.type === "latest-posts" ? <LatestPostsBlock block={block} /> : null}
21
- {block.type === "legacy" ? (
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={i18n.defaultLocale} />
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
- body: string;
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 to the default-locale
2
- // translation when a target locale is missing (rule: hide).
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