🥹Hãy triển khai sử dụng đã ngôn ngữ nextjs, wordpress sử dụng plugin polylang
1. Triển khai đã ngôn ngữ cho bài viết
package.json
{
"name": "langw",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@apollo/client": "^3.13.8",
"graphql": "^16.11.0",
"next": "15.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"typescript": "^5"
}
}
.env
WORDPRESS_API_URL=https://test1.com/graphql
NODE_TLS_REJECT_UNAUTHORIZED=0
# Only required if you want to enable preview mode
# WORDPRESS_AUTH_REFRESH_TOKEN=
# WORDPRESS_PREVIEW_SECRET=
src\lib\apollo.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
export const createApolloClient = (locale: string) => {
return new ApolloClient({
link: new HttpLink({
uri: 'https://test1.com/graphql',
headers: {
'Accept-Language': locale,
},
}),
cache: new InMemoryCache(),
})
}
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['en', 'vi']
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}`),
)
if (pathnameIsMissingLocale) {
const defaultLocale = req.headers.get('accept-language')?.startsWith('vi') ? 'vi' : 'en'
return NextResponse.redirect(new URL(`/${defaultLocale}${pathname}`, req.url))
}
return NextResponse.next()
}
src\graphql\queries.ts
import { gql } from '@apollo/client'
export const GET_HOMEPAGE = gql`
query {
page(id: "home", idType: URI) {
title
content
translations {
uri
language {
code
}
}
}
}
`
export const GET_POST = gql`
query GetPost($slug: String!) {
postBy(slug: $slug) {
title
content
translations {
uri
language {
code
}
}
language {
code
}
}
}
`
export const GET_PAGE = gql`
query GetPage($slug: String!) {
pageBy(uri: $slug) {
title
content
translations {
uri
language {
code
}
}
language {
code
}
}
}
`;

query {
pageBy(uri: "test") {
title
content
translations {
uri
language {
code
}
}
language {
code
}
}
}
{
"data": {
"pageBy": {
"title": "Test",
"content": "<p>Test</p>\n",
"translations": [
{
"uri": "/vi/kiem-tra/",
"language": {
"code": "VI"
}
}
],
"language": {
"code": "EN"
}
}
}
}
src\components\LanguageSwitcher.tsx
"use client";
import { usePathname, useRouter } from "next/navigation";
type Translation = {
uri: string;
language: {
code: string;
};
};
type Props = {
currentLang: string;
translations: Translation[];
};
const supportedLocales = ["en", "vi"];
export default function LanguageSwitcher({ currentLang, translations }: Props) {
const router = useRouter();
const pathname = usePathname();
console.log("LanguageSwitcher Translations:", translations);
const handleSwitch = (lang: string) => {
if (lang === currentLang) return;
const translation = translations.find(
(t) => t.language.code.toLowerCase() === lang
);
console.log("Target lang:", lang, "Found translation:", translation);
if (translation) {
let uri = translation.uri;
// Handle homepage: uri is /lang or /lang/ -> navigate to /lang
if (uri === `/${lang}` || uri === `/${lang}/`) {
console.log("Navigating to homepage:", `/${lang}`);
router.push(`/${lang}`);
return;
}
// For non-homepage, uri is already normalized (e.g., /vi/that-bai-la-me-thanh-cong)
console.log("Navigating to:", uri);
router.push(uri);
} else {
// Fallback: Replace the language prefix in the current pathname
const newPath = pathname.replace(/^\/[^\/]+/, `/${lang}`);
console.log("Fallback URL:", newPath);
router.push(newPath);
}
};
return (
<div className="flex gap-2 text-sm">
{supportedLocales.map((lang) => (
<button
key={lang}
onClick={() => handleSwitch(lang)}
className={`px-2 py-1 rounded ${
lang === currentLang ? "bg-blue-600 text-white" : "bg-gray-200"
}`}
>
{lang.toUpperCase()}
</button>
))}
</div>
);
}

Home Page
src\app\[lang]\page.tsx
import { createApolloClient } from "@/lib/apollo";
import { GET_PAGE } from "@/graphql/queries";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { supportedLocales } from "./layout";
export default async function Home({ params }: { params: { lang: string } }) {
const { lang } = await params;
// Map lang to CMS homepage slug
const slugMap: { [key: string]: string } = {
en: "home",
vi: "trang-chu",
};
const pageSlug = slugMap[lang] || lang; // Fallback to lang if slug not found
const client = createApolloClient(lang);
const { data } = await client.query({
query: GET_PAGE,
variables: {
slug: pageSlug
},
});
// Normalize translations: always use /lang for homepage
const translations = supportedLocales.map((locale) => ({
uri: `/${locale}`,
language: { code: locale.toUpperCase() },
}));
return (
<main>
<LanguageSwitcher currentLang={lang} translations={translations} />
<h1>{data.pageBy.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.pageBy.content }} />
</main>
);
}
Layout
src\app\[lang]\layout.tsx
import { Metadata } from "next";
export const supportedLocales = ["en", "vi"] as const; // Use 'as const' for literal types
export function generateStaticParams() {
return supportedLocales.map((lang) => ({ lang }));
}
export async function generateMetadata({ params }: { params: { lang: string }; }): Promise<Metadata> {
const { lang } = await params;
return {
title: lang === "vi" ? "Ứng dụng Next" : "Next App",
description: lang === "vi" ? "Được tạo bởi create next app" : "Generated by create next app",
};
}
async function LangLayout({ children, params}: { children: React.ReactNode; params: { lang: string }; }) {
const { lang } = await params;
return (
<>
<header>
<nav>
<a href={`/${lang}`}>Home</a>
</nav>
</header>
{children}
</>
);
}
export default LangLayout;
Page Detail
src\app\[lang]\[slug]\page.tsx
import { createApolloClient } from "@/lib/apollo";
import { GET_PAGE } from "@/graphql/queries";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { supportedLocales } from "../layout";
export default async function Page({ params }: { params: { lang: string; slug: string } }) {
const { lang, slug } = await params;
const client = createApolloClient(lang);
const { data } = await client.query({
query: GET_PAGE,
variables: {
slug
},
});
console.log("Page Translations:", data);
// console.log("Page Language:", data.pageBy.language.code);
// Normalize translations: use /lang/[slug] for non-homepage
const translations = supportedLocales.map((locale) => {
const cmsTranslation = Array.isArray(data.pageBy.translations)
? data.pageBy.translations.find((t: any) => t.language.code.toLowerCase() === locale)
: null;
if (cmsTranslation) {
let uri = cmsTranslation.uri;
const langPrefix = `/${locale}/`;
if (uri.startsWith(langPrefix)) {
uri = uri.replace(langPrefix, "/");
}
uri = uri.endsWith("/") ? uri.slice(0, -1) : uri;
return {
uri: `/${locale}${uri}`,
language: { code: locale.toUpperCase() },
};
}
return {
uri: `/${locale}/${slug}`,
language: { code: locale.toUpperCase() },
};
});
console.log("Normalized Translations:", translations);
return (
<main>
<LanguageSwitcher currentLang={lang} translations={translations} />
<h1>{data.pageBy.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.pageBy.content }} />
</main>
);
}
export async function generateStaticParams() {
const slugsByLang = {
en: ["test"],
vi: ["kiem-tra"],
};
return supportedLocales.flatMap((lang) =>
slugsByLang[lang].map((slug) => ({ lang, slug }))
);
}
Post Details
src\app\[lang]\post\[slug]\page.tsx
import { createApolloClient } from '@/lib/apollo';
import { GET_POST } from '@/graphql/queries';
import LanguageSwitcher from '@/components/LanguageSwitcher'
export async function generateStaticParams() {
return []
}
export default async function PostPage({ params }: { params: { lang: string; slug: string } }) {
const { lang, slug } = await params;
const client = createApolloClient(lang)
const { data } = await client.query({
query: GET_POST,
variables: { slug }
});
const post = data.postBy;
const updatedData = { ...post };
updatedData.translations = updatedData.translations.map((translation:any) => ({
...translation,
uri: translation.uri.replace(/\/([^/]+)\/$/, '/post/$1/')
}));
return (
<main>
<LanguageSwitcher currentLang={lang} translations={updatedData.translations} />
<h1>{updatedData.title}</h1>
<div dangerouslySetInnerHTML={{ __html: updatedData.content }} />
<hr />
<ul>
{updatedData.translations.map((trans: any) => (
<li key={trans.language.code}>
<a href={`/${trans.language.code}${trans.uri}`}>
{trans.language.code.toUpperCase()}
</a>
</li>
))}
</ul>
</main>
)
}
Show Category In Home Page
src\app\[lang]\page.tsx
import { createApolloClient } from "@/lib/apollo";
import { GET_PAGE,GET_CATEGORIES } from "@/graphql/queries";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { supportedLocales } from "./layout";
import Link from "next/link";
export default async function Home({ params }: { params: { lang: string } }) {
const { lang } = await params;
// Map lang to CMS homepage slug
const slugMap: { [key: string]: string } = {
en: "home",
vi: "trang-chu",
};
const pageSlug = slugMap[lang] || lang; // Fallback to lang if slug not found
const client = createApolloClient(lang);
const { data } = await client.query({
query: GET_PAGE,
variables: {
slug: pageSlug
},
});
// Fetch categories
const { data: categoriesData } = await client.query({
query: GET_CATEGORIES,
});
// Filter categories by current language
const categories = categoriesData.categories.nodes.filter(
(category: any) => category.language.code.toLowerCase() === lang
);
// Normalize translations: always use /lang for homepage
const translations = supportedLocales.map((locale) => ({
uri: `/${locale}`,
language: { code: locale.toUpperCase() },
}));
return (
<main>
<LanguageSwitcher currentLang={lang} translations={translations} />
<h1>{data.pageBy.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.pageBy.content }} />
<h2>Categories</h2>
<ul>
{categories.map((category: any) => (
<li key={category.slug}>
<Link href={`/${lang}/category/${category.slug}`}>
{category.name}
</Link>
</li>
))}
</ul>
</main>
);
}
Get Posts By Category
src\app[lang]\category\[categorySlug]\page.tsx
import { createApolloClient } from "@/lib/apollo";
import { GET_CATEGORIES, GET_POSTS_BY_CATEGORY } from "@/graphql/queries";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { supportedLocales } from "../../layout";
import Link from "next/link";
import { notFound } from "next/navigation";
export default async function CategoryPage({
params,
}: {
params: { lang: string; categorySlug?: string };
}) {
const { lang, categorySlug } = await params;
console.log("Category Page Params:", { lang, categorySlug });
// Validate categorySlug
if (!categorySlug || typeof categorySlug !== "string") {
console.error("Invalid categorySlug:", categorySlug);
notFound();
}
const client = createApolloClient(lang);
const { data } = await client.query({
query: GET_POSTS_BY_CATEGORY,
variables: { slug: categorySlug },
});
// Handle missing category
if (!data.category) {
console.warn(`Category not found for slug: ${categorySlug}`);
notFound();
}
console.log("Category Data:", {
slug: data.category.slug,
name: data.category.name,
language: data.category.language.code,
translations: data.category.translations,
});
// Normalize translations: use /lang/category/[category-slug]
const translations = supportedLocales.map((locale) => {
const cmsTranslation = Array.isArray(data.category.translations)
? data.category.translations.find((t: any) => t.language.code.toLowerCase() === locale)
: null;
if (cmsTranslation) {
return {
uri: `/${locale}/category/${cmsTranslation.slug}`,
language: { code: locale.toUpperCase() },
};
}
// Fallback: use current category slug
return {
uri: `/${locale}/category/${categorySlug}`,
language: { code: locale.toUpperCase() },
};
});
console.log("Normalized Translations:", translations);
return (
<main>
<LanguageSwitcher currentLang={lang} translations={translations} />
<h1>{data.category.name}</h1>
<ul>
{data.category.posts.nodes.length > 0 ? (
data.category.posts.nodes.map((post: any) => (
<li key={post.slug}>
<Link href={`/${lang}/${post.slug}`}>{post.title}</Link>
</li>
))
) : (
<li>No posts found in this category.</li>
)}
</ul>
</main>
);
}
export async function generateStaticParams() {
const client = createApolloClient("en");
const { data } = await client.query({ query: GET_CATEGORIES });
console.log("CMS Categories:", data.categories.nodes);
const slugsByLang: { [key: string]: string[] } = { en: [], vi: [] };
data.categories.nodes.forEach((category: any) => {
const lang = category.language.code.toLowerCase();
const slug = category.slug?.trim();
if (slugsByLang[lang] && slug && slug !== "uncategorized") {
slugsByLang[lang].push(slug);
}
});
console.log("Slugs by Language:", slugsByLang);
return supportedLocales.flatMap((lang) =>
slugsByLang[lang].map((categorySlug) => ({ lang, categorySlug }))
);
}
src\graphql\queries.ts
import { gql } from '@apollo/client'
export const GET_HOMEPAGE = gql`
query {
page(id: "home", idType: URI) {
title
content
translations {
uri
language {
code
}
}
}
}
`
export const GET_POST = gql`
query GetPost($slug: String!) {
postBy(slug: $slug) {
title
content
translations {
uri
language {
code
}
}
language {
code
}
}
}
`
export const GET_PAGE = gql`
query GetPage($slug: String!) {
pageBy(uri: $slug) {
title
content
translations {
uri
language {
code
}
}
language {
code
}
}
}
`;
// Query để lấy danh sách chuyên mục
export const GET_CATEGORIES = gql`
query GetCategories {
categories {
nodes {
slug
name
language {
code
}
translations {
slug
language {
code
}
}
}
}
}
`;
// Query để lấy bài viết theo chuyên mục
export const GET_POSTS_BY_CATEGORY = gql`
query GetPostsByCategory($slug: ID!) {
category(id:$slug, idType: SLUG) {
slug
name
language {
code
}
translations {
slug
language {
code
}
}
posts {
nodes {
slug
title
content
language {
code
}
translations {
slug
language {
code
}
}
}
}
}
}
`;


Tiếp tục triển khai gói i18next
Cài đặt các gói cần thiết
npm install i18next react-i18next i18next-http-backend i18next-browser-languagedetector i18next-resources-to-backend
Để triển khai một website sử dụng Next.js làm frontend và WordPress (với plugin Polylang) làm headless CMS hỗ trợ đa ngôn ngữ, ta cần:
🧩 Công nghệ sử dụng:
Next.js (App Router hoặc Pages Router tùy chọn)
WordPress với plugin Polylang
WPGraphQL (để truy vấn dữ liệu)
WPGraphQL Polylang (kết nối Polylang với WPGraphQL)
Apollo Client hoặc
graphql-request
trên Next.js để fetch dữ liệu
✅ Cài đặt bên WordPress
Cài Plugin bắt buộc:
WPGraphQL
Polylang
WPGraphQL for Polylang
Tạo nội dung đa ngôn ngữ:
Vào Polylang → chọn ngôn ngữ như
vi
,en
Khi viết post/page/custom post type, tạo phiên bản cho mỗi ngôn ngữ
Cấu hình Permalink:
Chọn kiểu hiển thị ngôn ngữ (ví dụ:
/en/slug
,/vi/slug
)
🚀 Next.js – Thiết lập fetch dữ liệu đa ngôn ngữ
1. Cài thư viện
npm install @apollo/client graphql
2. Cấu hình Apollo Client
tsCopyEdit// lib/apollo.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
export const createApolloClient = (locale: string) => {
return new ApolloClient({
link: new HttpLink({
uri: 'https://your-wp-site.com/graphql',
headers: {
'Accept-Language': locale,
},
}),
cache: new InMemoryCache(),
})
}
3. Truy vấn GraphQL có đa ngôn ngữ
graphqlCopyEditquery GetPost($slug: String!) {
postBy(slug: $slug) {
title
content
language {
code
locale
}
translations {
uri
language {
code
}
}
}
}
🌐 Next.js – Cấu trúc thư mục đa ngôn ngữ
Với App Router, dùng cấu trúc như sau:
bashCopyEdit/app
/[lang]
/page.tsx
/post
/[slug]
/page.tsx
Ví dụ app/[lang]/post/[slug]/page.tsx
app/[lang]/post/[slug]/page.tsx
import { createApolloClient } from '@/lib/apollo'
export async function generateStaticParams() {
// Fetch slugs in all languages
return [
{ lang: 'vi', slug: 'bai-viet-1' },
{ lang: 'en', slug: 'post-1' },
]
}
export default async function PostPage({ params }: { params: { lang: string; slug: string } }) {
const client = createApolloClient(params.lang)
const { data } = await client.query({
query: POST_QUERY,
variables: { slug: params.slug },
})
return (
<main>
<h1>{data.postBy.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.postBy.content }} />
</main>
)
}
🌍 Chuyển đổi ngôn ngữ
Bạn có thể tạo một Language Switcher như sau:
tsxCopyEditconst LanguageSwitcher = ({ translations }: { translations: any[] }) => {
return (
<ul>
{translations.map((trans) => (
<li key={trans.language.code}>
<a href={`/${trans.language.code}${trans.uri}`}>{trans.language.code.toUpperCase()}</a>
</li>
))}
</ul>
)
}
📌 Gợi ý nâng cao
Dùng middleware để detect locale từ URL hoặc trình duyệt
Kết hợp với
next-intl
nếu muốn quản lý text tĩnh dễ hơnTạo layout đa ngôn ngữ:
app/[lang]/layout.tsx
→ giúp inject metadata theo locale
Bạn muốn mình giúp dựng sẵn một template đơn giản không? Hoặc bạn đang dùng App Router hay Pages Router để mình gửi đúng kiểu cấu trúc nhé.
Tuyệt! Dưới đây là cách tạo layout đa ngôn ngữ trong App Router (Next.js 13/14) khi bạn dùng WordPress + Polylang + WPGraphQL, với thư mục như sau:
🧩 Cấu trúc thư mục app/
app/
bashCopyEdit/app
/[lang] // locale dynamic route
layout.tsx // shared layout per language
page.tsx // homepage theo ngôn ngữ
/post
/[slug]
page.tsx // chi tiết bài viết theo ngôn ngữ
📁 app/[lang]/layout.tsx
– Layout đa ngôn ngữ
app/[lang]/layout.tsx
– Layout đa ngôn ngữtsxCopyEdit// app/[lang]/layout.tsx
import { ReactNode } from 'react'
import { notFound } from 'next/navigation'
const supportedLocales = ['en', 'vi']
export function generateStaticParams() {
return supportedLocales.map((lang) => ({ lang }))
}
export default function LangLayout({
children,
params,
}: {
children: ReactNode
params: { lang: string }
}) {
const { lang } = params
if (!supportedLocales.includes(lang)) {
notFound()
}
return (
<html lang={lang}>
<body>
<header>
<nav>
<a href={`/${lang}`}>Home</a> | <a href={`/${lang}/post/sample-post`}>Sample Post</a>
</nav>
</header>
{children}
</body>
</html>
)
}
🏠 app/[lang]/page.tsx
– Trang chủ theo ngôn ngữ
app/[lang]/page.tsx
– Trang chủ theo ngôn ngữtsxCopyEdit// app/[lang]/page.tsx
import { createApolloClient } from '@/lib/apollo'
import { gql } from '@apollo/client'
const HOMEPAGE_QUERY = gql`
query GetHomepage {
page(id: "home", idType: URI) {
title
content
}
}
`
export default async function HomePage({ params }: { params: { lang: string } }) {
const client = createApolloClient(params.lang)
const { data } = await client.query({ query: HOMEPAGE_QUERY })
return (
<main>
<h1>{data.page.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.page.content }} />
</main>
)
}
🔥 Chú ý: Trang Home ở WordPress phải có slug là
home
ở cả 2 ngôn ngữ (và đặt làm Front Page).
✨ Tự động chuyển đổi ngôn ngữ (Language Switcher)
Bạn có thể lấy các bản dịch từ WPGraphQL (translations
) và render nút chuyển ngôn ngữ:
tsxCopyEditconst LanguageSwitcher = ({ translations }: { translations: any[] }) => (
<div>
{translations.map((t) => (
<a key={t.language.code} href={`/${t.language.code}${t.uri}`}>
{t.language.code.toUpperCase()}
</a>
))}
</div>
)
🧠 Middleware (tùy chọn) – Redirect /
→ /vi
hoặc /en
/
→ /vi
hoặc /en
tsCopyEdit// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const PUBLIC_FILE = /\.(.*)$/
const locales = ['en', 'vi']
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl
// Bỏ qua file tĩnh
if (PUBLIC_FILE.test(pathname)) return
// Nếu chưa có locale → redirect theo browser (hoặc mặc định)
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}`),
)
if (pathnameIsMissingLocale) {
const defaultLocale = req.headers.get('accept-language')?.startsWith('vi') ? 'vi' : 'en'
return NextResponse.redirect(new URL(`/${defaultLocale}${pathname}`, req.url))
}
return NextResponse.next()
}
Đừng quên thêm middleware config trong
next.config.js
nếu cần.
Tuyệt vời! Dưới đây là bản template boilerplate dành cho Next.js (App Router) kết nối với WordPress sử dụng Polylang + WPGraphQL, hỗ trợ đa ngôn ngữ đầy đủ 🎉
🔧 1. Cấu trúc thư mục
bashCopyEdit/app
/[lang]
layout.tsx
page.tsx
/post
/[slug]
page.tsx
/lib
apollo.ts
/graphql
queries.ts
/middleware.ts
/next.config.js
📦 2. Cài đặt thư viện
bashCopyEditnpm install @apollo/client graphql
🧠 3. File lib/apollo.ts
lib/apollo.ts
tsCopyEditimport { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
export const createApolloClient = (locale: string) => {
return new ApolloClient({
link: new HttpLink({
uri: 'https://your-wordpress-site.com/graphql',
headers: {
'Accept-Language': locale,
},
}),
cache: new InMemoryCache(),
})
}
📁 4. graphql/queries.ts
graphql/queries.ts
tsCopyEditimport { gql } from '@apollo/client'
export const GET_HOMEPAGE = gql`
query {
page(id: "home", idType: URI) {
title
content
}
}
`
export const GET_POST = gql`
query GetPost($slug: String!) {
postBy(slug: $slug) {
title
content
language {
code
}
translations {
uri
language {
code
}
}
}
}
`
🌍 5. app/[lang]/layout.tsx
app/[lang]/layout.tsx
tsxCopyEditexport const supportedLocales = ['en', 'vi']
export function generateStaticParams() {
return supportedLocales.map((lang) => ({ lang }))
}
export default function LangLayout({ children, params }: { children: React.ReactNode, params: { lang: string } }) {
return (
<html lang={params.lang}>
<body>
<header>
<nav>
<a href={`/${params.lang}`}>Home</a>
</nav>
</header>
{children}
</body>
</html>
)
}
🏠 6. app/[lang]/page.tsx
app/[lang]/page.tsx
tsxCopyEditimport { createApolloClient } from '@/lib/apollo'
import { GET_HOMEPAGE } from '@/graphql/queries'
export default async function Home({ params }: { params: { lang: string } }) {
const client = createApolloClient(params.lang)
const { data } = await client.query({ query: GET_HOMEPAGE })
return (
<main>
<h1>{data.page.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.page.content }} />
</main>
)
}
📝 7. app/[lang]/post/[slug]/page.tsx
app/[lang]/post/[slug]/page.tsx
tsxCopyEditimport { createApolloClient } from '@/lib/apollo'
import { GET_POST } from '@/graphql/queries'
export async function generateStaticParams() {
// TODO: Fetch slugs from WP if muốn pre-render
return []
}
export default async function PostPage({ params }: { params: { lang: string; slug: string } }) {
const client = createApolloClient(params.lang)
const { data } = await client.query({
query: GET_POST,
variables: { slug: params.slug },
})
const post = data.postBy
return (
<main>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<hr />
<ul>
{post.translations.map((trans: any) => (
<li key={trans.language.code}>
<a href={`/${trans.language.code}${trans.uri}`}>
{trans.language.code.toUpperCase()}
</a>
</li>
))}
</ul>
</main>
)
}
🌐 8. middleware.ts
middleware.ts
tsCopyEditimport { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['en', 'vi']
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}`),
)
if (pathnameIsMissingLocale) {
const defaultLocale = req.headers.get('accept-language')?.startsWith('vi') ? 'vi' : 'en'
return NextResponse.redirect(new URL(`/${defaultLocale}${pathname}`, req.url))
}
return NextResponse.next()
}
⚙️ 9. next.config.js
next.config.js
jsCopyEdit/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
}
module.exports = nextConfig
🚀 Sử dụng
Tạo trang "home" trong WordPress cho cả
vi
vàen
Cài WPGraphQL, Polylang, WPGraphQL Polylang
Đảm bảo bạn truy cập
/vi
,/en
,/vi/post/ten-bai-viet
, ...
👉 Muốn clone nhanh?
Nếu bạn muốn mình tạo repo GitHub public hoặc tạo file .zip để bạn tải về nhanh, mình có thể chuẩn bị ngay. Bạn muốn dạng nào? (GitHub hay .zip?)
4o
You said:
.zip
ChatGPT said:
Mình đã tạo xong file .zip
cho boilerplate Next.js + WordPress + Polylang. Bạn có thể tải tại đây:
👉 Tải về dự án .zip
Nếu bạn cần thêm phần:
Tự động tạo sitemap theo từng ngôn ngữ
Kết nối với WP bằng Auth headers
Giao diện Tailwind UI đẹp mắt
Cứ nói nhé, mình hỗ trợ liền! 💪
Dưới đây là bản .zip
đã cập nhật với các tính năng bạn yêu cầu:
✅ Sitemap động theo từng ngôn ngữ ✅ Kết nối WordPress sử dụng Authorization Bearer Token ✅ Giao diện đẹp với Tailwind CSS và theme dark mode
👉 Tải về dự án cập nhật (.zip)
Nếu bạn cần:
Cấu hình Tailwind nâng cao hơn
Hỗ trợ SSR hoặc ISR
Tự động tạo sitemap từ bài viết thực tế trong WordPress
Cứ nói nhé, mình làm tiếp cho bạn! 💻✨
Dĩ nhiên! Mình sẽ giúp bạn thêm menu chuyển đổi ngôn ngữ (LanguageSwitcher
) dùng Polylang + WPGraphQL. Mục tiêu là:
Hiển thị các ngôn ngữ (VD: EN | VI)
Biết đang ở ngôn ngữ nào
Tự động điều hướng đúng route với slug tương ứng
Sử dụng WPGraphQL để lấy
translations
🧩 Bước 1: Tạo components/LanguageSwitcher.tsx
components/LanguageSwitcher.tsx
tsxCopyEdit'use client'
import { usePathname, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
type Translation = {
uri: string
language: {
code: string
}
}
type Props = {
currentLang: string
translations: Translation[]
}
const supportedLocales = ['en', 'vi']
export default function LanguageSwitcher({ currentLang, translations }: Props) {
const router = useRouter()
const pathname = usePathname()
const handleSwitch = (lang: string) => {
if (lang === currentLang) return
const translation = translations.find(t => t.language.code === lang)
if (translation) {
router.push(`/${lang}${translation.uri}`)
} else {
// fallback: stay on same path but change lang
const newPath = pathname.replace(`/${currentLang}`, `/${lang}`)
router.push(newPath)
}
}
return (
<div className="flex gap-2 text-sm">
{supportedLocales.map((lang) => (
<button
key={lang}
onClick={() => handleSwitch(lang)}
className={`px-2 py-1 rounded ${
lang === currentLang ? 'bg-blue-600 text-white' : 'bg-gray-200'
}`}
>
{lang.toUpperCase()}
</button>
))}
</div>
)
}
🛠 Bước 2: Gọi LanguageSwitcher
trong post page
LanguageSwitcher
trong post page
Trong app/[lang]/post/[slug]/page.tsx
, sau khi lấy post.translations
, thêm:
tsxCopyEditimport LanguageSwitcher from '@/components/LanguageSwitcher'
// ...
return (
<main>
<LanguageSwitcher currentLang={params.lang} translations={post.translations} />
<h1 className="text-2xl font-bold mt-4">{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</main>
)
🎁 Tuỳ chọn: Đặt vào layout.tsx
nếu muốn luôn hiển thị trên header
layout.tsx
nếu muốn luôn hiển thị trên headerBạn cũng có thể truyền translations qua layout (phức tạp hơn vì phải "hoist") — nếu bạn muốn mình làm phần này thì mình có thể hỗ trợ mở rộng logic layout để chứa LanguageSwitcher
ở header trên mọi page.
Mình có thể cập nhật luôn trong project .zip
nếu bạn muốn. Bạn muốn:
✅ Mình cập nhật lại
.zip
vớiLanguageSwitcher
✏️ Bạn tự thêm vào
Bạn chọn cách nào?
4o
You said:
Đặt vào layout.tsx
ChatGPT said:
🎉 Dự án đã được cập nhật với LanguageSwitcher
trong layout.tsx và đã sẵn sàng tải về:
👉 Tải về project cuối cùng (.zip)
Tóm tắt thay đổi:
Tạo
components/LanguageSwitcher.tsx
Gọi nó trong
layout.tsx
, hiển thị trên mọi trangHỗ trợ chuyển đổi
slug
hoặc fallback mặc địnhGiao diện Tailwind đơn giản, có thể tuỳ chỉnh dễ dàng
Nếu bạn muốn lấy translations
từ post cụ thể thay vì hardcoded (/
), mình có thể giúp bạn tích hợp WPGraphQL theo URL hiện tại.
Muốn mở rộng nữa không? 😄
Last updated
Was this helpful?