Next.jsReactTypeScripti18nAWSS3CloudFront

Own Info App をリリースしました — Next.js(SSG)×S3×CloudFrontでつくる最小構成アプリ

Sloth255
Sloth255
·3 min read·543 words

アプリ概要

ネットワーク情報(自分のグローバルIPアドレスなど)やブラウザ・端末情報をサッと確認して、そのままコピー/共有できる小さなWebアプリ「Own Info App」を公開しました。
https://x.com/Sloth255000/status/1986792593569763395https://x.com/Sloth255000/status/1986792593569763395

  • ソース(一部構成抜粋): Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
  • 配信: S3 + CloudFront(Static Export)
  • 多言語: 英語/日本語/中国語/韓国語/ドイツ語/フランス語/スペイン語/ポルトガル語
  • 解析: Firebase Analytics

ページ遷移やUIは極力シンプル。開いて、必要な情報を「コピー」または「共有」して閉じる、それだけです。


技術スタックと設計のポイント

1) Next.js 15 App Router × Static Export

  • output: 'export' による完全静的出力(SSG)
  • 出力ファイルは out 直下に index.html, en.html, ja.html … のフラット構造

この構成により、CDNでの配信とキャッシュが容易になり、動的サーバやSSR無しでもサクサク動くアプリになりました。

2) CloudFront Functions で URL リライト

CloudFront Functions(cloudfront-js-2.0)を使って、URL → 実ファイルの軽量なマッピングを行っています。

  • //index.html
  • /ja/ja.html(末尾スラッシュも同様)
  • /contact, /privacy, /licenses → それぞれ *.html
  • 拡張子(.svg, .js, .css, ...)が付いているURIは素通し(書き換えしない)
  • 未サポートロケールは /en/_not-found にリダイレクト

3) i18nとルーティング

  • 言語は en, ja, zh, ko, de, fr, es, pt をサポート
  • 翻訳は src/locales/*.json に分離し、App Router でロケールごとに SSG
  • ルート / は SEO&ボット向けにデフォルト言語(en)をSSR相当で描画しつつ、ブラウザ訪問時のみ navigator.languages を見て1回だけロケールページへクライアントリダイレクト

4) SEOとOGP/Twitterカード(ロケール単位)

  • generateMetadata() をページごとに実装して「canonical / og:url / og:image / og:locale」を言語単位で正しく出力
  • OGP画像(public/og-*.png)も言語ごとに用意し、かつ画像内の説明文言を翻訳テキストから作成
  • 構造化データ(JSON-LD)はサーバコンポーネントでロケール別に出力(application/ld+json)。8言語すべてを inLanguage にも明示
// 例: 構造化データ出力 (抜粋)
export default function StructuredData({ title, description, siteUrl }: Props) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'WebApplication',
    name: title,
    description,
    url: siteUrl || '/',
    applicationCategory: 'UtilityApplication',
    operatingSystem: 'Web Browser',
    offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
    inLanguage: [
      { '@type': 'Language', name: 'English', alternateName: 'en' },
      { '@type': 'Language', name: 'Japanese', alternateName: 'ja' },
      { '@type': 'Language', name: 'Chinese', alternateName: 'zh' },
      { '@type': 'Language', name: 'Korean', alternateName: 'ko' },
      { '@type': 'Language', name: 'German', alternateName: 'de' },
      { '@type': 'Language', name: 'French', alternateName: 'fr' },
      { '@type': 'Language', name: 'Spanish', alternateName: 'es' },
      { '@type': 'Language', name: 'Portuguese', alternateName: 'pt' },
    ],
  };
  return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />;
}

5) <html lang> をロケールごとに正しく

App Routerでは「ルートレイアウトの <html> が全ページ共通」という前提があり、ビルド時に <html lang="en"> のままになる課題がありました。
今回は「クライアントで <html lang> を上書き」する薄いコンポーネントをロケールレイアウトとルートページの両方に組み込んで、実利用時に正しい言語タグへ更新しています。

'use client'
import { useEffect } from 'react'
export default function HtmlLangSetterClient({ lang }: { lang: string }) {
  useEffect(() => {
    document.documentElement.setAttribute('lang', lang)
  }, [lang])
  return null
}

ビルド時の静的HTMLと、実ユーザーが見るDOMの差分を最小に保ちながら、Static Exportの制約下で実用的な実装に着地できました。


デプロイ/運用

インフラ

  • S3(静的ホスティング)+CloudFront(オリジンはS3のWebsiteエンドポイント)
  • CloudFront Functions は別途デプロイ(SAMテンプレートはリソース作成用、コード更新はCI側で)

CI/CD(GitHub Actions)

  • main への push をトリガーに:
    • Next.js ビルド(Static Export)
    • S3 同期
      • /_next/staticmax-age=31536000, immutable
      • HTMLは max-age=300, must-revalidate
    • CloudFront Function のコード更新(update-functionpublish-function
    • 必要に応じて CloudFront Invalidation(/*

Secrets/Vars として AWS 資格情報や S3バケット、CloudFront Distribution ID、NEXT_PUBLIC_* を設定しています。
CLIでの手動更新も一応可能ですが、基本はCI任せにしています。


まとめ

  • ローカライズ
  • 最低限のSEO
  • 最小構成のインフラ
  • CI/CD・IaC

個人開発をはじめるにあたって、とにかくなんでもいいからアプリをリリースするまでの一連の流れをやってみたかったので、こういったシンプル・低コストのアプリになりました。
Next.jsをはじめて使ってみることができてよかったです。次はSSGではなくSSRでVercelにデプロイしてみようと思います。

つくったアプリはこちら📱

https://own-info-app.sloth255.com/jahttps://own-info-app.sloth255.com/ja