Next.jsReactTypeScripti18nAWSS3CloudFront

Lançamento do Own Info App — App Minimalista com Next.js (SSG) × S3 × CloudFront

Sloth255
Sloth255
·3 min read·621 words

Visão Geral do App

Lancei o "Own Info App", uma pequena aplicação web onde você pode verificar rapidamente informações de rede (como seu endereço IP global) e informações do navegador/dispositivo, copiar/compartilhar imediatamente.

https://x.com/Sloth255000/status/1986792593569763395
  • Stack (configuração parcial): Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS
  • Distribuição: S3 + CloudFront (Static Export)
  • Multilíngue: Inglês/Japonês/Chinês/Coreano/Alemão/Francês/Espanhol/Português
  • Analytics: Firebase Analytics

As transições de página e a interface são mantidas o mais simples possível. Abra, "copie" ou "compartilhe" a informação que precisa, e feche — só isso.


Stack Tecnológica e Pontos de Design

1) Next.js 15 App Router × Static Export

  • Saída totalmente estática (SSG) com output: 'export'
  • Os arquivos de saída têm uma estrutura plana com index.html, en.html, ja.html, etc. diretamente no diretório out

Essa configuração facilita a distribuição e o cache via CDN, criando um app ágil sem servidores dinâmicos ou SSR.

2) Reescrita de URL com CloudFront Functions

Usando CloudFront Functions (cloudfront-js-2.0) para mapeamento leve de URL → arquivo real.

  • //index.html
  • /ja/ja.html (trailing slash tratado similarmente)
  • /contact, /privacy, /licenses → arquivos *.html respectivos
  • URIs com extensões (.svg, .js, .css, ...) passam sem reescrita
  • Locales não suportados redirecionam para /en/_not-found

3) i18n e Roteamento

  • Idiomas suportados: en, ja, zh, ko, de, fr, es, pt
  • Traduções separadas em src/locales/*.json, SSG para cada locale no App Router
  • O caminho raiz / renderiza o idioma padrão (en) equivalente ao SSR para SEO & bots, enquanto realiza um redirecionamento único no cliente para a página do locale com base em navigator.languages apenas para visitas via navegador

4) SEO e OGP/Twitter Cards (Por Locale)

  • Implementado generateMetadata() para cada página a fim de gerar corretamente "canonical / og:url / og:image / og:locale" por idioma
  • Imagens OGP (public/og-*.png) preparadas para cada idioma, com texto de descrição nas imagens criadas a partir das traduções
  • Dados estruturados (JSON-LD) por locale em server components (application/ld+json). Todos os 8 idiomas especificados explicitamente em inLanguage
// Exemplo: saída de dados estruturados (trecho)
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> Correto Por Locale

No App Router, existe uma restrição de que "o <html> do layout raiz é comum a todas as páginas", resultando em <html lang="en"> permanecendo em tempo de build.
Desta vez, incorporei um componente leve que "sobrescreve <html lang> no lado do cliente" tanto no layout de locale quanto na página raiz, atualizando para a tag de idioma correta durante o uso real.

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

Mantendo ao mínimo a diferença entre o HTML estático em tempo de build e o DOM que os usuários reais veem, alcancei uma implementação prática sob as restrições do Static Export.


Deploy/Operações

Infraestrutura

  • S3 (hospedagem estática) + CloudFront (origem é o endpoint Website do S3)
  • CloudFront Functions implantadas separadamente (template SAM para criação de recursos, atualizações de código pelo CI)

CI/CD (GitHub Actions)

  • Acionado por push para main:
    • Build do Next.js (Static Export)
    • Sync com S3
      • /_next/static com max-age=31536000, immutable
      • HTML com max-age=300, must-revalidate
    • Atualização do código da CloudFront Function (update-functionpublish-function)
    • Invalidação do CloudFront (/*) conforme necessário

Credenciais AWS, bucket S3, CloudFront Distribution ID e NEXT_PUBLIC_* são configurados como Secrets/Vars.
Atualizações manuais via CLI são possíveis, mas basicamente deixadas para o CI.


Resumo

  • Localização
  • SEO mínimo
  • Configuração de infraestrutura mínima
  • CI/CD & IaC

Ao começar o desenvolvimento pessoal, queria passar por todo o processo de lançar um app — qualquer coisa — então acabou sendo esse app simples e de baixo custo.
Fico feliz que pude experimentar o Next.js pela primeira vez. A seguir, vou tentar fazer deploy no Vercel com SSR em vez de SSG.

Confira o app que criei aqui 📱

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