Released Own Info App — Minimal App with Next.js (SSG) × S3 × CloudFront

by Sloth255
Next.jsReactTypeScripti18nAWSS3CloudFront

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 *.html files
  • 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 on navigator.languages only 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 in inLanguage
// 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/static with max-age=31536000, immutable
      • HTML with max-age=300, must-revalidate
    • CloudFront Function code update (update-functionpublish-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.