5

I've been trying to setup react-helmet with server-side-rendering. I followed the docs and some blog posts on how to setup react-helmet with SSR, but have been unable to produce the desired results. Here's a code snippet of how I'm rendering the App:

import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './src/App';
const express = require('express');

const app = express();
app.get('*', (req, res) => {
  const app = renderToString(<App />);
  const helmet = Helmet.renderStatic();

  res.send(formatHTML(app, helmet));
})

function formatHTML(appStr, helmet) {
  return `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
      </head>
      <body>
        <div id="root">
          ${ appStr }
        </div>
        <script src="./bundle.js"></script>
      </body>
    </html>
  `
}

When I run the above code, I get an error saying 'cannot use import statement outside a module'. Is it possible to use both es5 and es6 syntax at the same time? Or is there is better way to setup React-helmet?

This is my babel configuration file

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ],
    "@babel/preset-react",
    "@babel/preset-flow"
  ],
  "env": {
    "development": {
      "only": [
        "app",
        "internals/scripts"
      ],
      "plugins": [
        "@babel/plugin-transform-react-jsx-source"
      ]
    },
    "production": {
      "only": [
        "app"
      ],
      "plugins": [
        "transform-react-remove-prop-types",
        "@babel/plugin-transform-react-constant-elements",
        "@babel/plugin-transform-react-inline-elements"
      ]
    },
    "test": {
      "plugins": [
        "@babel/plugin-transform-modules-commonjs",
        "dynamic-import-node"
      ]
    }
  },
  "compact": true,
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-syntax-import-meta",
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-proposal-json-strings",
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    "@babel/plugin-proposal-function-sent",
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-proposal-numeric-separator",
    "@babel/plugin-proposal-throw-expressions",
    "@babel/plugin-proposal-export-default-from",
    "@babel/plugin-proposal-logical-assignment-operators",
    "@babel/plugin-proposal-optional-chaining",
    [
      "@babel/plugin-proposal-pipeline-operator",
      {
        "proposal": "minimal"
      }
    ],
    "@babel/plugin-proposal-nullish-coalescing-operator",
    "@babel/plugin-proposal-do-expressions",
    "@babel/plugin-proposal-function-bind",
    "lodash"
  ]
}
Ghouse Mohamed
  • 387
  • 4
  • 10
  • Have a look at [this example](https://gist.github.com/garmjs/2a3cebc0fe3c9f0fe0007a9e5c476b87). – JMadelaine Mar 24 '20 at 06:18
  • I tried this. I still get an error at the line where I'm importing the react component. [SyntaxError: Cannot use import statement outside a module] – Ghouse Mohamed Mar 24 '20 at 16:18
  • How are you bundling the app? Are you using Webpack? What does your config look like? – JMadelaine Mar 24 '20 at 19:54
  • I'm using babel webpack for bundling. I've included the .babelrc config file – Ghouse Mohamed Mar 25 '20 at 13:29
  • @GhouseMohamed I just updated my answer with my latest research. I am now replacing `
    ` with the rendered HTML which speeds up [first contentful paint](https://developer.mozilla.org/en-US/docs/Glossary/First_contentful_paint) leading to better SEO performance.
    – sunknudsen Mar 29 '20 at 18:07

1 Answers1

1

You need to wrap your server using @babel/register.

This is how I handle that for my TypeScript CRA projects without ejecting.

NOTICE: I use this method to inject metadata into index.html vs render the whole app (some components I use don’t play well with SSR).

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

index.js

"use strict"

require("ignore-styles")

require("@babel/register")({
  ignore: [/(node_modules)/],
  presets: [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript",
  ],
  extensions: [".tsx"],
  cache: false,
})

require("./server")

server.js (excerpt)

const indexPath = path.join(__dirname, "build/index.html")

const middleware = async (req, res, next) => {
  let context = {}
  let html = renderToString(
    React.createElement(StaticRouter, {
      location: req.url,
      context: context,
    })
  )
  const helmet = Helmet.renderStatic()
  if (context.url) {
    res.redirect(context.url)
  } else if (!fs.existsSync(indexPath)) {
    next("Site is updating... please reload page in a few minutes.")
  } else {
    let index = fs.readFileSync(indexPath, "utf8")
    let status = 200
    if (typeof context.status === "number") {
      status = context.status
    }
    return res.status(status).send(
      index
        .replace('<div id="root"></div>', `<div id="root">${html}</div>`)
        .replace("</head>", `${helmet.meta.toString()}</head>`)
        .replace("</head>", `${helmet.title.toString()}</head>`)
        .replace("</head>", `${helmet.script.toString()}</head>`)
    )
  }
}

server.get("/", middleware)

server.use(express.static(path.join(__dirname, "build")))

server.get("*", middleware)
sunknudsen
  • 6,356
  • 3
  • 39
  • 76