アプリ概要
ネットワーク情報(自分のグローバルIPアドレスなど)やブラウザ・端末情報をサッと確認して、そのままコピー/共有できる小さなWebアプリ「Own Info App」を公開しました。 https://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/staticはmax-age=31536000, immutable- HTMLは
max-age=300, must-revalidate
- CloudFront Function のコード更新(
update-function→publish-function) - 必要に応じて CloudFront Invalidation(
/*)
Secrets/Vars として AWS 資格情報や S3バケット、CloudFront Distribution ID、NEXT_PUBLIC_* を設定しています。
CLIでの手動更新も一応可能ですが、基本はCI任せにしています。
まとめ
- ローカライズ
- 最低限のSEO
- 最小構成のインフラ
- CI/CD・IaC
個人開発をはじめるにあたって、とにかくなんでもいいからアプリをリリースするまでの一連の流れをやってみたかったので、こういったシンプル・低コストのアプリになりました。
Next.jsをはじめて使ってみることができてよかったです。次はSSGではなくSSRでVercelにデプロイしてみようと思います。
