カランのブログ

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

ソフトウェアエンジニア / 台湾人 / 福岡生活
このブログはRSS Feed をサポートしています。RSSリンクをクリックして設定してください。技術に関する記事はコードがあるのでブログで閲覧することをお勧めします。

今のモード ライト

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

記事のタイトルや概要は自動翻訳であるため(中身は翻訳されてない場合が多い)、変な言葉が出たり、意味伝わらない場合がございます。空いてる時間で翻訳します。

Next.jsを使用してブログ全体を書き直す

まえがき

2015年から痞客邦(台湾のブログサービス)、Logdown、hexo、Mediumなどいろんなサービスを使ってブログ書いてきたが、2019 年はちょうど Gastby は人気になって、自分もノリで Gatsby に移行しました。それからもう三年に経ちましたね。

繰り返しになりますが、ブログを始めたい方はどのサービスでもいいので、いますぐ記事を書いてください。 「xxx を使用してブログを作成する」というブログ投稿が多すぎて、その後更新されない、または Web サイトのセットアップに多くの時間を費やし、その中の記事が「hello world」しかないという非常に残念なことが多かったです。

Gatsby をやめた理由は?

話に戻りますが、いくつか厄介なところがあると思います:

  • 静的:記事を書くたびに再構築する必要があります。 (実行するのは CI だが、やはり大変面倒です) 。
  • 数:記事の件数が140件超えました。そろそろ他の方法で保存したい
  • 楽しみ: Next.js はここ数年で大幅に進化し、多くの機能がサポートされています。 Vercel にデプロイするのも簡単で、私にとっては非常に便利です

Gatsby は機能が豊富ですが、結局のところ静的ファイルであり、すべての記事をローカルで管理する必要があります。 もちろん Gatsby はファイルシステム以外のサポートを提供しますが、セットアップが少し面倒であり、決められたフォーマットを準拠する必要があります。 カスタマイズが必要な場合は、パッケージを探すか、自分で作成する必要があります。

以上の理由から、よりカスタマイズ性の高い Next.js を選定しました。

要件

私が考えた要件は次のとおりです:

  • Markdown から HTML を生成する: これは私にとって必須です。すべての記事が Markdown 形式であるため、さらに数学の文法、脚注などを使用します。
  • Syntax highlight
  • データベース:テーブルまで自分で作れるのが望ましい、記事を便利に管理したいからです
  • RSS サポート: RSS サポートを非常に重視しています。ブログはシンプルでもかまわないが、RSS は絶対。多くの読者は、RSS を通じて新記事の通知を受け取っています。
  • 画像とのアップロード:記事の画像は CDN に配置されています。 アップロード機能をサポートする簡単なエディターがあると便利
  • 多言語サポート (i18n): 外国のコミュニティと交流したい場合は翻訳があると便利
  • UI は自分で調整できる

技術選定

  • ページやバックエンド: Next.js実装は後述
  • 多言語サポート: Next.js の i18n 機能を通じて実装
  • Markdown変換: remarkshiki によって完了します
  • データベース: Google Cloud SQL の PostgreSQLを使っています。 (データベースは想像以上に高いです😱)
  • RSS フィード: Cloud Function と Cloud Scheduler を介して定期的に生成します
  • 静的ファイル ストレージ: S3、CDN は Cloud Front
  • デプロイ: Vercel と GitHub で。新しいコミットを push すると自動的にデプロイされます

なぜ S3 だけが Amazon 使っているかというと、元々使っていたからだけです。時間があるときに移行の検討もしてみたいですね

実装

記事をデータベースに移動する (Cloud SQL)

過去の記事は Markdown ファイルに記述されていたため、最初のステップはすべてのファイルコンテンツをデータベースに移行することです。 私の設計は投稿とカテゴリの 2 つのテーブルを持つことです。 i18nに対応するため、タイトルや概要などにjson型を採用しており、検索が少し面倒になるが、他の言語を追加するときに便利です。 次のステップは、記事を対応する列に配置するための SQL を記述することです。 データベースのテーブル作成操作などはすべて「.sql」として分けて保存されているので、万一の際のロールバックや再構築になる場合、回復は便利です。

経験のあるエンジニアにとっては常識ですが、意外とフレームワークが提供する機能に頼っていたり、cli でコマンドを入力したりして、結局変更したいときに困っている人が多いことがわかりました。

Next.js

まず、Next.js を選定した理由について説明しましょう。 主なフロントエンドフレームワークには対応する SSR フレームワークがありますが、私の経験では、最も機能が多く、色んな連携を備えているのは Next.js だと思います。ほとんどの場合設定なしで開発を開始できます。

SSR (Server Side Rendering) と ISR (Incremental Static Regeneration)

Next.js は 三つのレンダリングモードがあります:

  • Static: ビルド時に静的ページをビルドし、getStaticProps を介して props を入れて HTML をレンダリングします
  • ISR: ビルド時に定義したパスだけ静的ページを作成します 。指定されたパスが指定されていない場合は SSR を使用してページをレンダリングします
  • SSR: リクエストが行われるたびにページをレンダリングし、getServerSideProps が実行され、レンダリングされた HTML が返されて、フロント エンドで JavaScript ロジックを実行します (イベントリスナーの追加など)

当たり前のことですが、確かに静的無敵で爆速です。Vercel の無料プランでは SSR だと cold start なので、さらに私の場合データベースの接続もあるので、SSRである限りとても時間がかかります。

i18n

Next.js には i18n 機能があり (ルーティング レベルで)、リダイレクトするように設定できます。異なる言語の異なるドメイン、または異なるパスにリダイレクトされます。例:

  • /zh/posts: 言語を zh に設定
  • /en/posts: 言語を en に設定 また、(ヘッダーまたは navigator.languages を介した) 自動言語検出もサポートしています。値を簡単に取得できます
// Server 
const getServerSideProps = ({ locale }) => {
  // current locale
} 

// Client
import { useRouter } from 'next/router'
const Component = () => {
  const { locale } = useRouter()
}

通常は i18n を実装する際、react-intlreact-i18next のようなライブラリを使うと思うが、ドキュメントを見た感じ、提供した機能が非常に多くて、自分の場合はそもそも i18n サービス使わないし、障害なども考えてないし、キーも最大 20 個だけなので、わざわざ導入する理由がないと思いました。代わりにこういうふうに実装しました:

import React, { createContext, useCallback, useContext } from "react";
import { en } from "./en";
import { ja } from "./ja";
import { zh } from "./zh";
type I18nData = {
  _i18n: {
    locales: string[];
    data: {
      [key: string]: {
        [key: string]: string;
      };
    };
  };
};
export default function createI18n() {
  const datas = {
    en,
    ja,
    zh
  };
  return {
    _i18n: {
      locales: ["en", "zh", "ja"],
      data: datas
    }
  };
}

const I18nContext = createContext<{
  locale: string;
  _i18n: I18nData["_i18n"];
}>(null);

export const I18nProvider: React.FC<{
  _i18n: I18nData["_i18n"];
  locale: string;
  children: React.ReactElement;
}> = ({ _i18n, locale, children }) => {
  return (
    <I18nContext.Provider value={{ _i18n, locale }}>
      {children}
    </I18nContext.Provider>
  );
};

export const useTrans = () => {
  const { _i18n, locale } = useContext(I18nContext);
  const t = useCallback(
    (key: string) => {
      const data = _i18n.data[locale] || _i18n.data['zh'];
      if (data) {
        return data[key] || key;
      }
      return key;
    },
    [_i18n.data, locale]
  );
  return { t };
};

シンプルだけどこれで十分。変数として保存してちょっとバンドルサイズが大きくなるが、ISR 機能をいかせば少なくともレンダリングには支障が出ないはずです。

本当に改善したい場合は、例えば:

  • CDNに移行する
  • locale 変更する時だけローディングする

などの解決案があるので、とりあえず物事を動かすことは大事です。

画像処理

画像の読み込みと最適化するために、Next.js では next/image を提供しています。 処理を行わないと、すべてのデバイス (モバイル、デスクトップ) で同じサイズの画像がレンダリングされる可能性があります。ただし、これは UX には良くありません。大きなサイズのデバイスではより高い解像度の画像が必要になる場合がありますが、小さなサイズのデバイスでは解像度が高くても結局わからないので、ネットワークを無駄にします。

もう1つは、画像のサイズが定義されていない場合、読み込まれていないときは高さを取りませんが、読み込み後、突然入れたり、レイアウトシフトが発生することがあります。画像がたくさんある場合やネットワーク遅延が発生すると非常に煩わしくなります。

したがって、Next.js で画像を処理するには、next/image を使用することを強くお勧めします (幅と高さを明確に定義するか、比率を自分で定義する必要があります)。さらに、next/image を使用する場合、デフォルトでは、画像は返される前にサーバーによって処理されます。

CDN にこの URL があるとします: https://cdn.kalan.dev/images/avatar.jpeg、実際のリクエストは次のようになります: /_next/image?url=${URL}&w=128&q=75 next/image を介したリクエストは、元の URL に直接リクエストするのではなく、最初にサーバーを通過し、サーバーによって処理された後に返されます。

Next.js の画像処理

メリットとしては、端末によって最適の画像フォーマット、サイズで返しますが、これはサーバーへの負荷も高くなることも意味します。私は Vercel を使っていて、画像の最適化の制限を事前に設定しているので、あまり心配する必要はありませんが、手前のサーバー作る時には注意が必要です。

じゃ他の人もこのAPIを悪用してもいいかというと、Next.js では画像のドメインを設定するように求められるので、画像のドメインが一致しない場合、リクエストは拒否されるのでとにかく安心です。

画像の最適化処理を自分のサーバーで実行したくない場合は、追加で loader を設定することができます。

remark と shiki

現在人気のある remark を使用して、markdown を HTML に変換します。具体的にはまず Markdown を unified で AST に変換してから、さまざまなパッケージを利用して HTML に変換するという流れです。また、Syntax Highlight には shiki を採用しています。

実際の開発では、shiki が実行時にテーマやコード構文の設定ファイルを読み込んでしまうという問題があります。Vercel にデプロイすると Serverless Function になるため、fs.readFile では node_modules 内のファイルでは読み込まれないらしいので、定義ファイルを別の場所に移動してみました。

const languagesPath = path.join(
  process.cwd(),
  'src',
  'utils',
  'shiki',
  'languages'
)

const langs = shiki.BUNDLED_LANGUAGES.map((lang) => ({
  ...lang,
  path: path.join(languagesPath, lang.path || "")
}));

export default async function convertMdToHTML() {
  const nordTheme = shiki.toShikiTheme(theme as any);
  const highlighter = await shiki.getHighlighter({ theme: nordTheme, langs });
  ...
}

Vercel Edge 関数

Next.js 13 は Edge API Routes という実験的な機能を導入しています。Vercel の Edge Functionsを使えば、追加の設定は必要ありません。

Vercel 上の Edge Functions は、一般的な Serverless Function とは異なり、独立したV8エンジン環境で動作します。実行環境が同じであるため、起動速度がはるかに速く、グローバルノードにデプロイできます。ユーザーはより近いところにリクエストができ、API の応答時間を短縮することができます。

By taking advantage of this small runtime, Edge Functions can have faster cold boots and higher scalability than Serverless Functions.

この機能がすごい!と思って公式のサンプルに従ってタイトルを応じて動的に OG Image を生成する API をデプロイしてみました。記事に OG 画像がない場合は、この API にフォールバックします。

ただし、VM環境で実行するため、DB操作を直接実行する方法がなく、HTTPリクエストが実行できるかどうかも不明なため、使用にはいくつかの制限があることを気をつけましょう。

Serverless Function

デフォルトでは、Next.js を Vercel にデプロイするときに、ページが動的で SSR または ISR を使用している場合、Vercel はサーバーレス関数を自動的に生成します。

これは、Next.js アプリケーションが Vercel にデプロイされた後、一つのサーバーでは実行されず、複数のServerless Functionsに分けていることを認識する必要があります。そして無料プランなので cold start の遅延も発生します。

具体的にはこういう感じです:

Vercel にデプロイされた Next.js の概略図

したがって、実装ではいくつか注意を払う必要があります。

  • 不整合が許容されない限り、グローバル変数としてのキャッシュを避けてください
  • レスポンスとリクエストに関する Vercel の 制限 を理解する必要があります
  • 私のアプリケーションはデータベースの接続があるので、サーバーレス関数が同時に実行されて接続数が急増するのを避けるために、アイドルタイムアウトを下げて、データベース接続に時間がかかりすぎないようにして、さらにエラーが発生しないように、接続数の上限も増やしてみました

Cloud Functions と Cloud Scheduler

Cloud Functions と Cloud Run の関係を明確にするのに時間がかかりました(笑。

Cloud Functions は Google がサポートしているプログラミング言語でしか書けないが、Cloud Run は Docker 上で動作するため、イメージさえあれば使用できます。最近 Cloud Functions には第 2 世代があり、その背後では Cloud Run を使用しているようです。

RSS フィードを実行するような、スクリプトを簡単で作れて、ただし時間がかかりる場合、Cloud Functions を Scheduler で実装するのは非常に相応しいと感じました。ただ注意すべきこととしては、多くのメモリを使用する場合は、 Cloud Function のメモリサイズを調整する必要があります。

Pub/Sub や直接の HTTP 呼び出しなど、さまざまなイベントを通じて Cloud Functions を発火させることができます。ここでは Pub/Sub 実装を採用しており、generate-rss イベントを受信したときに実行されます。

RSSフィード生成の模式図

Cloud Schedulerはジョブが1つしかないので無料で、Cloud Functionsなので外部からアクセスされないため、トラフィックも無料割り当てに含まれます。これは、個人開発にとって非常に便利かと思いますね。

AWS S3とCDNの設定

AWS S3はブログ開設時から利用しているサービスで、CloudFrontと併用しています。当初はデフォルトで提供されているドメイン名を使用していたのですが、調べてみると、カスタムドメイン名を連携させることは実際に可能であり、SSL証明書はAWS Certficateを通じて直接取得できることがわかりました。

さらに、CDN を使用して外界の静的リソースを取得する場合は、S3 アクセス権をオフにして、すべてのトラフィックが CloudFront からのみ来るようにすることをお勧めします。この機会に cdn.kalan.dev というかっこい URL を作りました。心地いい

ノート

この時点で、ようやく使用可能なブログシステムが完成したが、今後修正および最適化する必要があることがいくつかあると考えます:

  • テストと E2E テスト、現在記事が多すぎます。いちいち確認するのは面倒くさい
  • DB Queryを減らして、記事の内容をキャッシュしてCDNに載せる
  • JavaScript バンドル サイズを減らす
  • ファイルをデータベースに直接アップロード
  • 記事のエクスポートとインポート
  • TBD

次の記事

C言語における文字列処理

前の記事

配置に役立つ擬似クラス

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee