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

如何将Create React App TypeScript项目改造为SSR实现动态Meta标签?

基于TypeScript Create React App改造SSR实现动态Meta标签方案

现有代码存在的核心问题

  • readIndexHtml方法使用异步回调的fs.readFile,同步返回时永远拿到undefined,Meta替换逻辑完全失效
  • 缺少服务端TypeScript编译运行依赖,package.json中未安装express等服务端核心依赖
  • 未适配服务端路由与react-helmet的服务端提取逻辑,无法做到页面对应Meta自动生成
  • public/index.html中未预留Meta占位符与SSR内容注入占位符

需新增的依赖包

生产依赖

npm install express react-helmet-async

开发依赖

npm install -D @types/express ts-node cross-env concurrently

配置调整

1. 新增服务端TS配置文件server/tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["dom", "ES2020"],
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "../dist-server"
  },
  "include": ["./**/*"]
}

2. 修改package.json脚本配置

替换原有scripts字段:

"scripts": {
  "start:client": "react-scripts start",
  "start:server": "ts-node --project server/tsconfig.json server/server.ts",
  "dev": "concurrently \"npm run start:client\" \"npm run start:server\"",
  "build:client": "react-scripts build",
  "build:server": "tsc --project server/tsconfig.json",
  "build": "npm run build:client && npm run build:server",
  "start": "cross-env NODE_ENV=production node dist-server/server.js",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
}

3. 修改public/index.html添加占位符

在head标签内添加:

<meta name="description" content="__META_DESCRIPTION__">
<meta property="og:title" content="__META_OG_TITLE__">
<meta property="og:description" content="__META_OG_DESCRIPTION__">

在root div内添加占位符:

<div id="root">__SSR_CONTENT__</div>

代码修改

1. 修改server/server.ts

import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import { HelmetProvider, Helmet } from "react-helmet-async";
import App from '../src/App'
import * as path from 'path';
import * as fs from 'fs'
import { getArticle } from "../src/api";
const app = express();

const PORT = process.env.PORT || 3006;
const isProd = process.env.NODE_ENV === 'production';
const publicPath = isProd ? path.resolve(__dirname, '../build') : path.resolve(__dirname, '../public');
const indexPath = path.resolve(publicPath, 'index.html')

app.use(express.static(publicPath, { index: false }));

// 同步读取html模板,避免异步回调问题
const indexHtml = fs.readFileSync(indexPath, 'utf8');

app.get('*', async (req, res) => {
  try {
    const helmetContext = {};
    // 渲染组件为字符串
    const appHtml = renderToString(
      <HelmetProvider context={helmetContext}>
        <StaticRouter location={req.url}>
          <App />
        </StaticRouter>
      </HelmetProvider>
    );
    const { helmet } = helmetContext as { helmet: Helmet };

    // 替换占位符
    let html = indexHtml
      .replace('__SSR_CONTENT__', appHtml)
      // 如果要更灵活,也可以直接把helmet的所有head内容注入
      .replace('</head>', `${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}</head>`);

    // 针对文章页单独处理动态Meta也可以保留原有逻辑
    if (req.path.startsWith('/article/')) {
      const id = req.params.id;
      const articleRes = await getArticle(parseInt(id));
      html = html
        .replace('__META_OG_TITLE__', articleRes.data.heading)
        .replace('__META_OG_DESCRIPTION__', articleRes.data.shortDescription)
        .replace('__META_DESCRIPTION__', articleRes.data.shortDescription);
    }

    res.send(html);
  } catch (error) {
    console.log('渲染出错', error);
    res.status(500).send('服务器错误');
  }
})

app.listen(PORT, () => {
  console.log(`server is listening on ${PORT}`);
});

2. 修改src/index.tsx添加HelmetProvider

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';

ReactDOM.hydrate(
  <React.StrictMode>
    <HelmetProvider>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </HelmetProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

3. 页面组件中使用Helmet定义Meta

以文章页src/pages/article/index.tsx为例:

import { Helmet } from 'react-helmet-async';

const Article = () => {
  return (
    <>
      <Helmet>
        <title>文章标题</title>
        <meta name="description" content="文章描述" />
        <meta property="og:title" content="文章OG标题" />
        <meta property="og:description" content="文章OG描述" />
      </Helmet>
      {/* 页面其他内容 */}
    </>
  )
}

运行方式

  • 开发环境执行npm run dev,同时启动前端开发服务和后端SSR服务
  • 生产环境执行npm run build编译前后端代码,再执行npm start启动生产SSR服务

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 17:54:03