From d7e22b6e5bc82198449b7644b45bf44f6e0d2712 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 09:23:09 +0000 Subject: [PATCH 01/35] feat: implement i18n support with Japanese and English languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add remix-i18next and related i18n dependencies - Create i18n configuration and language switcher component - Implement language-prefix routing (/en/* for English, /* for Japanese) - Restructure content files into language directories (ja/, en/) - Update Content Collections to extract language from file paths - Create English route components for all pages - Update root layout to detect language and initialize i18n - Update existing routes to use language-filtered content 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Yosuke Okuwaki --- app/components/I18nProvider.tsx | 21 +++++ app/components/LanguageSwitcher.tsx | 91 ++++++++++++++++++ app/components/layout/Header.tsx | 2 + app/contents/en/hello-world.md | 11 +++ app/contents/en/my-first-document.md | 9 ++ app/contents/en/test-mdx.mdx | 11 +++ app/contents/ja/hello-world.md | 11 +++ app/contents/ja/my-first-document.md | 9 ++ app/contents/ja/test-mdx.mdx | 11 +++ app/lib/content-collections/index.ts | 7 ++ app/lib/content.ts | 12 +++ app/lib/i18n/i18n.client.ts | 26 +++++ app/lib/i18n/i18n.server.ts | 23 +++++ app/lib/i18n/index.ts | 22 +++++ app/root.tsx | 11 ++- app/routes.ts | 14 ++- app/routes/contents.$slug.tsx | 6 +- app/routes/contents.tsx | 7 +- app/routes/en/contents.$slug.tsx | 136 +++++++++++++++++++++++++++ app/routes/en/contents.tsx | 55 +++++++++++ app/routes/en/home.tsx | 27 ++++++ app/routes/en/layout.tsx | 5 + app/routes/en/tags.$tag.tsx | 92 ++++++++++++++++++ app/routes/en/tags.tsx | 97 +++++++++++++++++++ app/routes/tags.$tag.tsx | 8 +- app/routes/tags.tsx | 6 +- package.json | 7 +- public/locales/en/common.json | 21 +++++ public/locales/ja/common.json | 21 +++++ 29 files changed, 759 insertions(+), 20 deletions(-) create mode 100644 app/components/I18nProvider.tsx create mode 100644 app/components/LanguageSwitcher.tsx create mode 100644 app/contents/en/hello-world.md create mode 100644 app/contents/en/my-first-document.md create mode 100644 app/contents/en/test-mdx.mdx create mode 100644 app/contents/ja/hello-world.md create mode 100644 app/contents/ja/my-first-document.md create mode 100644 app/contents/ja/test-mdx.mdx create mode 100644 app/lib/content.ts create mode 100644 app/lib/i18n/i18n.client.ts create mode 100644 app/lib/i18n/i18n.server.ts create mode 100644 app/lib/i18n/index.ts create mode 100644 app/routes/en/contents.$slug.tsx create mode 100644 app/routes/en/contents.tsx create mode 100644 app/routes/en/home.tsx create mode 100644 app/routes/en/layout.tsx create mode 100644 app/routes/en/tags.$tag.tsx create mode 100644 app/routes/en/tags.tsx create mode 100644 public/locales/en/common.json create mode 100644 public/locales/ja/common.json diff --git a/app/components/I18nProvider.tsx b/app/components/I18nProvider.tsx new file mode 100644 index 0000000..ffcff7b --- /dev/null +++ b/app/components/I18nProvider.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useEffect } from "react"; +import { I18nextProvider } from "react-i18next"; +import "../lib/i18n/i18n.client"; +import { i18next } from "../lib/i18n"; + +interface I18nProviderProps { + children: React.ReactNode; + language?: string; +} + +export function I18nProvider({ children, language = "ja" }: I18nProviderProps) { + useEffect(() => { + if (i18next.language !== language) { + i18next.changeLanguage(language); + } + }, [language]); + + return {children}; +} \ No newline at end of file diff --git a/app/components/LanguageSwitcher.tsx b/app/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..e87126d --- /dev/null +++ b/app/components/LanguageSwitcher.tsx @@ -0,0 +1,91 @@ +import { useTranslation } from "react-i18next"; +import { supportedLngs } from "../lib/i18n"; + +export function LanguageSwitcher() { + const { t, i18n } = useTranslation(); + + const handleLanguageChange = (lng: string) => { + i18n.changeLanguage(lng); + + // Update URL to include/remove language prefix + const currentPath = window.location.pathname; + let newPath: string; + + if (lng === "ja") { + // Remove language prefix for Japanese (default) + if (currentPath.startsWith("/en")) { + newPath = currentPath.replace("/en", "") || "/"; + } else { + newPath = currentPath; + } + } else { + // Add language prefix for other languages + if (currentPath.startsWith("/en")) { + newPath = currentPath; + } else if (currentPath === "/") { + newPath = `/${lng}`; + } else { + newPath = `/${lng}${currentPath}`; + } + } + + window.history.pushState({}, "", newPath); + }; + + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index ee36efb..8ba8e4c 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -1,4 +1,5 @@ import { ThemeSwitcher } from "../ThemeSwitcher"; +import { LanguageSwitcher } from "../LanguageSwitcher"; export function Header() { return ( @@ -46,6 +47,7 @@ export function Header() {
+
diff --git a/app/contents/en/hello-world.md b/app/contents/en/hello-world.md new file mode 100644 index 0000000..8fcc652 --- /dev/null +++ b/app/contents/en/hello-world.md @@ -0,0 +1,11 @@ +--- +title: "Hello world" +tags: ["hello", "world"] +summary: "This is my first post!" +--- + +# Hello world + +This is my first post! +... rest of the content in English +hoge \ No newline at end of file diff --git a/app/contents/en/my-first-document.md b/app/contents/en/my-first-document.md new file mode 100644 index 0000000..684974b --- /dev/null +++ b/app/contents/en/my-first-document.md @@ -0,0 +1,9 @@ +--- +title: "My first document" +tags: ["operating system", "unix"] +summary: "This is my first document!" +--- + +# My first document + +This is my first document in English... \ No newline at end of file diff --git a/app/contents/en/test-mdx.mdx b/app/contents/en/test-mdx.mdx new file mode 100644 index 0000000..96ca558 --- /dev/null +++ b/app/contents/en/test-mdx.mdx @@ -0,0 +1,11 @@ +--- +title: Test MDX +tags: ["react", "mdx"] +summary: This is a test MDX file. +--- + +import Counter from "./components/Counter.tsx"; + +# Test MDX + + \ No newline at end of file diff --git a/app/contents/ja/hello-world.md b/app/contents/ja/hello-world.md new file mode 100644 index 0000000..3962334 --- /dev/null +++ b/app/contents/ja/hello-world.md @@ -0,0 +1,11 @@ +--- +title: "Hello world" +tags: ["hello", "world"] +summary: "This is my first post!" +--- + +# Hello world + +This is my first post! +... rest of the content +hoge \ No newline at end of file diff --git a/app/contents/ja/my-first-document.md b/app/contents/ja/my-first-document.md new file mode 100644 index 0000000..2117d1d --- /dev/null +++ b/app/contents/ja/my-first-document.md @@ -0,0 +1,9 @@ +--- +title: "My first document" +tags: ["operating system", "unix"] +summary: "This is my first document!" +--- + +# My first document + +howhow... \ No newline at end of file diff --git a/app/contents/ja/test-mdx.mdx b/app/contents/ja/test-mdx.mdx new file mode 100644 index 0000000..96ca558 --- /dev/null +++ b/app/contents/ja/test-mdx.mdx @@ -0,0 +1,11 @@ +--- +title: Test MDX +tags: ["react", "mdx"] +summary: This is a test MDX file. +--- + +import Counter from "./components/Counter.tsx"; + +# Test MDX + + \ No newline at end of file diff --git a/app/lib/content-collections/index.ts b/app/lib/content-collections/index.ts index dcc6239..0fbd7f1 100644 --- a/app/lib/content-collections/index.ts +++ b/app/lib/content-collections/index.ts @@ -10,6 +10,7 @@ const FrontmatterSchema = z.object({ title: z.string(), tags: z.array(z.string()).optional(), summary: z.string(), + language: z.string().optional(), }); const getAppDir = () => { @@ -40,6 +41,10 @@ const contents = defineCollection({ typeName: "Content", schema: FrontmatterSchema, transform: async (document, context) => { + // Extract language from file path (e.g. "ja/hello-world" -> "ja") + const pathParts = document._meta.path.split('/'); + const language = pathParts.length > 1 ? pathParts[0] : 'ja'; // default to Japanese + if (document._meta.extension === "mdx") { const mdx = await compileMDX({ cache: context.cache }, document, { cwd: appDir, @@ -47,6 +52,7 @@ const contents = defineCollection({ return { ...document, + language, mdx, html: "", }; @@ -55,6 +61,7 @@ const contents = defineCollection({ return { ...document, + language, mdx: "", html, }; diff --git a/app/lib/content.ts b/app/lib/content.ts new file mode 100644 index 0000000..93589f4 --- /dev/null +++ b/app/lib/content.ts @@ -0,0 +1,12 @@ +import { allContents } from "content-collections"; +import type { Content } from "content-collections"; + +export function getContentsByLanguage(language: string): Content[] { + return allContents.filter(content => content.language === language); +} + +export function getContentBySlugAndLanguage(slug: string, language: string): Content | undefined { + return allContents.find(content => + content._meta.path.endsWith(slug) && content.language === language + ); +} \ No newline at end of file diff --git a/app/lib/i18n/i18n.client.ts b/app/lib/i18n/i18n.client.ts new file mode 100644 index 0000000..2dcaa51 --- /dev/null +++ b/app/lib/i18n/i18n.client.ts @@ -0,0 +1,26 @@ +import i18next from "i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import Backend from "i18next-http-backend"; +import { initReactI18next } from "react-i18next"; +import { defaultNS, fallbackLng, supportedLngs } from "."; + +export const clientI18n = i18next + .use(initReactI18next) + .use(LanguageDetector) + .use(Backend) + .init({ + supportedLngs, + defaultNS, + fallbackLng, + ns: [defaultNS], + backend: { + loadPath: "/locales/{{lng}}/{{ns}}.json", + }, + detection: { + order: ["htmlTag", "localStorage", "navigator"], + lookupLocalStorage: "i18nextLng", + caches: ["localStorage"], + }, + }); + +export default clientI18n; \ No newline at end of file diff --git a/app/lib/i18n/i18n.server.ts b/app/lib/i18n/i18n.server.ts new file mode 100644 index 0000000..8dfc402 --- /dev/null +++ b/app/lib/i18n/i18n.server.ts @@ -0,0 +1,23 @@ +import { createRemixI18NextBackend } from "remix-i18next/server"; +import i18next from "i18next"; +import { defaultNS, fallbackLng, supportedLngs } from "."; + +export const i18n = createRemixI18NextBackend({ + detection: { + supportedLanguages: supportedLngs, + fallbackLanguage: fallbackLng, + order: ["pathPrefix"], + }, + i18next: { + ...i18next, + supportedLngs, + defaultNS, + fallbackLng, + ns: [defaultNS], + backend: { + loadPath: "./public/locales/{{lng}}/{{ns}}.json", + }, + }, +}); + +export default i18n; \ No newline at end of file diff --git a/app/lib/i18n/index.ts b/app/lib/i18n/index.ts new file mode 100644 index 0000000..f715d63 --- /dev/null +++ b/app/lib/i18n/index.ts @@ -0,0 +1,22 @@ +import { createRemixI18NextBackend } from "remix-i18next/server"; +import i18next from "i18next"; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; + +export const defaultNS = "common"; +export const fallbackLng = "ja"; +export const supportedLngs = ["ja", "en"]; + +export const remixI18Next = createRemixI18NextBackend({ + detection: { + supportedLanguages: supportedLngs, + fallbackLanguage: fallbackLng, + }, + i18next: { + ...i18next, + backend: Backend, + }, + backend: Backend, +}); + +export { i18next }; \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index a144248..fa9ac6f 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -5,11 +5,13 @@ import { Outlet, Scripts, ScrollRestoration, + useLocation, } from "react-router"; import type { Route } from "./+types/root"; import "./app.css"; import { Header } from "./components/layout/Header"; import { ThemeScript } from "./components/layout/ThemeScript"; +import { I18nProvider } from "./components/I18nProvider"; export const links: Route.LinksFunction = () => [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, @@ -44,13 +46,18 @@ export function Layout({ children }: { children: React.ReactNode }) { } export default function App() { + const location = useLocation(); + + // Detect language from URL path + const language = location.pathname.startsWith("/en") ? "en" : "ja"; + return ( - <> +
- + ); } diff --git a/app/routes.ts b/app/routes.ts index 488e60f..478e5d6 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,13 +1,19 @@ import { index, type RouteConfig, route } from "@react-router/dev/routes"; export default [ + // Japanese (default) routes - no language prefix index("routes/home.tsx"), - - // /contents route("contents", "routes/contents.tsx"), route("contents/:slug", "routes/contents.$slug.tsx"), - - // /tags route("tags", "routes/tags.tsx"), route("tags/:tag", "routes/tags.$tag.tsx"), + + // English routes with language prefix + route("en", "routes/en/layout.tsx", [ + index("routes/en/home.tsx"), + route("contents", "routes/en/contents.tsx"), + route("contents/:slug", "routes/en/contents.$slug.tsx"), + route("tags", "routes/en/tags.tsx"), + route("tags/:tag", "routes/en/tags.$tag.tsx"), + ]), ] satisfies RouteConfig; diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index d0f2a21..e1c0c8a 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -1,9 +1,10 @@ "use client"; import { MDXContent } from "@content-collections/mdx/react"; -import { allContents } from "content-collections"; import { useEffect, useState } from "react"; import { useParams } from "react-router"; +import { getContentBySlugAndLanguage } from "../lib/content"; +import { allContents } from "content-collections"; type ClientOnlyProps = { children: React.ReactNode; @@ -71,9 +72,8 @@ const Content = ({ content }: ContentProps) => { }; export default function PostDetail() { - // slug に一致するコンテンツを取得 const { slug } = useParams(); - const content = allContents.find((p) => p._meta.path === slug); + const content = getContentBySlugAndLanguage(slug!, "ja"); if (!content) { return
Content not found
; } diff --git a/app/routes/contents.tsx b/app/routes/contents.tsx index 6e1735b..91be21b 100644 --- a/app/routes/contents.tsx +++ b/app/routes/contents.tsx @@ -1,4 +1,4 @@ -import { allContents } from "content-collections"; +import { getContentsByLanguage } from "../lib/content"; import type { Route } from "./+types/contents"; export function meta(_: Route.MetaArgs) { @@ -13,7 +13,8 @@ export function meta(_: Route.MetaArgs) { } export function loader(_: Route.LoaderArgs) { - return { contents: allContents }; + const contents = getContentsByLanguage("ja"); + return { contents }; } export default function Contents({ loaderData }: Route.ComponentProps) { @@ -27,7 +28,7 @@ export default function Contents({ loaderData }: Route.ComponentProps) {
{content.title} diff --git a/app/routes/en/contents.$slug.tsx b/app/routes/en/contents.$slug.tsx new file mode 100644 index 0000000..d057b73 --- /dev/null +++ b/app/routes/en/contents.$slug.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { MDXContent } from "@content-collections/mdx/react"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router"; +import { getContentBySlugAndLanguage } from "../../lib/content"; +import { allContents } from "content-collections"; + +type ClientOnlyProps = { + children: React.ReactNode; +}; + +export function ClientOnly({ children }: ClientOnlyProps) { + const [mounted, setMounted] = useState(false); + const [show, setShow] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; + const id = requestAnimationFrame(() => setShow(true)); + return () => cancelAnimationFrame(id); + }, [mounted]); + + if (!mounted) return null; + + return ( +
+ {children} +
+ ); +} + +type ContentProps = { + content: (typeof allContents)[number]; +}; + +const Content = ({ content }: ContentProps) => { + const isContentMdx = content._meta.extension === "mdx"; + const body = isContentMdx ? ( + + ) : ( + // biome-ignore lint/security/noDangerouslySetInnerHtml: We can ignore this because `content.html` is safe content created by us +
+ ); + + return ( + + {body} + +
+
+ + ); +}; + +export default function EnglishPostDetail() { + const { slug } = useParams(); + const content = getContentBySlugAndLanguage(slug!, "en"); + + if (!content) { + return
Content not found
; + } + + return ( +
+
+ +
+ +

{content.title}

+ + {content.tags && content.tags.length > 0 && ( +
+ {content.tags.map((tag) => ( + + #{tag} + + ))} +
+ )} + +
+ + + + {content.summary} +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/routes/en/contents.tsx b/app/routes/en/contents.tsx new file mode 100644 index 0000000..d469488 --- /dev/null +++ b/app/routes/en/contents.tsx @@ -0,0 +1,55 @@ +import { getContentsByLanguage } from "../../lib/content"; +import type { Route } from "../+types/contents"; + +export function meta(_: Route.MetaArgs) { + return [ + { title: "Contents - Chasing the Kernel" }, + { + name: "description", + content: + "All articles about kernel development and low-level programming", + }, + ]; +} + +export function loader(_: Route.LoaderArgs) { + const contents = getContentsByLanguage("en"); + return { contents }; +} + +export default function EnglishContents({ loaderData }: Route.ComponentProps) { + const { contents } = loaderData; + + return ( +
+

Contents

+
+ {contents.map((content) => ( +
+
+ + {content.title} + +

{content.summary}

+
+ {content.tags?.map((tag) => ( + e.stopPropagation()} + > + #{tag} + + ))} +
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/app/routes/en/home.tsx b/app/routes/en/home.tsx new file mode 100644 index 0000000..4dcc51b --- /dev/null +++ b/app/routes/en/home.tsx @@ -0,0 +1,27 @@ +import type { Route } from "../+types/home"; + +export function meta(_: Route.MetaArgs) { + return [ + { title: "Chasing the Kernel" }, + { name: "description", content: "I'm chasing the kernel." }, + ]; +} + +export default function EnglishHome() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/routes/en/layout.tsx b/app/routes/en/layout.tsx new file mode 100644 index 0000000..71c9666 --- /dev/null +++ b/app/routes/en/layout.tsx @@ -0,0 +1,5 @@ +import { Outlet } from "react-router"; + +export default function EnglishLayout() { + return ; +} \ No newline at end of file diff --git a/app/routes/en/tags.$tag.tsx b/app/routes/en/tags.$tag.tsx new file mode 100644 index 0000000..2e0004d --- /dev/null +++ b/app/routes/en/tags.$tag.tsx @@ -0,0 +1,92 @@ +import { getContentsByLanguage } from "../../lib/content"; +import { data } from "react-router"; +import type { Route } from "../+types/tags.$tag"; + +export function loader({ params }: Route.LoaderArgs) { + const { tag } = params; + const decodedTag = decodeURIComponent(tag); + + const contents = getContentsByLanguage("en"); + const postsWithTag = contents.filter((post) => + post.tags?.includes(decodedTag), + ); + + if (postsWithTag.length === 0) { + throw data(`Tag "${decodedTag}" not found`, { status: 404 }); + } + + return { + tag: decodedTag, + posts: postsWithTag, + }; +} + +export default function EnglishTagDetail({ loaderData }: Route.ComponentProps) { + const { tag, posts } = loaderData; + + return ( +
+
+ +
+ +
+
+
+

#{tag}

+

{posts.length} post{posts.length !== 1 ? 's' : ''}

+
+
+
+ +
+ {posts.map((post) => ( +
+
+ + {post.title} + +

{post.summary}

+ +
+ {post.tags?.map((postTag) => ( + + #{postTag} + + ))} +
+
+
+ ))} +
+ +
+ +
+ ); +} \ No newline at end of file diff --git a/app/routes/en/tags.tsx b/app/routes/en/tags.tsx new file mode 100644 index 0000000..f7dcf2f --- /dev/null +++ b/app/routes/en/tags.tsx @@ -0,0 +1,97 @@ +import { getContentsByLanguage } from "../../lib/content"; +import type { Route } from "../+types/tags"; + +export function meta(_: Route.MetaArgs) { + return [ + { title: "Tags - Chasing the Kernel" }, + { name: "description", content: "Browse articles by tags" }, + ]; +} + +export function loader(_: Route.LoaderArgs) { + const contents = getContentsByLanguage("en"); + const tagCounts = new Map(); + + contents.forEach((post) => { + if (post.tags) { + post.tags.forEach((tag) => { + tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); + }); + } + }); + + const sortedTags = Array.from(tagCounts.entries()) + .sort(([, a], [, b]) => b - a) + .map(([tag, count]) => ({ tag, count })); + + return { tags: sortedTags }; +} + +export default function EnglishTags({ loaderData }: Route.ComponentProps) { + const { tags } = loaderData; + + return ( +
+
+

Tags

+

Browse articles by tags

+
+ + {tags.length === 0 ? ( +
+
🏷️
+

+ No tags found. +

+
+ ) : ( + + )} + + +
+ ); +} \ No newline at end of file diff --git a/app/routes/tags.$tag.tsx b/app/routes/tags.$tag.tsx index 79e224d..8ccd21d 100644 --- a/app/routes/tags.$tag.tsx +++ b/app/routes/tags.$tag.tsx @@ -1,4 +1,4 @@ -import { allContents } from "content-collections"; +import { getContentsByLanguage } from "../lib/content"; import { data } from "react-router"; import type { Route } from "./+types/tags.$tag"; @@ -6,8 +6,8 @@ export function loader({ params }: Route.LoaderArgs) { const { tag } = params; const decodedTag = decodeURIComponent(tag); - // 指定されたタグを持つ投稿を検索 - const postsWithTag = allContents.filter((post) => + const contents = getContentsByLanguage("ja"); + const postsWithTag = contents.filter((post) => post.tags?.includes(decodedTag), ); @@ -56,7 +56,7 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) {
{post.title} diff --git a/app/routes/tags.tsx b/app/routes/tags.tsx index 6bccca8..ce437cb 100644 --- a/app/routes/tags.tsx +++ b/app/routes/tags.tsx @@ -1,4 +1,4 @@ -import { allContents } from "content-collections"; +import { getContentsByLanguage } from "../lib/content"; import type { Route } from "./+types/tags"; export function meta(_: Route.MetaArgs) { @@ -9,10 +9,10 @@ export function meta(_: Route.MetaArgs) { } export function loader(_: Route.LoaderArgs) { - // 全ての投稿からタグを収集 + const contents = getContentsByLanguage("ja"); const tagCounts = new Map(); - allContents.forEach((post) => { + contents.forEach((post) => { if (post.tags) { post.tags.forEach((tag) => { tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); diff --git a/package.json b/package.json index 1db7162..e9bfce8 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,15 @@ "test": "echo \"No tests yet\"" }, "dependencies": { + "i18next": "^24.2.0", + "i18next-browser-languagedetector": "^8.1.1", + "i18next-http-backend": "^3.1.0", "isbot": "^5.1.27", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router": "^7.7.1" + "react-i18next": "^15.2.0", + "react-router": "^7.7.1", + "remix-i18next": "^7.1.0" }, "devDependencies": { "@biomejs/biome": "2.2.2", diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..ed28e57 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,21 @@ +{ + "nav": { + "home": "Home", + "contents": "Contents", + "tags": "Tags" + }, + "theme": { + "title": "Switch theme", + "select": "Select theme", + "selected": "Selected" + }, + "language": { + "title": "Switch language", + "select": "Select language", + "ja": "日本語", + "en": "English" + }, + "site": { + "title": "Chasing the Kernel" + } +} \ No newline at end of file diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json new file mode 100644 index 0000000..760311a --- /dev/null +++ b/public/locales/ja/common.json @@ -0,0 +1,21 @@ +{ + "nav": { + "home": "ホーム", + "contents": "コンテンツ", + "tags": "タグ" + }, + "theme": { + "title": "テーマを切り替える", + "select": "テーマを選択", + "selected": "選択中" + }, + "language": { + "title": "言語を切り替える", + "select": "言語を選択", + "ja": "日本語", + "en": "English" + }, + "site": { + "title": "Chasing the Kernel" + } +} \ No newline at end of file From a2bc8d5c08ca860e290a879ecfa4532531152271 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 11:36:58 +0900 Subject: [PATCH 02/35] fix: deps version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ```sh $ pnpm i  ERR_PNPM_NO_MATCHING_VERSION  No matching version found for i18next-http-backend@^3.1.0 This error happened while installing a direct dependency of /Users/nukopy/Projects/chasing-the-kernel ``` --- package.json | 2 +- pnpm-lock.yaml | 140 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e9bfce8..de8f7c2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dependencies": { "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.1.1", - "i18next-http-backend": "^3.1.0", + "i18next-http-backend": "^3.0.2", "isbot": "^5.1.27", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6d7e8d..7bba115 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + i18next: + specifier: ^24.2.0 + version: 24.2.3(typescript@5.9.2) + i18next-browser-languagedetector: + specifier: ^8.1.1 + version: 8.2.0 + i18next-http-backend: + specifier: ^3.0.2 + version: 3.0.2 isbot: specifier: ^5.1.27 version: 5.1.30 @@ -17,9 +26,15 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.1(react@19.1.1) + react-i18next: + specifier: ^15.2.0 + version: 15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) react-router: specifier: ^7.7.1 version: 7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + remix-i18next: + specifier: ^7.1.0 + version: 7.3.0(i18next@24.2.3(typescript@5.9.2))(react-i18next@15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2))(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) devDependencies: '@biomejs/biome': specifier: 2.2.2 @@ -1280,6 +1295,9 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1515,9 +1533,26 @@ packages: resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + i18next-browser-languagedetector@8.2.0: + resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==} + + i18next-http-backend@3.0.2: + resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==} + + i18next@24.2.3: + resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -1849,6 +1884,15 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -1961,6 +2005,22 @@ packages: peerDependencies: react: ^19.1.1 + react-i18next@15.7.3: + resolution: {integrity: sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==} + peerDependencies: + i18next: '>= 25.4.1' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -2021,6 +2081,15 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remix-i18next@7.3.0: + resolution: {integrity: sha512-KbuOTr5G7+GYZRgXfd/ugpT0gw0kCN+ESbLrPyTrKKQfMY/s/y5ejAjXYK94ODsO9stuVh8AAchQ60uoA4EUJA==} + engines: {node: '>=20.0.0'} + peerDependencies: + i18next: ^24.0.0 || ^25.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-i18next: ^13.0.0 || ^14.0.0 || ^15.0.0 + react-router: ^7.0.0 + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -2171,6 +2240,9 @@ packages: toml@3.0.0: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -2334,9 +2406,19 @@ packages: vite: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3515,6 +3597,12 @@ snapshots: cookie@1.0.2: {} + cross-fetch@4.0.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3856,8 +3944,28 @@ snapshots: dependencies: lru-cache: 7.18.3 + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + html-void-elements@3.0.0: {} + i18next-browser-languagedetector@8.2.0: + dependencies: + '@babel/runtime': 7.28.3 + + i18next-http-backend@3.0.2: + dependencies: + cross-fetch: 4.0.0 + transitivePeerDependencies: + - encoding + + i18next@24.2.3(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.3 + optionalDependencies: + typescript: 5.9.2 + inline-style-parser@0.2.4: {} is-alphabetical@2.0.1: {} @@ -4357,6 +4465,10 @@ snapshots: nanoid@3.3.11: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.19: {} normalize-package-data@5.0.0: @@ -4465,6 +4577,16 @@ snapshots: react: 19.1.1 scheduler: 0.26.0 + react-i18next@15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2): + dependencies: + '@babel/runtime': 7.28.3 + html-parse-stringify: 3.0.1 + i18next: 24.2.3(typescript@5.9.2) + react: 19.1.1 + optionalDependencies: + react-dom: 19.1.1(react@19.1.1) + typescript: 5.9.2 + react-refresh@0.14.2: {} react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): @@ -4570,6 +4692,13 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remix-i18next@7.3.0(i18next@24.2.3(typescript@5.9.2))(react-i18next@15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2))(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + dependencies: + i18next: 24.2.3(typescript@5.9.2) + react: 19.1.1 + react-i18next: 15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) + react-router: 7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -4753,6 +4882,8 @@ snapshots: toml@3.0.0: {} + tr46@0.0.3: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -4908,8 +5039,17 @@ snapshots: optionalDependencies: vite: 6.3.5(@types/node@20.19.11)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1) + void-elements@3.1.0: {} + web-namespaces@2.0.1: {} + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 From 5a2cfc6d7625f17cf090cc4867282d2fe24ba691 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 11:40:24 +0900 Subject: [PATCH 03/35] chore: format --- app/components/I18nProvider.tsx | 2 +- app/components/LanguageSwitcher.tsx | 8 ++++---- app/components/layout/Header.tsx | 2 +- app/lib/content-collections/index.ts | 6 +++--- app/lib/content.ts | 16 ++++++++++------ app/lib/i18n/i18n.client.ts | 2 +- app/lib/i18n/i18n.server.ts | 4 ++-- app/lib/i18n/index.ts | 5 ++--- app/root.tsx | 6 +++--- app/routes/contents.$slug.tsx | 2 +- app/routes/contents.tsx | 2 +- app/routes/en/contents.$slug.tsx | 6 +++--- app/routes/en/contents.tsx | 4 ++-- app/routes/en/home.tsx | 2 +- app/routes/en/layout.tsx | 2 +- app/routes/en/tags.$tag.tsx | 10 ++++++---- app/routes/en/tags.tsx | 8 +++----- app/routes/tags.$tag.tsx | 4 ++-- 18 files changed, 47 insertions(+), 44 deletions(-) diff --git a/app/components/I18nProvider.tsx b/app/components/I18nProvider.tsx index ffcff7b..0303bb5 100644 --- a/app/components/I18nProvider.tsx +++ b/app/components/I18nProvider.tsx @@ -18,4 +18,4 @@ export function I18nProvider({ children, language = "ja" }: I18nProviderProps) { }, [language]); return {children}; -} \ No newline at end of file +} diff --git a/app/components/LanguageSwitcher.tsx b/app/components/LanguageSwitcher.tsx index e87126d..6762baa 100644 --- a/app/components/LanguageSwitcher.tsx +++ b/app/components/LanguageSwitcher.tsx @@ -6,11 +6,11 @@ export function LanguageSwitcher() { const handleLanguageChange = (lng: string) => { i18n.changeLanguage(lng); - + // Update URL to include/remove language prefix const currentPath = window.location.pathname; let newPath: string; - + if (lng === "ja") { // Remove language prefix for Japanese (default) if (currentPath.startsWith("/en")) { @@ -28,7 +28,7 @@ export function LanguageSwitcher() { newPath = `/${lng}${currentPath}`; } } - + window.history.pushState({}, "", newPath); }; @@ -88,4 +88,4 @@ export function LanguageSwitcher() {
); -} \ No newline at end of file +} diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index 8ba8e4c..fe765c9 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -1,5 +1,5 @@ -import { ThemeSwitcher } from "../ThemeSwitcher"; import { LanguageSwitcher } from "../LanguageSwitcher"; +import { ThemeSwitcher } from "../ThemeSwitcher"; export function Header() { return ( diff --git a/app/lib/content-collections/index.ts b/app/lib/content-collections/index.ts index 0fbd7f1..50eb9e8 100644 --- a/app/lib/content-collections/index.ts +++ b/app/lib/content-collections/index.ts @@ -42,9 +42,9 @@ const contents = defineCollection({ schema: FrontmatterSchema, transform: async (document, context) => { // Extract language from file path (e.g. "ja/hello-world" -> "ja") - const pathParts = document._meta.path.split('/'); - const language = pathParts.length > 1 ? pathParts[0] : 'ja'; // default to Japanese - + const pathParts = document._meta.path.split("/"); + const language = pathParts.length > 1 ? pathParts[0] : "ja"; // default to Japanese + if (document._meta.extension === "mdx") { const mdx = await compileMDX({ cache: context.cache }, document, { cwd: appDir, diff --git a/app/lib/content.ts b/app/lib/content.ts index 93589f4..c651bb9 100644 --- a/app/lib/content.ts +++ b/app/lib/content.ts @@ -1,12 +1,16 @@ -import { allContents } from "content-collections"; import type { Content } from "content-collections"; +import { allContents } from "content-collections"; export function getContentsByLanguage(language: string): Content[] { - return allContents.filter(content => content.language === language); + return allContents.filter((content) => content.language === language); } -export function getContentBySlugAndLanguage(slug: string, language: string): Content | undefined { - return allContents.find(content => - content._meta.path.endsWith(slug) && content.language === language +export function getContentBySlugAndLanguage( + slug: string, + language: string, +): Content | undefined { + return allContents.find( + (content) => + content._meta.path.endsWith(slug) && content.language === language, ); -} \ No newline at end of file +} diff --git a/app/lib/i18n/i18n.client.ts b/app/lib/i18n/i18n.client.ts index 2dcaa51..3f80a78 100644 --- a/app/lib/i18n/i18n.client.ts +++ b/app/lib/i18n/i18n.client.ts @@ -23,4 +23,4 @@ export const clientI18n = i18next }, }); -export default clientI18n; \ No newline at end of file +export default clientI18n; diff --git a/app/lib/i18n/i18n.server.ts b/app/lib/i18n/i18n.server.ts index 8dfc402..e8c2dba 100644 --- a/app/lib/i18n/i18n.server.ts +++ b/app/lib/i18n/i18n.server.ts @@ -1,5 +1,5 @@ -import { createRemixI18NextBackend } from "remix-i18next/server"; import i18next from "i18next"; +import { createRemixI18NextBackend } from "remix-i18next/server"; import { defaultNS, fallbackLng, supportedLngs } from "."; export const i18n = createRemixI18NextBackend({ @@ -20,4 +20,4 @@ export const i18n = createRemixI18NextBackend({ }, }); -export default i18n; \ No newline at end of file +export default i18n; diff --git a/app/lib/i18n/index.ts b/app/lib/i18n/index.ts index f715d63..07c0044 100644 --- a/app/lib/i18n/index.ts +++ b/app/lib/i18n/index.ts @@ -1,7 +1,6 @@ -import { createRemixI18NextBackend } from "remix-i18next/server"; import i18next from "i18next"; import Backend from "i18next-http-backend"; -import LanguageDetector from "i18next-browser-languagedetector"; +import { createRemixI18NextBackend } from "remix-i18next/server"; export const defaultNS = "common"; export const fallbackLng = "ja"; @@ -19,4 +18,4 @@ export const remixI18Next = createRemixI18NextBackend({ backend: Backend, }); -export { i18next }; \ No newline at end of file +export { i18next }; diff --git a/app/root.tsx b/app/root.tsx index fa9ac6f..6f7b344 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -9,9 +9,9 @@ import { } from "react-router"; import type { Route } from "./+types/root"; import "./app.css"; +import { I18nProvider } from "./components/I18nProvider"; import { Header } from "./components/layout/Header"; import { ThemeScript } from "./components/layout/ThemeScript"; -import { I18nProvider } from "./components/I18nProvider"; export const links: Route.LinksFunction = () => [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, @@ -47,10 +47,10 @@ export function Layout({ children }: { children: React.ReactNode }) { export default function App() { const location = useLocation(); - + // Detect language from URL path const language = location.pathname.startsWith("/en") ? "en" : "ja"; - + return (
diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index e1c0c8a..872571d 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -1,10 +1,10 @@ "use client"; import { MDXContent } from "@content-collections/mdx/react"; +import type { allContents } from "content-collections"; import { useEffect, useState } from "react"; import { useParams } from "react-router"; import { getContentBySlugAndLanguage } from "../lib/content"; -import { allContents } from "content-collections"; type ClientOnlyProps = { children: React.ReactNode; diff --git a/app/routes/contents.tsx b/app/routes/contents.tsx index 91be21b..da6e656 100644 --- a/app/routes/contents.tsx +++ b/app/routes/contents.tsx @@ -28,7 +28,7 @@ export default function Contents({ loaderData }: Route.ComponentProps) {
{content.title} diff --git a/app/routes/en/contents.$slug.tsx b/app/routes/en/contents.$slug.tsx index d057b73..8fd53ed 100644 --- a/app/routes/en/contents.$slug.tsx +++ b/app/routes/en/contents.$slug.tsx @@ -1,10 +1,10 @@ "use client"; import { MDXContent } from "@content-collections/mdx/react"; +import type { allContents } from "content-collections"; import { useEffect, useState } from "react"; import { useParams } from "react-router"; import { getContentBySlugAndLanguage } from "../../lib/content"; -import { allContents } from "content-collections"; type ClientOnlyProps = { children: React.ReactNode; @@ -70,7 +70,7 @@ const Content = ({ content }: ContentProps) => { export default function EnglishPostDetail() { const { slug } = useParams(); const content = getContentBySlugAndLanguage(slug!, "en"); - + if (!content) { return
Content not found
; } @@ -133,4 +133,4 @@ export default function EnglishPostDetail() {
); -} \ No newline at end of file +} diff --git a/app/routes/en/contents.tsx b/app/routes/en/contents.tsx index d469488..7095335 100644 --- a/app/routes/en/contents.tsx +++ b/app/routes/en/contents.tsx @@ -28,7 +28,7 @@ export default function EnglishContents({ loaderData }: Route.ComponentProps) { ); -} \ No newline at end of file +} diff --git a/app/routes/en/home.tsx b/app/routes/en/home.tsx index 4dcc51b..aceda24 100644 --- a/app/routes/en/home.tsx +++ b/app/routes/en/home.tsx @@ -24,4 +24,4 @@ export default function EnglishHome() {
); -} \ No newline at end of file +} diff --git a/app/routes/en/layout.tsx b/app/routes/en/layout.tsx index 71c9666..2b632e5 100644 --- a/app/routes/en/layout.tsx +++ b/app/routes/en/layout.tsx @@ -2,4 +2,4 @@ import { Outlet } from "react-router"; export default function EnglishLayout() { return ; -} \ No newline at end of file +} diff --git a/app/routes/en/tags.$tag.tsx b/app/routes/en/tags.$tag.tsx index 2e0004d..af13af6 100644 --- a/app/routes/en/tags.$tag.tsx +++ b/app/routes/en/tags.$tag.tsx @@ -1,5 +1,5 @@ -import { getContentsByLanguage } from "../../lib/content"; import { data } from "react-router"; +import { getContentsByLanguage } from "../../lib/content"; import type { Route } from "../+types/tags.$tag"; export function loader({ params }: Route.LoaderArgs) { @@ -46,7 +46,9 @@ export default function EnglishTagDetail({ loaderData }: Route.ComponentProps) {

#{tag}

-

{posts.length} post{posts.length !== 1 ? 's' : ''}

+

+ {posts.length} post{posts.length !== 1 ? "s" : ""} +

@@ -56,7 +58,7 @@ export default function EnglishTagDetail({ loaderData }: Route.ComponentProps) {
); -} \ No newline at end of file +} diff --git a/app/routes/en/tags.tsx b/app/routes/en/tags.tsx index f7dcf2f..33e8c5d 100644 --- a/app/routes/en/tags.tsx +++ b/app/routes/en/tags.tsx @@ -40,9 +40,7 @@ export default function EnglishTags({ loaderData }: Route.ComponentProps) { {tags.length === 0 ? (
🏷️
-

- No tags found. -

+

No tags found.

) : (
@@ -58,7 +56,7 @@ export default function EnglishTags({ loaderData }: Route.ComponentProps) { #{tag}
- {count} post{count !== 1 ? 's' : ''} + {count} post{count !== 1 ? "s" : ""}
@@ -94,4 +92,4 @@ export default function EnglishTags({ loaderData }: Route.ComponentProps) {
); -} \ No newline at end of file +} diff --git a/app/routes/tags.$tag.tsx b/app/routes/tags.$tag.tsx index 8ccd21d..fc43308 100644 --- a/app/routes/tags.$tag.tsx +++ b/app/routes/tags.$tag.tsx @@ -1,5 +1,5 @@ -import { getContentsByLanguage } from "../lib/content"; import { data } from "react-router"; +import { getContentsByLanguage } from "../lib/content"; import type { Route } from "./+types/tags.$tag"; export function loader({ params }: Route.LoaderArgs) { @@ -56,7 +56,7 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) {
{post.title} From 7db7ab26b077b1d652a225573df50836d6cd4273 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 11:40:30 +0900 Subject: [PATCH 04/35] fix: lint error --- app/routes/contents.$slug.tsx | 2 +- app/routes/en/contents.$slug.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index 872571d..c74ba1e 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -73,7 +73,7 @@ const Content = ({ content }: ContentProps) => { export default function PostDetail() { const { slug } = useParams(); - const content = getContentBySlugAndLanguage(slug!, "ja"); + const content = getContentBySlugAndLanguage(slug ?? "", "ja"); if (!content) { return
Content not found
; } diff --git a/app/routes/en/contents.$slug.tsx b/app/routes/en/contents.$slug.tsx index 8fd53ed..42156fc 100644 --- a/app/routes/en/contents.$slug.tsx +++ b/app/routes/en/contents.$slug.tsx @@ -69,7 +69,7 @@ const Content = ({ content }: ContentProps) => { export default function EnglishPostDetail() { const { slug } = useParams(); - const content = getContentBySlugAndLanguage(slug!, "en"); + const content = getContentBySlugAndLanguage(slug ?? "", "en"); if (!content) { return
Content not found
; From 19f42d92fd989fa93cfbeb9b7312ecc466509c24 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 11:42:58 +0900 Subject: [PATCH 05/35] fix: import error --- app/lib/i18n/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/i18n/index.ts b/app/lib/i18n/index.ts index 07c0044..28627a3 100644 --- a/app/lib/i18n/index.ts +++ b/app/lib/i18n/index.ts @@ -1,12 +1,12 @@ import i18next from "i18next"; import Backend from "i18next-http-backend"; -import { createRemixI18NextBackend } from "remix-i18next/server"; +import { RemixI18Next } from "remix-i18next/server"; export const defaultNS = "common"; export const fallbackLng = "ja"; export const supportedLngs = ["ja", "en"]; -export const remixI18Next = createRemixI18NextBackend({ +export const remixI18Next = new RemixI18Next({ detection: { supportedLanguages: supportedLngs, fallbackLanguage: fallbackLng, From fe7595b482bb7bfa6602ae8c98933257a3fbb10b Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 11:44:17 +0900 Subject: [PATCH 06/35] fix: import error --- app/lib/i18n/i18n.server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/i18n/i18n.server.ts b/app/lib/i18n/i18n.server.ts index e8c2dba..c693eae 100644 --- a/app/lib/i18n/i18n.server.ts +++ b/app/lib/i18n/i18n.server.ts @@ -1,12 +1,12 @@ import i18next from "i18next"; -import { createRemixI18NextBackend } from "remix-i18next/server"; +import { RemixI18Next } from "remix-i18next/server"; import { defaultNS, fallbackLng, supportedLngs } from "."; -export const i18n = createRemixI18NextBackend({ +export const i18n = new RemixI18Next({ detection: { supportedLanguages: supportedLngs, fallbackLanguage: fallbackLng, - order: ["pathPrefix"], + order: ["header"], }, i18next: { ...i18next, From 98b54536124714e9f3af8216e12e2f153112fb14 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 02:54:02 +0000 Subject: [PATCH 07/35] refactor: use dynamic routing for i18n instead of duplicate route files - Remove duplicate English route files under /en/ directory - Update routing configuration to reuse components for both languages - Implement dynamic language detection in all route loaders using request.url - Update all components to build language-aware URLs dynamically - Maintain language prefix routing (/ for ja, /en/ for en) with shared components Co-authored-by: Yosuke Okuwaki --- app/components/I18nProvider.tsx | 18 +++- app/routes.ts | 14 ++-- app/routes/contents.$slug.tsx | 22 +++-- app/routes/contents.tsx | 22 +++-- app/routes/en/contents.$slug.tsx | 136 ------------------------------- app/routes/en/contents.tsx | 55 ------------- app/routes/en/home.tsx | 27 ------ app/routes/en/layout.tsx | 5 -- app/routes/en/tags.$tag.tsx | 94 --------------------- app/routes/en/tags.tsx | 95 --------------------- app/routes/home.tsx | 11 ++- app/routes/tags.$tag.tsx | 25 ++++-- app/routes/tags.tsx | 18 ++-- 13 files changed, 93 insertions(+), 449 deletions(-) delete mode 100644 app/routes/en/contents.$slug.tsx delete mode 100644 app/routes/en/contents.tsx delete mode 100644 app/routes/en/home.tsx delete mode 100644 app/routes/en/layout.tsx delete mode 100644 app/routes/en/tags.$tag.tsx delete mode 100644 app/routes/en/tags.tsx diff --git a/app/components/I18nProvider.tsx b/app/components/I18nProvider.tsx index 0303bb5..c71d739 100644 --- a/app/components/I18nProvider.tsx +++ b/app/components/I18nProvider.tsx @@ -1,10 +1,20 @@ "use client"; -import { useEffect } from "react"; +import { createContext, useContext, useEffect } from "react"; import { I18nextProvider } from "react-i18next"; import "../lib/i18n/i18n.client"; import { i18next } from "../lib/i18n"; +interface LanguageContextType { + language: string; +} + +const LanguageContext = createContext({ language: "ja" }); + +export function useLanguage() { + return useContext(LanguageContext); +} + interface I18nProviderProps { children: React.ReactNode; language?: string; @@ -17,5 +27,9 @@ export function I18nProvider({ children, language = "ja" }: I18nProviderProps) { } }, [language]); - return {children}; + return ( + + {children} + + ); } diff --git a/app/routes.ts b/app/routes.ts index 478e5d6..98b23f3 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -8,12 +8,12 @@ export default [ route("tags", "routes/tags.tsx"), route("tags/:tag", "routes/tags.$tag.tsx"), - // English routes with language prefix - route("en", "routes/en/layout.tsx", [ - index("routes/en/home.tsx"), - route("contents", "routes/en/contents.tsx"), - route("contents/:slug", "routes/en/contents.$slug.tsx"), - route("tags", "routes/en/tags.tsx"), - route("tags/:tag", "routes/en/tags.$tag.tsx"), + // English routes with language prefix - reusing same components + route("en", undefined, [ + index("routes/home.tsx"), + route("contents", "routes/contents.tsx"), + route("contents/:slug", "routes/contents.$slug.tsx"), + route("tags", "routes/tags.tsx"), + route("tags/:tag", "routes/tags.$tag.tsx"), ]), ] satisfies RouteConfig; diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index c74ba1e..6b5b32e 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -3,7 +3,7 @@ import { MDXContent } from "@content-collections/mdx/react"; import type { allContents } from "content-collections"; import { useEffect, useState } from "react"; -import { useParams } from "react-router"; +import { useParams, useLocation } from "react-router"; import { getContentBySlugAndLanguage } from "../lib/content"; type ClientOnlyProps = { @@ -63,7 +63,7 @@ const Content = ({ content }: ContentProps) => { {/* 戻るリンク */}
@@ -73,22 +73,32 @@ const Content = ({ content }: ContentProps) => { export default function PostDetail() { const { slug } = useParams(); - const content = getContentBySlugAndLanguage(slug ?? "", "ja"); + const location = useLocation(); + + // Detect language from current URL path + const language = location.pathname.startsWith("/en") ? "en" : "ja"; + + const content = getContentBySlugAndLanguage(slug ?? "", language); if (!content) { return
Content not found
; } + + const getHomeUrl = () => language === "en" ? "/en" : "/"; + const getContentsUrl = () => language === "en" ? "/en/contents" : "/contents"; + const getTagUrl = (tag: string) => + language === "en" ? `/en/tags/${encodeURIComponent(tag)}` : `/tags/${encodeURIComponent(tag)}`; return (
); From 78283c4486608582afa08389225df30fdd326dde Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 11:56:49 +0900 Subject: [PATCH 09/35] chore: format --- app/routes/contents.$slug.tsx | 19 +++++++++++-------- app/routes/contents.tsx | 8 +++++--- app/routes/home.tsx | 9 +++++---- app/routes/tags.$tag.tsx | 11 ++++++----- app/routes/tags.tsx | 11 +++++++---- 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index 6b5b32e..4ccbe64 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -3,7 +3,7 @@ import { MDXContent } from "@content-collections/mdx/react"; import type { allContents } from "content-collections"; import { useEffect, useState } from "react"; -import { useParams, useLocation } from "react-router"; +import { useLocation, useParams } from "react-router"; import { getContentBySlugAndLanguage } from "../lib/content"; type ClientOnlyProps = { @@ -74,19 +74,22 @@ const Content = ({ content }: ContentProps) => { export default function PostDetail() { const { slug } = useParams(); const location = useLocation(); - + // Detect language from current URL path const language = location.pathname.startsWith("/en") ? "en" : "ja"; - + const content = getContentBySlugAndLanguage(slug ?? "", language); if (!content) { return
Content not found
; } - - const getHomeUrl = () => language === "en" ? "/en" : "/"; - const getContentsUrl = () => language === "en" ? "/en/contents" : "/contents"; - const getTagUrl = (tag: string) => - language === "en" ? `/en/tags/${encodeURIComponent(tag)}` : `/tags/${encodeURIComponent(tag)}`; + + const getHomeUrl = () => (language === "en" ? "/en" : "/"); + const getContentsUrl = () => + language === "en" ? "/en/contents" : "/contents"; + const getTagUrl = (tag: string) => + language === "en" + ? `/en/tags/${encodeURIComponent(tag)}` + : `/tags/${encodeURIComponent(tag)}`; return (
diff --git a/app/routes/contents.tsx b/app/routes/contents.tsx index fecce9c..d5b99f4 100644 --- a/app/routes/contents.tsx +++ b/app/routes/contents.tsx @@ -21,13 +21,15 @@ export function loader({ request }: Route.LoaderArgs) { export default function Contents({ loaderData }: Route.ComponentProps) { const { contents, language } = loaderData; - + const getContentUrl = (slug: string) => { return language === "en" ? `/en/contents/${slug}` : `/contents/${slug}`; }; - + const getTagUrl = (tag: string) => { - return language === "en" ? `/en/tags/${encodeURIComponent(tag)}` : `/tags/${encodeURIComponent(tag)}`; + return language === "en" + ? `/en/tags/${encodeURIComponent(tag)}` + : `/tags/${encodeURIComponent(tag)}`; }; return ( diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 9a710e5..bc7884a 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -11,10 +11,11 @@ export function meta(_: Route.MetaArgs) { export default function Home() { const location = useLocation(); const language = location.pathname.startsWith("/en") ? "en" : "ja"; - - const getContentsUrl = () => language === "en" ? "/en/contents" : "/contents"; - const getTagsUrl = () => language === "en" ? "/en/tags" : "/tags"; - + + const getContentsUrl = () => + language === "en" ? "/en/contents" : "/contents"; + const getTagsUrl = () => (language === "en" ? "/en/tags" : "/tags"); + return (
diff --git a/app/routes/tags.$tag.tsx b/app/routes/tags.$tag.tsx index 4ee230e..c34df99 100644 --- a/app/routes/tags.$tag.tsx +++ b/app/routes/tags.$tag.tsx @@ -26,11 +26,12 @@ export function loader({ params, request }: Route.LoaderArgs) { export default function TagDetail({ loaderData }: Route.ComponentProps) { const { tag, posts, language } = loaderData; - - const getHomeUrl = () => language === "en" ? "/en" : "/"; - const getTagsUrl = () => language === "en" ? "/en/tags" : "/tags"; - const getContentsUrl = () => language === "en" ? "/en/contents" : "/contents"; - const getContentUrl = (slug: string) => + + const getHomeUrl = () => (language === "en" ? "/en" : "/"); + const getTagsUrl = () => (language === "en" ? "/en/tags" : "/tags"); + const getContentsUrl = () => + language === "en" ? "/en/contents" : "/contents"; + const getContentUrl = (slug: string) => language === "en" ? `/en/contents/${slug}` : `/contents/${slug}`; return ( diff --git a/app/routes/tags.tsx b/app/routes/tags.tsx index 1822ddd..0793d86 100644 --- a/app/routes/tags.tsx +++ b/app/routes/tags.tsx @@ -32,10 +32,13 @@ export function loader({ request }: Route.LoaderArgs) { export default function Tags({ loaderData }: Route.ComponentProps) { const { tags, language } = loaderData; - - const getTagUrl = (tag: string) => - language === "en" ? `/en/tags/${encodeURIComponent(tag)}` : `/tags/${encodeURIComponent(tag)}`; - const getContentsUrl = () => language === "en" ? "/en/contents" : "/contents"; + + const getTagUrl = (tag: string) => + language === "en" + ? `/en/tags/${encodeURIComponent(tag)}` + : `/tags/${encodeURIComponent(tag)}`; + const getContentsUrl = () => + language === "en" ? "/en/contents" : "/contents"; return (
From 4e48472a7d6c7ea2a2de4d0c791f6e0d1fd2d157 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 12:02:12 +0900 Subject: [PATCH 10/35] chore: modify npm scripts name --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index de8f7c2..5855e86 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "----- deploy -----": "", "deploy": "pnpm run build && wrangler deploy", "----- type generation, check -----": "", - "cf-typegen": "wrangler types", - "react-router-typegen": "react-router typegen", - "typegen": "pnpm run cf-typegen && pnpm run react-router-typegen", + "typegen:cf": "wrangler types", + "typegen:react-router": "react-router typegen", + "typegen": "pnpm run typegen:cf && pnpm run typegen:react-router", "typecheck": "pnpm run typegen && tsc -b", "----- format, lint -----": "", "format": "biome format --write .", From 6bd36d80f474adc7028e1610aa5a924696c29d64 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 12:11:21 +0900 Subject: [PATCH 11/35] fix: type error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ➜ pnpm run typegen:react-router > chasing-the-kernel@ typegen:react-router /Users/nukopy/Projects/chasing-the-kernel > react-router typegen Error: Route config in "routes.ts" is invalid. Error: Unable to define routes with duplicate route id: "routes/home" at walk (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:244:13) at walk (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:251:9) at configRoutesToRouteManifest (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:256:5) at resolveConfig (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:465:17) at createConfigLoader (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:531:29) at createContext2 (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:803:24) at run (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:1230:15) at typegen (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:2253:3) at run2 (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:2415:7) at createConfigLoader (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:533:11) at createContext2 (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:803:24) at run (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:1230:15) at typegen (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:2253:3) at run2 (/Users/nukopy/Projects/chasing-the-kernel/node_modules/.pnpm/@react-router+dev@7.8.2_@types+node@20.19.11_jiti@2.5.1_lightningcss@1.30.1_react-dom@1_bd6ffc1b269b89c5c0550053ae27f3bd/node_modules/@react-router/dev/dist/cli/index.js:2415:7)  ELIFECYCLE  Command failed with exit code 1. --- app/routes.ts | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/app/routes.ts b/app/routes.ts index 98b23f3..43ad28c 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,19 +1,27 @@ -import { index, type RouteConfig, route } from "@react-router/dev/routes"; +import { + index, + prefix, + type RouteConfig, + route, +} from "@react-router/dev/routes"; -export default [ - // Japanese (default) routes - no language prefix - index("routes/home.tsx"), - route("contents", "routes/contents.tsx"), - route("contents/:slug", "routes/contents.$slug.tsx"), - route("tags", "routes/tags.tsx"), - route("tags/:tag", "routes/tags.$tag.tsx"), +const routesJa = [ + index("./routes/home.tsx", { id: "home-ja" }), + route("contents", "./routes/contents.tsx", { id: "contents-ja" }), + route("contents/:slug", "./routes/contents.$slug.tsx", { + id: "contents-slug-ja", + }), + route("tags", "./routes/tags.tsx", { id: "tags-ja" }), + route("tags/:tag", "./routes/tags.$tag.tsx", { id: "tags-tag-ja" }), +]; +const routesEn = [ + index("./routes/home.tsx", { id: "home-en" }), + route("contents", "./routes/contents.tsx", { id: "contents-en" }), + route("contents/:slug", "./routes/contents.$slug.tsx", { + id: "contents-slug-en", + }), + route("tags", "./routes/tags.tsx", { id: "tags-en" }), + route("tags/:tag", "./routes/tags.$tag.tsx", { id: "tags-tag-en" }), +]; - // English routes with language prefix - reusing same components - route("en", undefined, [ - index("routes/home.tsx"), - route("contents", "routes/contents.tsx"), - route("contents/:slug", "routes/contents.$slug.tsx"), - route("tags", "routes/tags.tsx"), - route("tags/:tag", "routes/tags.$tag.tsx"), - ]), -] satisfies RouteConfig; +export default [...routesJa, ...prefix("en", routesEn)] satisfies RouteConfig; From a41baaa94674c99724a915e4e86faba2a51d1c10 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 12:26:33 +0900 Subject: [PATCH 12/35] docs: japanize content --- app/contents/hello-world.md | 11 ----------- app/contents/ja/hello-world.md | 12 ++++++------ app/contents/ja/my-first-document.md | 8 ++++---- app/contents/ja/test-mdx.mdx | 6 +++--- app/contents/my-first-document.md | 9 --------- app/contents/test-mdx.mdx | 11 ----------- 6 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 app/contents/hello-world.md delete mode 100644 app/contents/my-first-document.md delete mode 100644 app/contents/test-mdx.mdx diff --git a/app/contents/hello-world.md b/app/contents/hello-world.md deleted file mode 100644 index 38e5fa0..0000000 --- a/app/contents/hello-world.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: "Hello world" -tags: ["hello", "world"] -summary: "This is my first post!" ---- - -# Hello world - -This is my first post! -... rest of the content -hoge diff --git a/app/contents/ja/hello-world.md b/app/contents/ja/hello-world.md index 3962334..c1c2120 100644 --- a/app/contents/ja/hello-world.md +++ b/app/contents/ja/hello-world.md @@ -1,11 +1,11 @@ --- -title: "Hello world" +title: "こんにちは、世界" tags: ["hello", "world"] -summary: "This is my first post!" +summary: "これははじめてのポストです。" --- -# Hello world +# こんにちは、世界 -This is my first post! -... rest of the content -hoge \ No newline at end of file +これははじめてのポストです。 + +ほげ diff --git a/app/contents/ja/my-first-document.md b/app/contents/ja/my-first-document.md index 2117d1d..01013fc 100644 --- a/app/contents/ja/my-first-document.md +++ b/app/contents/ja/my-first-document.md @@ -1,9 +1,9 @@ --- -title: "My first document" +title: "はじめてのドキュメント" tags: ["operating system", "unix"] -summary: "This is my first document!" +summary: "これははじめてのドキュメントです。" --- -# My first document +# はじめてのドキュメント -howhow... \ No newline at end of file +ほうほう diff --git a/app/contents/ja/test-mdx.mdx b/app/contents/ja/test-mdx.mdx index 96ca558..3209b4d 100644 --- a/app/contents/ja/test-mdx.mdx +++ b/app/contents/ja/test-mdx.mdx @@ -1,11 +1,11 @@ --- -title: Test MDX +title: "MDX のテスト" tags: ["react", "mdx"] -summary: This is a test MDX file. +summary: "MDX のテストです。" --- import Counter from "./components/Counter.tsx"; # Test MDX - \ No newline at end of file + diff --git a/app/contents/my-first-document.md b/app/contents/my-first-document.md deleted file mode 100644 index 7b4991f..0000000 --- a/app/contents/my-first-document.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: "My first document" -tags: ["operating system", "unix"] -summary: "This is my first document!" ---- - -# My first document - -howhow... diff --git a/app/contents/test-mdx.mdx b/app/contents/test-mdx.mdx deleted file mode 100644 index 91a2ba1..0000000 --- a/app/contents/test-mdx.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Test MDX -tags: ["react", "mdx"] -summary: This is a test MDX file. ---- - -import Counter from "./components/Counter.tsx"; - -# Test MDX - - From d220a0252a94fc06eabe7cc9a840ab69b650f9f6 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 12:26:48 +0900 Subject: [PATCH 13/35] fix: type error --- app/routes/contents.$slug.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index 4ccbe64..b834c1f 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -44,9 +44,10 @@ export function ClientOnly({ children }: ClientOnlyProps) { type ContentProps = { content: (typeof allContents)[number]; + getContentsUrl: () => string; }; -const Content = ({ content }: ContentProps) => { +const Content = ({ content, getContentsUrl }: ContentProps) => { const isContentMdx = content._meta.extension === "mdx"; const body = isContentMdx ? ( @@ -149,7 +150,7 @@ export default function PostDetail() { {/* コンテンツ */}
{/* */} - +
); From 3f60db2cff9d2a0a32907ae9c96c1ffa93686be2 Mon Sep 17 00:00:00 2001 From: nukopy Date: Tue, 2 Sep 2025 17:16:49 +0900 Subject: [PATCH 14/35] chore: reinstall i18n dependencies --- package.json | 8 ++++---- pnpm-lock.yaml | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 5855e86..bed8a69 100644 --- a/package.json +++ b/package.json @@ -26,15 +26,15 @@ "test": "echo \"No tests yet\"" }, "dependencies": { - "i18next": "^24.2.0", - "i18next-browser-languagedetector": "^8.1.1", + "i18next": "^25.4.2", + "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "isbot": "^5.1.27", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-i18next": "^15.2.0", + "react-i18next": "^15.7.3", "react-router": "^7.7.1", - "remix-i18next": "^7.1.0" + "remix-i18next": "^7.3.0" }, "devDependencies": { "@biomejs/biome": "2.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bba115..cb10704 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,10 +9,10 @@ importers: .: dependencies: i18next: - specifier: ^24.2.0 - version: 24.2.3(typescript@5.9.2) + specifier: ^25.4.2 + version: 25.4.2(typescript@5.9.2) i18next-browser-languagedetector: - specifier: ^8.1.1 + specifier: ^8.2.0 version: 8.2.0 i18next-http-backend: specifier: ^3.0.2 @@ -27,14 +27,14 @@ importers: specifier: ^19.1.0 version: 19.1.1(react@19.1.1) react-i18next: - specifier: ^15.2.0 - version: 15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) + specifier: ^15.7.3 + version: 15.7.3(i18next@25.4.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) react-router: specifier: ^7.7.1 version: 7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) remix-i18next: - specifier: ^7.1.0 - version: 7.3.0(i18next@24.2.3(typescript@5.9.2))(react-i18next@15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2))(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + specifier: ^7.3.0 + version: 7.3.0(i18next@25.4.2(typescript@5.9.2))(react-i18next@15.7.3(i18next@25.4.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2))(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) devDependencies: '@biomejs/biome': specifier: 2.2.2 @@ -1545,8 +1545,8 @@ packages: i18next-http-backend@3.0.2: resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==} - i18next@24.2.3: - resolution: {integrity: sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==} + i18next@25.4.2: + resolution: {integrity: sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3960,7 +3960,7 @@ snapshots: transitivePeerDependencies: - encoding - i18next@24.2.3(typescript@5.9.2): + i18next@25.4.2(typescript@5.9.2): dependencies: '@babel/runtime': 7.28.3 optionalDependencies: @@ -4577,11 +4577,11 @@ snapshots: react: 19.1.1 scheduler: 0.26.0 - react-i18next@15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2): + react-i18next@15.7.3(i18next@25.4.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2): dependencies: '@babel/runtime': 7.28.3 html-parse-stringify: 3.0.1 - i18next: 24.2.3(typescript@5.9.2) + i18next: 25.4.2(typescript@5.9.2) react: 19.1.1 optionalDependencies: react-dom: 19.1.1(react@19.1.1) @@ -4692,11 +4692,11 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - remix-i18next@7.3.0(i18next@24.2.3(typescript@5.9.2))(react-i18next@15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2))(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): + remix-i18next@7.3.0(i18next@25.4.2(typescript@5.9.2))(react-i18next@15.7.3(i18next@25.4.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2))(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): dependencies: - i18next: 24.2.3(typescript@5.9.2) + i18next: 25.4.2(typescript@5.9.2) react: 19.1.1 - react-i18next: 15.7.3(i18next@24.2.3(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) + react-i18next: 15.7.3(i18next@25.4.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) react-router: 7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) resolve@1.22.10: From ad76461834362f0a9f6ddf3a8a0ce5d7f1c652d4 Mon Sep 17 00:00:00 2001 From: nukopy Date: Wed, 3 Sep 2025 00:31:32 +0900 Subject: [PATCH 15/35] feat: fix layout --- app/root.tsx | 4 +++- app/routes/contents.$slug.tsx | 2 +- app/routes/contents.tsx | 4 ++-- app/routes/home.tsx | 2 +- app/routes/tags.$tag.tsx | 4 ++-- app/routes/tags.tsx | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index 6f7b344..77f678d 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -55,7 +55,9 @@ export default function App() {
- +
+ +
); diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index b834c1f..a9aec6a 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -93,7 +93,7 @@ export default function PostDetail() { : `/tags/${encodeURIComponent(tag)}`; return ( -
+
  • diff --git a/app/routes/contents.tsx b/app/routes/contents.tsx index d5b99f4..094a464 100644 --- a/app/routes/contents.tsx +++ b/app/routes/contents.tsx @@ -33,7 +33,7 @@ export default function Contents({ loaderData }: Route.ComponentProps) { }; return ( -
    + <>

    Contents

    {contents.map((content) => ( @@ -62,6 +62,6 @@ export default function Contents({ loaderData }: Route.ComponentProps) {
    ))}
    -
+ ); } diff --git a/app/routes/home.tsx b/app/routes/home.tsx index bc7884a..67af868 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -17,7 +17,7 @@ export default function Home() { const getTagsUrl = () => (language === "en" ? "/en/tags" : "/tags"); return ( -
+
diff --git a/app/routes/tags.$tag.tsx b/app/routes/tags.$tag.tsx index c34df99..859664e 100644 --- a/app/routes/tags.$tag.tsx +++ b/app/routes/tags.$tag.tsx @@ -35,7 +35,7 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) { language === "en" ? `/en/contents/${slug}` : `/contents/${slug}`; return ( -
+ <>
  • @@ -99,6 +99,6 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) { 投稿一覧を見る
-
+ ); } diff --git a/app/routes/tags.tsx b/app/routes/tags.tsx index 0793d86..2794325 100644 --- a/app/routes/tags.tsx +++ b/app/routes/tags.tsx @@ -41,7 +41,7 @@ export default function Tags({ loaderData }: Route.ComponentProps) { language === "en" ? "/en/contents" : "/contents"; return ( -
+ ); } From 338b2ae5e939ec02126c5714d579d0993b724835 Mon Sep 17 00:00:00 2001 From: nukopy Date: Wed, 3 Sep 2025 01:48:34 +0900 Subject: [PATCH 16/35] feat: impl middleware error, logging --- app/middlewares/error.ts | 26 ++++++++++++++++++++++++ app/middlewares/logging.ts | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 app/middlewares/error.ts create mode 100644 app/middlewares/logging.ts diff --git a/app/middlewares/error.ts b/app/middlewares/error.ts new file mode 100644 index 0000000..cbf69d8 --- /dev/null +++ b/app/middlewares/error.ts @@ -0,0 +1,26 @@ +import { requestIdContext } from "app/contexts/requestIdContext"; +import type { unstable_MiddlewareFunction as MiddlewareFunction } from "react-router"; + +export const errorMiddleware: MiddlewareFunction = async ( + { context }, + next, +) => { + try { + return await next(); + } catch (error) { + const requestId = context.get(requestIdContext); + + // エラーをログに記録 + console.error({ + requestId, + level: "ERROR", + message: `Route error: ${error}`, + timestamp: new Date().toISOString(), + errorType: error?.constructor?.name, + error, + }); + + // React Routerに処理させるために再スロー + throw error; + } +}; diff --git a/app/middlewares/logging.ts b/app/middlewares/logging.ts new file mode 100644 index 0000000..42b7775 --- /dev/null +++ b/app/middlewares/logging.ts @@ -0,0 +1,41 @@ +import type { unstable_MiddlewareFunction as MiddlewareFunction } from "react-router"; +import { requestIdContext } from "../contexts/requestIdContext"; + +export const loggingMiddleware: MiddlewareFunction = async ( + { request, context }, + next, +) => { + // get request id + const requestId = context.get(requestIdContext); + + // TODO: ロガーライブラリ入れて JSON Lines 化 (UA とか IP アドレスとか諸々出力) + console.info({ + requestId, + level: "INFO", + method: request.method, + url: request.url, + message: `Request ${request.method} ${request.url}`, + timestamp: new Date().toISOString(), + ua: request.headers.get("user-agent"), + ip: request.headers.get("x-forwarded-for"), + cf: request.headers.get("cf-ipcountry"), + }); + + const start = performance.now(); + const response = (await next()) as Response; + const duration = performance.now() - start; + + console.info({ + requestId, + level: "INFO", + method: request.method, + url: request.url, + message: `Response with status code ${response.status}`, + timestamp: new Date().toISOString(), + status: response.status, + duration: duration, + durationUnit: "ms", + }); + + return response; +}; From 75ea8a23e1ee9f41b9d4e312d8821078d5867c39 Mon Sep 17 00:00:00 2001 From: nukopy Date: Wed, 3 Sep 2025 01:48:58 +0900 Subject: [PATCH 17/35] feat: add util routes --- app/routes/misc/test-props.tsx | 24 ++++++ app/routes/not-found.tsx | 130 +++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 app/routes/misc/test-props.tsx create mode 100644 app/routes/not-found.tsx diff --git a/app/routes/misc/test-props.tsx b/app/routes/misc/test-props.tsx new file mode 100644 index 0000000..88ead83 --- /dev/null +++ b/app/routes/misc/test-props.tsx @@ -0,0 +1,24 @@ +import type { Route } from "../../routes/misc/+types/test-props"; + +export function loader() { + return { + message: "Hello, world!", + }; +} + +export default function MyRouteComponent({ + loaderData, + actionData, + params, + matches, +}: Route.ComponentProps) { + return ( +
+

Props 付きのマイルートへようこそ!

+

ローダーデータ: {JSON.stringify(loaderData)}

+

アクションデータ: {JSON.stringify(actionData)}

+

ルートパラメータ: {JSON.stringify(params)}

+

一致したルート: {JSON.stringify(matches, null, 2)}

+
+ ); +} diff --git a/app/routes/not-found.tsx b/app/routes/not-found.tsx new file mode 100644 index 0000000..1d399d8 --- /dev/null +++ b/app/routes/not-found.tsx @@ -0,0 +1,130 @@ +import { useId } from "react"; + +interface NotFoundIconProps { + width?: number; + className?: string; +} + +const DEFAULT_WIDTH = 1200; +const DEFAULT_HEIGHT = 630; +const ICON_ASPECT_RATIO = DEFAULT_WIDTH / DEFAULT_HEIGHT; +const ICON_WIDTH = 2000; + +export function NotFoundIcon({ + width = ICON_WIDTH, + className = "w-full h-full", +}: NotFoundIconProps) { + const height = width / ICON_ASPECT_RATIO; + + // generate ids + const glowId = useId(); + const scanlinesId = useId(); + const glitch404Id = useId(); + + return ( + + 404 Not Found + + {/* Colors */} + + + {/* Glow filter */} + + + + + + + + + + {/* Subtle noise / scanlines */} + + + + + + + + + {/* Simple glitch offsets by color layers */} + + + 404 Not Found + + + 404 Not Found + + + 404 Not Found + + + + + {/* 404 Title with glow/glitch */} + + + + + ); +} + +export default function NotFound() { + return ( +
+ +
+ ); +} From c5c38c368849796d919c70a47d51a8149753cb28 Mon Sep 17 00:00:00 2001 From: nukopy Date: Wed, 3 Sep 2025 01:49:25 +0900 Subject: [PATCH 18/35] refactor: for multi language --- app/routes.ts | 76 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/app/routes.ts b/app/routes.ts index 43ad28c..b89a975 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -5,23 +5,59 @@ import { route, } from "@react-router/dev/routes"; -const routesJa = [ - index("./routes/home.tsx", { id: "home-ja" }), - route("contents", "./routes/contents.tsx", { id: "contents-ja" }), - route("contents/:slug", "./routes/contents.$slug.tsx", { - id: "contents-slug-ja", - }), - route("tags", "./routes/tags.tsx", { id: "tags-ja" }), - route("tags/:tag", "./routes/tags.$tag.tsx", { id: "tags-tag-ja" }), -]; -const routesEn = [ - index("./routes/home.tsx", { id: "home-en" }), - route("contents", "./routes/contents.tsx", { id: "contents-en" }), - route("contents/:slug", "./routes/contents.$slug.tsx", { - id: "contents-slug-en", - }), - route("tags", "./routes/tags.tsx", { id: "tags-en" }), - route("tags/:tag", "./routes/tags.$tag.tsx", { id: "tags-tag-en" }), -]; - -export default [...routesJa, ...prefix("en", routesEn)] satisfies RouteConfig; +const LANGUAGE_ID_MAP = { + ja: "ja", + en: "en", +} as const; + +function createBaseRoutes(language: keyof typeof LANGUAGE_ID_MAP) { + return [ + // root + index(`./routes/home.tsx`, { id: `home-${language}` }), + + // /contents + route("contents", `./routes/contents.tsx`, { + id: `contents-${language}`, + }), + // /contents/:slug + route("contents/:slug", `./routes/contents.$slug.tsx`, { + id: `contents-slug-${language}`, + }), + + // /tags + route("tags", `./routes/tags.tsx`, { id: `tags-${language}` }), + // /tags/:tag + route("tags/:tag", `./routes/tags.$tag.tsx`, { + id: `tags-slug-${language}`, + }), + + // /misc + ...prefix("misc", [ + route("test-props", `./routes/misc/test-props.tsx`, { + id: `misc-test-props-${language}`, + }), + ]), + + // /not-found + route("*", `./routes/not-found.tsx`, { id: `not-found-${language}` }), + ]; +} + +function createRoutes(language: keyof typeof LANGUAGE_ID_MAP) { + const baseRoutes = createBaseRoutes(language); + + // if ja, we don't use prefix + if (language === LANGUAGE_ID_MAP.ja) { + return baseRoutes; + } + + // if not ja, we use prefix + return prefix(language, baseRoutes); +} + +// create routes +const routesJa = createRoutes(LANGUAGE_ID_MAP.ja); +const routesEn = createRoutes(LANGUAGE_ID_MAP.en); +const routes = [...routesJa, ...routesEn]; + +export default routes satisfies RouteConfig; From 11bb74f18e68fcd63e99347dd6a8e108f93f7cb6 Mon Sep 17 00:00:00 2001 From: nukopy Date: Wed, 3 Sep 2025 01:50:56 +0900 Subject: [PATCH 19/35] fix: impl i18n basis --- app/components/LanguageSwitcher.tsx | 12 ++++++--- app/lib/i18n/i18n.client.ts | 26 ------------------- app/lib/i18n/i18n.server.ts | 23 ----------------- app/lib/i18n/index.ts | 23 ++--------------- app/lib/i18n/locales/en.ts | 23 +++++++++++++++++ app/lib/i18n/locales/ja.ts | 21 ++++++++++++++++ app/middlewares/i18next.ts | 39 +++++++++++++++++++++++++++++ app/middlewares/index.ts | 11 ++++++++ app/root.tsx | 34 +++++++++++++++++-------- public/locales/en/common.json | 21 ---------------- public/locales/ja/common.json | 21 ---------------- workers/app.ts | 24 +++++++++++++++--- 12 files changed, 148 insertions(+), 130 deletions(-) delete mode 100644 app/lib/i18n/i18n.client.ts delete mode 100644 app/lib/i18n/i18n.server.ts create mode 100644 app/lib/i18n/locales/en.ts create mode 100644 app/lib/i18n/locales/ja.ts create mode 100644 app/middlewares/i18next.ts create mode 100644 app/middlewares/index.ts delete mode 100644 public/locales/en/common.json delete mode 100644 public/locales/ja/common.json diff --git a/app/components/LanguageSwitcher.tsx b/app/components/LanguageSwitcher.tsx index 6762baa..c2664b2 100644 --- a/app/components/LanguageSwitcher.tsx +++ b/app/components/LanguageSwitcher.tsx @@ -1,12 +1,10 @@ import { useTranslation } from "react-i18next"; -import { supportedLngs } from "../lib/i18n"; +import { SUPPORTED_LANGUAGES } from "../lib/i18n"; export function LanguageSwitcher() { const { t, i18n } = useTranslation(); const handleLanguageChange = (lng: string) => { - i18n.changeLanguage(lng); - // Update URL to include/remove language prefix const currentPath = window.location.pathname; let newPath: string; @@ -28,8 +26,14 @@ export function LanguageSwitcher() { newPath = `/${lng}${currentPath}`; } } + console.info( + `[LanguageSwitcher] changing language to ${lng}, newPath: ${newPath}`, + ); window.history.pushState({}, "", newPath); + + // redirect to new path + window.location.href = newPath; }; return ( @@ -57,7 +61,7 @@ export function LanguageSwitcher() {
  • {t("language.select")}
  • - {supportedLngs.map((lng) => ( + {SUPPORTED_LANGUAGES.map((lng) => (
  • -
      -
    • - Contents -
    • -
    • - Tags -
    • +
        + {MENU_ITEMS.map((item) => ( +
      • + { + // ドロップダウンを閉じるためにフォーカスを外す + (document.activeElement as HTMLElement)?.blur(); + }} + > + {item.label} + +
      • + ))}
  • - + + {/* logo */} + Chasing the Kernel - +
    -
    +
      -
    • - Contents -
    • -
    • - Tags -
    • + {MENU_ITEMS.map((item) => ( +
    • + {item.label} +
    • + ))}
    diff --git a/app/root.tsx b/app/root.tsx index 7452b37..e17832a 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,6 +1,7 @@ import { data, isRouteErrorResponse, + Link, Links, Meta, Outlet, @@ -106,9 +107,9 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
    )} - + ホームに戻る - +
    diff --git a/app/routes/contents.$slug.tsx b/app/routes/contents.$slug.tsx index a9aec6a..17c64d7 100644 --- a/app/routes/contents.$slug.tsx +++ b/app/routes/contents.$slug.tsx @@ -3,7 +3,7 @@ import { MDXContent } from "@content-collections/mdx/react"; import type { allContents } from "content-collections"; import { useEffect, useState } from "react"; -import { useLocation, useParams } from "react-router"; +import { Link, useLocation, useParams } from "react-router"; import { getContentBySlugAndLanguage } from "../lib/content"; type ClientOnlyProps = { @@ -64,9 +64,9 @@ const Content = ({ content, getContentsUrl }: ContentProps) => { {/* 戻るリンク */}
    ); @@ -97,14 +97,14 @@ export default function PostDetail() {
    @@ -116,13 +116,13 @@ export default function PostDetail() { {content.tags && content.tags.length > 0 && (
    {content.tags.map((tag) => ( - #{tag} - + ))}
    )} diff --git a/app/routes/contents.tsx b/app/routes/contents.tsx index 094a464..e29637e 100644 --- a/app/routes/contents.tsx +++ b/app/routes/contents.tsx @@ -1,3 +1,4 @@ +import { Link, useNavigate } from "react-router"; import { getContentsByLanguage } from "../lib/content"; import type { Route } from "./+types/contents"; @@ -13,6 +14,8 @@ export function meta(_: Route.MetaArgs) { } export function loader({ request }: Route.LoaderArgs) { + console.info("[Contents: loader] try"); + const url = new URL(request.url); const language = url.pathname.startsWith("/en") ? "en" : "ja"; const contents = getContentsByLanguage(language); @@ -21,6 +24,7 @@ export function loader({ request }: Route.LoaderArgs) { export default function Contents({ loaderData }: Route.ComponentProps) { const { contents, language } = loaderData; + const navigate = useNavigate(); const getContentUrl = (slug: string) => { return language === "en" ? `/en/contents/${slug}` : `/contents/${slug}`; @@ -37,25 +41,34 @@ export default function Contents({ loaderData }: Route.ComponentProps) {

    Contents

    {contents.map((content) => ( -
    +
    { + navigate( + getContentUrl(content._meta.path.split("/").pop() || ""), + ); + }} + >
    - e.stopPropagation()} > {content.title} - +

    {content.summary}

    {content.tags?.map((tag) => ( - e.stopPropagation()} > #{tag} - + ))}
    diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 67af868..98b82b0 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -1,4 +1,4 @@ -import { useLocation } from "react-router"; +import { Link, useLocation } from "react-router"; import type { Route } from "./+types/home"; export function meta(_: Route.MetaArgs) { @@ -21,12 +21,12 @@ export default function Home() { diff --git a/app/routes/tags.$tag.tsx b/app/routes/tags.$tag.tsx index 859664e..28a935f 100644 --- a/app/routes/tags.$tag.tsx +++ b/app/routes/tags.$tag.tsx @@ -1,4 +1,4 @@ -import { data } from "react-router"; +import { data, Link, useNavigate } from "react-router"; import { getContentsByLanguage } from "../lib/content"; import type { Route } from "./+types/tags.$tag"; @@ -26,6 +26,7 @@ export function loader({ params, request }: Route.LoaderArgs) { export default function TagDetail({ loaderData }: Route.ComponentProps) { const { tag, posts, language } = loaderData; + const navigate = useNavigate(); const getHomeUrl = () => (language === "en" ? "/en" : "/"); const getTagsUrl = () => (language === "en" ? "/en/tags" : "/tags"); @@ -39,14 +40,14 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) {
    @@ -63,14 +64,21 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) {
    {posts.map((post) => ( -
    +
    { + navigate(getContentUrl(post._meta.path.split("/").pop() || "")); + }} + >
    - e.stopPropagation()} > {post.title} - +

    {post.summary}

    {/* タグ表示 */} @@ -92,12 +100,12 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) { {/* ナビゲーションリンク */}
    ); diff --git a/app/routes/tags.tsx b/app/routes/tags.tsx index 2794325..8511145 100644 --- a/app/routes/tags.tsx +++ b/app/routes/tags.tsx @@ -1,3 +1,4 @@ +import { Link } from "react-router"; import { getContentsByLanguage } from "../lib/content"; import type { Route } from "./+types/tags"; @@ -58,9 +59,9 @@ export default function Tags({ loaderData }: Route.ComponentProps) { ) : (
    - + ))}
    )} {/* 戻るリンク */} ); From 962e3ac53a875533edccc326035e7ada3b7fda7a Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 00:00:26 +0900 Subject: [PATCH 27/35] feat: add misc page and some loggings for understanding React Router v7 --- app/entry.server.tsx | 2 ++ app/root.tsx | 6 +++- app/routes.ts | 19 +++++++++++ app/routes/misc/index.tsx | 40 ++++++++++++++++++++++++ app/routes/misc/test-get-root-loader.tsx | 30 ++++++++++++++++++ app/routes/misc/test-props.tsx | 10 ++++-- workers/app.ts | 4 +++ 7 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 app/routes/misc/index.tsx create mode 100644 app/routes/misc/test-get-root-loader.tsx diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 78cb542..bc84c64 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -10,6 +10,8 @@ export default async function handleRequest( routerContext: EntryContext, _loadContext: AppLoadContext, ) { + console.info("[entry.server] handleRequest"); + let shellRendered = false; const userAgent = request.headers.get("user-agent"); diff --git a/app/root.tsx b/app/root.tsx index e17832a..5f573a0 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -21,7 +21,11 @@ export const unstable_middleware: unstable_MiddlewareFunction[] = [ ...rootMiddlewares, ]; -export async function loader({ context }: Route.LoaderArgs) { +export async function loader({ context, request }: Route.LoaderArgs) { + const path = new URL(request.url).pathname; + console.info("[loader: Root] try", { + path, + }); const locale = getLocale(context); console.info("[loader: Root] detected locale:", locale); diff --git a/app/routes.ts b/app/routes.ts index 9404402..6914de8 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -32,6 +32,25 @@ function createBaseRoutes(language: keyof typeof LANGUAGE_ID_MAP) { }), // /misc + route( + "misc", + `./routes/misc/index.tsx`, + { + id: `misc-${language}`, + }, + [ + route("test-props", `./routes/misc/test-props.tsx`, { + id: `misc-test-props-${language}`, + }), + route( + "test-get-root-loader", + `./routes/misc/test-get-root-loader.tsx`, + { + id: `misc-test-get-root-loader-${language}`, + }, + ), + ], + ), // /about route("about", `./routes/about.tsx`, { id: `about-${language}` }), diff --git a/app/routes/misc/index.tsx b/app/routes/misc/index.tsx new file mode 100644 index 0000000..4b37c5a --- /dev/null +++ b/app/routes/misc/index.tsx @@ -0,0 +1,40 @@ +import { Link, Outlet } from "react-router"; +import type { Route } from "./+types/index"; + +export function loader() { + console.info("[loader: Misc] try"); + + return { + links: [ + { + id: "test-get-root-loader", + href: "/misc/test-get-root-loader", + label: "Test Get Root Loader", + }, + { + id: "test-props", + href: "/misc/test-props", + label: "Test Props", + }, + ], + }; +} + +export default function Misc({ loaderData }: Route.ComponentProps) { + console.info("[Misc] Getting loader data..."); + const { links } = loaderData; + console.info("[Misc] Got loader data: ", { links }); + + return ( +
    +

    Testing React Router Functions

    + {links.map((link) => ( + + {link.label} + + ))} + + +
    + ); +} diff --git a/app/routes/misc/test-get-root-loader.tsx b/app/routes/misc/test-get-root-loader.tsx new file mode 100644 index 0000000..e6b8db1 --- /dev/null +++ b/app/routes/misc/test-get-root-loader.tsx @@ -0,0 +1,30 @@ +import { useMatches } from "react-router"; +import type { Route } from "../../routes/misc/+types/test-get-root-loader"; + +export async function loader() { + console.info("[loader: TestGetRootLoader] try"); + + return { + message: "Hello, world!", + }; +} + +export default function TestGetRootLoader({ + loaderData, +}: Route.ComponentProps) { + // get data from loader + console.info("[TestGetRootLoader] Getting loader data..."); + const { message } = loaderData; + console.info("[TestGetRootLoader] Got loader data: ", { message }); + + // get data from root loader + const matches = useMatches(); + console.info("[TestGetRootLoader] Got matches: ", { matches }); + + return ( +
    +

    TestGetRootLoader: useMatches

    +
    {JSON.stringify(matches, null, 2)}
    +
    + ); +} diff --git a/app/routes/misc/test-props.tsx b/app/routes/misc/test-props.tsx index 88ead83..5b2de07 100644 --- a/app/routes/misc/test-props.tsx +++ b/app/routes/misc/test-props.tsx @@ -1,6 +1,7 @@ import type { Route } from "../../routes/misc/+types/test-props"; export function loader() { + console.info("[loader: TestProps] try"); return { message: "Hello, world!", }; @@ -12,13 +13,18 @@ export default function MyRouteComponent({ params, matches, }: Route.ComponentProps) { + console.info("[TestProps] Getting loader data..."); + const { message } = loaderData; + console.info("[TestProps] Got loader data: ", { message }); + return (

    Props 付きのマイルートへようこそ!

    -

    ローダーデータ: {JSON.stringify(loaderData)}

    +

    ローダーデータ: {JSON.stringify({ message })}

    アクションデータ: {JSON.stringify(actionData)}

    ルートパラメータ: {JSON.stringify(params)}

    -

    一致したルート: {JSON.stringify(matches, null, 2)}

    +

    一致したルート:

    +
    {JSON.stringify(matches, null, 2)}
    ); } diff --git a/workers/app.ts b/workers/app.ts index 72bff51..8fb581e 100644 --- a/workers/app.ts +++ b/workers/app.ts @@ -22,6 +22,8 @@ const requestHandler = createRequestHandler( export default { async fetch(request, env, ctx) { + console.info("[entrypoint] fetch"); + // initalize contexts const map = new Map(); @@ -34,6 +36,8 @@ export default { // initialize context provider const provider = new unstable_RouterContextProvider(map); + // call React Router request handler + console.info("[entrypoint] call React Router request handler"); return requestHandler(request, provider); }, } satisfies ExportedHandler; From f2346f9c771a10ccc8ada93f61373b29d778b672 Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 00:09:22 +0900 Subject: [PATCH 28/35] perf: display image as static image assets, not as React component for minify bundle size and caching --- app/components/icons/LinkedInIconPng.tsx | 7 ++----- app/components/icons/NukopyIconPng.tsx | 7 ++----- .../images/LinkedIn/LinkedInIconBackgroundBlack.png | Bin .../images/LinkedIn/LinkedInIconBackgroundBlue.png | Bin .../images/LinkedIn/LinkedInIconBackgroundWhite.png | Bin .../icons => public}/images/LinkedIn/README.md | 0 .../icons => public}/images/NukopyIcon.png | Bin 7 files changed, 4 insertions(+), 10 deletions(-) rename {app/components/icons => public}/images/LinkedIn/LinkedInIconBackgroundBlack.png (100%) rename {app/components/icons => public}/images/LinkedIn/LinkedInIconBackgroundBlue.png (100%) rename {app/components/icons => public}/images/LinkedIn/LinkedInIconBackgroundWhite.png (100%) rename {app/components/icons => public}/images/LinkedIn/README.md (100%) rename {app/components/icons => public}/images/NukopyIcon.png (100%) diff --git a/app/components/icons/LinkedInIconPng.tsx b/app/components/icons/LinkedInIconPng.tsx index 21049f8..f14997c 100644 --- a/app/components/icons/LinkedInIconPng.tsx +++ b/app/components/icons/LinkedInIconPng.tsx @@ -1,5 +1,3 @@ -import LinkedInIconPngImage from "./images/LinkedIn/LinkedInIconBackgroundBlack.png"; - /** * LinkedIn png icon */ @@ -8,7 +6,6 @@ export default function LinkedInIconPng({ }: { className?: string; }) { - return ( - LinkedIn Icon - ); + const src = "/images/LinkedIn/LinkedInIconBackgroundBlack.png"; + return LinkedIn Icon; } diff --git a/app/components/icons/NukopyIconPng.tsx b/app/components/icons/NukopyIconPng.tsx index f11ec58..8171d8b 100644 --- a/app/components/icons/NukopyIconPng.tsx +++ b/app/components/icons/NukopyIconPng.tsx @@ -1,5 +1,3 @@ -import NucopyIconPngImage from "./images/NukopyIcon.png"; - /** * Nukopy png icon */ @@ -8,7 +6,6 @@ export default function NukopyIconPng({ }: { className?: string; }) { - return ( - Nucopy Icon - ); + const src = "/images/NukopyIcon.png"; + return Nucopy Icon; } diff --git a/app/components/icons/images/LinkedIn/LinkedInIconBackgroundBlack.png b/public/images/LinkedIn/LinkedInIconBackgroundBlack.png similarity index 100% rename from app/components/icons/images/LinkedIn/LinkedInIconBackgroundBlack.png rename to public/images/LinkedIn/LinkedInIconBackgroundBlack.png diff --git a/app/components/icons/images/LinkedIn/LinkedInIconBackgroundBlue.png b/public/images/LinkedIn/LinkedInIconBackgroundBlue.png similarity index 100% rename from app/components/icons/images/LinkedIn/LinkedInIconBackgroundBlue.png rename to public/images/LinkedIn/LinkedInIconBackgroundBlue.png diff --git a/app/components/icons/images/LinkedIn/LinkedInIconBackgroundWhite.png b/public/images/LinkedIn/LinkedInIconBackgroundWhite.png similarity index 100% rename from app/components/icons/images/LinkedIn/LinkedInIconBackgroundWhite.png rename to public/images/LinkedIn/LinkedInIconBackgroundWhite.png diff --git a/app/components/icons/images/LinkedIn/README.md b/public/images/LinkedIn/README.md similarity index 100% rename from app/components/icons/images/LinkedIn/README.md rename to public/images/LinkedIn/README.md diff --git a/app/components/icons/images/NukopyIcon.png b/public/images/NukopyIcon.png similarity index 100% rename from app/components/icons/images/NukopyIcon.png rename to public/images/NukopyIcon.png From 71427a2355c684a04325d10c7e3c037597354d5b Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 00:09:50 +0900 Subject: [PATCH 29/35] style: fix layout --- app/root.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index 5f573a0..5fe010c 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -60,7 +60,7 @@ export function Layout({ children }: { children: React.ReactNode }) { - + {children} @@ -71,14 +71,14 @@ export function Layout({ children }: { children: React.ReactNode }) { export default function App() { return ( - <> +
    -
    +
    - +
    ); } @@ -99,7 +99,7 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { } return ( -
    +

    {message}

    From cbb469c584c1726a5d4f095f153732f6ff8d9693 Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 00:13:35 +0900 Subject: [PATCH 30/35] refactor: make cookie key name easier to understand --- app/middlewares/i18next.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/middlewares/i18next.ts b/app/middlewares/i18next.ts index a60c97f..0010d07 100644 --- a/app/middlewares/i18next.ts +++ b/app/middlewares/i18next.ts @@ -4,7 +4,7 @@ import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES } from "../lib/i18n"; import en from "../lib/i18n/locales/en"; import ja from "../lib/i18n/locales/ja"; -export const localeCookie = createCookie("lng", { +export const localeCookie = createCookie("language", { path: "/", sameSite: "lax", secure: process.env.NODE_ENV === "production", From 73694fffb974182d9b906fb02dddd4d27332e279 Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 00:23:08 +0900 Subject: [PATCH 31/35] fix: lint error --- app/routes/contents.tsx | 5 +++-- app/routes/tags.$tag.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/routes/contents.tsx b/app/routes/contents.tsx index e29637e..da57961 100644 --- a/app/routes/contents.tsx +++ b/app/routes/contents.tsx @@ -41,7 +41,8 @@ export default function Contents({ loaderData }: Route.ComponentProps) {

    Contents

    {contents.map((content) => ( -
    { @@ -72,7 +73,7 @@ export default function Contents({ loaderData }: Route.ComponentProps) { ))}
    -
    + ))}
    diff --git a/app/routes/tags.$tag.tsx b/app/routes/tags.$tag.tsx index 28a935f..f555345 100644 --- a/app/routes/tags.$tag.tsx +++ b/app/routes/tags.$tag.tsx @@ -64,7 +64,8 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) {
    {posts.map((post) => ( -
    { @@ -93,7 +94,7 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) { ))}
    -
    + ))} From 2b04440d570b19e9a057a99a51a24d8af6a2ec42 Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 00:52:21 +0900 Subject: [PATCH 32/35] feat: enhance logging for debug --- app/middlewares/logging.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/middlewares/logging.ts b/app/middlewares/logging.ts index 3403e9e..01b85d5 100644 --- a/app/middlewares/logging.ts +++ b/app/middlewares/logging.ts @@ -23,16 +23,25 @@ export const loggingMiddleware: MiddlewareFunction = async ( console.info({ requestId, level: "INFO", - method: request.method, - url: request.url, // e.g. http://localhost:5173/contents - path, - headers: { - // ...request.headers, - host: request.headers.get("host"), - userAgent: request.headers.get("user-agent"), - }, message: `${response.status} ${request.method} ${path} (${duration}${DURATION_UNIT})`, - status: response.status, + request: { + method: request.method, + url: request.url, // e.g. http://localhost:5173/contents + path, + headers: { + // ...request.headers, + host: request.headers.get("host"), + userAgent: request.headers.get("user-agent"), + cookie: request.headers.get("cookie"), + }, + }, + response: { + status: response.status, + headers: { + "Content-Type": response.headers.get("Content-Type"), + "Set-Cookie": response.headers.get("Set-Cookie"), + }, + }, duration: duration, durationUnit: DURATION_UNIT, timestamp: new Date().toISOString(), From b1a3f1b4f98a796cf31e36fbb76a04758807649e Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 01:17:51 +0900 Subject: [PATCH 33/35] feat: impl prefetch for Links --- app/routes/contents.tsx | 2 ++ app/routes/tags.$tag.tsx | 1 + app/routes/tags.tsx | 1 + 3 files changed, 4 insertions(+) diff --git a/app/routes/contents.tsx b/app/routes/contents.tsx index da57961..7cbfb23 100644 --- a/app/routes/contents.tsx +++ b/app/routes/contents.tsx @@ -54,6 +54,7 @@ export default function Contents({ loaderData }: Route.ComponentProps) {
    e.stopPropagation()} > @@ -65,6 +66,7 @@ export default function Contents({ loaderData }: Route.ComponentProps) { e.stopPropagation()} > diff --git a/app/routes/tags.$tag.tsx b/app/routes/tags.$tag.tsx index f555345..1ae0e21 100644 --- a/app/routes/tags.$tag.tsx +++ b/app/routes/tags.$tag.tsx @@ -75,6 +75,7 @@ export default function TagDetail({ loaderData }: Route.ComponentProps) {
    e.stopPropagation()} > diff --git a/app/routes/tags.tsx b/app/routes/tags.tsx index 8511145..29fd97f 100644 --- a/app/routes/tags.tsx +++ b/app/routes/tags.tsx @@ -62,6 +62,7 @@ export default function Tags({ loaderData }: Route.ComponentProps) {
    From cb0d4e3e5435b8293d263f7107e5567a8606a281 Mon Sep 17 00:00:00 2001 From: nukopy Date: Fri, 5 Sep 2025 01:18:13 +0900 Subject: [PATCH 34/35] chore: add comments to understand React Router v7's process --- app/entry.server.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/entry.server.tsx b/app/entry.server.tsx index bc84c64..46e0ea2 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -3,6 +3,27 @@ import { renderToReadableStream } from "react-dom/server"; import type { AppLoadContext, EntryContext } from "react-router"; import { ServerRouter } from "react-router"; +/** + * Server-side rendering entry point + * + * Steps (ref: https://zenn.dev/coji/articles/react-router-v7-internal-flow#%E8%B5%B7%E5%8B%95%E3%81%8B%E3%82%89%E5%88%9D%E5%9B%9E%E8%A1%A8%E7%A4%BA%E3%81%BE%E3%81%A7%E3%81%AE%E9%81%93%E3%81%AE%E3%82%8A-(ssr%E3%83%95%E3%83%AD%E3%83%BC)) + * (0. Run middlewares) + * 1. [server] [entrypoint of React Router] `fetch` function (workers/app.ts) + * 2. [server] route matching & run loader (app/rotues.ts & matched loaders on all matched routes) + * 3. [server] server-side rendering (SSR) (entry.server.tsx) + * 4. [server] send response, including rendered HTML stream, headers, etc., to client + * --- network --- + * 5. [client] receive response + * 6. [client] parse HTML and create DOM tree / load CSS and JavaScript modules + * 7. [client] run client entry point (app/entry.client.tsx) from `