The brief
A friend, Gabi Abinajm, a product designer, needed a portfolio. The constraints were the interesting part: she had to be able to add, reorder, and translate case studies without ever opening a code editor, videos had to play cleanly on mobile without dragging the page down, and a few of the projects had to sit behind a password because they were under NDA.
I built it as a side project across a couple of evenings. The stack sounds unremarkable, Next.js 16, TypeScript, Tailwind, Vercel, but the interesting decisions live below that surface.
Live: gabinajm.com.br
Source: https://github.com/orrevua/gabinajm-portfolio
Why Sanity, not a folder of MDX files
My own portfolio ships content as MDX in the repo. That works because I am the only author and I like commits. For a portfolio where the designer owns the content lifecycle, that model breaks in two places: every content change becomes a pull request, and translations bloat the diff review beyond anything a non-engineer wants to see.
Sanity solved both. The schema lives in studio/, the CMS runs at a dedicated route, and Gabi drops in projects the same way she drops assets into Figma. The site rebuilds via ISR when content changes, so no manual redeploy.
The tradeoff I accepted: one more service in the stack, plus a monthly cap on Sanity's free tier that this site will not hit for years.
A CMS is not a "nice to have" the moment a non-engineer needs to publish. It is the whole product.
Clean architecture in a small Next.js app
For a five-page site, clean architecture reads like overkill. I did it anyway, and I would do it again:
src/
├── app/ # Next.js routes, thin — mostly composition
├── adapters/ # UI components, presentation-only
├── domain/ # Types and pure business rules
├── services/ # Sanity client, queries, SendGrid client
└── i18n/ # Locale provider, EN/PT dictionaries
The payoff shows up the first time you swap something. When SendGrid's free tier tightened, replacing the mail service was a single file. When Sanity's query shape changed, one adapter absorbed the migration. Routes did not know either service existed.
For a project this small the boundaries feel ceremonial, but the moment you add a second developer or a second CMS, ceremony pays for itself.
Bilingual without a framework
Cookie-based locale, no /en or /pt in the URL, one canonical per page. Locale flips are instant because they swap a context, not a route. This is the same pattern I use on my own site and it survives fine at this scale.
Sanity's content is authored in both languages inside a single document, with each field carrying { en: string; pt: string }. Missing a translation is a compile-time guarantee at the type layer and a soft fallback at render.
Video the responsible way
Case studies lean on motion, screencasts of interactions, prototype walkthroughs, that kind of thing. Serving raw MP4s would murder mobile bandwidth. I encoded to HLS and used hls.js with a small lazy-loading wrapper:
const HLSVideo = dynamic(() => import("@/adapters/hls-video"), { ssr: false })
The bundle only ships to visitors who actually scroll to a video. Cost of the trick: a client-only component and one dynamic import. Payoff: a portfolio full of long videos that scores well on Core Web Vitals.
Access control on private case studies
A few projects were under NDA. The compromise: a short passphrase per project, verified server-side against a hashed value in Sanity, unlocking the case study for the current session. No user accounts, no database, no signup flow, just a form and a cookie.
For a portfolio that lives entirely to be shared with recruiters, this is the right altitude of security. It gates the content without turning the site into an app.
The takeaway
Collaboration is the untold headline of shipping something like this. Gabi handed me a Figma with real components, real states, real content, and answered questions in minutes. I could focus on the parts I care about, architecture, performance, boring reliability, because the design was not a moving target.
The stack matters less than the seam between people. Pick tools that let the non-engineer keep shipping after you leave.
Relatório informativo
Minha amiga Gabi Abinajm, uma designer de produto, precisava de um portfólio. As restrições eram a parte interessante: ele tinha que ser capaz de adicionar, reordenar e traduzir estudos de caso sem nunca abrir um editor de código, os vídeos tinham que ser reproduzidos de forma limpa no celular sem arrastar a página para baixo e alguns dos projetos tinham que se estar protegidos por uma senha porque estavam sob NDA.
O construí como um projeto paralelo ao longo de algumas noites. A stack parece normal, Next.js 16, TypeScript, Tailwind, Vercel, mas as decisões interessantes ficam abaixo dessa superfície.
Live: gabinajm.com.br
Fonte: https://github.com/orrevua/gabinajm-portfolio
Porquê Sanity, e não uma pasta de arquivos MDX
Meu próprio portfólio envia conteúdo como MDX no repo. Isso funciona porque sou o único autor e gosto de commits. Para um portfólio em que o designer é dono do ciclo de vida do conteúdo, esse modelo quebraria em dois lugares: toda alteração de conteúdo se torna uma pull request e as traduções incham a revisão do diff além do que um não engenheiro queira ver.
Sanity resolveu ambos. O schema vive em studio/, o CMS é executado em uma rota dedicada, e Gabi lança projetos da mesma forma que lança ativos no Figma. O site é reconstruído via ISR quando o conteúdo é alterado, portanto, nenhum redeploy manual.
O trade off que aceitei: mais um serviço na stack, além de um limite mensal no nível gratuito da Sanity que este site não atingirá por anos.
Um CMS não é algo "bom de se ter" no momento que um não engenheiro precisa publicar. É todo o produto.
Arquitetura limpa numa pequena aplicação Next.js
Para um site de cinco páginas, arquitetura limpa parece um exagero. Eu fiz assim mesmo, e faria de novo:
src/
├── app/ # Next.js routes, thin — mostly composition
├── adapters/ # UI components, presentation-only
├── domain/ # Types and pure business rules
├── services/ # Sanity client, queries, SendGrid client
└── i18n/ # Locale provider, EN/PT dictionaries
A recompensa aparece na primeira vez que você troca algo. Quando o nível gratuito do SendGrid ficou mais restrito, a substituição do serviço de e-mail era um único arquivo. Quando a forma de consulta do Sanity mudou, um adaptador absorveu a migração. As rotas não sabiam que nenhum dos serviços existia.
Para um projeto tão pequeno, os limites parecem cerimoniais, mas no momento em que você adiciona um segundo desenvolvedor ou um segundo CMS, a cerimônia se paga.
Bilíngue sem estrutura
Localidade baseada em cookies, não /en ou /pt no URL, um canônico por página. As inversões de local são instantâneas porque trocam um contexto, não uma rota. Este é o mesmo padrão que uso no meu próprio site e sobrevive bem a esta escala.
O conteúdo da Sanity é criado em ambos os idiomas dentro de um único documento, com cada campo carregando { en: string; pt: string }. A falta de uma tradução é uma garantia de tempo de compilação na camada type e um fallback suave na renderização.
Vídeos da maneira responsável
Estudos de caso se baseiam em movimento, screencasts de interações, passo a passo de protótipos, esse tipo de coisa. Servir MP4s brutos mataria a largura de banda móvel. Eu codifiquei para HLS e usei hls.js com um pequeno invólucro de carregamento lento:
const HLSVideo = dynamic(() => import("@/adapters/hls-video"), { ssr: false })
O pacote é enviado apenas para visitantes que realmente rolam para um vídeo. Custo do truque: um componente exclusivo do cliente e uma importação dinâmica. Payoff: um portfólio cheio de vídeos longos com boa pontuação no Core Web Vitals.
Controle de acesso em estudos de caso privados
Alguns projetos estavam sob NDA. O compromisso: uma senha curta por projeto, verificada no lado do servidor em relação a um valor de hash em Sanity, desbloqueando o estudo de caso para a sessão atual. Sem contas de utilizador, sem base de dados, sem fluxo de inscrição, apenas um formulário e um cookie.
Para um portfólio que vive inteiramente para ser compartilhado com os recrutadores, esta é a altitude certa de segurança. Ele bloqueia o conteúdo sem transformar o site num aplicativo.
Conclusão
A colaboração é o título incalculável do envio de algo assim. Gabi me entregou um Figma com componentes reais, estados reais, conteúdo real e respondeu a perguntas em minutos. Eu poderia me concentrar nas partes com as quais me importo, arquitetura, desempenho, confiabilidade chata, porque o design não era um alvo em movimento.
A stack importa menos do que a interação entre as pessoas. Escolha ferramentas que permitam que o não-engenheiro continue a enviar depois de sair.