Next.jsReactTypeScripti18nAWSS3CloudFront

Lancement de Own Info App — Application minimaliste avec Next.js (SSG) × S3 × CloudFront

Sloth255
Sloth255
·4 min read·680 words

Présentation de l'application

J'ai publié « Own Info App », une petite application web qui permet de vérifier rapidement les informations réseau (comme votre adresse IP publique) et les informations sur le navigateur/appareil, puis de les copier/partager immédiatement.

https://x.com/Sloth255000/status/1986792593569763395
  • Stack (configuration partielle) : Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
  • Distribution : S3 + CloudFront (Static Export)
  • Multilingue : Anglais/Japonais/Chinois/Coréen/Allemand/Français/Espagnol/Portugais
  • Analytics : Firebase Analytics

Les transitions de page et l'UI sont gardées aussi simples que possible. Il suffit de l'ouvrir, de « copier » ou « partager » les informations dont vous avez besoin, et de la fermer — c'est tout.


Stack technique et points de conception

1) Next.js 15 App Router × Static Export

  • Sortie entièrement statique (SSG) avec output: 'export'
  • Les fichiers de sortie ont une structure plate avec index.html, en.html, ja.html, etc. directement dans le répertoire out

Cette configuration facilite la distribution CDN et la mise en cache, créant une application réactive sans serveurs dynamiques ni SSR.

2) Réécriture d'URL avec CloudFront Functions

Utilisation de CloudFront Functions (cloudfront-js-2.0) pour le mappage léger URL → fichier réel.

  • //index.html
  • /ja/ja.html (les trailing slashes sont gérés de la même façon)
  • /contact, /privacy, /licenses → fichiers *.html respectifs
  • Les URIs avec extensions (.svg, .js, .css, ...) passent sans réécriture
  • Les locales non supportées redirigent vers /en/_not-found

3) i18n et routage

  • Langues supportées : en, ja, zh, ko, de, fr, es, pt
  • Traductions séparées dans src/locales/*.json, SSG pour chaque locale dans App Router
  • La racine / affiche la langue par défaut (en) équivalente au SSR pour le SEO & les bots, tout en effectuant une redirection client unique vers la page de locale basée sur navigator.languages uniquement pour les visites via navigateur

4) SEO et OGP/Twitter Cards (par locale)

  • generateMetadata() implémenté pour chaque page afin de générer correctement « canonical / og:url / og:image / og:locale » par langue
  • Images OGP (public/og-*.png) préparées pour chaque langue, avec le texte de description dans les images créé à partir des textes de traduction
  • Données structurées (JSON-LD) générées par locale dans les composants serveur (application/ld+json). Les 8 langues explicitement spécifiées dans inLanguage
// Exemple : sortie de données structurées (extrait)
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> correct par locale

Dans App Router, il y a une contrainte selon laquelle « le <html> du layout racine est commun à toutes les pages », résultant en <html lang="en"> qui reste au moment du build.
Cette fois, j'ai incorporé un composant léger qui « écrase <html lang> côté client » dans le layout de la locale et la page racine, mettant à jour la balise de langue correcte lors de l'utilisation réelle.

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

Tout en maintenant la différence entre le HTML statique au moment du build et le DOM que les utilisateurs voient réellement à un minimum, j'ai réalisé une implémentation pratique sous les contraintes du Static Export.


Déploiement/Opérations

Infrastructure

  • S3 (hébergement statique) + CloudFront (l'origine est l'endpoint Website de S3)
  • CloudFront Functions déployées séparément (template SAM pour la création des ressources, mises à jour du code côté CI)

CI/CD (GitHub Actions)

  • Déclenché par un push sur main :
    • Build Next.js (Static Export)
    • Synchronisation S3
      • /_next/static avec max-age=31536000, immutable
      • HTML avec max-age=300, must-revalidate
    • Mise à jour du code CloudFront Function (update-functionpublish-function)
    • Invalidation CloudFront (/*) si nécessaire

Les identifiants AWS, le bucket S3, l'ID de distribution CloudFront et les NEXT_PUBLIC_* sont configurés en tant que Secrets/Vars.
Les mises à jour manuelles via CLI sont possibles, mais laissées au CI par défaut.


Résumé

  • Localisation
  • SEO minimal
  • Configuration d'infrastructure minimale
  • CI/CD & IaC

En commençant le développement personnel, je voulais parcourir l'ensemble du processus de publication d'une application — n'importe laquelle — alors c'est devenu ce genre d'application simple et peu coûteuse.
Je suis content d'avoir pu essayer Next.js pour la première fois. Ensuite, j'essaierai de déployer sur Vercel avec SSR au lieu de SSG.

Découvrez l'application que j'ai créée ici 📱

https://own-info-app.sloth255.comhttps://own-info-app.sloth255.com