Skip to content

Commit 2edd7ef

Browse files
Merge pull request #122 from focusreactive/fix/storyblok-preview-locale
fix(storyblok): pass the locale through to the live preview
2 parents 9c8fda6 + 1db22f5 commit 2edd7ef

3 files changed

Lines changed: 75 additions & 4 deletions

File tree

‎apps/storyblok/src/app/live-preview/[[...slug]]/page.tsx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,33 @@ import type { Metadata } from "next";
22
import { notFound } from "next/navigation";
33
import { StoryblokStory } from "@storyblok/react/rsc";
44

5-
import { fetchStory, fetchStoryMetadata } from "@/lib/storyblok";
5+
import { splitLanguageFromSlug } from "@/constants/languages";
6+
import {
7+
fetchLanguageCodes,
8+
fetchStory,
9+
fetchStoryMetadata,
10+
} from "@/lib/storyblok";
611
import CoreLayout from "@/components/CoreLayout";
712

813
export async function generateMetadata(props: Props): Promise<Metadata> {
914
const params = await props.params;
15+
const { language, slug } = splitLanguageFromSlug(
16+
params.slug,
17+
await fetchLanguageCodes(),
18+
);
1019

11-
return fetchStoryMetadata("draft", params.slug);
20+
return fetchStoryMetadata("draft", slug, language);
1221
}
1322

1423
export default async function Home(props: Props) {
1524
const params = await props.params;
25+
const { language, slug } = splitLanguageFromSlug(
26+
params.slug,
27+
await fetchLanguageCodes(),
28+
);
1629
const {
1730
data: { story, links },
18-
} = await fetchStory("draft", params.slug);
31+
} = await fetchStory("draft", slug, language);
1932

2033
if (!story || process.env.NEXT_PUBLIC_IS_PREVIEW !== "true") {
2134
notFound();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Storyblok preview URLs are {domain}/{language}/{slug}, but the space translates
2+
// fields, not slugs, so the language must travel as the `language` query param.
3+
export function splitLanguageFromSlug(
4+
slug: string[] | undefined,
5+
languageCodes: readonly string[],
6+
): {
7+
language?: string;
8+
slug?: string[];
9+
} {
10+
const [first, ...rest] = slug ?? [];
11+
12+
if (!first || !languageCodes.includes(first)) {
13+
return { slug };
14+
}
15+
16+
// undefined, not []: `fetchStory` falls back to "home" only on a falsy slug, and
17+
// an empty path segment hits the delivery API's list endpoint instead of a story.
18+
return { language: first, slug: rest.length > 0 ? rest : undefined };
19+
}

‎apps/storyblok/src/lib/storyblok.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { Metadata } from "next";
22
import { apiPlugin, storyblokInit } from "@storyblok/react/rsc";
33

4+
import { SB_CACHE_VERSION_TAG } from "@/constants/cacheTags";
45
import { COMPONENTS } from "@/constants/sbComponents";
56

67
import { fetcher, getNextCachingParams } from "./utils";
78

9+
const LANGUAGE_CODES_REVALIDATE_SECONDS = 3600;
10+
811
export const getStoryblokApi = storyblokInit({
912
accessToken: process.env.NEXT_PUBLIC_STORYBLOK_TOKEN,
1013
use: [apiPlugin],
@@ -14,6 +17,7 @@ export const getStoryblokApi = storyblokInit({
1417
export async function fetchStory(
1518
version: "draft" | "published",
1619
slug?: string[],
20+
language?: string,
1721
) {
1822
getStoryblokApi();
1923
const correctSlug = `/${slug ? slug.join("/") : "home"}`;
@@ -23,6 +27,10 @@ export async function fetchStory(
2327
token: process.env.NEXT_PUBLIC_STORYBLOK_TOKEN || "",
2428
});
2529

30+
if (language) {
31+
searchParams.set("language", language);
32+
}
33+
2634
const data = await fetcher(
2735
`${process.env.NEXT_PUBLIC_STORYBLOK_API_GATE}/stories${correctSlug}?${searchParams.toString()}`,
2836
{
@@ -57,13 +65,44 @@ export async function fetchStories(
5765
return data;
5866
}
5967

68+
// Empty means "could not determine" as well as "no languages", and the caller must
69+
// strip nothing in either case. A hardcoded fallback would instead serve the wrong
70+
// story under a 200 — the failure nobody notices.
71+
export async function fetchLanguageCodes(): Promise<string[]> {
72+
const searchParams = new URLSearchParams({
73+
token: process.env.NEXT_PUBLIC_STORYBLOK_TOKEN || "",
74+
});
75+
76+
try {
77+
const { data } = await fetcher(
78+
`${process.env.NEXT_PUBLIC_STORYBLOK_API_GATE}/spaces/me?${searchParams.toString()}`,
79+
{
80+
method: "GET",
81+
next: {
82+
revalidate: LANGUAGE_CODES_REVALIDATE_SECONDS,
83+
tags: [SB_CACHE_VERSION_TAG],
84+
},
85+
},
86+
);
87+
88+
// `fetcher` does not check `response.ok`, so a 401 arrives here as a parsed body
89+
// with no `space` key rather than as a thrown error.
90+
return Array.isArray(data?.space?.language_codes)
91+
? data.space.language_codes
92+
: [];
93+
} catch {
94+
return [];
95+
}
96+
}
97+
6098
export async function fetchStoryMetadata(
6199
version: "draft" | "published",
62100
slug?: string[],
101+
language?: string,
63102
) {
64103
const {
65104
data: { story },
66-
} = await fetchStory(version, slug);
105+
} = await fetchStory(version, slug, language);
67106

68107
if (!story) {
69108
console.log(`missing metadata for story: ${slug?.join("/")}`);

0 commit comments

Comments
 (0)