Webアプリケーションのパス戦略
作成日:
web frontend URL design
概要
Web アプリケーションでリソース(画像、CSS、JS、リンク)を参照する際、パスの指定方法によって動作が変わる。特にサブディレクトリでホスティングする場合、パス戦略の理解が重要。
パスの種類
1. 絶対パス(Absolute Path)
ルート(/)から始まるパス。
<!-- 常にドメインルートからの参照 -->
<img src="/images/logo.png">
<a href="/about">About</a>
<script src="/js/app.js"></script>
動作例:
https://example.com/でアクセス →https://example.com/images/logo.pnghttps://example.com/blog/でアクセス →https://example.com/images/logo.png(同じ)
2. 相対パス(Relative Path)
現在のディレクトリからの相対的なパス。
<!-- 現在のディレクトリからの参照 -->
<img src="images/logo.png">
<img src="./images/logo.png"> <!-- 同じ意味 -->
<a href="../about">About</a> <!-- 親ディレクトリへ -->
動作例:
https://example.com/でアクセス →https://example.com/images/logo.pnghttps://example.com/blog/でアクセス →https://example.com/blog/images/logo.png
3. プロトコル相対パス
現在のプロトコル(http/https)を継承。
<!-- 非推奨:現在は HTTPS 統一が標準 -->
<script src="//cdn.example.com/lib.js"></script>
4. 完全 URL
プロトコルからすべて指定。
<img src="https://cdn.example.com/images/logo.png">
サブディレクトリでの問題
問題のシナリオ
アプリケーションを https://example.com/blog/ でホスティングする場合:
<!-- 問題のあるコード:絶対パス -->
<img src="/images/logo.png">
<!-- 参照先: https://example.com/images/logo.png ❌ -->
<!-- 期待値: https://example.com/blog/images/logo.png -->
解決方法
方法 1: ベースパスの設定(推奨)
フレームワークの設定でベースパスを指定。
Astro:
// astro.config.mjs
export default defineConfig({
site: 'https://example.com',
base: '/blog',
});
<!-- コンポーネント内 -->
<img src={`${import.meta.env.BASE_URL}images/logo.png`}>
Next.js:
// next.config.js
module.exports = {
basePath: '/blog',
assetPrefix: '/blog/',
};
Vite / React:
// vite.config.js
export default {
base: '/blog/',
};
方法 2: 相対パスの使用
<!-- 現在位置からの相対パス -->
<img src="./images/logo.png">
注意点:
- ページの階層が変わると壊れる可能性
- 深いネストでは
../../が増えて管理が困難
方法 3: ルートからの相対パス変換
ビルド時にパスを変換するプラグインを使用。
// Vite の例
import { defineConfig } from 'vite';
export default defineConfig({
base: '/blog/',
build: {
assetsDir: 'assets',
},
});
各パターンの比較
| パターン | 例 | サブディレクトリ対応 | 保守性 |
|---|---|---|---|
| 絶対パス | /images/logo.png | × 設定必要 | ○ 明確 |
| 相対パス | ./images/logo.png | ○ | △ 階層依存 |
| ベース付き絶対パス | ${base}/images/logo.png | ○ | ○ 推奨 |
| 完全 URL | https://cdn.example.com/... | ○ | △ 環境依存 |
フレームワーク別の対応
Astro
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://example.com',
base: '/blog',
build: {
assets: '_astro',
},
});
---
// コンポーネント
---
<!-- 画像 -->
<img src={`${import.meta.env.BASE_URL}images/logo.png`}>
<!-- リンク -->
<a href={`${import.meta.env.BASE_URL}about`}>About</a>
<!-- Astro の Image コンポーネント(自動でベースパス考慮) -->
import { Image } from 'astro:assets';
import logo from '../images/logo.png';
<Image src={logo} alt="Logo" />
Next.js
// next.config.js
module.exports = {
basePath: '/blog',
assetPrefix: '/blog/',
};
// コンポーネント
import Image from 'next/image';
import Link from 'next/link';
export default function Page() {
return (
<>
{/* next/image は自動でベースパス考慮 */}
<Image src="/images/logo.png" alt="Logo" width={100} height={100} />
{/* next/link も自動でベースパス考慮 */}
<Link href="/about">About</Link>
</>
);
}
Vite (React/Vue)
// vite.config.js
export default {
base: '/blog/',
};
// React コンポーネント
function App() {
return (
<>
{/* import.meta.env.BASE_URL を使用 */}
<img src={`${import.meta.env.BASE_URL}images/logo.png`} alt="Logo" />
{/* public フォルダの画像 */}
<img src={new URL('/images/logo.png', import.meta.url).href} alt="Logo" />
</>
);
}
Vue Router
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory('/blog/'),
routes: [/* ... */],
});
リバースプロキシとの組み合わせ
Traefik でのパスベースルーティング
services:
blog:
image: blog:latest
labels:
- traefik.enable=true
- traefik.http.routers.blog.rule=Host(`example.com`) && PathPrefix(`/blog`)
- traefik.http.middlewares.blog-strip.stripprefix.prefixes=/blog
- traefik.http.routers.blog.middlewares=blog-strip
StripPrefix の動作:
- クライアントが
/blog/aboutにアクセス - Traefik が
/blogを除去 - アプリケーションには
/aboutとして届く
注意点:
- アプリケーション側はルート(
/)で動作するように設計 - または、StripPrefix を使わずアプリケーション側で
/blogベースパスを設定
Nginx でのリバースプロキシ
location /blog/ {
# そのままプロキシ(アプリ側で /blog ベース設定が必要)
proxy_pass http://localhost:3000/blog/;
# または、パスを書き換え(アプリはルートで動作)
# proxy_pass http://localhost:3000/;
}
ベストプラクティス
1. フレームワークの機能を活用
// ❌ ハードコード
<img src="/blog/images/logo.png">
// ✅ フレームワークの変数を使用
<img src={`${import.meta.env.BASE_URL}images/logo.png`}>
2. 環境変数での切り替え
// vite.config.js
export default defineConfig({
base: process.env.BASE_PATH || '/',
});
# 開発環境
npm run dev
# 本番環境(サブディレクトリ)
BASE_PATH=/blog npm run build
3. 画像は import で管理
// ❌ 文字列パス
<img src="/images/logo.png">
// ✅ import(ビルド時に最適化&パス解決)
import logo from './images/logo.png';
<img src={logo}>
4. CSS 内の相対パス
/* CSS ファイル内では相対パスを使用 */
.hero {
/* CSS ファイルからの相対パス */
background-image: url('../images/hero.jpg');
}
トラブルシューティング
画像が 404 になる
- ベースパスの設定を確認
- ビルド後の出力ディレクトリを確認
- 開発サーバーと本番で動作が異なる場合は
base設定を確認
リンクが動作しない
- フレームワークのルーターがベースパスを考慮しているか確認
<a href="">ではなくフレームワークのリンクコンポーネントを使用
開発と本番で動作が違う
// 環境に応じたベースパス
const base = import.meta.env.DEV ? '/' : '/blog/';
関連トピック
- ドメイン戦略 - サブドメイン vs サブディレクトリ
- URL - URL の基本構造
- Astro Pages Routing - Astro でのルーティング