dev-diarynuxtroutingseocontent-managementlayouts

Dev Diary: Building godegit.dev - Phase 4: Page Architecture & Dynamic Routing

From components to complete pages: implementing dynamic routing, SEO optimization, and content-first layouts. Learn how we built scalable page architecture with Nuxt 3's file-based routing and content management.

godegit Development Team
godegit Development Team
11 min read
File explorer showing Nuxt pages directory structure with dynamic routes

Nuxt's file-based routing system organizing our page architecture

From Components to Complete Experiences

Welcome back to our godegit.dev development diary! With our component library battle-tested and green, Phase 4 tackles the exciting challenge of assembling these pieces into complete, dynamic pages. This is where individual components become cohesive user experiences.

Phase 4 taught us that great pages aren't just collections of components—they're carefully orchestrated experiences that guide users through content while maintaining performance and accessibility standards.

The Page Assembly Challenge

Our testing framework had already defined what success looked like:

  • Landing page must load in under 1 second with perfect brand compliance
  • Dynamic documentation must handle any content structure while maintaining SEO
  • Blog routing must provide excellent reading experience with proper typography
  • Error handling must be helpful and brand-consistent
  • Layout system must provide consistent experience across all pages

Each page needed to pass both component-level tests and full integration tests.

Page 1: The Landing Page - First Impressions Matter

Architecture Decisions Driven by Performance Tests

Our performance tests demanded aggressive optimization:

// tests/e2e/performance.spec.ts
test('landing page should meet Core Web Vitals', async ({ page }) => {
  await page.goto('/')

  // Largest Contentful Paint < 2.5s
  const lcp = await page.evaluate(() => {
    return new Promise(resolve => {
      new PerformanceObserver(list => {
        const entries = list.getEntries()
        const lcpEntry = entries[entries.length - 1]
        resolve(lcpEntry.startTime)
      }).observe({ type: 'largest-contentful-paint', buffered: true })
    })
  })

  expect(lcp).toBeLessThan(2500)
})

This test drove us toward a specific landing page architecture:

<!-- pages/index.vue -->
<template>
  <div>
    <!-- Hero Section - Above the fold priority -->
    <MarketingHero />

    <!-- Features Section - Lazy loaded -->
    <MarketingFeatures />

    <!-- Installation Section - Critical for conversion -->
    <MarketingInstallation />

    <!-- Final CTA Section -->
    <section class="bg-brand-dark py-16 sm:py-20 lg:py-24">
      <div class="mx-auto max-w-7xl px-4 text-center sm:px-6 lg:px-8">
        <h2
          class="text-brand-light mb-6 font-heading text-3xl font-bold sm:text-4xl lg:text-5xl"
        >
          Ready to Speed Up Your Git Workflow?
        </h2>

        <!-- Quick stats -->
        <div class="mx-auto grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-3">
          <div class="text-center">
            <div class="text-brand-accent mb-2 font-heading text-3xl font-bold">
              10x
            </div>
            <div class="text-brand-light/80">Faster Downloads</div>
          </div>
          <div class="text-center">
            <div class="text-brand-accent mb-2 font-heading text-3xl font-bold">
              95%
            </div>
            <div class="text-brand-light/80">Less Bandwidth</div>
          </div>
          <div class="text-center">
            <div class="text-brand-accent mb-2 font-heading text-3xl font-bold">
              Zero
            </div>
            <div class="text-brand-light/80">Configuration</div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script setup lang="ts">
// SEO and meta configuration
useSeoMeta({
  title: 'godegit - Fast Git Downloads Without History',
  description:
    'Download Git repository contents efficiently without the full history. Perfect for developers who value speed and simplicity. Get started in seconds.',
  ogTitle: 'godegit - Fast Git Downloads Without History',
  ogDescription:
    'Download Git repository contents efficiently without the full history. Perfect for developers who value speed and simplicity.',
  ogImage: '/og-image.png',
  ogUrl: 'https://godegit.dev',
  twitterCard: 'summary_large_image',
})

// Structured data for SEO
useHead({
  script: [
    {
      type: 'application/ld+json',
      children: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'SoftwareApplication',
        name: 'godegit',
        description: 'Fast Git repository downloads without the full history',
        applicationCategory: 'DeveloperApplication',
        operatingSystem: ['Linux', 'macOS', 'Windows'],
        softwareVersion: '1.0.0',
        downloadUrl: 'https://github.com/godegit/godegit/releases',
      }),
    },
  ],
})
</script>

The Results: Performance Goals Met

$ pnpm test tests/e2e/performance.spec.ts

 landing page should meet Core Web Vitals
 should have minimal layout shift (CLS < 0.1)
 should load critical resources efficiently
 should optimize image loading

Tests  4 passed (4)
Duration  2.1s

Page 2: Dynamic Documentation - Content Architecture at Scale

The Routing Challenge

Documentation sites have unique requirements:

  • Flexible content structure (some docs have subsections, others don't)
  • SEO optimization for every documentation page
  • Navigation generation from content structure
  • Search functionality (future requirement)
  • Brand consistency across all content types

Nuxt's dynamic routing with [...slug].vue became our solution:

<!-- pages/docs/[...slug].vue -->
<template>
  <div class="documentation-layout">
    <div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
      <div class="lg:grid lg:grid-cols-4 lg:gap-8">
        <!-- Documentation Navigation -->
        <aside class="lg:col-span-1">
          <ContentDocNav />
        </aside>

        <!-- Main Documentation Content -->
        <div class="lg:col-span-3">
          <div class="documentation-content">
            <!-- Breadcrumb Navigation -->
            <nav class="breadcrumb mb-6" aria-label="Breadcrumb">
              <ol class="text-brand-muted flex items-center space-x-2 text-sm">
                <li>
                  <NuxtLink
                    to="/docs"
                    class="hover:text-brand-accent transition-colors"
                  >
                    Documentation
                  </NuxtLink>
                </li>
                <li v-if="breadcrumbs.length > 0">
                  <!-- Dynamic breadcrumb generation -->
                  <span v-for="(crumb, index) in breadcrumbs" :key="index">
                    <svg
                      class="text-brand-muted mx-2 h-3 w-3"
                      fill="currentColor"
                      viewBox="0 0 20 20"
                    >
                      <path
                        fill-rule="evenodd"
                        d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
                        clip-rule="evenodd"
                      />
                    </svg>
                    <NuxtLink
                      v-if="index < breadcrumbs.length - 1"
                      :to="crumb.path"
                    >
                      {{ crumb.title }}
                    </NuxtLink>
                    <span v-else class="text-brand-dark font-medium">{{
                      crumb.title
                    }}</span>
                  </span>
                </li>
              </ol>
            </nav>

            <!-- Document Content -->
            <article class="prose prose-lg max-w-none">
              <ContentDoc v-slot="{ doc }" :path="$route.path">
                <ContentRenderer :value="doc" />
              </ContentDoc>
            </article>

            <!-- Navigation Footer -->
            <footer
              class="doc-navigation-footer mt-12 border-t border-gray-200 pt-8"
            >
              <div class="flex items-center justify-between">
                <div v-if="prev">
                  <p class="text-brand-muted mb-1 text-sm">Previous</p>
                  <NuxtLink
                    :to="prev.path"
                    class="text-brand-accent hover:text-brand-accent/80 font-medium"
                  >
                    {{ prev.title }}
                  </NuxtLink>
                </div>
                <div v-if="next" class="text-right">
                  <p class="text-brand-muted mb-1 text-sm">Next</p>
                  <NuxtLink
                    :to="next.path"
                    class="text-brand-accent hover:text-brand-accent/80 font-medium"
                  >
                    {{ next.title }}
                  </NuxtLink>
                </div>
              </div>
            </footer>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
const route = useRoute()

// Fetch document data
const { data } = await useAsyncData('doc-' + route.path, () =>
  queryContent(route.path)
    .findOne()
    .catch(() => null)
)

// Handle 404 for missing documentation
if (!data.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Documentation page not found',
  })
}

// Generate breadcrumbs from route path
const breadcrumbs = computed(() => {
  const pathSegments = route.path.split('/').filter(Boolean)
  const crumbs = []

  for (let i = 1; i < pathSegments.length; i++) {
    const path = '/' + pathSegments.slice(0, i + 1).join('/')
    const title = pathSegments[i]
      .replace(/-/g, ' ')
      .replace(/\b\w/g, l => l.toUpperCase())
    crumbs.push({ path, title })
  }

  return crumbs
})

// Previous/Next navigation
const [prev, next] = await queryContent('docs')
  .only(['_path', 'title'])
  .findSurround(route.path)

// SEO configuration
useSeoMeta({
  title: data.value?.title
    ? `${data.value.title} - godegit Documentation`
    : 'godegit Documentation',
  description:
    data.value?.description ||
    'Learn how to use godegit for fast Git repository downloads without history.',
})
</script>

Content Processing Innovation

One of our biggest breakthroughs was automated content processing. Instead of manually managing navigation and metadata, we built systems that extract structure from content:

// Content processing logic
const generateNavigation = async () => {
  const docs = await queryContent('docs')
    .only(['_path', 'title', 'category', 'order'])
    .sort({ category: 1, order: 1 })
    .find()

  const navigation = {}
  docs.forEach(doc => {
    if (!navigation[doc.category]) {
      navigation[doc.category] = []
    }
    navigation[doc.category].push({
      title: doc.title,
      path: doc._path,
    })
  })

  return navigation
}

Page 3: Blog Architecture - Typography-First Design

The Reading Experience Challenge

Our blog tests demanded exceptional reading experience:

test('should provide excellent reading typography', async ({ page }) => {
  await page.goto('/blog/dev-diary-phase-1-foundation')

  // Test serif typography for body text
  const blogContent = page.locator('.blog-content')
  await expect(blogContent).toHaveCSS('font-family', /serif/)

  // Test optimal line length for readability
  const contentWidth = await blogContent.evaluate(el => el.offsetWidth)
  expect(contentWidth).toBeLessThanOrEqual(800) // Optimal reading width

  // Test proper heading hierarchy
  const headings = await page.locator('h1, h2, h3').all()
  expect(headings.length).toBeGreaterThan(0)
})

This drove us toward a typography-focused blog architecture:

<!-- pages/blog/[...slug].vue -->
<template>
  <div class="blog-layout">
    <div class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
      <article v-if="data" class="blog-post">
        <!-- Post Header -->
        <header class="mb-12">
          <div class="mb-6">
            <!-- Categories/Tags -->
            <div
              v-if="data.tags && data.tags.length > 0"
              class="mb-4 flex flex-wrap gap-2"
            >
              <span
                v-for="tag in data.tags"
                :key="tag"
                class="bg-brand-accent/10 text-brand-accent inline-flex items-center rounded-full px-3 py-1 text-sm font-medium"
              >
                {{ tag }}
              </span>
            </div>

            <!-- Title with serif typography -->
            <h1
              class="text-brand-dark mb-6 font-serif text-4xl font-bold leading-tight sm:text-5xl lg:text-6xl"
            >
              {{ data.title }}
            </h1>

            <!-- Subtitle/Description -->
            <p
              v-if="data.description"
              class="text-brand-muted mb-8 font-serif text-xl leading-relaxed sm:text-2xl"
            >
              {{ data.description }}
            </p>
          </div>

          <!-- Post Meta -->
          <div
            class="text-brand-muted flex items-center space-x-6 border-b border-gray-200 pb-6 text-sm"
          >
            <div v-if="data.author" class="flex items-center space-x-2">
              <span class="font-medium">{{ data.author }}</span>
            </div>
            <div v-if="data.publishedAt" class="flex items-center space-x-1">
              <time :datetime="data.publishedAt">{{
                formatDate(data.publishedAt)
              }}</time>
            </div>
            <div v-if="data.readingTime" class="flex items-center space-x-1">
              <span>{{ data.readingTime }} min read</span>
            </div>
          </div>
        </header>

        <!-- Post Content with serif typography -->
        <div class="blog-content prose prose-lg max-w-none">
          <ContentDoc v-slot="{ doc }" :path="$route.path">
            <ContentRenderer :value="doc" />
          </ContentDoc>
        </div>
      </article>
    </div>
  </div>
</template>

<style scoped>
/* Blog-specific typography with serif fonts */
.blog-post h1,
.blog-post .font-serif {
  font-family: Georgia, 'Times New Roman', serif;
}

/* Blog content prose styling with serif typography */
:deep(.blog-content.prose) {
  font-family: Georgia, 'Times New Roman', serif;
  line-height: 1.8;
}

:deep(.blog-content.prose p) {
  @apply mb-6 text-lg leading-relaxed;
}

:deep(.blog-content.prose blockquote) {
  @apply border-brand-accent my-8 border-l-4 bg-gray-50 py-4 pl-6 text-lg italic;
  font-family: Georgia, 'Times New Roman', serif;
}
</style>

Page 4: Error Handling - When Things Go Wrong

User-Friendly Error Experience

Even our error pages needed to maintain brand consistency and provide helpful guidance:

<!-- error.vue -->
<template>
  <div
    class="error-page bg-brand-light flex min-h-screen items-center justify-center"
  >
    <div class="w-full max-w-md text-center">
      <!-- Error illustration -->
      <div class="mb-8">
        <div
          class="bg-brand-accent/10 mx-auto flex h-24 w-24 items-center justify-center rounded-full"
        >
          <svg
            v-if="error.statusCode === 404"
            class="text-brand-accent h-12 w-12"
          >
            <!-- 404 icon -->
          </svg>
          <svg v-else class="text-brand-accent h-12 w-12">
            <!-- General error icon -->
          </svg>
        </div>
      </div>

      <!-- Error content -->
      <div class="mb-8">
        <h1
          class="text-brand-dark mb-4 font-heading text-6xl font-bold sm:text-7xl"
        >
          {{ error.statusCode }}
        </h1>

        <h2
          class="text-brand-dark mb-4 font-heading text-2xl font-semibold sm:text-3xl"
        >
          {{ errorTitle }}
        </h2>

        <p class="text-brand-muted text-lg leading-relaxed">
          {{ errorMessage }}
        </p>
      </div>

      <!-- Action buttons -->
      <div class="flex flex-col justify-center gap-4 sm:flex-row">
        <NuxtLink to="/" class="btn-primary">Go Home</NuxtLink>
        <button @click="$router.go(-1)" class="btn-outline">Go Back</button>
      </div>

      <!-- Help links for 404 -->
      <div
        v-if="error.statusCode === 404"
        class="mt-12 border-t border-gray-200 pt-8"
      >
        <p class="text-brand-muted mb-4 text-sm">
          Looking for something specific?
        </p>
        <div class="flex flex-col gap-2 sm:flex-row sm:justify-center sm:gap-6">
          <NuxtLink
            to="/docs"
            class="text-brand-accent hover:text-brand-accent/80 text-sm font-medium"
          >
            Documentation
          </NuxtLink>
          <NuxtLink
            to="/blog"
            class="text-brand-accent hover:text-brand-accent/80 text-sm font-medium"
          >
            Blog
          </NuxtLink>
          <a
            href="https://github.com/godegit/godegit"
            class="text-brand-accent hover:text-brand-accent/80 text-sm font-medium"
          >
            GitHub
          </a>
        </div>
      </div>
    </div>
  </div>
</template>

Layout System: Consistency Across All Pages

The Default Layout Foundation

Our layout system provides consistent structure while allowing page-specific customization:

<!-- layouts/default.vue -->
<template>
  <div class="bg-brand-light min-h-screen">
    <!-- Header -->
    <AppHeader />

    <!-- Main content -->
    <main role="main">
      <slot />
    </main>

    <!-- Footer -->
    <AppFooter />
  </div>
</template>

<script setup lang="ts">
/**
 * Default Layout
 * Provides consistent structure across all pages
 * Maintains brand color scheme and accessibility standards
 */
</script>

<style scoped>
/* Ensure proper layout structure */
.min-h-screen {
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
}

/* Focus management for accessibility */
main:focus {
  @apply outline-none;
}
</style>

SEO Architecture: Built for Discovery

Comprehensive Meta Tag Management

Every page includes comprehensive SEO optimization:

// SEO composable for reusable meta tag management
export const useSEO = (options: {
  title?: string
  description?: string
  image?: string
  type?: string
  url?: string
}) => {
  const { $config } = useNuxtApp()
  const route = useRoute()

  const seoData = {
    title: options.title || 'godegit - Fast Git Downloads Without History',
    description:
      options.description ||
      'Download Git repository contents efficiently without the full history.',
    image: options.image || '/og-image.png',
    type: options.type || 'website',
    url: options.url || `https://godegit.dev${route.path}`,
  }

  useSeoMeta({
    title: seoData.title,
    description: seoData.description,
    ogTitle: seoData.title,
    ogDescription: seoData.description,
    ogImage: seoData.image,
    ogUrl: seoData.url,
    ogType: seoData.type,
    twitterCard: 'summary_large_image',
    twitterTitle: seoData.title,
    twitterDescription: seoData.description,
    twitterImage: seoData.image,
  })

  // Structured data for rich snippets
  useHead({
    script: [
      {
        type: 'application/ld+json',
        children: JSON.stringify({
          '@context': 'https://schema.org',
          '@type': seoData.type === 'article' ? 'BlogPosting' : 'WebPage',
          headline: seoData.title,
          description: seoData.description,
          image: seoData.image,
          url: seoData.url,
        }),
      },
    ],
  })
}

Performance Optimization Results

Our page-level optimizations delivered measurable results:

$ pnpm build && pnpm preview

# Lighthouse scores for key pages:
# Landing Page: 98 Performance, 100 Accessibility, 100 Best Practices, 100 SEO
# Documentation: 96 Performance, 100 Accessibility, 100 Best Practices, 100 SEO
# Blog Posts: 95 Performance, 100 Accessibility, 100 Best Practices, 100 SEO

Key optimizations that made the difference:

  1. Critical CSS Inlining: Above-the-fold styles loaded immediately
  2. Image Optimization: WebP/AVIF with proper sizing and lazy loading
  3. JavaScript Code Splitting: Route-based chunks for optimal loading
  4. Font Optimization: Preload critical fonts with font-display: swap
  5. Static Generation: Pre-rendered HTML for instant loading

Challenges and Solutions

Challenge 1: Dynamic Content SEO

Problem: How do you ensure dynamically generated pages have proper SEO?

Solution: We built SEO into the content processing pipeline:

## <!-- content/docs/installation.md -->

title: "Installation Guide" description: "Complete installation instructions for
godegit across all platforms" keywords: ["installation", "setup", "download",
"godegit"] category: "getting-started" order: 2

---

Every content file includes SEO metadata that gets automatically processed into proper meta tags.

Challenge 2: Route-Based Code Splitting

Problem: How do you optimize JavaScript loading without hurting user experience?

Solution: We used Nuxt's automatic code splitting with strategic preloading:

<!-- Preload critical routes -->
<template>
  <div>
    <NuxtLink to="/docs" prefetch>Documentation</NuxtLink>
    <NuxtLink to="/blog" prefetch>Blog</NuxtLink>
  </div>
</template>

Challenge 3: Content Processing Performance

Problem: How do you handle content processing without slowing down builds?

Solution: We implemented intelligent caching and incremental builds:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      routes: ['/sitemap.xml'],
      crawlLinks: true,
    },
    storage: {
      content: {
        driver: 'fs',
        base: './content',
      },
    },
  },
})

The Integration Victory

After implementing all page architecture, our integration tests showed the full system working together:

$ pnpm test tests/e2e/

 tests/e2e/landing-page.spec.ts (5 tests)
 tests/e2e/documentation.spec.ts (6 tests)
 tests/e2e/blog.spec.ts (4 tests)
 tests/e2e/responsive.spec.ts (8 tests)
 tests/e2e/performance.spec.ts (4 tests)
 tests/e2e/accessibility.spec.ts (7 tests)

Test Files  6 passed (6)
Tests       34 passed (34)
Duration    12.4s

Every page working perfectly across all devices and browsers.

What's Next: Phase 5 - Content Management

With our page architecture solid, Phase 5 will focus on empowering content creators with a powerful, brand-compliant content management system. We'll cover:

  • Decap CMS integration for non-technical content editing
  • Content validation to maintain brand standards
  • Preview systems for safe content publishing
  • Workflow management for content approval processes

Key Page Architecture Takeaways

  1. Performance is User Experience: Fast pages aren't just nice to have—they're essential for user engagement
  2. SEO as Architecture: Build SEO considerations into your routing and content systems from the start
  3. Typography Drives Design: Great reading experiences start with thoughtful typography choices
  4. Error States Matter: How you handle failure is part of your brand experience
  5. Content-First Routing: Let your content structure drive your routing decisions

Building scalable page architecture taught us that the best systems are invisible to users. When routing, SEO, and content management work seamlessly together, users can focus on what matters—consuming and engaging with your content.


Next time, we'll dive into content management systems and show how to empower content creators while maintaining technical and brand standards.

Live pages: Experience our page architecture at godegit.devPerformance scores: View our Lighthouse reports

Tagged with

dev-diarynuxtroutingseocontent-managementlayouts

Share this post

godegit Development Team

godegit Development Team

The engineering team behind godegit, sharing insights on building modern, SEO-optimized web applications with Vue and Nuxt.

Related Posts