 
 2025年1月21日
タイトルのような現象が起きてしまったので、その解消法を記録しておきます。なお、環境は以下の通りです。
はじめに Astro での SSR の運用方法について書くので現象の解決方法が知りたい方はこちらまで飛んでください。
今までは Next.js を使ってサイトを構築していましたが、Astro を見つけてからは使いやすさにこちらを使ってサイト運営をしています。
デプロイは Vercel を使っているのですが、始めは元から設定されている SSG(Static Site Generator:静的サイトジェネレーター)として運用をしていました。
ただ、どうしても GET メソッドを使いたい場面が出てきたため、一部を SSR(Sever Side Rendring:サーバーサイドレンダリング)で運用することにしました。
Astro で SSR をするためには、全体を始めからの設定である SSG のままで一部を SSR にするか、全体を SSR にして一部を SSG にするか、の選択が必要になります。(もちろんすべて SSR にすることも可能)私は前者を選びました。設定方法は公式サイトのOn-demand renderingにありますが、簡単に言うと@astrojs/vercel アダプターをインストールすることで SSR を使用できるようにします。
ます、
$ npx astro add vercelを実行します。
 ╭──────────────────────╮
 │ npm install @astrojs/vercel@^8.0.2 │
 ╰──────────────────────╯
 Continue?(Y/n)と@astrojs/vercel アダプターをインストールするが聞かれるので yes
その上で astro.config.mjs に
  ╭ astro.config.mjs ───────────────────╮
  │import { defineConfig } from 'astro/config';     │
 │import vercel from '@astrojs/vercel/serverless'; │
  │                                                 │
  │export default defineConfig({                    │
 │  adapter: vercel(),                             │
  │});                                              │
  ╰──────────────────────────────╯
 Continue?(Y/n)が追加されるがいいか聞かれるので yes
を入力すると@astrojs/vercel アダプターが使用できるようになり SSR が使える状態になります。
この場合はとても簡単です。SSR にしたいファイルに
---
export const prerender = false
---
<html>
<p>このページはSSRです。</p>
<html>を追加するだけで、そのページが SSR になります。
この場合は astro.config.mjs に手を加えます。
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
  output: 'server',
  adapter: vercel(),
});を追加します。これで、すべてが SSR になりました。
あとは、SSG にしたいファイルに
---
+ export const prerender = true
---
<html>
<p>このページはSSGです。</p>
<html>とすればできあがります。
Astro でサイトマップを設定するのは非常に簡単です。@astrojs/vercel アダプターをインストールしたように@astrojs/sitemapを参考に
$ npx astro add sitemapを実行します。
 ╭────────────────────────╮
 │ npm install @astrojs/sitemap@^3.2.1   │
 ╰────────────────────────╯
 Continue?(Y/n)と@astrojs/sitemap をインストールするが聞かれるので yes
その上で astro.config.mjs に
  ╭ astro.config.mjs ───────────────────╮
  │import { defineConfig } from 'astro/config';     │
  │import vercel from '@astrojs/vercel/serverless'; │
 │import sitemap from "@astrojs/sitemap";          │
  │                                                 │
  │export default defineConfig({                    │
 │  integrations: [sitemap()],                     │
  │  adapter: vercel(),                             │
  │});                                              │
  ╰──────────────────────────────╯
 Continue?(Y/n)が追加されるがいいか聞かれるので yes
を入力すると下準備の完了です。
このあともう少し手を加えないといけません。引き続き astro.config.mjs に
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
import sitemap from "@astrojs/sitemap";
export default defineConfig({
 site: 'https://<自分のサイトアドレス>',
  integrations: [sitemap()],
  adapter: vercel(),
});を記入して build すると sitemap-index.xml と sitemap-0.xml が作成されます。
満を持してできあがったファイルをコミットし、GitHub にプッシュして、Vercel でデプロイしました。
その後で、`https://<自分のサイトアドレス>/sitemap-index.xml` を確認してみると、「404 Not found」の表示。「はて、設定を間違えたか?」色々試しデプロイし直しますが、どうやっても sitemap が見つかりません。
ほとほと困っていたところ検索に「@astrojs/sitemap excluded from final static output with Vercel adapter(訳:@astrojs/sitemap は、Vercel アダプターで最終的な静的出力から除外されます。)」という内容の issue を発見。
どうにも、今の@astrojs/vercel のバージョンだと@astrojs/sitemap プラグインが、Vercel がすでに静的ファイルの出力を終えた後に sitemap を生成する設定になっているため、結果的にsitemap が静的ファイルから除外されてしまう問題があるのだそうです。
さて、その解決方法ですが、上の issueにありました。mohdlatif さんの 2024/12/28の書き込みを参考に
まず、フォルダーのトップに copy-files.ts という名前で以下の内容のファイルを作成します。
import type { AstroIntegration } from 'astro';
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// フォーマットされたログ記録のためのユーティリティ関数
function formatLog(tag: string, message: string) {
  const timestamp = new Date().toLocaleTimeString('en-US', {
    hour: '2-digit',
    hour12: false,
    minute: '2-digit',
    second: '2-digit'
  });
  // 下の行はeslintのno-consoleエラー対策のため
  // eslint-disable-next-line no-console
  console.log(
    '\n' + // 上にスペースを追加
      `\x1b[90m${timestamp}\x1b[0m ` + // グレーのタイムスタンプ
      `[\x1b[36m${tag}\x1b[0m] ` + // シアン色のタグ
      `${message}` + // メッセージ
      '\n' // 下にスペースを追加
  );
}
// ファイルを静的フォルダに移動する
async function copyFiles(srcDir: string, destDir: string) {
  const files = await fs.readdir(srcDir);
  for (const file of files) {
    const srcPath = path.join(srcDir, file);
    const destPath = path.join(destDir, file);
    const stat = await fs.stat(srcPath);
    if (stat.isDirectory()) {
      await fs.mkdir(destPath, {recursive: true});
      await copyFiles(srcPath, destPath);
    } else {
      await fs.copyFile(srcPath, destPath);
    }
  }
}
// コピー実行フック関数
export function CopyFilesPlugin(): AstroIntegration {
  return {
    hooks: {
      'astro:build:done': async ({dir}) => {
        formatLog('copy-files', 'Copying files to .vercel/output/static');
        const distDir = fileURLToPath(dir.href);
        const staticDir = path.resolve('.vercel/output/static');
        await fs.mkdir(staticDir, {recursive: true});
        await copyFiles(distDir, staticDir);
      }
    },
    name: 'copy-files'
  };
}その後 astro.config.mjs に以下を書き込みます
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
import sitemap from "@astrojs/sitemap";
import {CopyFilesPlugin} from './copy-files';
export default defineConfig({
  site: 'https://<自分のサイトアドレス>',
  integrations: [
    sitemap()
   // 他のintegrationsをすべて書き終えた一番最後に記入する
   CopyFilesPlugin()
  ],
  adapter: vercel(),
});これでコミット、プッシュ、デプロイでちゃんと sitemap が表示できるようになりました。