こんにちは。アイレット株式会社デザイン事業部の鶴若です。
Astroは強力なフレームワークですが、何も設定せずにビルドしたデフォルトの状態では、そのままクライアントに納品する静的サイトとしては多くの問題点を抱えています。

デフォルトビルドの主な問題点

  • CSS: “タグ内にインラインで展開され、外部CSSとして管理できない。
  • HTML: 改行やインデントが削除され、一行に圧縮されてしまい可読性が著しく低い。
  • ファイルパス: hoge/fuga/index.html というディレクトリ形式で出力され、 hoge/fuga.html のようにシンプルなファイル名にならない。
  • CSSセレクタ: コンポーネントのスコープを維持するために、不要なデータ属性([data-astro-cid-...])が付与されてしまう。

本記事では、これらの問題点を解決し、Astroで納品に適したクリーンで管理しやすい静的ファイルを出力するための設定方法について、実装でつまづきやすい点を中心に解説していきます。

Astroのインストール

コマンド実行後指示に従ってインストールしてください。

npm create astro@latest

問題点の確認:デフォルト設定でビルドした結果

まずは、インストールした状態のままビルドしてみましょう。出力されたHTMLは以下のようになります。

<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v5.13.7"><title>Astro Basics</title><style>#background[data-astro-cid-mmc7otgs]{position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;filter:blur(100px)}#container[data-astro-cid-mmc7otgs]{font-family:Inter,Roboto,Helvetica Neue,Arial Nova,Nimbus Sans,Arial,sans-serif;height:100%}main[data-astro-cid-mmc7otgs]{height:100%;display:flex;justify-content:center}#hero[data-astro-cid-mmc7otgs]{display:flex;align-items:start;flex-direction:column;justify-content:center;padding:16px}h1[data-astro-cid-mmc7otgs]{font-size:22px;margin-top:.25em}#links[data-astro-cid-mmc7otgs]{display:flex;gap:16px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]{display:flex;align-items:center;padding:10px 12px;color:#111827;text-decoration:none;transition:color .2s}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs]:hover{color:#4e5056}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs] svg[data-astro-cid-mmc7otgs]{height:1em;margin-left:8px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{color:#fff;background:linear-gradient(83.21deg,#3245ff,#bc52ee);box-shadow:inset 0 0 0 1px #ffffff1f,inset 0 -2px #0000003d;border-radius:10px}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button:hover{color:#e6e6e6;box-shadow:none}pre[data-astro-cid-mmc7otgs]{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-weight:400;background:linear-gradient(14deg,#d83333,#f041ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin:0}h2[data-astro-cid-mmc7otgs]{margin:0 0 1em;font-weight:400;color:#111827;font-size:20px}p[data-astro-cid-mmc7otgs]{color:#4b5563;font-size:16px;line-height:24px;letter-spacing:-.006em;margin:0}code[data-astro-cid-mmc7otgs]{display:inline-block;background:linear-gradient(66.77deg,#f3cddd,#f5cee7) padding-box,linear-gradient(155deg,#d83333,#f041ff 18%,#f5cee7 45%) border-box;border-radius:8px;border:1px solid transparent;padding:6px 8px}.box[data-astro-cid-mmc7otgs]{padding:16px;background:#fff;border-radius:16px;border:1px solid white}#news[data-astro-cid-mmc7otgs]{position:absolute;bottom:16px;right:16px;max-width:300px;text-decoration:none;transition:background .2s;backdrop-filter:blur(50px)}#news[data-astro-cid-mmc7otgs]:hover{background:#ffffff8c}@media screen and (max-height: 368px){#news[data-astro-cid-mmc7otgs]{display:none}}@media screen and (max-width: 768px){#container[data-astro-cid-mmc7otgs]{display:flex;flex-direction:column}#hero[data-astro-cid-mmc7otgs]{display:block;padding-top:10%}#links[data-astro-cid-mmc7otgs]{flex-wrap:wrap}#links[data-astro-cid-mmc7otgs] a[data-astro-cid-mmc7otgs].button{padding:14px 18px}#news[data-astro-cid-mmc7otgs]{right:16px;left:16px;bottom:2.5rem;max-width:100%}h1[data-astro-cid-mmc7otgs]{line-height:1.5}}html,body{margin:0;width:100%;height:100%}
</style></head> <body>  <div id="container" data-astro-cid-mmc7otgs> <img id="background" src="/assets/img/background.svg" alt="" fetchpriority="high" data-astro-cid-mmc7otgs> <main data-astro-cid-mmc7otgs> <section id="hero" data-astro-cid-mmc7otgs> <a href="https://astro.build" data-astro-cid-mmc7otgs><img src="/assets/img/astro.svg" width="115" height="48" alt="Astro Homepage" data-astro-cid-mmc7otgs></a> <h1 data-astro-cid-mmc7otgs>
Astro Test
</h1> </section> </main> <a href="https://astro.build/blog/astro-5/" id="news" class="box" data-astro-cid-mmc7otgs> <h2 data-astro-cid-mmc7otgs>sub title</h2> <p data-astro-cid-mmc7otgs>text text text text text text</p> </a> </div>   <a href="/assets/js/bundle.js">/assets/js/bundle.js</a> </body> </html> 

ご覧の通り、HTMLは圧縮され、CSSはタグに直接書き出されています。 このままでは、手動での修正やファイル管理が非常に困難であり、静的サイトとして納品するには適した状態とは言えません。

また、hoge/fuga.htmlとしたい所をhoge/fuga/index.htmlになってしまうので少し設定の修正が必要です。

ここから、これらの問題を一つずつ設定で解決していきます。

解決策①:CSSを外部ファイルとして出力し、不要なデータ属性を削除する

astro.config.mjsの修正

まず、CSSがインラインで展開されるのを防ぎ、外部ファイルとして出力されるように設定します。

  • buildinlineStylesheets: "never" を追加します。
  • viterollupOptions で、出力されるCSSファイルの名前と場所を指定します。
// @ts-check
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
    build: {
        // CSSのインライン化を無効にする
        inlineStylesheets: "never",
    },
    vite: {
       css: {
        build: {
        minify: false,
        cssCodeSplit: false,
        rollupOptions: {
            output: {
                // 出力されるCSSファイル名を指定
                assetFileNames: "assets/css/style.css",
            },
        },
        },
    },
});

.astroファイルの修正

次に、CSSセレクタに不要なデータ属性が付与される問題を防ぎます。
各.astroファイル内のタグを に修正してください。

この is:globalを指定しないと、Astroが自動でCSSスコープを維持しようとし、以下のように不要なデータ属性セレクタが出力されてしまいます。 静的サイトとしてシンプルなCSSを納品するためには、この指定が不可欠です。

#background[data-astro-cid-mmc7otgs]{position:fixed;top:0;left:0;width:100%;height:100%;z-index:-1;filter:blur(100px)}

画像の格納ディレクトリの変更

/src/assetsからimportして画像パスを設定していると、添付画像のように不要なCSSがビルド時に生成されることがあります。
今回のテーマのように静的サイトとしての画像などの素材はpublicフォルダに格納するようにしてください。

---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---

<div id="container">
    <img id="background" src={background.src} alt="" fetchpriority="high" />
</div>

解決策②:HTMLの圧縮を停止し、ファイルパスの形式を修正する

format: "directory"format: fileや他の設定でも静的サイトとして納品する場合ビルド時のhtmlのディレクトリとファイル名はあまり適切ではありません。
ディレクトリとファイル名を整える処理をastroのIntegrationとしてastro.config.mjsに組み込み、ビルド完了時にdistを正規化するフックを追加します。現在のastro.config.mjsを読み取り、差分を安全に適用します。

astro.config.mjs

// @ts-check
import { defineConfig } from 'astro/config';
import path from 'node:path';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob';

// https://astro.build/config
function normalizeDistIntegration() {
    return {
        name: 'normalize-dist-structure',
        hooks: {
            /** @param {{ dir: URL, logger: import('astro').AstroIntegrationLogger }} ctx */
            'astro:build:done': async (ctx) => {
                const { dir, logger } = ctx;
                const projectRoot = process.cwd();
                const distDir = fileURLToPath(dir);
                const pagesDir = path.resolve(projectRoot, 'src/pages');

                logger.info('[normalize-dist] dist を src/pages の構造に正規化します...');

                /** @param {string} p */
                async function exists(p) {
                    try {
                        await fs.access(p);
                        return true;
                    } catch {
                        return false;
                    }
                }

                /** @param {string} dirPath */
                async function removeIfEmpty(dirPath) {
                    if (!dirPath.startsWith(distDir)) return;
                    try {
                        const items = await fs.readdir(dirPath);
                        if (items.length === 0) {
                            await fs.rmdir(dirPath);
                            const parent = path.dirname(dirPath);
                            if (parent !== distDir) {
                                await removeIfEmpty(parent);
                            }
                        }
                    } catch {
                        // ignore
                    }
                }

                const srcPages = await glob('**/*.astro', { cwd: pagesDir, dot: false, nodir: true });

                for (const rel of srcPages) {
                    const relDir = path.dirname(rel);
                    const base = path.basename(rel, '.astro');

                    const expected = base === 'index'
                        ? path.join(distDir, relDir === '.' ? '' : relDir, 'index.html')
                        : path.join(distDir, relDir === '.' ? '' : relDir, `${base}.html`);

                    if (await exists(expected)) continue;

                    // パターン1: index.astro が dist/.html になっている
                    if (base === 'index') {
                        if (relDir !== '.') {
                            const flattened = path.join(distDir, `${relDir}.html`);
                            if (await exists(flattened)) {
                                await fs.mkdir(path.dirname(expected), { recursive: true });
                                await fs.rename(flattened, expected);
                                logger.info(`fix: ${path.relative(projectRoot, flattened)} -> ${path.relative(projectRoot, expected)}`);
                                continue;
                            }
                        }
                        continue;
                    }

                    // パターン2: 非 index が dist/.../name/index.html になっている
                    const nestedIndex = path.join(distDir, relDir === '.' ? '' : relDir, base, 'index.html');
                    if (await exists(nestedIndex)) {
                        await fs.mkdir(path.dirname(expected), { recursive: true });
                        await fs.rename(nestedIndex, expected);
                        await removeIfEmpty(path.join(distDir, relDir === '.' ? '' : relDir, base));
                        logger.info(`fix: ${path.relative(projectRoot, nestedIndex)} -> ${path.relative(projectRoot, expected)}`);
                        continue;
                    }
                }

                logger.info('[normalize-dist] 正規化完了。');
            },
        },
    };
}

export default defineConfig({
    compressHTML: false,
    build: {
        inlineStylesheets: "never",
        format: "directory",
    },
    integrations: [normalizeDistIntegration()],
    // (viteの設定は省略)
});

上記の処理を追加することによって /src/pages 内のディレクトリとファイル名を維持したままhtmlをビルドすることが可能です。

JavaScript

.astro内のscriptタグをロールアップできればよいのですが、色々調べても良い方法が見つからなかったので妥協になってしまいますがrollupを使用してpublicにバンドルしたjsを出力するように修正しました。

npm i rollup @rollup/plugin-terser -d

rollup.config.mjsを追加

import terser from '@rollup/plugin-terser';

export default {
  input: 'src/script/bundle.js',
  output: {
    file: 'public/assets/js/bundle.js',
    format: 'iife',
  },
  watch: {
    include: 'src/script/**'
  }
};

package.json
rollup系の設定を追加

"scripts": {
  "build": "npm run build:js && astro build && prettier --write \"./dist/**/*.html\" --ignore-path .prettierignore",
  "watch:js": "rollup -c -w",
  "build:js": "rollup -c"
},

仕上げ:Prettierでコードをフォーマットする

ここまでの設定で出力されるHTMLはまだ整形されていません。最後にprettierをビルドプロセスに組み込み、自動でコードが整形されるようにします。

astro.config.mjs

npm i prettier prettier-plugin-astro -d

package.json
build部分にprettierの設定を追加

"build": "astro build && prettier --write \"./dist/**/*.html\" --ignore-path .prettierignore",

納品するにあたって問題ないレベルになったかと思います。

html内の構造

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="generator" content="Astro v5.13.7" />
    <title>Astro Basics</title>
    <link rel="stylesheet" href="/assets/css/style.css" />
  </head>
  <body>
    <div id="container">
      <img
        id="background"
        src="/assets/img/background.svg"
        alt=""
        fetchpriority="high"
      />
      <main>
        <section id="hero">
          <a href="https://astro.build"
            ><img
              src="/assets/img/astro.svg"
              width="115"
              height="48"
              alt="Astro Homepage"
          /></a>
          <h1>Astro Test</h1>
        </section>
      </main>

      <a href="https://astro.build/blog/astro-5/" id="news" class="box">
        <h2>sub title</h2>
        <p>text text text text text text</p>
      </a>
    </div>

    <a href="/assets/js/bundle.js">/assets/js/bundle.js</a>
  </body>
</html>

以上の設定を施すことで、Astroのビルド成果物がより見通しの良い、実用的な静的ファイル群になったかと思います。今回は多くの現場で共通して問題になりがちな点を中心に解説しましたが、これらを基礎として、ご自身のプロジェクトに合わせた調整を加えてみてください。