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
セットアップ
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "dev": "next"
},
確認
export default () => <div>Hellow World</div>
Typescript
インストール
npm i -D @zeit/next-typescript typescript
npm i -D @types/react @types/react-dom @types/next
セットアップ
{
"presets": [
"next/babel",
"@zeit/next-typescript/babel"
]
}
const withTypescript = require("@zeit/next-typescript")
module.exports = withTypescript()
{
"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
の設定はお好みに。
確認
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
セットアップ
{
"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"
}
}
{
"semi": false,
"trailingComma": "all",
}
.next/
node_modules/
.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
セットアップ
"scripts": {
"dev": "next",
+ "storybook": "start-storybook -p 6006 -c .storybook"
},
import { configure } from "@storybook/react"
const req = require.context("../components", true, /.stories.tsx$/)
function loadStories() {
req.keys().forEach(filename => req(filename))
}
configure(loadStories, module)
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
}
確認
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
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
セットアップ
{
"presets": [
"next/babel",
"@zeit/next-typescript/babel"
- ]
+ ],
+ "plugins": [["styled-components", { "ssr": true }]]
}
確認
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ユーザを作成
[ProfileForApexUp]
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
{
"name": "your name",
"profile": "your profile name",
"regions": [
"ap-northeast-1"
],
"lambda": {
"memory": 256,
"runtime": "nodejs8.10"
}
}
!.next
"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の各ステージにカスタムドメインを割り当てておく。
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)
})
"scripts": {
- "dev": "next",
+ "dev": "node server.js",
"build": "next build",
- "start": "next start"
+ "start": "NODE_ENV=production node server.js"
},
"environment": {
"NODE_ENV": "production",
+ "PRERENDER_TOKEN": "Your Prerender Token Here"
},
+ "stages": {
+ "production": {
+ "domain": "Your Domain"
+ }
+ }
.next/
node_modules/
デプロイ&prerender.io設定
up production
した後、prerender.ioの設定画面に作成したページのURLをキャッシュするよう設定して終了