ブログ
目的
技術的負債
1.Nuxt(Vue)の問題
旧サイトは Nuxt(Vue)アプリを採用して 2020 年中旬に作成した。 Node、javascript Framework、仮想 DOM 等の概念をあまり理解していないがモダンなフロントエンド開発を学習しなければいけないと思っている状態だった。
仮想 DOM を採用してフロントエンドでリッチな組み換えを行うのが主流になりつつあること、そのフレームワークは React と Vue がおすすめだという情報を入手。WEB サイトのフロントエンド構築は HTML + CSS + javascript しか採用したことがない状態では Vue の Template 構文は非常に魅力的に見えたので採用した。
ある程度仮想 DOM の概念に慣れた後に React 及び Next も学習した所、最初は理解が難しかったが JSX の自由度、Typescript との親和性が圧倒的に React のほうが高く、新規開発はすべて Vue は使わずに React を採用していた。特に Typescript は敬遠していたが、型を先につけることで自然と test diven に近い開発ができたことは、とても驚くとともに良い開発体験となった。
Vue は Vue3 + Composition API により Typescript フレンドリーになりつつ合ったが、新旧の仕組みが入り混じった状態ではライブラリの相性問題等に時間を取られることが多かった。インターネット上の情報も新旧入り混じっていたので、ストレスが非常に高かった。Vue にはもう触りたくなかったが、せっかく構築したサイトを継続開発せずに放置するのももったいないと感じたため、React(Next)への移行を決めた。
2.設計の問題
学習のため Wordpress や Hatenablog を使用せずブログシステムを自分で作成したが、URL 設計やコンポーネント設計をきちんとせずにゴリ押しで開発をすすめた。ブログもサイトのサブディレクトリに設置したため、構成が複雑になり修正がしにくい状態となっていた。
将来的に CMS やブログサービスに移行する可能性も考えて、サブディレクトリ式ではなくサブドメインで切り分け、移行しやすい状態に変更した。サブドメイン式にすれば機能ごとに移行がしやすいというのも大きな利点である(複数のマイクロサービスを組み合わせるイメージ)。
構成
旧構成
DNS 等
ドメイン:ムームードメインで取得
DNS:Cloudflare を参照
Cloudflare:SSL 化 と Heroku への DNS プロキシ
メインサイト
Hosting : Heroku
Framework : Nuxt(SSR)
https://k4a.me
L document
| L index
| L topic
| | L ?id=XX
| L poem
| L ?id=XX
L other.```
### 新構成
#### DNS 等
ドメイン:ムームードメインで取得
DNS:Cloudflare を参照
Cloudflare:SSL 化 と Heroku、Netlify への DNS プロキシ
#### メインサイト
Hosting : Heroku
Framework : Nuxt(SSR)
```text
https://k4a.me
L other.```
#### ブログサイト
Hosting : Netlify
Framework : Next(Static Export)
```text
https://log.k4a.me
L [slug]
作業
Hosting 先の選定
前提
サイトにはアドセンスやアフィリエイトを一応配置はしてあるが、本格的に広告収入を見込む予定は無い。そのため維持費は可能な限り抑えたい。
旧サイトはすべて Heroku の 1dyno で起動している。
Heroku は非常に使いやすいが、無料ユーザでは 1000h の dyno 起動制限があり、常に立ち上げておくのは 1dyno が限界である。
候補
-
Heroku 有料プラン($7/月)
-
メリット
-
1000h 制限がなくなる
-
SSL のために Cloudflare をプロキシする必要がなくなる
-
デメリット
-
課金はユーザごとではなくプロジェクト毎のため、2つプロジェクトを動かすには 2× $7 = $14/月 がかかる
-
firebase hosting(無料プラン)
-
メリット
-
Heroku ほどではないが手軽
-
コストは掛からない
-
デメリット
-
Netlify(無料プラン)
-
メリット
-
手軽
-
無料のホスティングサービスの中では制限が少ない
-
デメリット
-
静的 Hosting のため、SSR やサーバサイドの処理はできない
-
その他 VPS
結論
ブログサイトではサーバサイドの処理をする予定はないため静的ファイルの Hosting で十分なこと、触ってみた所 Heroku よりも簡単なくらいにすぐにデプロイできたことから Netlify を選択した。
DNS やデプロイの設定
DNS は Cloudflare の DNS の CNAME にサブドメインを追加するだけですぐに Netlify に接続された。
デプロイは Heroku と同様に Github のリポジトリをセットし push されたら Continuous Deployment が行われる構成にした。
build settings は各自設定が異なると思うが、nuxt と next のビルドコマンドは違うので注意する。
また、package.json には export(静的出力)のスクリプトがデフォルトで記載されていないため追加する。
{
. "scripts": {
"dev": "next dev",
"postbuild": "node ./src/scripts/sitemap.xml.mjs",
"build": "next build && next export && npm run postbuild",
"start": "next start",
},
.}
Build command: npm run build
Publish directory: out
URL
URL 設計
旧サイトではk4a.me/document/topic?id=XXX
という URL を採用していたが、新サイトではシンプルにlog.k4a.me/XXX
で記事にアクセスできるようにする。
Next.js のDynsmic Routesを使用し、pages 配下に存在しないルートは記事 ID としてアクセスさせる。
L pages
- [id].tsx -> 記事
- _app.tsx
- index.tsx
...other
リダイレクト
旧サイトの document 配下にアクセスした場合は、新サイトにリダイレクトされるように nuxt プロジェクトを更新する。
$route.push()
を使用したリダイレクトはサイト内に変換されてしまうため、window.location.replace
を使用する。
mounted() {
const id = this.$route.query.id
window.location.replace(`https://log.k4a.me/${id}`)
// this.$router.push({
// path: `https://log.k4a.me/${id}`, → https://k4a.me/documents/https:/log.k4a.me/XXXXとなってしまった
// })
}
記事の管理
マークダウンファイル
記事はマークダウンで記述する。
public/assets/articles
配下に作成したディレクトリを記事 ID として、その中に配置したindex.md
を記事内容として扱う。
public
L /assets
L /articles
L /XXX
L index.md
L /image
記事の取得
Next.js で Markdown ブログを作るを参考に記事一覧、内容を取得する module を作成する。
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { Post } from 'types'
const postsDirectory = path.join(process.cwd(), 'public/assets/articles')
/**
* postsDirectory 以下のディレクトリ名を取得する
*/
export function getPostids() {
const allDirents = fs.readdirSync(postsDirectory, { withFileTypes: true })
return allDirents.filter((dirent) => dirent.isDirectory()).map(({ name }) => name)
}
/**
* 指定したフィールド名から、記事のフィールドの値を取得する
*/
export function getPostById(id: string, fields: string[] = []) {
const fullPath = path.join(postsDirectory, id, 'index.md')
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
const items: Post = {
id: '',
content: '',
title: '',
created: '',
updated: '',
thumbnailPath: '',
tags: [],
}
fields.forEach((field) => {
if (field === 'id') {
items[field] = id ? id : items[field]
}
if (field === 'content') {
items[field] = content ? content : items[field]
}
if (field === 'title' || field === 'createdate' || field === 'tags' || field === 'sumbnail') {
items[field] = data[field] ? data[field] : items[field]
}
})
return items
}
/**
* すべての記事について、指定したフィールドの値を取得して返す
* @param fields 取得するフィールド
*/
export function getAllPosts(fields: Array<keyof Post>) {
const ids = getPostids()
const posts = ids
.map((id) => getPostById(id, fields))
.sort((a, b) => (a.createdate > b.createdate ? -1 : 1))
return posts
}
pages/index.tsx
からはgetAllPosts
で記事一覧を、pages/[id].tsx
からはgetPostById
で記事内容を取得する。
マークダウンの変換
既存のマークダウンパーサはたくさん存在するが、ここでは nuxt 時代に自作したパーサを使用する。
このパーサは markdown→html に変換するのではなく、マークダウンを解析し階層化したオブジェクトに格納する。
React 等の仮想 DOM では HTML に変換するよりオブジェクトにしたほうがコンポーネントが組み立てやすい。また、nuxt 時代のブログではアウトライナーのように階層を折り畳めるようなしくみにしたかったのでこのパーサを作成した。
# タイトル1
## タイトル2
こんにちは
{
"tag": "h1",
"content": "タイトル1",
"child": {
"tag": "h2",
"content": "タイトル2",
"child": {
"tag": "none",
"content": "こんにちは"
}
}
}
独自パーサを使用すると、マークダウンファイル内に<ad/>
等と記述すると Google Adsence ブロックが挿入されるなどのカスタマイズがしやすいため、新ブログでも引き続き使用しようと考えている。ただし、ソースコードが非常に汚いのでリファクタリングをする必要がある。
サイトマップの追加
next-sitemap
最初にnext-sitemapを試してみた。
公式を参考プロジェクトルートにnext-sitemap.config.js
を追加してスクリプトに追加し実行する。
module.exports = {
siteUrl: process.env.SITE_URL,
generateRobotsTxt: true,
sourceDir: out,
};
{
"postbuild": "next-sitemap --config ./next-sitemap.config.js"
"build": "next build && next export && npm run postbuild",
}
開発環境でも Netlify でも正常に動作したが、Google Search Console 上で問題が発生した。
next-sitemap は sitemap.xml をインデックスサイトマップにして、sitemap-1.xml、sitemap-2.xml...に実際の sitemap を記載する形で出力される。
sitemap.xml を GSC に登録すると、検出 URL が 0 になり、sitemap-1.xml を登録するとエラーとなった。他ツール等で確認し XML 記法に間違いがないことは確認した。
この情報が正しいかは定かではないが、Search Console で検出された URL が 0 になるバグを回避する方法。によると「ハイフン」が含まれている、sitemap の url が長い、などの条件で正しい sitemap でも GSC 上ではエラーとなる場合があることがわかった。
next-sitemap でこの命名規則を変更できないかを調べたが、ハイフン前は変更できるがハイフン自体は変更する方法が見つからなかった。
他の有用なパッケージが見つからなかったため、パッケージは使わないことにした。
getServerSideProps
まず、Next.js で動的に XML サイトマップを生成するを参考に、page/sitemap.xml.tsx
を作成し、xxx/sitemap にアクセスした時に、動的にgetServerSideProps
からxml
を返却する方式を採用した。
next dev
やnext start
でサーバ動作をさせているときは正常に動いたが、next export
で静的出力するとエラーが出力された。
静的出力ではgetServerSideProps
は使用できないためgetStaticProps
に変更したが、getStaticProps
ではGetServerSidePropsContext
を使用した responce は返却できない(サーバが存在しないため)。
そのため、この方法も断念した。
postbuild
次に、Create a Dynamic Sitemap with Next.jsを参考に、ビルド時にカスタムスクリプトを起動し、out
にsitemap.xml
を吐き出す方式を採用した。
export function getAllPosts(fields) {
. return posts
}
async function generateSitemapXml() {
let xml = `<?xml version="1.0" encoding="UTF-8"?>`
xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`
const appHost = 'https://log.k4a.me/'
// ここでurlを足していく
const allPosts = await getAllPosts(['id', 'createdate', 'updatedate'])
allPosts.forEach((post) => {
xml += `
<url>
<loc>${appHost}${post.id}</loc>
<lastmod>${post.updatedate ? post.updatedate : post.createdate}</lastmod>
<changefreq>weekly</changefreq>
</url>
`
})
xml += `</urlset>`
return xml
}
async function generate() {
const xml = await generateSitemapXml()
// eslint-disable-next-line no-sync
writeFileSync('out/sitemap.xml', xml)
}
generate()
"scripts": {
"postbuild": "node ./src/scripts/sitemap.xml.mjs",
"build": "next build && next export && npm run postbuild",
},
ローカルの動作、Netlify 上での動作ともに正常で、GSC でも sitemap が認識されたためこの方式で決定する。
Google Adsence の追加
Next.js に Google Adsense を設置する
サブドメインサイトはどうするべき?Google Analytics のプロパティ設定方法
その他
Netlify 上のプラグインが失敗
ローカルでは問題ないのに、Netlify 上でPlugin "@netlify/plugin-nextjs" failed
となる。
Netlify サイトのPlugins
タブにあるplugin-nextjs
を削除した所正常に動作した。