T-CREATOR

Nuxt のレイアウト機能でサイト全体を美しくデザイン

Nuxt のレイアウト機能でサイト全体を美しくデザイン

Web サイトのデザインにおいて、一貫性のある美しいレイアウトは成功の鍵となります。Nuxt のレイアウト機能を使えば、複数のページで共通のデザインパターンを効率的に管理でき、開発時間を大幅に短縮できます。

この記事では、Nuxt のレイアウト機能を活用して、サイト全体を美しく統一されたデザインに仕上げる方法を詳しく解説します。初心者の方でも理解しやすいように、段階的に実装していきましょう。

レイアウト機能の基本概念

Nuxt のレイアウト機能は、ページの共通部分(ヘッダー、フッター、ナビゲーションなど)を効率的に管理するための仕組みです。従来の方法では、各ページで同じコードを繰り返し書く必要がありましたが、レイアウト機能を使えば一度定義するだけで全ページに適用できます。

レイアウトの仕組み

Nuxt では、layoutsディレクトリ内にレイアウトファイルを配置します。各レイアウトファイルは、<NuxtLayout>コンポーネントを通じてページコンテンツを包み込みます。

vue<!-- layouts/default.vue -->
<template>
  <div class="layout-container">
    <header class="header">
      <nav class="navigation">
        <NuxtLink to="/">ホーム</NuxtLink>
        <NuxtLink to="/about">会社概要</NuxtLink>
        <NuxtLink to="/contact">お問い合わせ</NuxtLink>
      </nav>
    </header>

    <main class="main-content">
      <slot />
    </main>

    <footer class="footer">
      <p>&copy; 2024 あなたのサイト名</p>
    </footer>
  </div>
</template>

<style scoped>
.layout-container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.header {
  background: #2c3e50;
  color: white;
  padding: 1rem;
}

.navigation {
  display: flex;
  gap: 2rem;
}

.navigation a {
  color: white;
  text-decoration: none;
  transition: color 0.3s ease;
}

.navigation a:hover {
  color: #3498db;
}

.main-content {
  flex: 1;
  padding: 2rem;
}

.footer {
  background: #34495e;
  color: white;
  text-align: center;
  padding: 1rem;
}
</style>

このレイアウトファイルは、すべてのページで共通して使用されるヘッダー、ナビゲーション、フッターを定義しています。<slot ​/​>の部分に各ページのコンテンツが挿入されます。

デフォルトレイアウトの活用

Nuxt では、layouts​/​default.vueがデフォルトレイアウトとして自動的に適用されます。特別な指定がない限り、すべてのページでこのレイアウトが使用されます。

ページでのレイアウト指定

特定のページでレイアウトを指定したい場合は、ページコンポーネントでdefinePageMetaを使用します。

vue<!-- pages/about.vue -->
<template>
  <div class="about-page">
    <h1>会社概要</h1>
    <p>私たちの会社について詳しくご紹介します。</p>
  </div>
</template>

<script setup>
// デフォルトレイアウトを使用(指定不要)
</script>

<style scoped>
.about-page {
  max-width: 800px;
  margin: 0 auto;
}

h1 {
  color: #2c3e50;
  margin-bottom: 1rem;
}
</style>

よくあるエラーと解決方法

レイアウト機能を使用する際によく発生するエラーをご紹介します。

エラー 1: レイアウトファイルが見つからない

arduinoError: Cannot find module './layouts/default.vue'

このエラーは、layoutsディレクトリが存在しないか、ファイル名が間違っている場合に発生します。

解決方法:

bash# layoutsディレクトリを作成
mkdir layouts

# デフォルトレイアウトファイルを作成
touch layouts/default.vue

エラー 2: レイアウトのスロットが正しく動作しない

vue<!-- 間違った例 -->
<template>
  <div>
    <header>ヘッダー</header>
    <!-- slotが抜けている -->
    <footer>フッター</footer>
  </div>
</template>

解決方法:

vue<!-- 正しい例 -->
<template>
  <div>
    <header>ヘッダー</header>
    <slot />
    <!-- これが必要 -->
    <footer>フッター</footer>
  </div>
</template>

カスタムレイアウトの作成

デフォルトレイアウト以外にも、用途に応じて複数のレイアウトを作成できます。例えば、管理画面用、ブログ用、ランディングページ用など、目的別にレイアウトを分けることで、より柔軟なデザインが可能になります。

管理画面用レイアウト

vue<!-- layouts/admin.vue -->
<template>
  <div class="admin-layout">
    <aside class="sidebar">
      <div class="logo">
        <h2>管理画面</h2>
      </div>
      <nav class="admin-nav">
        <NuxtLink to="/admin/dashboard"
          >ダッシュボード</NuxtLink
        >
        <NuxtLink to="/admin/users">ユーザー管理</NuxtLink>
        <NuxtLink to="/admin/posts">記事管理</NuxtLink>
        <NuxtLink to="/admin/settings">設定</NuxtLink>
      </nav>
    </aside>

    <div class="admin-content">
      <header class="admin-header">
        <div class="user-info">
          <span>管理者: 田中太郎</span>
          <button @click="logout">ログアウト</button>
        </div>
      </header>

      <main class="admin-main">
        <slot />
      </main>
    </div>
  </div>
</template>

<script setup>
const logout = () => {
  // ログアウト処理
  console.log('ログアウトしました');
};
</script>

<style scoped>
.admin-layout {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  width: 250px;
  background: #1a1a1a;
  color: white;
  padding: 1rem;
}

.logo h2 {
  margin-bottom: 2rem;
  color: #3498db;
}

.admin-nav {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.admin-nav a {
  color: white;
  text-decoration: none;
  padding: 0.75rem;
  border-radius: 4px;
  transition: background-color 0.3s ease;
}

.admin-nav a:hover {
  background: #2c2c2c;
}

.admin-content {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.admin-header {
  background: #f8f9fa;
  padding: 1rem 2rem;
  border-bottom: 1px solid #dee2e6;
}

.user-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.admin-main {
  flex: 1;
  padding: 2rem;
  background: #f8f9fa;
}
</style>

ブログ用レイアウト

vue<!-- layouts/blog.vue -->
<template>
  <div class="blog-layout">
    <header class="blog-header">
      <div class="container">
        <h1 class="blog-title">技術ブログ</h1>
        <p class="blog-description">
          最新の技術情報をお届けします
        </p>
      </div>
    </header>

    <div class="blog-container">
      <aside class="blog-sidebar">
        <div class="categories">
          <h3>カテゴリー</h3>
          <ul>
            <li><a href="#">フロントエンド</a></li>
            <li><a href="#">バックエンド</a></li>
            <li><a href="#">DevOps</a></li>
            <li><a href="#">デザイン</a></li>
          </ul>
        </div>

        <div class="recent-posts">
          <h3>最近の記事</h3>
          <ul>
            <li><a href="#">Nuxt 3の新機能</a></li>
            <li><a href="#">Vue 3のComposition API</a></li>
            <li><a href="#">TypeScript入門</a></li>
          </ul>
        </div>
      </aside>

      <main class="blog-main">
        <slot />
      </main>
    </div>

    <footer class="blog-footer">
      <div class="container">
        <p>&copy; 2024 技術ブログ. All rights reserved.</p>
      </div>
    </footer>
  </div>
</template>

<style scoped>
.blog-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.blog-header {
  background: linear-gradient(
    135deg,
    #667eea 0%,
    #764ba2 100%
  );
  color: white;
  padding: 3rem 0;
  text-align: center;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;
}

.blog-title {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

.blog-description {
  font-size: 1.1rem;
  opacity: 0.9;
}

.blog-container {
  flex: 1;
  display: flex;
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem 1rem;
  gap: 2rem;
}

.blog-sidebar {
  width: 300px;
  flex-shrink: 0;
}

.blog-sidebar h3 {
  color: #2c3e50;
  margin-bottom: 1rem;
  border-bottom: 2px solid #3498db;
  padding-bottom: 0.5rem;
}

.blog-sidebar ul {
  list-style: none;
  padding: 0;
}

.blog-sidebar li {
  margin-bottom: 0.5rem;
}

.blog-sidebar a {
  color: #34495e;
  text-decoration: none;
  transition: color 0.3s ease;
}

.blog-sidebar a:hover {
  color: #3498db;
}

.blog-main {
  flex: 1;
  max-width: 800px;
}

.blog-footer {
  background: #2c3e50;
  color: white;
  text-align: center;
  padding: 2rem 0;
  margin-top: auto;
}
</style>

ページでのカスタムレイアウト使用

vue<!-- pages/admin/dashboard.vue -->
<template>
  <div class="dashboard">
    <h1>ダッシュボード</h1>
    <div class="stats-grid">
      <div class="stat-card">
        <h3>総ユーザー数</h3>
        <p class="stat-number">1,234</p>
      </div>
      <div class="stat-card">
        <h3>今日のアクセス</h3>
        <p class="stat-number">567</p>
      </div>
      <div class="stat-card">
        <h3>記事数</h3>
        <p class="stat-number">89</p>
      </div>
    </div>
  </div>
</template>

<script setup>
definePageMeta({
  layout: 'admin',
});
</script>

<style scoped>
.dashboard {
  padding: 1rem;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(250px, 1fr)
  );
  gap: 1rem;
  margin-top: 2rem;
}

.stat-card {
  background: white;
  padding: 1.5rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.stat-number {
  font-size: 2rem;
  font-weight: bold;
  color: #3498db;
  margin: 0;
}
</style>

レイアウトの動的切り替え

ユーザーの操作や状態に応じて、レイアウトを動的に切り替えることができます。これにより、より柔軟でインタラクティブなユーザー体験を提供できます。

条件付きレイアウト切り替え

vue<!-- pages/blog/[slug].vue -->
<template>
  <div class="blog-post">
    <article>
      <h1>{{ post.title }}</h1>
      <div class="meta">
        <span>{{ post.date }}</span>
        <span>{{ post.author }}</span>
      </div>
      <div class="content" v-html="post.content"></div>
    </article>
  </div>
</template>

<script setup>
const route = useRoute();
const { data: post } = await useFetch(
  `/api/posts/${route.params.slug}`
);

// 投稿の種類に応じてレイアウトを動的に切り替え
const layout = computed(() => {
  if (post.value?.type === 'premium') {
    return 'premium-blog';
  }
  return 'blog';
});

definePageMeta({
  layout: false, // 動的レイアウトのためfalseに設定
});
</script>

<style scoped>
.blog-post {
  max-width: 800px;
  margin: 0 auto;
}

.meta {
  color: #666;
  margin-bottom: 2rem;
}

.meta span {
  margin-right: 1rem;
}

.content {
  line-height: 1.8;
}
</style>

レイアウトコンポーネントでの動的切り替え

vue<!-- layouts/dynamic.vue -->
<template>
  <div>
    <NuxtLayout :name="currentLayout">
      <slot />
    </NuxtLayout>
  </div>
</template>

<script setup>
const route = useRoute();
const user = useUser(); // ユーザー情報を取得するコンポーザブル

// ルートとユーザー状態に応じてレイアウトを決定
const currentLayout = computed(() => {
  // 管理画面の場合はadminレイアウト
  if (route.path.startsWith('/admin')) {
    return 'admin';
  }

  // プレミアムユーザーの場合はpremiumレイアウト
  if (user.value?.isPremium) {
    return 'premium';
  }

  // デフォルトはblogレイアウト
  return 'blog';
});
</script>

エラー処理とフォールバック

vue<!-- layouts/error.vue -->
<template>
  <div class="error-layout">
    <header class="error-header">
      <h1>エラーが発生しました</h1>
    </header>

    <main class="error-main">
      <div class="error-content">
        <h2>{{ error.statusCode }}</h2>
        <p>{{ error.message }}</p>

        <div class="error-actions">
          <button @click="handleError">
            トップページに戻る
          </button>
          <button @click="reload">
            ページを再読み込み
          </button>
        </div>
      </div>
    </main>
  </div>
</template>

<script setup>
const error = useError();

const handleError = () => {
  clearError();
  navigateTo('/');
};

const reload = () => {
  window.location.reload();
};
</script>

<style scoped>
.error-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #f8f9fa;
}

.error-header h1 {
  color: #e74c3c;
  margin-bottom: 2rem;
}

.error-content {
  text-align: center;
  max-width: 500px;
}

.error-content h2 {
  font-size: 4rem;
  color: #e74c3c;
  margin-bottom: 1rem;
}

.error-actions {
  margin-top: 2rem;
  display: flex;
  gap: 1rem;
  justify-content: center;
}

.error-actions button {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.error-actions button:first-child {
  background: #3498db;
  color: white;
}

.error-actions button:last-child {
  background: #95a5a6;
  color: white;
}

.error-actions button:hover {
  opacity: 0.8;
}
</style>

レスポンシブデザイン対応

現代の Web サイトでは、様々なデバイスに対応したレスポンシブデザインが必須です。Nuxt のレイアウト機能と CSS を組み合わせることで、美しいレスポンシブデザインを実現できます。

モバイルファーストのレイアウト

vue<!-- layouts/responsive.vue -->
<template>
  <div class="responsive-layout">
    <header class="header">
      <div class="header-content">
        <div class="logo">
          <h1>サイト名</h1>
        </div>

        <!-- デスクトップ用ナビゲーション -->
        <nav class="desktop-nav">
          <NuxtLink to="/">ホーム</NuxtLink>
          <NuxtLink to="/services">サービス</NuxtLink>
          <NuxtLink to="/about">会社概要</NuxtLink>
          <NuxtLink to="/contact">お問い合わせ</NuxtLink>
        </nav>

        <!-- モバイル用ハンバーガーメニュー -->
        <button
          class="mobile-menu-toggle"
          @click="toggleMobileMenu"
          :aria-expanded="isMobileMenuOpen"
        >
          <span></span>
          <span></span>
          <span></span>
        </button>
      </div>

      <!-- モバイル用ナビゲーション -->
      <nav
        class="mobile-nav"
        :class="{ 'is-open': isMobileMenuOpen }"
      >
        <NuxtLink to="/" @click="closeMobileMenu"
          >ホーム</NuxtLink
        >
        <NuxtLink to="/services" @click="closeMobileMenu"
          >サービス</NuxtLink
        >
        <NuxtLink to="/about" @click="closeMobileMenu"
          >会社概要</NuxtLink
        >
        <NuxtLink to="/contact" @click="closeMobileMenu"
          >お問い合わせ</NuxtLink
        >
      </nav>
    </header>

    <main class="main-content">
      <slot />
    </main>

    <footer class="footer">
      <div class="footer-content">
        <div class="footer-section">
          <h3>会社情報</h3>
          <p>私たちは最高品質のサービスを提供します。</p>
        </div>
        <div class="footer-section">
          <h3>お問い合わせ</h3>
          <p>Email: info@example.com</p>
          <p>Tel: 03-1234-5678</p>
        </div>
        <div class="footer-section">
          <h3>SNS</h3>
          <div class="social-links">
            <a href="#" aria-label="Twitter">Twitter</a>
            <a href="#" aria-label="Facebook">Facebook</a>
            <a href="#" aria-label="Instagram">Instagram</a>
          </div>
        </div>
      </div>
      <div class="footer-bottom">
        <p>&copy; 2024 サイト名. All rights reserved.</p>
      </div>
    </footer>
  </div>
</template>

<script setup>
const isMobileMenuOpen = ref(false);

const toggleMobileMenu = () => {
  isMobileMenuOpen.value = !isMobileMenuOpen.value;
};

const closeMobileMenu = () => {
  isMobileMenuOpen.value = false;
};

// 画面サイズが変更されたときにメニューを閉じる
onMounted(() => {
  const handleResize = () => {
    if (window.innerWidth > 768) {
      closeMobileMenu();
    }
  };

  window.addEventListener('resize', handleResize);

  onUnmounted(() => {
    window.removeEventListener('resize', handleResize);
  });
});
</script>

<style scoped>
.responsive-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.header {
  background: #2c3e50;
  color: white;
  position: sticky;
  top: 0;
  z-index: 100;
}

.header-content {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.logo h1 {
  margin: 0;
  font-size: 1.5rem;
}

.desktop-nav {
  display: flex;
  gap: 2rem;
}

.desktop-nav a {
  color: white;
  text-decoration: none;
  transition: color 0.3s ease;
}

.desktop-nav a:hover {
  color: #3498db;
}

.mobile-menu-toggle {
  display: none;
  flex-direction: column;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.5rem;
}

.mobile-menu-toggle span {
  width: 25px;
  height: 3px;
  background: white;
  margin: 3px 0;
  transition: 0.3s;
}

.mobile-nav {
  display: none;
  background: #34495e;
  padding: 1rem;
}

.mobile-nav.is-open {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.mobile-nav a {
  color: white;
  text-decoration: none;
  padding: 0.5rem 0;
  border-bottom: 1px solid #4a5a6a;
}

.main-content {
  flex: 1;
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem 1rem;
  width: 100%;
}

.footer {
  background: #34495e;
  color: white;
  margin-top: auto;
}

.footer-content {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem 1rem;
  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(250px, 1fr)
  );
  gap: 2rem;
}

.footer-section h3 {
  margin-bottom: 1rem;
  color: #3498db;
}

.social-links {
  display: flex;
  gap: 1rem;
}

.social-links a {
  color: white;
  text-decoration: none;
}

.footer-bottom {
  background: #2c3e50;
  text-align: center;
  padding: 1rem;
}

/* レスポンシブ対応 */
@media (max-width: 768px) {
  .desktop-nav {
    display: none;
  }

  .mobile-menu-toggle {
    display: flex;
  }

  .main-content {
    padding: 1rem;
  }

  .footer-content {
    grid-template-columns: 1fr;
    text-align: center;
  }
}

@media (max-width: 480px) {
  .header-content {
    padding: 0.5rem;
  }

  .logo h1 {
    font-size: 1.2rem;
  }

  .main-content {
    padding: 0.5rem;
  }
}
</style>

ブレークポイントの管理

vue<!-- composables/useBreakpoints.js -->
export const useBreakpoints = () => { const breakpoints = {
sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536 } const
currentBreakpoint = ref('lg') const updateBreakpoint = () =>
{ const width = window.innerWidth if (width <
breakpoints.sm) { currentBreakpoint.value = 'xs' } else if
(width < breakpoints.md) { currentBreakpoint.value = 'sm' }
else if (width < breakpoints.lg) { currentBreakpoint.value =
'md' } else if (width < breakpoints.xl) {
currentBreakpoint.value = 'lg' } else if (width <
breakpoints['2xl']) { currentBreakpoint.value = 'xl' } else
{ currentBreakpoint.value = '2xl' } } const isMobile =
computed(() => ['xs',
'sm'].includes(currentBreakpoint.value) ) const isTablet =
computed(() => currentBreakpoint.value === 'md' ) const
isDesktop = computed(() => ['lg', 'xl',
'2xl'].includes(currentBreakpoint.value) ) onMounted(() => {
updateBreakpoint() window.addEventListener('resize',
updateBreakpoint) }) onUnmounted(() => {
window.removeEventListener('resize', updateBreakpoint) })
return { currentBreakpoint: readonly(currentBreakpoint),
isMobile, isTablet, isDesktop } }

レイアウトとコンポーネントの連携

レイアウトとコンポーネントを効果的に連携させることで、再利用性が高く保守しやすいコードを書くことができます。

レイアウト内でのコンポーネント使用

vue<!-- components/AppHeader.vue -->
<template>
  <header class="app-header">
    <div class="header-container">
      <div class="logo">
        <NuxtLink to="/">
          <img src="/logo.svg" alt="ロゴ" />
        </NuxtLink>
      </div>

      <nav class="navigation">
        <AppNavLink
          v-for="item in navItems"
          :key="item.path"
          :to="item.path"
          :label="item.label"
        />
      </nav>

      <div class="user-actions">
        <AppThemeToggle />
        <AppUserMenu v-if="user" />
        <AppLoginButton v-else />
      </div>
    </div>
  </header>
</template>

<script setup>
const user = useUser();

const navItems = [
  { path: '/', label: 'ホーム' },
  { path: '/products', label: '製品' },
  { path: '/about', label: '会社概要' },
  { path: '/contact', label: 'お問い合わせ' },
];
</script>

<style scoped>
.app-header {
  background: var(--header-bg);
  border-bottom: 1px solid var(--border-color);
  position: sticky;
  top: 0;
  z-index: 100;
}

.header-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.logo img {
  height: 40px;
}

.navigation {
  display: flex;
  gap: 2rem;
}

.user-actions {
  display: flex;
  align-items: center;
  gap: 1rem;
}
</style>

レイアウト間でのコンポーネント共有

vue<!-- components/AppSidebar.vue -->
<template>
  <aside
    class="app-sidebar"
    :class="{ 'is-collapsed': isCollapsed }"
  >
    <div class="sidebar-header">
      <h3>メニュー</h3>
      <button
        class="collapse-toggle"
        @click="toggleCollapse"
        :aria-label="
          isCollapsed
            ? 'メニューを展開'
            : 'メニューを折りたたみ'
        "
      >
        <IconChevronLeft v-if="!isCollapsed" />
        <IconChevronRight v-else />
      </button>
    </div>

    <nav class="sidebar-nav">
      <AppSidebarItem
        v-for="item in sidebarItems"
        :key="item.path"
        :item="item"
        :is-collapsed="isCollapsed"
      />
    </nav>
  </aside>
</template>

<script setup>
const isCollapsed = ref(false);

const toggleCollapse = () => {
  isCollapsed.value = !isCollapsed.value;
};

const sidebarItems = [
  {
    path: '/dashboard',
    label: 'ダッシュボード',
    icon: 'IconDashboard',
  },
  {
    path: '/users',
    label: 'ユーザー管理',
    icon: 'IconUsers',
  },
  {
    path: '/settings',
    label: '設定',
    icon: 'IconSettings',
  },
];
</script>

<style scoped>
.app-sidebar {
  width: 250px;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--border-color);
  transition: width 0.3s ease;
  overflow: hidden;
}

.app-sidebar.is-collapsed {
  width: 60px;
}

.sidebar-header {
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.collapse-toggle {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.5rem;
  border-radius: 4px;
  transition: background-color 0.3s ease;
}

.collapse-toggle:hover {
  background: var(--hover-bg);
}

.sidebar-nav {
  padding: 1rem 0;
}
</style>

レイアウトのスロット活用

vue<!-- layouts/advanced.vue -->
<template>
  <div class="advanced-layout">
    <AppHeader />

    <div class="layout-body">
      <AppSidebar v-if="showSidebar" />

      <main class="main-content">
        <div class="content-header" v-if="$slots.header">
          <slot name="header" />
        </div>

        <div class="content-body">
          <slot />
        </div>

        <div class="content-footer" v-if="$slots.footer">
          <slot name="footer" />
        </div>
      </main>
    </div>

    <AppFooter />
  </div>
</template>

<script setup>
const route = useRoute();

// サイドバーを表示するかどうかを判定
const showSidebar = computed(() => {
  return (
    route.path.startsWith('/admin') ||
    route.path.startsWith('/dashboard')
  );
});
</script>

<style scoped>
.advanced-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.layout-body {
  flex: 1;
  display: flex;
}

.main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 2rem;
}

.content-header {
  margin-bottom: 2rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border-color);
}

.content-body {
  flex: 1;
}

.content-footer {
  margin-top: 2rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border-color);
}
</style>

ページでのスロット使用

vue<!-- pages/admin/users.vue -->
<template>
  <div class="users-page">
    <template #header>
      <div class="page-header">
        <h1>ユーザー管理</h1>
        <button class="add-user-btn">
          新規ユーザー追加
        </button>
      </div>
    </template>

    <div class="users-table">
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>名前</th>
            <th>メールアドレス</th>
            <th>ステータス</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="user in users" :key="user.id">
            <td>{{ user.id }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
            <td>
              <AppStatusBadge :status="user.status" />
            </td>
            <td>
              <AppActionButtons :user="user" />
            </td>
          </tr>
        </tbody>
      </table>
    </div>

    <template #footer>
      <div class="page-footer">
        <p>合計 {{ users.length }} 件のユーザー</p>
      </div>
    </template>
  </div>
</template>

<script setup>
definePageMeta({
  layout: 'advanced',
});

const { data: users } = await useFetch('/api/users');
</script>

<style scoped>
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.add-user-btn {
  background: #3498db;
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  cursor: pointer;
}

.users-table {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  padding: 1rem;
  text-align: left;
  border-bottom: 1px solid var(--border-color);
}

th {
  background: var(--table-header-bg);
  font-weight: 600;
}

.page-footer {
  text-align: center;
  color: var(--text-muted);
}
</style>

パフォーマンス最適化

レイアウト機能を使用する際のパフォーマンス最適化について解説します。適切な最適化を行うことで、ユーザー体験を向上させることができます。

レイアウトの遅延読み込み

vue<!-- layouts/lazy.vue -->
<template>
  <div class="lazy-layout">
    <Suspense>
      <template #default>
        <LazyAppHeader />
      </template>
      <template #fallback>
        <div class="header-skeleton">
          <div class="skeleton-item"></div>
          <div class="skeleton-item"></div>
          <div class="skeleton-item"></div>
        </div>
      </template>
    </Suspense>

    <main class="main-content">
      <slot />
    </main>

    <Suspense>
      <template #default>
        <LazyAppFooter />
      </template>
      <template #fallback>
        <div class="footer-skeleton">
          <div class="skeleton-item"></div>
        </div>
      </template>
    </Suspense>
  </div>
</template>

<style scoped>
.lazy-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.main-content {
  flex: 1;
  padding: 2rem;
}

.header-skeleton,
.footer-skeleton {
  padding: 1rem;
  background: #f8f9fa;
}

.skeleton-item {
  height: 20px;
  background: #e9ecef;
  border-radius: 4px;
  margin-bottom: 0.5rem;
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}
</style>

レイアウトのキャッシュ戦略

vue<!-- layouts/cached.vue -->
<template>
  <div class="cached-layout">
    <ClientOnly>
      <AppHeader :key="headerKey" />
      <template #fallback>
        <div class="header-placeholder"></div>
      </template>
    </ClientOnly>

    <main class="main-content">
      <slot />
    </main>

    <ClientOnly>
      <AppFooter :key="footerKey" />
      <template #fallback>
        <div class="footer-placeholder"></div>
      </template>
    </ClientOnly>
  </div>
</template>

<script setup>
// レイアウトのキャッシュキーを生成
const headerKey = computed(() => {
  const route = useRoute();
  return `header-${route.path}`;
});

const footerKey = computed(() => {
  const route = useRoute();
  return `footer-${route.path}`;
});

// レイアウトのプリロード
onMounted(() => {
  // 次のページのレイアウトをプリロード
  const preloadNextLayout = () => {
    const links = document.querySelectorAll('a[href]');
    links.forEach((link) => {
      if (link.href.startsWith(window.location.origin)) {
        const url = new URL(link.href);
        if (url.pathname !== window.location.pathname) {
          // 次のページのレイアウトをプリロード
          const linkElement =
            document.createElement('link');
          linkElement.rel = 'prefetch';
          linkElement.href = link.href;
          document.head.appendChild(linkElement);
        }
      }
    });
  };

  // ページ読み込み完了後にプリロードを実行
  setTimeout(preloadNextLayout, 1000);
});
</script>

<style scoped>
.cached-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.main-content {
  flex: 1;
  padding: 2rem;
}

.header-placeholder,
.footer-placeholder {
  height: 60px;
  background: #f8f9fa;
}
</style>

レイアウトの最適化テクニック

vue<!-- layouts/optimized.vue -->
<template>
  <div class="optimized-layout">
    <!-- 重要なコンテンツを最初に表示 -->
    <main class="main-content">
      <slot />
    </main>

    <!-- 非重要なコンテンツを後で読み込み -->
    <template v-if="showHeader">
      <AppHeader />
    </template>

    <template v-if="showFooter">
      <AppFooter />
    </template>
  </div>
</template>

<script setup>
const route = useRoute();

// レイアウトの表示条件を最適化
const showHeader = computed(() => {
  // 特定のページではヘッダーを非表示
  return !route.path.startsWith('/embed');
});

const showFooter = computed(() => {
  // 特定のページではフッターを非表示
  return !route.path.startsWith('/embed');
});

// レイアウトのメモ化
const layoutConfig = computed(() => {
  return {
    showHeader: showHeader.value,
    showFooter: showFooter.value,
    layoutType: route.path.startsWith('/admin')
      ? 'admin'
      : 'default',
  };
});
</script>

<style scoped>
.optimized-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.main-content {
  flex: 1;
  padding: 2rem;
}

/* 重要なスタイルを最初に読み込み */
@media (max-width: 768px) {
  .main-content {
    padding: 1rem;
  }
}
</style>

エラー処理とフォールバック

vue<!-- layouts/error-handling.vue -->
<template>
  <div class="error-handling-layout">
    <ErrorBoundary>
      <template #default>
        <AppHeader />
      </template>
      <template #error="{ error }">
        <div class="header-error">
          <p>ヘッダーの読み込みに失敗しました</p>
          <button @click="retryHeader">再試行</button>
        </div>
      </template>
    </ErrorBoundary>

    <main class="main-content">
      <slot />
    </main>

    <ErrorBoundary>
      <template #default>
        <AppFooter />
      </template>
      <template #error="{ error }">
        <div class="footer-error">
          <p>フッターの読み込みに失敗しました</p>
        </div>
      </template>
    </ErrorBoundary>
  </div>
</template>

<script setup>
const retryHeader = () => {
  // ヘッダーの再読み込み処理
  window.location.reload();
};
</script>

<style scoped>
.error-handling-layout {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.main-content {
  flex: 1;
  padding: 2rem;
}

.header-error,
.footer-error {
  background: #f8d7da;
  color: #721c24;
  padding: 1rem;
  text-align: center;
  border: 1px solid #f5c6cb;
}

.header-error button {
  background: #dc3545;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 1rem;
}
</style>

まとめ

Nuxt のレイアウト機能を活用することで、サイト全体を美しく統一されたデザインに仕上げることができます。この記事で紹介したテクニックを実践することで、以下のような効果が期待できます。

開発効率の向上

  • 共通部分のコード重複を削減
  • レイアウトの変更が全ページに反映
  • 保守性の高いコード構造

ユーザー体験の向上

  • 一貫性のあるデザイン
  • レスポンシブ対応
  • 高速なページ読み込み

拡張性の確保

  • 新しいレイアウトの追加が容易
  • コンポーネントの再利用
  • 動的なレイアウト切り替え

レイアウト機能は、Nuxt の強力な機能の一つです。適切に活用することで、美しく保守しやすい Web サイトを効率的に構築できます。今回学んだ内容を実践に活かして、素晴らしい Web サイトを作成してください。

関連リンク