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.png
  • https://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.png
  • https://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○ 推奨
完全 URLhttps://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 の動作:

  1. クライアントが /blog/about にアクセス
  2. Traefik が /blog を除去
  3. アプリケーションには /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 になる

  1. ベースパスの設定を確認
  2. ビルド後の出力ディレクトリを確認
  3. 開発サーバーと本番で動作が異なる場合は base 設定を確認

リンクが動作しない

  1. フレームワークのルーターがベースパスを考慮しているか確認
  2. <a href=""> ではなくフレームワークのリンクコンポーネントを使用

開発と本番で動作が違う

// 環境に応じたベースパス
const base = import.meta.env.DEV ? '/' : '/blog/';

関連トピック