かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

かもメモ 10周年と活動の見直しについて

自分は忘れっぽいのでやったことを後から自分でまた検索するだろうから検索しやすいブログとして書き残しておこうと始めたこのブログは 2024年で10年目になりました。
早いもので私自身は何も技術的に進歩できてない気もします… その一方役に立ったとかSNSで見かけると嬉しくもなりました。

そんなこんなで 10周年なのですが、本ブログについて活動を見直そうと思います。
端的に言えば、深く調べず備忘録的に書き残す程度になる想定です。

なぜ方針を変えるのか

  1. 無料公開する労力と合わなくなってきたから
  2. 問題解決は生成AIの時代になってきたから

1. 無料公開する労力と合わなくなってきたから

収益目的のブログではないのですが、それなりに調べたり、パターンを洗い出していると記事を作るにも時間が必要です。
時間がかかっている = 工数が発生している状態です。

今までは貼っている広告である程度の収益が出ていたのでモチベーションにもなっていました。
コロナ時期からPV数は下がってないのに広告収益が下がり始めました。その後広告収益は回復基調にあったのですが2022年を転機に PV / 収益とも下降の一途を辿っておりもはやこのブログでの広告収益は見込めないという認識になっています。

無料公開したまま収益を目的にするなら、まとめサイトみたいに広告を貼りまくり誤クリックを誘発するような広告枠も設けていく方向性しか無いでしょう (広告と仕組み的には大差ないマイニングプログラムが日本では否定されてしまったので)、しかしその様な広告を貼りまくったり誤クリック誘発するような広告は自分が良いと思うユーザー体験とはかけ離れている。むしろ反対なのでそうはしたくありません。(今入っている記事内に自動で差し込まれる広告も個人的には辞めたい)

閲覧数も減り収益にもならないので、このブログで書いているような内容は需要がなくなってきているという事だと思います。であれば自分だけ分かれば良い備忘録・ログとして書き残す程度以上に工数をかける必然性がないという判断となりました。

2. 問題解決は生成AIの時代になってきたから

コロナ時期に広告単価が下がるのは理解できます。では、2022年の転機は何だったのしょう? ChatGTP や Copilot といった AI がより身近になった年だったのではないかと思います。

プログラミング領域の疑問は生成AIでが得意とする領域です。このブログで扱っているようなプログラミングでの質問はもはや ChatGTP に聞いたり Copilot を使ったほうがより早く簡単に解決できる時代になっているのではないでしょうか?そしてデキるエンジニアほどそれらのツールを使いこなしているものです。
このブログに辿り着くような人は、今ハマっている問題の解決方法が欲しかったのであり、長ったらしい説明が欲しい訳ではでは無いでしょう。
多くは公式ドキュメントや GitHub / Stackoverflow を探せば分かることです。そのような AI が得意とする分野とモロに被ってしまう内容を懇切丁寧にまとめてもあまり意味がありません。なので自分用のメモなら調べた際に出てきた参考リンクを貼っておく程度で事足りるのでその様な方針にしたいと思います

 

以上のような理由から、かもメモ は 10周年ですがただの雑な自分用備忘録に転換します。
今までの記事はそのまま残しておこうと思いますが、今後しっかり調べたまとめは Zenn や Note に移行させる可能性があります。

生成 AI の台頭は無料公開されているサイトはまとめられるソースとなっていくでしょうから、どんどんページが閲覧することに依存する広告収入は下がっていくことになるのではないでしょうか。
車が発明され一般化した時、馬車の時代は終わり道端の馬糞を片付ける仕事は無くなりました。それでも車を無かったことにはできないように技術の進歩はラチェットです。
生成AIの台頭と一般化は無かったことには出来ない。しかしそうなっていくとコンテンツを作るにもお金が必要である以上、コンテンツはどんどん有料化したりログインしないと見れない世界になっていくのかもしれません。
本がかつてはお金持ちの知識階層だけのものだったのが、大学・図書館の発明で無料公開され、 web の発明で更に誰しもが知にアクセスできるようになりました。
コンテンツがどんどん有料化していくのであれば情報や知が時代に逆行しているようにも思えますが、資本主義的な最適化である捉えれば致し方ないようにも思えます。そうなればお金を払えない者はアクセスできる知や情報が制限されるかもしれません。

情報や知の時代の変わり目のようなものに立ち会えている肌感覚があり、歴史好きとしては未来がどうなるかとても興味深いです。実際問題として、どのような未来になるかは分かりませんが知へのアクセスはオープンで平等であって欲しいと願います。

おわり


歴ログは10周年ですがnoteに引っ越します - 歴ログ -世界史専門ブログ-

OpenStreetMap + React Leaflet で地図を表示したメモ

最近は地図関係のアプリを開発してて、ライブラリも多く実装したものを定期的にメモに残しておこうと思う

Google Map の API が高額でいろいろなレイヤーを重ね合わせるのが少し面倒そうだったので、OpenStreetMap を使う方法を探索した

OpenSteetMap を使えるライブラリはかなりたくさんあり色々試したが、本記事では React Leaflet でシンプルな地図を表示させたメモです

環境
  • React 18.2.0
  • leaflet 1.9.4
  • react-leaflet: 4.2.1

React Leaflet を使う準備

$ npm i react-leaflet leaflet
$ npm i -D @types/leaflet

React Leafret で OpenStreetMap を表示する

// Map.tsx
import { latLng } from "leaflet";
import { MapContainer, TileLayer } from "react-leaflet";
// leaflet のスタイルがないと地図が崩れる
import "leaflet/dist/leaflet.css";
// 地図を表示させるコンテナの height が必要
import "./map.css";

const position = latLng([35.3607411, 138.727262]);
const zoom = 5;

export const Map = (): JSX.Element => {
  return (
    <MapContainer center={position} zoom={zoom}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
        {/* OpenStreetMap のスタイルを選択して指定 */}
        url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
        maxZoom={13}
        minZoom={2}
      />
    </MapContainer>
  );
};
/* map.css */
.leaflet-container {
  width: 100%;
  height: 100vh;
}

React Leaflet + OpenStreetMap

これでシンプルな地図が表示できた。
ハマりどころは Leaflet の CSS (leaflet/dist/leaflet.css) が必要なことと、Leaflet が出力するコンテナの高さがないと地図が表示されないこと。TypeScript だと position を latLng 関数を挟まないと Type Error になることくらい

クリックした箇所に marker を表示させる

useMapEvent hook で地図上のイベントを取得できるので、クリックされた lat, long を取得して Marker として表示させる

// LocationMarkers.tsx
import { useState } from "react";
import { LatLng, latLng, icon } from "leaflet";
import { Marker, Popup, useMapEvent } from "react-leaflet";

const ICON = icon({
  iconUrl: "/images/leaflet/marker-icon.png",
  iconSize: [25, 41],
  iconAnchor: [12, 41],
  popupAnchor: [0, -30],
});

export const LocationMarkers = (): JSX.Element => {
const [markers, setMarkers] = useState<LatLng[]>([]);
  // useMapEvent は `<MapContainer>` の内側からしか呼び出せない
  const map = useMapEvent("click", (e) => {
    const { lat, lng } = e.latlng;
    setMarkers((markers) => [...markers, latLng(lat, lng)]);
  });

  return (
    <>
      {markers.map((marker) => (
        <Marker key={marker.toString()} position={marker} icon={ICON}>
          <Popup>{marker.toString()}</Popup>
        </Marker>
      ))}
    </>
  );
};

<MapContainer> 内に作成した <LocationMarkers /> コンポーネントを追加すれば OK

// Map.tsx
export const Map = (): JSX.Element => {
  return (
    <MapContainer center={position} zoom={zoom}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
        url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
        maxZoom={13}
        minZoom={2}
      />
      <LocationMarkers />
    </MapContainer>
  );
};

markers

地図のクリックした箇所にマーカーが表示されマーカーをクリックするとポップアップが表示されるようになった!

🐞 デフォルトのマーカー画像が表示されない問題

Leaflet.Marker.prototype.options.iconLeaflet.icon を渡す方法・Leaflet.Icon.Default.mergeOptions にアイコン画像を設定する方法を issue でみつけたが leaflet v1.9.4, react-leaflet v4.2.1 の環境では画像が壊れたままになってしまったので、カスタムアイコンを作って表示させた

h3-js で取得したエリアを React Leaflet で表示させる

h3geo.org h3-js は Uber の開発した世界を六角形のグリッドで表すJavaScript のライブラリ。
h3-js で取得した六角形のエリアを lat, long の矩形 (polygon) として地図上に表示させてみます

$ npm i h3-js

h3 では世界をどのサイズの六角形のグリッド化しているのかというサイズを resolution という数字で表しています。
今回はマーカーと同じようにクリックした位置が含まれる resolution を Polygon 化して地図上にプロットします。

// HexLayer.tsx
import { FC, useState } from "react";
import { LatLng } from "leaflet";
import { Polygon, useMapEvent } from "react-leaflet";
import { cellToBoundary, CoordPair, latLngToCell } from "h3-js";

const resolution = 3;

export const HexLayer: FC = () => {
  const [hexagons, setHexagons] = useState<{index: string, polygon: CoordPair[]}[]>([]);
  const map = useMapEvent("click", (e) => {
    const { lat, lng } = e.latlng;
    const h3Index = latLngToCell(lat, lng, resolution);
    const boundary = cellToBoundary(h3Index);

    setHexagons((hexagons) => [...hexagons, {
      index: h3Index,
      polygon: boundary,
    }]);
  });

  return (
    <>
      {hexagons.map(({ index, polygon }) => (
        <Polygon key={index} positions={polygon} />
      ))}
    </>
  );
}

h3 hex

h3-js で取得したエリアを Polygon として表示させることが出来ました!
※ Leaflet は左右に地図を移動させると経度が -600 や 1000 のように -180 - 180 の範囲を超えてしまうようです。h3-js の cellToBoundary で変換した座標は -180 〜 180 の間に収まる数値に変換されるため、地図を移動させまくると Ploygon が表示されないようにみえてしまいます

Sample

所管

React-Leaflet の example には実例が色々とコードと共に載っているので、扱いやすいライブラリだなと感じました。
一方で Leaflet に依存しているので、ちょっとした問題があると Leaflet 側まで見に行かなければならない事もあるのは注意が必要そうです (経度が延々大きくも小さくもなる問題とか)

また React-Leaflet は地図上に表示させているマーカーや矩形を 画像や SVG といった HTML 要素で表現しているので表示量が多くなると Canvas ベースのものより処理が重くなるのではないかな〜という気がしています。 単体でできることが豊富なのでサクッと地図を使ったモックなどを作るのには向いてそうですね!

おわり


[参考]

13歳からの地政学 サクッと読めて面白かった!

Next.js SSG したサイトを別ドメインから参照させててハマった

あるプロジェクトで Page routing の Next.js を SSG で作成したサイトを Vercel でホスティングしたものを CloudFront と Lambda@Edge を使って別ドメインの URL からサイトを表示させるようにしていた。
図で書くとこんな感じ サービス構成

CloudFront を使った別ドメイン (b.com/foo) からサイトを見たときだけページ内リンク (next/link) で遷移時にアプリがクラッシュしてしまう問題が発生した。

Next.js error: a client-side exception has occurred

すっごくレアケースだと思うけど記録としてメモしておく

環境

  • Next 13.5.4
  • React 18.2.0
  • TypeScript 5.2.2

結論 /_next/data へのアクセスに問題があった

Next.js の SSG (Static Site Generation) は Static Exports ではない

今回の問題は Next.js の SSG は常に静的な HTML を配信するだけものだと勘違いしていた点にあった。

dev took の Network タブで観察してみた結果、Vercel でホスティングしている Next.js の SSG されたアプリは初回アクセスは生成された HTML が帰るがクライアントサイドで react 化され、アプリ内リンクなどは staticProps となる JSON を preftech で取得し、その JSON を元にページの再レンダリングを行っていてた。 SSG したのだから HTML だけで完結しているのでは?と思いドキュメントを読んで勘違いしていたことに気がついた

Statically generates both HTML and JSON
When a page with getStaticProps is pre-rendered at build time, in addition to the page HTML file, Next.js generates a JSON file holding the result of running getStaticProps.
This JSON file will be used in client-side routing through next/link or next/router. When you navigate to a page that’s pre-rendered using getStaticProps, Next.js fetches this JSON file (pre-computed at build time) and uses it as the props for the page component. This means that client-side page transitions will not call getStaticProps as only the exported JSON is used.
cf. Data Fetching: getStaticProps | Next.js

Static Exports
Next.js enables starting as a static site or Single-Page Application (SPA), then later optionally upgrading to use features that require a server.
Since Next.js supports this static export, it can be deployed and hosted on any web server that can serve HTML/CSS/JS static assets.
cf. Deploying: Static Exports | Next.js

next.config.jsoutput: "export" オプションを指定していると build 時に out ディレクトリに HTML が出力されるが、これは Static Exports であり SSG ではなく、Static Export は完全に静的な HTML を生成するものではなく使っている機能によっては SPA として動作するのだと解った。

今回のサイトでは getStaticPaths を使ったダイナミックルーティングを行い、ページで表示する内容を getStaticProps で事前に取得するようにしていたので、各ページの HTML と共に各ページの static props になる JSON が出力されており、サイトは初回アクセス以降はクライアントサイドで SPA となりページ内リンクでは JSON を取得してページを再レンダリングしているという事だった。
(static props となる JSON が取得できないとか、next/link を使わず a タグでリンクした場合は通常のページ遷移になるのだと思うが検証していない)

static props の JSON/_next/data 内に格納されている

SSG で build されたサイトは static props となるデータが /_next/data/{BUILD_ID} 内に ページ名.json の形で出力されていた。
SSG されたサイトが SPA のように振る舞うとき、その json を取得してそれを元に画面を再レンダリングしており、CloudFront 側のドメイン (b.com) からアクセスした際に /_next/data で ferch が走っていたために、コードの存在しないドメイン宛の b.com/_next/data にリクエストが飛んでいた。
本来存在しないリクエストなのでエラーになるはずだが、今回のケースでは status code 200 が返っていたために SPA としてページを更新しようとして不正なレスポンスを static props として使ったためにページがクラッシュしてしまっていたというオチ

解決方法

  1. next/link の箇所が SPA として動作するので、next/link を辞めて a タグを使う
  2. /_next/data のパスに実際のコードのあるドメイン (origin) を追加する

next/link を使わない方法はシンプルにタグを置き換えればよいだけなので、今回は /_next/dataドメインを追加する方法を試してみた

ビルドされた /_next/data のパスにドメイン (origin) を追加する

assetPrefix というオプションがあるが、これは static ファイル (.next/static or /out/_next/static) に対してのみ有効で /_next/data へのアクセスにドメインを追加することはできない

While assetPrefix covers requests to _next/static, it does not influence the following paths:

  • Files in the public folder; if you want to serve those assets over a CDN, you'll have to introduce the prefix yourself
  • /_next/data/ requests for getServerSideProps pages. These requests will always be made against the main domain since they're not static.
  • /_next/data/ requests for getStaticProps pages. These requests will always be made against the main domain to support Incremental Static Generation, even if you're not using it (for consistency).

cf. next.config.js Options: assetPrefix | Next.js

/_next/dataドメインを設定できる dataPathPrefix というアイディアも提案されていたようだが App Router になれば解決するとして採用はされていないようだった

assetPrefix に似たオプションで basePath というものがあるが、これはあるドメインの別階層に Next のアプリをデプロイした場合にパスを設定できるオプションで / 始まりしか許容されておらずドメインを追加することは出来なかった

string-replace-loader を使ってビルド時にパスを置換する

Next.js の提供しているオプションでは /_next/dataドメインを追加することは不可能そうだった。 Next.js のビルドには webpack が使われているので、ビルド時に文字列を置換する string-replace-loader ローダーを使って /_next/dataドメイン付きに強制的に置換してしまう方法は可能だった。

// next.config.js
const isProd = process.env.VERCEL_ENV === 'production';

const nextConfig = {
  reactStrictMode: true,
  // souce map の出力
  productionBrowserSourceMaps: isProd ? false : true,
  // webpack の設定を追加
  webpack: (config) => {
    config.module.rules.push(
      ...[
        isProd
          ? {
              test: /.*.js$/,
              loader: 'string-replace-loader',
              options: {
                search: '/_next/data',
                replace: `${ORIGIN.replace(/\/$/, '')}/_next/data`,
              },
            }
          : undefined
      ].filter(Boolean)
    );
  },
}

これで /_next/data 内にある json を取得するドメインを変更することが出来た (hack的なので漏れや他のバグを産み出すある可能性はあるが)

CORS 問題

ユーザーがアクセスするドメインb.com で実際のコードが置かれているのが a.com。Next のアプリが裏側で fetch しようとした場合 b.com から a.com/_next/data/{BUILD_ID}/**/*.json にアクセスしようとするので、CORS 問題が発生する

検索していると CORS を許可する方法として下記の4パターンがあった

  1. Serverless Function でレスポンスに header を追加する
  2. middleware (/src/middleware.js) でレスポンスに header を追加する
  3. next.config.js に headers オプションを追加する
  4. vercel.jsonheaders オプションを追加する

cf.

今回はシンプルそうな 4 の vercel.json に headers を追加することで CORS が発生しなくなったのでそれ以外の調査はしていない

vercel.json に headers オプションを追加して CORS を許可する

verce.json

{
  "headers": [
    {
      "source": "/_next/data/(.*)",
      "headers": [
        { "key": "Access-Control-Allow-Credentials", "value": "true" },
        {
          "key": "Access-Control-Allow-Origin",
          "value": "*"
        },
        { "key": "Access-Control-Allow-Methods", "value": "GET" },
        {
          "key": "Access-Control-Allow-Headers",
          "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Nextjs-Data"
        }
      ]
    }
  ]
}

verce.jsonAccess-Control-Allow-Origin は whitelist のように複数のオリジンを配列で渡すことが出来なかった (build error になる) ので * として、メソッドを GET に制限することにした

これで CloudFront 経由の b.com からアクセスしても next/link が正常に動作して SPA のように json でページが更新されるようになった!!!

preview モードで prefetch だけが CORS になる問題

ここまでで 9割問題ないのだが、Vercel の preview モードでホスティングしているサイト (ステージング環境) も CloudFront 経由でアクセスできるようになっており、preview モードのサイトを CloudFront 経由の別ドメインでアクセスした時だけ /next/link の preftech が CORS のエラーになっていた。 (画面更新のための JSON の GET は正常に行えている)

謎…

dev tool でエラーの箇所のコードを見ていたら preview mode の場合は cookie が必要だと書かれたコメントを発見した

function fetchRetry(
  url: string,
  attempts: number,
  options: Pick<RequestInit, 'method' | 'headers'>
): Promise<Response> {
  return fetch(url, {
    // Cookies are required to be present for Next.js' SSG "Preview Mode".
    // Cookies may also be required for `getServerSideProps`.
    //
    // > `fetch` won’t send cookies, unless you set the credentials init
    // > option.
    // https://developer.mozilla.org/docs/Web/API/Fetch_API/Using_Fetch
    //
    // > For maximum browser compatibility when it comes to sending &
    // > receiving cookies, always supply the `credentials: 'same-origin'`
    // > option instead of relying on the default.
    // https://github.com/github/fetch#caveats
    credentials: 'same-origin',
    method: options.method || 'GET',
    headers: Object.assign({}, options.headers, {
      'x-nextjs-data': '1',
    }),
  }).then((response) => {

https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/router.ts#L425-L447

恐らく Vercel の Preview モードは本来コメントを残したりする機能が付いているので vercel にログインしている / アクセス可能であるという cookie が必要になっているのではないかと思った。
preftech がエラーになってもステージング環境だけだし、ページの遷移も問題なく行えているのでこの件に関しては対応しないものとした。

所管

SSG と static exports を混同していたことや、SSG すれば HTML しか使ってないのような勘違いもあったが、本来エラーになってほしいリクエストが 200 番で返されていたために予期せぬクラッシュが発生してしまっていた。
Next.js をビルドした状態でかつ本番環境でしか発生していない問題だったので、フレームワークをいい感じにしてくれるブラックボックスとして扱っていたために調査が難しく時間がかかる結果となった。 また Next.js はいろいろなオプションが用意されているが /_next/dataドメインを変更する方法が無いなど、別のドメインから参照させるような使い方はあまり想定してないようにも思った。

教訓
フレームワークをドキュメントにないような使い方をする時はフレームワークの挙動についてよく理解しておかないとバグを踏む!

ただのバグ報告が大作になってしまった…
おわり


[参照]

どういう意味でで SSG なんや?このマンガ!?