asopi tech OSS Developer
asopi tech
asopi tech OSS Developer
Astro Features Used in the Site Redesign: A Technical Deep Dive

【2025年11月版】

Astro Features Used in the Site Redesign: A Technical Deep Dive

公開日: 2025/11/27
読了時間: 約 5分

Introduction

In the previous article, I introduced the overview of the site redesign. Now let’s dive into the technical details.

I’ve fully utilized various Astro features to build this site, so I’ll share the key implementation points.

Leveraging Content Collections

Astro’s Content Collections was the most utilized feature in this redesign.

Schema Definition

Collection schemas are defined in src/content/config.ts.

src/content/config.ts
import { z, defineCollection } from 'astro:content';

const localeEnum = z.enum(['ja', 'en']);

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    locale: localeEnum.default('ja'),
    translationOf: z.string().optional(),
    title: z.string(),
    description: z.string(),
    category: z.enum(['dev', 'infra', 'data', 'ai', 'community', 'life']),
    cover: z.string().optional(),
    tags: z.array(z.string()).default([]),
    published: z.boolean(),
    published_at: z.date().optional(),
  }),
});

The key point is adding a locale field. This allows managing both Japanese and English articles within the same collection.

JSON/YAML Collections

Not just blog posts, but site copy (text) and navigation settings are also managed with Content Collections.

src/content/config.ts
const copy = defineCollection({
  type: 'data',
  schema: z.object({
    locale: localeEnum,
    namespace: z.string(),
    entries: z.record(z.string(), z.string()),
  }),
});

const navigation = defineCollection({
  type: 'data',
  schema: z.object({
    locale: localeEnum,
    brand: z.object({...}),
    primary: z.array(...),
    footer: z.object({...}),
  }),
});

By specifying type: 'data', JSON and YAML files can be treated as collections.

This enables managing locale-specific copy in files like src/content/copy/ja/home.yaml or src/content/navigation/en/site.json.

Multilingual (i18n) Support

Built-in i18n Configuration

Basic i18n settings are configured in astro.config.mjs.

astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'ja',
    locales: ['ja', 'en'],
    routing: {
      prefixDefaultLocale: false,
    },
  },
});

With prefixDefaultLocale: false, Japanese uses /blog while English uses /en/blog.

Custom i18n Utilities

Since Astro’s built-in i18n mainly handles routing configuration, I implemented custom text retrieval.

src/lib/i18n.ts
export async function loadCopy(locale: SupportedLocale): Promise<Record<string, string>> {
  const entries = await getCollection('copy', (entry) => entry.data.locale === locale);
  const aggregated = entries.reduce<Record<string, string>>((acc, entry) => {
    const namespaced = Object.entries(entry.data.entries).reduce<Record<string, string>>((map, [key, value]) => {
      const finalKey = entry.data.namespace ? `${entry.data.namespace}.${key}` : key;
      map[finalKey] = value;
      return map;
    }, {});
    return { ...acc, ...namespaced };
  }, {});
  return aggregated;
}

export async function t(locale: SupportedLocale, key: string): Promise<TranslationResult> {
  const copy = await loadCopy(locale);
  if (key in copy) {
    return { value: copy[key], resolvedLocale: locale, isFallback: false };
  }
  // Fallback handling
  const fallbackCopy = await loadCopy(DEFAULT_LOCALE);
  if (key in fallbackCopy) {
    return { value: fallbackCopy[key], resolvedLocale: DEFAULT_LOCALE, isFallback: true };
  }
  return { value: key, resolvedLocale: DEFAULT_LOCALE, isFallback: true };
}

The t() function retrieves copy for the specified locale and falls back to the default locale (Japanese) if not found.

Fallback Notification

When content falls back, a banner notifies the user.

src/components/i18n/FallbackBanner.astro
---
export interface Props {
  message: string;
}
const { message } = Astro.props;
---
{message && (
  <div class="bg-amber-100 border-l-4 border-amber-500 text-amber-700 p-4">
    <p>{message}</p>
  </div>
)}

Service Detail LP Implementation

Each OSS project’s detail page is implemented by defining structured data in Markdown frontmatter.

Service Content Structure

src/content/services/en/alopex-db.md
---
locale: en
serviceId: "alopex-db"
title: "Alopex DB"
summary: "Same data file from embedded to cluster..."
page:
  hero:
    eyebrow: "Unified Database"
    title: "Alopex DB"
    tagline: "1 File, 5 Modes, Infinite Scale"
    description: "..."
    badges: ["Pre-Alpha", "OSS"]
    metrics:
      - label: "Deployment Modes"
        value: "5"
  challenge:
    title: "4 databases for AI apps, 4 problems"
    pains:
      - "SQLite + Vector DB + Graph DB + distributed SQL = 4 systems to manage"
  capabilities:
    groups:
      - title: "WASM & Embedded"
        items: [...]
---

Each section’s data is defined within the page object.

Dynamic Routing

Service pages are generated with dynamic routing.

src/pages/services/[slug].astro
---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const services = await getCollection('services', (entry) => entry.data.locale === 'ja');
  return services.map((service) => ({
    params: { slug: service.data.serviceId },
    props: { service },
  }));
}

const { service } = Astro.props;
---
<ServiceLayout service={service} />

The [slug].astro filename automatically generates URLs like /services/alopex-db and /services/project-jv.

Layout Hierarchy

In this redesign, layouts were organized hierarchically.

Base.astro (Basic HTML structure, meta tags, OGP)
  └─ MarketingLayout.astro (Navigation, footer, ads)
       └─ ServiceLayout.astro (Service LP specific)
       └─ Blog.astro (Blog posts)

Base.astro

The foundation layout for all pages.

src/layouts/Base.astro
---
const currentLocale = Astro.locals.locale ?? 'ja';
const alternateLinks = SUPPORTED_LOCALES.map((lang) => ({
  lang,
  href: new URL(buildLocalizedPath(Astro.url.pathname, lang), SITE_URL).toString(),
}));
---
<html lang={currentLocale}>
  <head>
    <link rel="canonical" href={canonicalUrl} />
    {alternateLinks.map((alt) => (
      <link rel="alternate" hreflang={alt.lang} href={alt.href} />
    ))}
    <meta property="og:locale" content={currentLocale === 'ja' ? 'ja_JP' : 'en_US'} />
  </head>
</html>

For multilingual support, hreflang tags and og:locale are set dynamically.

MarketingLayout.astro

Layout including navigation and footer.

src/layouts/MarketingLayout.astro
---
export interface Props extends BaseProps {
  hideFooter?: boolean;
  showAds?: boolean;
}
---
<Base {...baseProps}>
  <Navigation />
  {showAds && <A8TopBanner />}
  <main>
    <slot />
  </main>
  {showAds && <RakutenBanner />}
  {!hideFooter && <Footer />}
</Base>

The showAds prop controls ad visibility per page.

OG Image Auto-Generation

OG image generation using Satori, introduced in a previous article, continues to be utilized.

Service page OG images are also auto-generated.

src/pages/og/services/[slug].png.ts
export async function getStaticPaths() {
  const services = await getCollection('services');
  return services.map((service) => ({
    params: { slug: service.data.serviceId },
    props: { service },
  }));
}

This auto-generates OG images at URLs like /og/services/alopex-db.png.

Alpine.js Usage

Alpine.js handles the mega menu and mobile menu open/close interactions.

src/components/Navigation.astro
<div x-data="{ open: false }" class="relative">
  <button @click="open = !open" :aria-expanded="open">
    Menu
  </button>
  <div x-show="open" x-transition @click.outside="open = false">
    <!-- Menu content -->
  </div>
</div>

Alpine.js is lightweight, works great with Astro, and is perfect for adding simple interactions.

Site Search with Pagefind

Site search functionality continues to use Pagefind, introduced in a previous article.

With multilingual support, search indexes are now generated for both Japanese and English.

package.json
{
  "scripts": {
    "postbuild": "pagefind --source dist --bundle-dir pagefind"
  }
}

Performance Optimization

Third-Party Script Optimization with Partytown

Third-party scripts like Google Analytics run in a Web Worker using Partytown.

astro.config.mjs
import partytown from "@astrojs/partytown";

export default defineConfig({
  integrations: [
    partytown({
      config: {
        forward: ["dataLayer.push"]
      }
    }),
  ],
});

This reduces main thread load and contributes to improved Core Web Vitals.

Image Optimization

Astro’s built-in image optimization converts images to WebP format and resizes them appropriately.

---
import { Image } from 'astro:assets';
import coverImage from '../../assets/cover/sample.png';
---
<Image src={coverImage} alt="Sample image" width={800} />

Summary

This site redesign utilized various Astro features:

  • Content Collections for type-safe content management
  • i18n support for global site deployment
  • Dynamic routing for efficient page generation
  • Layout hierarchy for improved maintainability
  • Alpine.js for lightweight interactions
  • Pagefind for fast site search
  • Partytown for performance optimization

Astro is an excellent framework that provides all these features while specializing in static site generation.

I hope this was helpful. See you next time!