Olá Mundo

Se hoje é possível criar um site com apenas um prompt, por que eu criei o meu site do zero? Este artigo responde esta pergunta e conta como foi a minha jornada criando este site.

Introdução

Ainda me lembro da sensação de ter criado o meu primeiro site. Não era fácil criar um site nos anos 90, mas eu tinha o privilégio de ter um computador com Internet em casa, então aproveitei a oportunidade.

Eu comecei estudando as tecnologias necessárias usando revistas sobre o assunto. Aprendi sobre HTML, HTTP, FTP, DNS e como usar o FrontPage. Criei as páginas, copiei os arquivos via FTP para os servidores da GeoCities e consegui um endereço DNS. O site estava publicado e eu me senti realizado.

Os sites evoluíram de simples documentos de hiper-texto (texto que podiam ter links entre si) para aplicações completas que rodam no seu navegador. Portais de notícias, lojas, sistemas bancários, redes sociais e plataformas de música e vídeo.

Hoje é possível criar um site com apenas um prompt. Em alguns minutos, o site estará disponível na Internet para qualquer pessoa acessar. Se você só precisa do site pronto, esse é o caminho que eu recomendo.

No entanto, como desenvolvedor, eu gosto de entender como as coisas funcionam. Gosto de criar, fazer, errar, acertar e aprender. O contexto me permitiu, pois era um projeto solo. Ainda, era uma oportunidade de relembrar e me divertir.

Esse artigo conta como foi a minha jornada criando este site.

Objetivo

É importante ter um objetivo claro para o projeto. Se alguém perguntar “Por que você está fazendo isso?”, a resposta deve ser natural, simples e objetiva.

“Este site é um projeto que faz parte do meu objetivo de criar uma marca profissional e compartilhar conhecimento com outras pessoas.” Eu comecei escrevendo esta frase que definiu o objetivo do projeto.

Com o objetivo definido (e escrito), segui para as ferramentas.

Ferramentas

As ferramentas impactam diretamente a sua produtividade, é importante escolher com sabedoria. Use uma ferramenta quando conseguir explicar como ela te ajuda. Crie uma ferramenta quando as ferramentas existentes não resolvem o teu problema. Não use ferramentas só porque elas estão na moda.

Usei ferramentas de IA em todo o processo. A stack que usei foi:

  • Gemini, no plano Google AI Pro.
  • Stitch, como gerador de ideias de design.
  • Antigravity, como agente de desenvolvimento síncrono.
  • Jules, como agente de desenvolvimento assíncrono.

Em um artigo futuro compartilho mais sobre a minha stack de ferramentas de inteligência artificial.

Com as ferramentas definidas, segui para o projeto.

Projeto

Um projeto pode ser definido como um grupo de pessoas, recursos e atividades que precisam ser feito para resolver um problema. Criar um espaço de trabalho, um lugar onde é possível encontrar e organizar as informações, é importante para manter alinhamento, mesmo que seja um projeto solo. Alinhamento é essencial para executar bem, com qualidade e no prazo.

Comecei criando um notebook no Gemini para organizar as minhas referências, prompts e documentos. Adicionei alguns sites de referência e gerei alguns documentos com ideias de conteúdo.

Também me inspirei em sites de pessoas que acompanho, como a Julia Evans, Will Larson e Kent Beck.

Com o projeto criado, segui para o design.

Design

O design é importante para materializar o projeto. Ao ver o projeto “saindo do papel”, aprendemos e entendemos melhor o que precisamos, o que queremos e principalmente o que não precisamos. Sempre tente materializar, mesmo que de forma simples. Design também cria alinhamento.

Usei o Stitch para gerar algumas ideias de design para o site. Usei os documentos gerados na etapa anterior para ajudar no contexto. Depois de alguns minutos já tinha uma boa ideia do estilo e arquitetura da informação.

Com o design idealizado, segui para o repositório.

Repositório

Definir a estratégia de repositórios é importante não apenas para manter a organização do código-fonte, mas também para garantir que as dependências não se tornem um problema de produtividade. Dependências não gerenciadas tornam o trabalho lento e confuso.

Criei um repositório para organizar o código-fonte. Usei a estratégia de monorepo, para garantir que as dependências entre as especificações, o site e a infraestrutura ficassem bem definidas.

Usei esta estrutura de arquivos e diretórios:

moyle-dev/      # Projeto principal.
    infra/      # Subprojeto para a infraestrutura.
    site/       # Subprojeto para o site.
    specs/      # Subprojeto para as especificações.
    AGENTS.md   # Instruções para agentes de IA.
    LICENSE     # Licença do projeto.
    README.md   # README do projeto.

Em um artigo futuro, compartilho mais sobre como organizo meus repositórios.

Com o repositório criado, segui para as especificações.

Especificações

Usar especificações claras é importante para garantir alinhamento e executar com qualidade dentro do prazo. Nem sempre é possível especificar tudo e é natural aprendermos e mudarmos durante o projeto. Mas não deixe de especificar o que é conhecido.

Usei SDD para aumentar o alinhamento entre os humanos (eu) e a IA (agentes). O SDD, ou Spec Driven Development, é uma abordagem de desenvolvimento onde as especificações devem ser escritas antes do código. As especificações são arquivos Markdown em linguagem natural, que definem e descrevem o porquê, o quê e o como.

Criei 3 especificações, nessa ordem:

  1. specs/PROJECT.md: especificação do projeto (porquê).
  2. specs/DESIGN.md: especificação do design (quê).
  3. specs/TASKS.md: especificação das tarefas (como).

Comecei com a especificação do projeto, definindo um resumo do projeto, quais métricas de sucesso, funcionalidades e testes (E2E). Por exemplo, defini que a página de artigos deveria apresentar uma lista de artigos ordenados por data decrescente, com título, data e resumo. Também defini que o teste E2E deveria acessar a página pela URL correta (/artigos) e validar se os artigos estavam visíveis e ordenados corretamente.

Na especificação de projeto, usei uma versão simplificada de PRD (Product Requirements Document) e Gherkin para descrever os teste E2E.

Depois segui para a especificação do design, definindo o que deveria ser feito para implementar usando a especificação do projeto. Por exemplo, qual a stack de tecnologias, como gerenciar os artigos, como provisionar a infraestrutura, etc.

A stack de tecnologias que defini foi:

  • Javascript como linguagem principal.
  • Node como runtime de execução.
  • NPM como gerenciador de pacotes e automações.
  • Playwright como ferramentas de teste E2E.
  • Astro como framework para a construção do site.
  • Markdown como linguagem de marcação para conteúdo.
  • Frontmatter como gerenciador de metadados para conteúdo.
  • GitHub como repositório de código e automação de CI/CD.
  • Terraform como infraestrutura como código.
  • AWS como provedor de infraestrutura em nuvem.
  • Google Analytics como ferramenta de métricas e análises.
  • Uptime Robot como ferramentas de monitoramento.

Por ser um projeto solo, misturei ADRs e Design Docs. Cada seção apresentou uma decisão, explicando o contexto, a decisão, as consequências e outras informações relevantes.

Finalmente, segui para a especificação das tarefas, definindo as tarefas necessárias para implement o projeto com o design.

Usei o agente para criar e revisar todas as especificações. Adicionei instruções no AGENTS.md para o agente considerar as especificações na geração de código.

Não usei ferramentas de SDD, como o Kiro, Spec Kit ou OpenSpec. Fiquei curioso para ver como seria usar SDD sem uma ferramenta.

Em um artigo futuro compartilho mais sobre como uso SDD.

Com as especificações criadas, segui para os testes.

Testes

Testar é importante para garantir velocidade e qualidade. Testar na mão é lento e frágil, uma hora alguém vai esquecer de testar algo e o projeto vai quebrar. Não testar é aceitar o risco do projeto não funcionar.

Usei TDD para garantir que o projeto continue funcionando conforme ele seja alterado. O TDD, ou Test Driven Development, é uma abordagem de desenvolvimento onde testes são criados antes do código. Os testes são códigos que verificam automaticamente se o projeto está funcionando e identificam se algo quebrou.

Escolhi testes E2E e o Playwright para escrever e executar os testes. Testes E2E funcionam bem para sites, simulando o acesso através de um navegador “de verdade”. O Playwright permite escrever, executar e ver os relatórios dos testes com velocidade e clareza.

Usei a estrutura de arquivos e diretórios recomendada pelo Playwright:

site/                       # Subprojeto para o site.
    tests/                  # Testes.
        articles.spec.js    # Teste da página Artigos.
    playwright.config.ts    # Configuração do Playwright.

Adicionei instruções no AGENTS.md para o agente sempre executar os testes a cada ciclo de desenvolvimento, evitando deixar o projeto quebrado.

Em um artigo futuro compartilho mais sobre como uso TDD.

Com os testes escritos, segui para a definição e configuração do framework.

Importante observar que antes de escolher o framework, eu já tinha definido os testes. Assim eu garanti que o framework se adaptasse ao meu projeto, e não o contrário.

Framework

Os frameworks são importantes para nos ajudar a desenvolver com mais velocidade e qualidade. A velocidade vem da estrutura definida, funcionalidades prontas e uma boa capacidade de customização. A qualidade vem das melhores práticas e do conhecimento agregado de todas as pessoas e empresas que criaram o framework.

Usei o Astro para criar o site, aproveitando as funcionalidades de rotas, componentes, coleções, Markdown e Frontmatter. Além disso, o modelo SSG (Static Site Generation) gera sites estáticos que tornam a arquitetura de publicação simples e têm boa performance (quase nenhum processamento no servidor e no cliente).

Usei a estrutura de arquivos e diretórios recomendada pelo Astro:

site/                           # Subprojeto do site.
    src/                        # Código-fonte.
        assets/                 # Assets.
            images/             # Imagens.
        components/             # Componentes reutilizáveis.
            ArticleCard.astro   # Card de artigo.
        content/                # Conteúdo.
            articles/           # Artigos.
                ola-mundo.md    # Este artigo.
        layouts/                # Layouts.
        pages/                  # Páginas.
            artigos/            # Páginas de Artigos.
                index.astro     # Página Artigos.
                [slug].astro    # Página Artigo.
    astro.config.mjs            # Configuração do Astro.
    package.json                # Dependências e automações.
    jsconfig.json               # Configuração do Javascript.

Na maioria das vezes que eu precisava de algo pronto, o Astro tinha, e poucas vezes precisei de algo que ele não tinha. Esse é um ótimo sinal que o framework está te ajudando e não ficando no seu caminho.

Com o framework configurado e estrutura inicial do projeto pronta, segui para o desenvolvimento.

Desenvolvimento

O fluxo de desenvolvimento é importante para criar consistência e ritmo. Consistência e ritmo aumentam a chance de entregar código dentro do prazo e com qualidade.

Usei um fluxo de desenvolvimento consistente, aplicando SDD e TDD:

  1. Ver qual a próxima tarefa em specs/TASKS.md. Por exemplo “Criar página Artigos.”
  2. Criar uma branch para a nova tarefa. Por exemplo pagina-artigos.
  3. Gerar/atualizar as especificações do projeto, design e tarefas.
  4. Gerar o código de testes usando o Playwright.
  5. Executar os testes e falhar.
  6. Gerar o código para implementar.
  7. Executar os testes e passar.
  8. Revisar e refatorar o código gerado.
  9. Executar os testes e passar.
  10. Criar o commit e fazer merge com a branch main.
  11. Atualizar a especificação de tarefas e ir para a próxima.

Usei o agente de desenvolvimento síncrono para executar esse fluxo. Eu começava com um prompt simples como “Vamos trabalhar na próxima tarefa.” e depois de algumas rodadas, eu precisava apenas revisar/refatorar o código, pois o agente já tinha acumulado contexto o suficiente para seguir praticamente sozinho.

A parte mais importante é revisar e refatorar o código, principalmente quando você ainda não acertou as especificações e as instruções no AGENTS.md. É comum o agente ou o humano criarem código duplicado, difícil de entender e que quebram o projeto.

Caso eu ou o agente quebrássemos o código, os testes identificariam rápido. Além disso, o uso de branches permite voltar para o último estado estável rapidamente.

Usei um modelo simples de branches para o Git. A branch fixa main sempre representa o código na versão mais estável. Todas as alterações devem ter suas próprias branches temporárias, criadas a partir da main e depois integradas novamente na main.

Usei uma estratégia local para o ambiente de desenvolvimento, sem containers ou máquinas virtuais. Essa estratégia funciona bem para um projeto simples e com poucas pessoas. Instalei o Node usando o Homebrew. Defini as dependências, como Playwright e Astro, usando o NPM. Automatizei os scripts do fluxo de desenvolvimento também usando o NPM.

Reduzi a chance de problemas de paridades entre os ambientes (diferentes versões em ambiente DEV e PRD) também usando o NPM, definindo as versões no package.json. Não garanti paridade total entre os ambientes.

Usei o Jules como agente de desenvolvimento assíncrono apenas em algumas tarefas mais triviais, como atualizar versões de dependência e garantir formatação nos arquivos. Funcionou bem, e em um momento eu consegui o momento de trabalho em paralelo: enquanto eu estava desenvolvimento no Antigravity com o agente síncrono, o Jules estava na paralela atualizando a versão do Astro.

Com a primeira versão do site pronta e funcionando localmente, segui para a configuração da integração contínua (CI).

CI

A integração contínua (CI) é importante para identificar problemas no projeto de forma automatizada e frequente. Todas vez que uma alteração é introduzida, os testes são executados automaticamente e as pessoas são notificadas se algo quebrou. Vale reforçar que é essencial ter testes automatizados para ter um bom processo de CI.

Usei o GitHub Actions para configurar a CI. Todo push de qualquer branch inicia o processo de CI, que:

  1. Inicia um novo container.
  2. Instala as dependências do projeto.
  3. Constrói o pacote.
  4. Executa os testes E2E.
  5. Procura vulnerabilidades de segurança.
  6. Salva os resultados do testes.

Não construir o pacote ou não passar em algum teste é considerado uma falha. Neste caso, o processo pára e uma notificação é enviada por email.

Usei caches para reduzir o tempo de execução. Pode parecer otimização prematura, mas um dos segredos da CI é ela executar rápido. Caso contrário, as pessoas podem começar a encontrar maneiras de evitar, para ir mais rápido.

Usei todos os scripts definidos no NPM, como test e build para aumentar a paridade entre os ambientes, e reduzir a chance de problemas de compatibilidade.

Para testar, usei um simple commit/push. Existem ferramentas que permitem testar localmente, mas eu não precisei usar.

O processo de CI nos faz pensar em como nosso projeto vai rodar fora da nossa máquina. É um bom momento para começar a pensar nos ambientes.

Ambientes

A definição dos ambientes é importante para isolar a infraestrutura e configurações. Cada ambiente tem um propósito e pode ser configurado de forma diferente. Ainda, o isolamento de ambientes é importante para reduzir o risco de causar uma indisponibilidade por erro de configuração.

Defini 2 ambientes:

  • DEV, ambiente de desenvolvimento, local, no meu notebook.
  • PRD, ambiente de produção, na AWS.

Defini como convenção a variável de ambiente ENV em todo o repositório, tanto para as automações de CI/CD no GitHub Actions, quanto nas configurações do Playwright e Astro.

É comum usar um terceiro ambiente antes da produção, geralmente chamado STG, mas para este projeto, não foi necessário.

Com os ambiente definidos, segui para algumas definições de segurança.

Segurança

Aplicar padrões e práticas de segurança é importante para reduzir o risco de sofrer com falhas que podem deixar seu projeto fora do ar, perder dinheiro ou expor dados sensíveis.

Apliquei estes padrões e práticas de segurança:

  • Chaves e senhas gerenciadas: um dos erros mais comuns é manter chaves e senhas em arquivos no repositório do código. Para este projeto, todas as chaves e senhas foram gerenciadas como variáveis de ambiente, e mantidas em ferramentas de gestão de segredos.
  • Usuários por função: criei usuários específicos para cada função. Um usuário para provisionar a infraestrutura, outro para fazer publicações (deploys).
  • Princípio do menor privilégio: configurei apenas os acessos necessários para cada usuário, e evitei acessos amplos como ”*” ou elevados como admin.
  • SAST: usei o Snyk para analisar o código e procurar por vulnerabilidades nas dependências (SCA), no código (SAST) e na infraestrutura como código (IaC Security). A execução da análise é feita automaticamente durante os processos de CI/CD.

Com os padrões e práticas de segurança definidos, segui para o provisionamento da infraestrutura.

Infraestrutura

A infraestrutura é essencial para a estabilidade e escalabilidade do projeto no ambiente de produção. No cenário atual de cloud, a infraestrutura também é um fator determinante no custo do projeto. É importante escolher bem os serviços e configurações, garantindo que a infraestrutura se adapte à sua arquitetura, e não o contrário.

Usei a estratégia 100% cloud, na AWS, e escolhi estes serviços:

  • S3 para armazenar arquivos estáticos e estado do Terraform.
  • CloudFront para CDN, responsável por servir os arquivos estáticos.
  • Route53 para gerenciar zonas e registros DNS.
  • ACM para gerenciar certificados digitais.

Para provisionar a infraestrutura, usei o Terraform com provedores AWS. Usei a estrutura recomendada de projeto:

infra/
    main.tf
    variables.tf
    outputs.tf
    data.tf
    locals.tf

E organizei os recursos por função, evitando amarrar nos serviços específicos:

infra/
    cdn.tf          # CDN, CloudFront.
    certificate.tf  # Certificados digitais, ACM.
    dns.tf          # DNS, Route53.
    iam.tf          # Usuários e políticas, IAM.
    storage.tf      # Armazenamento, S3.

Na primeira execução, observei que existia um problema de dependência cíclica nas configurações de DNS. Basicamente, eu precisava criar a zona DNS para ter os valores dos servidores NS, e para criar os registros DNS eu precisava do Registro.br configurado. Além disso, eu precisava de um usuário com permissões elevadas para criar outros usuários (usário dedicado para provisionar a infraestrutura e outro para a publicação). Também observei que estas partes aconteciam com pouca frequência, e eram mais complexas.

Decidi então separar o provisionamento em 2 partes. Primeiro, a parte mais complexa e com maior dependência, que chamei de bootstrap, que resolvi usando duas estratégias: instruções de pré-requisitos no README, como já ter uma zona DNS criada, e um script shell com comandos da AWS CLI para criar os usuários. A segunda parte, mais simples e frequente, seria simplesmente executar o fluxo padrão do Terraform: terraform plan, terraform apply.

Ainda, criei e destruí a infraestrutura algumas vezes, para garantir que estava funcionando e que não tinha efeitos colaterais e condições de corrida.

Com a infraestrutura definida como código e provisionada, eu segui para o processo de CD.

CD

A publicação contínua (CD) é importante para automatizar a publicação do projeto, evitando processos manuais e propensos a erros. Todo pacote de alterações vai para a produção usando o mesmo processo.

Antes de configurar a CD, garanti que era possível executar uma publicação (deploy) a partir da minha máquina, sem todo o contexto do GitHub Actions. Isso ajudou a validar os scripts de publicação com mais velocidade e menos acoplamento com o GitHub Actions. Se o GitHub Actions caísse, eu conseguiria fazer publicações sem problemas.

Usei o GitHub Actions para configurar a CD. Todo push da branch main com uma tag inicia o processo de CD, que:

  1. Inicia um novo container.
  2. Instala as dependências do projeto.
  3. Constrói o pacote.
  4. Executa os testes E2E.
  5. Procura vulnerabilidades de segurança.
  6. Salva os resultados do testes.
  7. Copia o pacote para o storage.
  8. Invalida o cache do CDN.

Também defini uma configuração para poder executar o CD manualmente pela interface do GitHub, o que é útil quando o processo falha por alguma configuração externa ao código, evitando criar commits artificiais só para disparar o processo.

Usei todos os scripts definidos no NPM, como build e deploy para aumentar a paridade entre os ambientes, e reduzir a chance de problemas de compatibilidade.

Com o processo de CD configurado e testado, segui para a definição de versionamento.

Versionamento

O versionamento é importante para garantir consistência e integridade no processo de publicação. Cada publicação define uma versão (identificador) que é único e imutável. Isso reduz o risco de publicar o pacote errado, ajuda a entender o histórico de mudanças e facilita a comunicação entre as pessoas.

Usei a estratégia de versionamento por calendário (CalVer), no formato: <ANO>.<MÊS>.<PATCH>. Por exemplo: 26.5.1. Fez mais sentido do que usar o versionamento semântico (SemVer), já que as alterações de um projeto de site são mais relacionados à conteúdo do que compatibilidade de funcionalidades.

Com o versionamento definido, fechei a parte de automações e infraestrutura, e segui para a coleta e análise de métricas.

Métricas

Métricas são importantes para entender o comportamento do projeto no ambiente de produção. Métricas também ajudam a alinhar o que é sucesso e o que é falha de uma forma geral no projeto.

Defini 3 métricas principais:

  • Disponibilidade do site, em percentual, ao longo do tempo.
  • Número de visitas (page views) únicos, ao longo do tempo.
  • Tempo p99 de carregamento das páginas, ao longo do tempo.

Usei o Google Analytics (GA) para inserir uma tag de coleta e usar a plaforma para acompanhar as métricas.

Com as métricas definidas, segui para o monitoramento.

Monitoramento

O monitoramento é importante para identificar se algo está errado no ambiente de produção, de preferência antes do cliente.

Como o Google Analytics não oferece uma funcionalidade direta de monitoramento, usei o Uptime Robot para configurar um simples monitor de uptime, que monitora a disponibilidade, validade do certificado digital e envia uma notificação via email e aplicativo caso algo esteja errado.

Conclusão

Foi uma jornada divertida. Nem tudo funcionou de primeira, ou como o esperado, mas funcionou e o site está no ar: tá entregue.

Aprendizados que vale a pena compartilhar:

  • Criar um site é um processo divertido e ajuda a aprender/reforçar sobre tecnologias importantes para o dia-a-dia de uma pessoa desenvolvedora. Se você é dev, e tem um tempo para investir, recomendo criar o seu site.
  • Sobre projeto e métricas:
    • Definir o objetivo do projeto e quais as métricas de sucesso é importante para criar alinhamento e e ter uma boa execução. Mesmo em um projeto solo, isso me ajudou a não perder o foco.
  • Sobre IA:
    • Usar IA ajuda bastante, não apenas no desenvolvimento, mas no fluxo todo. Pode levar algum tempo para conseguir encontrar a melhor configuração de instruções e acertar nos prompts, mas logo que acerta, melhora bastante.
    • Para o desenvolvimento de um projeto simples, é necessário ter algum plano pago, pois o limite de tokens diários acabam rápido.
  • Sobre SDD e TDD:
    • SDD é interessante e funcionou bem. Assim como TDD, é hábito, no começo vai parecer chato e cansativo, mas vale a pena insistir.
    • TDD é essencial, ainda mais usando IA. Não ter como validar se o código está funcionando depois de uma alteração , principalmente do agente de IA, deixa o trabalho muito instável e provavelmente as coisas vão quebrar bastante.
    • Para sites, TDD com E2E funcionou super bem.
  • Sobre CI/CD:
    • Mesmo sendo um projeto solo, configurar o CI e CD ajuda a entender melhor como o software vai funcionar fora da máquina, identificando dependências não óbvias, e a automatizar e acelerar o desenvolvimento.
  • Sobre a infraestrutura:
    • Embora possível e desejável, conseguir provisionar e destruir todas a infraestrutura automaticamente é mais complexo que parece. O ponto que mais pega é o desafio do bootstra, ou seja, você precisar de alguns recursos para criar os outros recursos. Separar o bootstrap do resto do provisionamento funcionou bem.

Muito obrigado pela sua atenção. Se você curtiu o artigo, fique à vontade para compartilhar.

Até a próxima!