Released Own Info App — Minimal App with Next.js (SSG) × S3 × CloudFront
Released Own Info App — Minimal App with Next.js (SSG) × S3 × CloudFront
App Overview
I've released "Own Info App," a small web application where you can quickly check network information (like your global IP address) and browser/device information, then copy/share it immediately.
https://x.com/Sloth255000/status/1986792593569763395- Source (partial configuration): Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
- Distribution: S3 + CloudFront (Static Export)
- Multilingual: English/Japanese/Chinese/Korean/German/French/Spanish/Portuguese
- Analytics: Firebase Analytics
Page transitions and UI are kept as simple as possible. Just open it, "copy" or "share" the information you need, and close it—that's all.
Tech Stack and Design Points
1) Next.js 15 App Router × Static Export
- Fully static output (SSG) with
output: 'export' - Output files have a flat structure with
index.html,en.html,ja.html, etc. directly under the out directory
This configuration makes CDN distribution and caching easy, creating a snappy app without dynamic servers or SSR.
2) URL Rewriting with CloudFront Functions
Using CloudFront Functions (cloudfront-js-2.0) for lightweight URL → actual file mapping.
/→/index.html/ja→/ja.html(trailing slash handled similarly)/contact,/privacy,/licenses→ respective*.htmlfiles- URIs with extensions (.svg, .js, .css, ...) pass through without rewriting
- Unsupported locales redirect to
/en/_not-found
3) i18n and Routing
- Supports languages:
en, ja, zh, ko, de, fr, es, pt - Translations separated into
src/locales/*.json, SSG for each locale in App Router - Root
/renders default language (en) equivalent to SSR for SEO & bots, while performing one-time client redirect to locale page based onnavigator.languagesonly for browser visits
4) SEO and OGP/Twitter Cards (Per Locale)
- Implemented
generateMetadata()for each page to correctly output "canonical / og:url / og:image / og:locale" per language - OGP images (
public/og-*.png) prepared for each language, with description text in images created from translation texts - Structured data (JSON-LD) output per locale in server components (
application/ld+json). All 8 languages explicitly specified ininLanguage
// Example: Structured data output (excerpt)
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) Correct <html lang> Per Locale
In App Router, there's a constraint that "the root layout's <html> is common to all pages," resulting in <html lang="en"> remaining at build time.
This time, I incorporated a thin component that "overwrites <html lang> on the client side" in both the locale layout and root page, updating to the correct language tag during actual use.
'use client'
import { useEffect } from 'react'
export default function HtmlLangSetterClient({ lang }: { lang: string }) {
useEffect(() => {
document.documentElement.setAttribute('lang', lang)
}, [lang])
return null
}
While keeping the difference between build-time static HTML and the DOM actual users see to a minimum, I achieved a practical implementation under Static Export constraints.
Deployment/Operations
Infrastructure
- S3 (static hosting) + CloudFront (origin is S3's Website endpoint)
- CloudFront Functions deployed separately (SAM template for resource creation, code updates on CI side)
CI/CD (GitHub Actions)
- Triggered by push to
main:- Next.js build (Static Export)
- S3 sync
/_next/staticwithmax-age=31536000, immutable- HTML with
max-age=300, must-revalidate
- CloudFront Function code update (
update-function→publish-function) - CloudFront Invalidation (
/*) as needed
AWS credentials, S3 bucket, CloudFront Distribution ID, and NEXT_PUBLIC_* are configured as Secrets/Vars.
Manual updates via CLI are possible, but basically left to CI.
Summary
- Localization
- Minimal SEO
- Minimal infrastructure configuration
- CI/CD & IaC
As I started personal development, I wanted to go through the entire process of releasing an app—anything at all—so it became this kind of simple, low-cost app.
I'm glad I got to try using Next.js for the first time. Next, I'll try deploying to Vercel with SSR instead of SSG.