You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为Markdoc + Next.js文档站点添加i18n多语言支持的问题

问题描述

我正在使用Markdoc + Next.js搭建文档站点,想要添加i18n多语言支持。已修改next.config.js,但不知道如何根据locale变量调用不同的.md文件。

我的文档文件结构如下:

  • src/pages/docs/analytics.en.md(对应locale=en)
  • src/pages/docs/analytics.fr.md(对应locale=fr)
  • src/pages/docs/analytics.es.md(对应locale=es)

next.config.js

const withMarkdoc = require('@markdoc/next.js')

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  pageExtensions: ['js', 'jsx', 'md'],
  experimental: {
    scrollRestoration: true,
  },
  i18n: {
    locales: ['en', 'fr', 'es', 'de', 'it'],
    defaultLocale: 'en'
  },
}

module.exports = withMarkdoc()(nextConfig)

src/pages/_document.jsx

class MyDocument extends Document {

  static async getInitialProps(ctx) {
    const originalRenderPage = ctx.renderPage

    console.log("Locale: ", ctx.locale)

    // Run the React rendering logic synchronously
    ctx.renderPage = () =>
      originalRenderPage({
        // Useful for wrapping the whole react tree
        enhanceApp: (App) => (props) => {
            let pageProps = props.pageProps;

            //console.log("props", pageProps.markdoc.file)
            return <App {...props} />
        },
        // Useful for wrapping in a per-page basis
        enhanceComponent: (Component) => Component,
      })

    // Run the parent `getInitialProps`, it now includes the custom `renderPage`
    const initialProps = await Document.getInitialProps(ctx)

    return initialProps
  }

  render() {
    return (
      <Html className="antialiased [font-feature-settings:'ss01']" lang="en">
        <Head>
          <script dangerouslySetInnerHTML={{ __html: themeScript }} />
        </Head>

        <body className="bg-white dark:bg-slate-900">
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

src/pages/_app.jsx

import Head from 'next/head'
import { slugifyWithCounter } from '@sindresorhus/slugify'
import { useRouter } from 'next/router'
import { Layout } from '@/components/Layout'

import 'focus-visible'
import '@/styles/tailwind.css'

function getNodeText(node) {
  let text = ''
  for (let child of node.children ?? []) {
    if (typeof child === 'string') {
      text += child
    }
    text += getNodeText(child)
  }
  return text
}

function collectHeadings(nodes, slugify = slugifyWithCounter()) {
  let sections = []

  for (let node of nodes) {
    if (node.name === 'h2' || node.name === 'h3') {
      let title = getNodeText(node)
      if (title) {
        let id = slugify(title)
        node.attributes.id = id
        if (node.name === 'h3') {
          if (!sections[sections.length - 1]) {
            throw new Error(
              'Cannot add `h3` to table of contents without a preceding `h2`'
            )
          }
          sections[sections.length - 1].children.push({
            ...node.attributes,
            title,
          })
        } else {
          sections.push({ ...node.attributes, title, children: [] })
        }
      }
    }

    sections.push(...collectHeadings(node.children ?? [], slugify))
  }

  return sections
}

export default function App({ Component, pageProps }) {
  let router = useRouter()

  let title = pageProps.markdoc?.frontmatter.title

  let pageTitle =
    pageProps.markdoc?.frontmatter.pageTitle ||
    `${pageProps.markdoc?.frontmatter.title} - Guides`

  let description = pageProps.markdoc?.frontmatter.description

  let tableOfContents = pageProps.markdoc?.content
    ? collectHeadings(pageProps.markdoc.content)
    : []

  return (
    <>
      <Head>
        <title>{pageTitle}</title>
        {description && <meta name="description" content={description} />}
      </Head>
      <Layout title={title} tableOfContents={tableOfContents}>
        <Component {...pageProps} />
      </Layout>
    </>
  )
}

markdoc/nodes.js

import { Fence } from '@/components/Fence'
import { nodes as defaultNodes } from '@markdoc/markdoc'

const nodes = {
  document: {
    render: undefined,
    allowComments: true
  },
  th: {
    ...defaultNodes.th,
    attributes: {
      ...defaultNodes.th.attributes,
      scope: {
        type: String,
        default: 'col',
      },
    },
  },
  fence: {
    render: Fence,
    attributes: {
      language: {
        type: String,
      },
    },
  },
}

export default nodes
解决方案

方法一:动态路由+getStaticProps按需加载

在src/pages/docs/[slug].jsx创建动态路由页面,通过getStaticProps根据当前locale拼接文件名,加载对应语言的Markdoc文件:

import React from 'react';
import { getMarkdocProps } from '@markdoc/next.js';
import Markdoc from '@markdoc/markdoc';
import nodes from '@/markdoc/nodes';

export default function DocsPage({ content, frontmatter }) {
  return (
    <div>
      <h1>{frontmatter.title}</h1>
      {content}
    </div>
  );
}

export async function getStaticProps({ params, locale }) {
  const fs = require('fs');
  const path = require('path');
  
  // 拼接本地化文件名
  const localizedPath = path.join(process.cwd(), `src/pages/docs/${params.slug}.${locale}.md`);
  // 备用默认语言路径
  const defaultPath = path.join(process.cwd(), `src/pages/docs/${params.slug}.en.md`);

  // 优先加载本地化文件,不存在则用默认语言
  const targetPath = fs.existsSync(localizedPath) ? localizedPath : defaultPath;
  const content = fs.readFileSync(targetPath, 'utf8');
  
  const ast = Markdoc.parse(content);
  const transformed = Markdoc.transform(ast, { nodes });
  const reactContent = Markdoc.renderers.react(transformed, React);

  return {
    props: {
      content: reactContent,
      frontmatter: ast.attributes.frontmatter || {}
    },
  };
}

export async function getStaticPaths({ locales }) {
  // 生成所有语言的文档路径(可根据实际目录动态获取slug列表)
  const slugs = ['analytics'];
  const paths = locales.flatMap(locale => 
    slugs.map(slug => ({ params: { slug }, locale }))
  );

  return { paths, fallback: false };
}

方法二:自定义Markdoc加载器(全局配置)

修改next.config.js,扩展withMarkdoc的配置,添加自定义解析逻辑,让它自动根据locale匹配对应文件:

const withMarkdoc = require('@markdoc/next.js');
const fs = require('fs');
const path = require('path');

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  pageExtensions: ['js', 'jsx', 'md'],
  experimental: {
    scrollRestoration: true,
  },
  i18n: {
    locales: ['en', 'fr', 'es', 'de', 'it'],
    defaultLocale: 'en'
  },
};

module.exports = withMarkdoc({
  nodes: './markdoc/nodes.js',
  // 自定义文件解析逻辑
  resolve: (filePath, ctx) => {
    const { locale = 'en' } = ctx;
    const dir = path.dirname(filePath);
    const baseName = path.basename(filePath, '.md');
    const localizedFilePath = path.join(dir, `${baseName}.${locale}.md`);

    if (fs.existsSync(localizedFilePath)) {
      return fs.readFileSync(localizedFilePath, 'utf8');
    }
    //  fallback到默认文件
    return fs.readFileSync(filePath, 'utf8');
  },
})(nextConfig);

额外优化:修正_document的lang属性

在_document.jsx中把固定的lang="en"改为动态获取locale:

static async getInitialProps(ctx) {
  const initialProps = await Document.getInitialProps(ctx);
  // 传递locale到props
  return { ...initialProps, locale: ctx.locale };
}

render() {
  return (
    <Html className="antialiased [font-feature-settings:'ss01']" lang={this.props.locale}>
      {/* ...其他内容 */}
    </Html>
  )
}

内容的提问来源于stack exchange,提问作者sparkle

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 08:35:04