5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Next.js使ってSSR(AWS Lambda) + Prerender.ioやってみた

Posted at

ReactのSEO対策をやることになったので自分用にc⌒っ゚д゚)っφ メモメモ...

いろいろ調査した結果SSRとDynamicRenderingを組み合わせて使うのが良さそうだったので、Next.js(SSR)+Prerender.io(DynamicRendering)を使ってみた。

Next.js

インストール

mkdir test && cd $_
npm init -y
npm i -S next react react-dom

セットアップ

package.json
  "scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1"
+   "dev": "next"
  },

確認

pages/index.js
export default () => <div>Hellow World</div>

Typescript

インストール

npm i -D @zeit/next-typescript typescript
npm i -D @types/react @types/react-dom @types/next

セットアップ

.babelrc
{
  "presets": [
    "next/babel",
    "@zeit/next-typescript/babel"
  ]
}
next.config.js
const withTypescript = require("@zeit/next-typescript")
module.exports = withTypescript()
tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "jsx": "preserve",
    "lib": [
      "dom",
      "es2018"
    ],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext"
  },
  "exclude": [
    "node_modules"
  ],
  "include": [
    "**/*.ts",
    "**/*.tsx"
  ]
}

tsconfig.jsonの設定はお好みに。

確認

pages/index.tsx
import * as React from "react"

const App: React.FC<{}> = () => <div>Hello World</div>

export default App

ESLint + Prettier

TypescriptなのでTSLintも検討したが、typescript-eslintのほうが良さげだったのとprettier-eslint-cli使えると便利なのでLinterはESLintにした。

インストール

npm i -D eslint @typescript-eslint/{eslint-plugin,parser} eslint-config-prettier eslint-plugin-prettier prettier prettier-eslint prettier-eslint-cli

セットアップ

.eslintrc
{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json",
    "tsconfigRootDir": "."
  },
  "plugins": ["@typescript-eslint", "prettier"],
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "rules": {
    "prettier/prettier": "error"
  }
}
.prettierrc
{
  "semi": false,
  "trailingComma": "all",
}
.eslintignore
.next/
node_modules/
.prettierignore
.next/
node_modules/

Storybook

せっかくなのでStorybookも入れてみる

インストール

npm i -D @storybook/react  @babel/core babel-loader babel-preset-react-app @types/storybook__react @types/node fork-ts-checker-webpack-plugin

セットアップ

package.json
  "scripts": {
    "dev": "next",
+   "storybook": "start-storybook -p 6006 -c .storybook"
  },
.storybook/config.js
import { configure } from "@storybook/react"

const req = require.context("../components", true, /.stories.tsx$/)
function loadStories() {
  req.keys().forEach(filename => req(filename))
}

configure(loadStories, module)
.storybook/webpack.config.js
const path = require("path")
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin")

module.exports = async ({ baseConfig, env, config }) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    loader: require.resolve("babel-loader"),
    options: {
      presets: [require.resolve("babel-preset-react-app")],
    },
  })

  config.resolve.extensions.push(".ts", ".tsx")

  config.plugins.push(
    new ForkTsCheckerWebpackPlugin({
      async: false,
      checkSyntacticErrors: true,
      formatter: require("react-dev-utils/typescriptFormatter"),
    }),
  )
  return config
}

確認

components/Button.tsx
import * as React from 'react'

interface Props {
  text: string
  onClick(): void
}

const Button: React.FC<Props> = (props: Props) => (
  <button onClick={props.onClick}>{props.text}</button>
)

export default Button
components/Button.stories.tsx
import * as React from "react"
import { storiesOf } from "@storybook/react"
import Button from "./Button"

storiesOf("Button", module).add("with text", () => {
  return <Button text="Hello World" onClick={() => alert("Hello")} />
})
npm run storybook

StyledComponents

インストール

npm i -S styled-components
npm i -D @types/styled-components babel-plugin-styled-components

セットアップ

package.json
{
  "presets": [
    "next/babel",
    "@zeit/next-typescript/babel"
- ]
+ ],
+ "plugins": [["styled-components", { "ssr": true }]]
}

確認

components/Button.tsx
import * as React from 'react'
import styled from 'styled-components'

interface Props {
  text: string
  onClick(): void
}

const StyledButton = styled.button`
  & {
    padding: 6px 16px;
    color: #fff;
    background-color: #2196f3;
    border-radius: 4px;

    &:hover {
      background-color: #1976d2;
    }
  }
`

const Button: React.FC<Props> = (props: Props) => (
  <StyledButton onClick={props.onClick}>{props.text}</StyledButton>
)

export default Button

Apex/Up

今回はAWS Lambdaにデプロイしたいので、その辺を楽にしてくれるApex/Upというツールを導入してみた。
何これ超楽

インストール

curl -sf https://up.apex.sh/install | sh
up version

セットアップ

ここを参考にIAMロール、IAMユーザを作成

~/.aws/credentials
[ProfileForApexUp]
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
up.json
{
  "name": "your name",
  "profile": "your profile name",
  "regions": [
    "ap-northeast-1"
  ],
  "lambda": {
    "memory": 256,
    "runtime": "nodejs8.10"
  }
}
.upignore
!.next
package.json
  "scripts": {
    "dev": "next",
+   "build": "next build",
+   "start": "next start"
  },

デプロイ

up # stagingデプロイ
up production # prodデプロイ

prerender.io

Expressでカスタムサーバを作成し、Next.jsのSSRの機構にprerenderを組み込む

登録

ここから登録し、Tokenを取得

インストール

npm i -S express prerender-node

セットアップ

事前にApex/UpでデプロイしたAPI Gatewayの各ステージにカスタムドメインを割り当てておく。

server.js
const express = require("express")
const next = require("next")

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== "production"
const app = next({ dev })
const handle = app.getRequestHandler()

app
  .prepare()
  .then(() => {
    const server = express()

    if (process.env.PRERENDER_TOKEN) {
      server.use(
        require("prerender-node").set(
          "prerenderToken",
          process.env.PRERENDER_TOKEN,
        ),
      )
    }

    server.get("*", (req, res) => {
      return handle(req, res)
    })

    server.listen(port, err => {
      if (err) throw err
      console.log(`> haha Ready on http://localhost:${port}`)
    })
  })
  .catch(ex => {
    console.log(ex)
    process.exit(1)
  })
package.json
  "scripts": {
-   "dev": "next",
+   "dev": "node server.js",
    "build": "next build",
-   "start": "next start"
+   "start": "NODE_ENV=production node server.js"
  },
up.json
  "environment": {
    "NODE_ENV": "production",
+   "PRERENDER_TOKEN": "Your Prerender Token Here"
  },
+ "stages": {
+   "production": {
+     "domain": "Your Domain"
+   }
+ }
.prettierignore
.next/
node_modules/

デプロイ&prerender.io設定

up productionした後、prerender.ioの設定画面に作成したページのURLをキャッシュするよう設定して終了

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?