Next.jsReactTypeScripti18nAWSS3CloudFront

Lanzamiento de Own Info App — Aplicación minimalista con Next.js (SSG) × S3 × CloudFront

Sloth255
Sloth255
·3 min read·656 words

Descripción de la aplicación

He lanzado "Own Info App", una pequeña aplicación web donde puedes consultar rápidamente información de red (como tu dirección IP global) e información del navegador/dispositivo, y luego copiarla/compartirla de inmediato.

https://x.com/Sloth255000/status/1986792593569763395
  • Fuente (configuración parcial): Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
  • Distribución: S3 + CloudFront (Static Export)
  • Multilingüe: Inglés/Japonés/Chino/Coreano/Alemán/Francés/Español/Portugués
  • Analytics: Firebase Analytics

Las transiciones de página y la UI se mantienen lo más simples posible. Solo ábrela, "copia" o "comparte" la información que necesites, y ciérrala—eso es todo.


Stack tecnológico y puntos de diseño

1) Next.js 15 App Router × Static Export

  • Salida completamente estática (SSG) con output: 'export'
  • Los archivos de salida tienen una estructura plana con index.html, en.html, ja.html, etc. directamente en el directorio out

Esta configuración facilita la distribución y el caché en CDN, creando una aplicación ágil sin servidores dinámicos ni SSR.

2) Reescritura de URL con CloudFront Functions

Usando CloudFront Functions (cloudfront-js-2.0) para el mapeo ligero de URL → archivo real.

  • //index.html
  • /ja/ja.html (trailing slash manejado similarmente)
  • /contact, /privacy, /licenses → archivos *.html respectivos
  • URIs con extensiones (.svg, .js, .css, ...) pasan sin reescritura
  • Los locales no soportados redirigen a /en/_not-found

3) i18n y Routing

  • Idiomas soportados: en, ja, zh, ko, de, fr, es, pt
  • Traducciones separadas en src/locales/*.json, SSG para cada locale en App Router
  • La raíz / renderiza el idioma predeterminado (en) equivalente a SSR para SEO & bots, mientras realiza una redirección de cliente única a la página del locale basada en navigator.languages solo para visitas del navegador

4) SEO y OGP/Twitter Cards (por locale)

  • Se implementó generateMetadata() para cada página para generar correctamente "canonical / og:url / og:image / og:locale" por idioma
  • Imágenes OGP (public/og-*.png) preparadas para cada idioma, con texto de descripción en imágenes creado desde textos de traducción
  • Datos estructurados (JSON-LD) generados por locale en componentes de servidor (application/ld+json). Los 8 idiomas especificados explícitamente en inLanguage
// Ejemplo: Salida de datos estructurados (extracto)
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> correcto por locale

En App Router, existe la restricción de que "el <html> del layout raíz es común a todas las páginas", lo que resulta en que <html lang="en"> permanece en el momento de la compilación.
Esta vez, incorporé un componente delgado que "sobreescribe <html lang> en el lado del cliente" tanto en el layout del locale como en la página raíz, actualizando al tag de idioma correcto durante el uso real.

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

Manteniendo la diferencia entre el HTML estático en tiempo de compilación y el DOM que ven los usuarios reales al mínimo, logré una implementación práctica bajo las restricciones de Static Export.


Despliegue/Operaciones

Infraestructura

  • S3 (alojamiento estático) + CloudFront (el origen es el endpoint de sitio web de S3)
  • CloudFront Functions desplegadas por separado (plantilla SAM para la creación de recursos, actualizaciones de código en el lado de CI)

CI/CD (GitHub Actions)

  • Activado por push a main:
    • Build de Next.js (Static Export)
    • Sincronización con S3
      • /_next/static con max-age=31536000, immutable
      • HTML con max-age=300, must-revalidate
    • Actualización del código de CloudFront Function (update-functionpublish-function)
    • Invalidación de CloudFront (/*) según sea necesario

Las credenciales de AWS, el bucket S3, el ID de distribución de CloudFront y NEXT_PUBLIC_* se configuran como Secrets/Vars.
Las actualizaciones manuales vía CLI son posibles, pero básicamente se dejan a CI.


Resumen

  • Localización
  • SEO mínimo
  • Configuración de infraestructura mínima
  • CI/CD & IaC

Al comenzar el desarrollo personal, quería pasar por todo el proceso de lanzar una aplicación—cualquier cosa—así que se convirtió en esta aplicación simple y de bajo costo.
Me alegra haber tenido la oportunidad de usar Next.js por primera vez. A continuación, intentaré desplegar en Vercel con SSR en lugar de SSG.

Mira la aplicación que creé aquí 📱

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