[{"data":1,"prerenderedAt":528},["ShallowReactive",2],{"allPosts":3},[4,377],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"description":10,"slug":11,"date":12,"summary":13,"readingTime":14,"body":15,"_type":371,"_id":372,"_source":373,"_file":374,"_stem":375,"_extension":376},"/blog/how-this-portfolio-was-built","blog",false,"","How This Portfolio Was Built","Most portfolio posts are tutorials. This one is a decisions log.","how-this-portfolio-was-built","2026-05-29","From a broken npm install to a full redesign in a single session. The decisions behind the stack, the design, and the content strategy for this site.","4 min",{"type":16,"children":17,"toc":363},"root",[18,25,39,44,51,56,61,66,71,76,94,109,124,139,156,161,174,180,198,223,233,246,252,257,277,297,303,323,328,334,339,344,349],{"type":19,"tag":20,"props":21,"children":22},"element","p",{},[23],{"type":24,"value":10},"text",{"type":19,"tag":20,"props":26,"children":27},{},[28,30,37],{"type":24,"value":29},"I recently rebuilt this site from scratch. The old version was a single-page scroll with a ",{"type":19,"tag":31,"props":32,"children":34},"code",{"className":33},[],[35],{"type":24,"value":36},"data.json",{"type":24,"value":38}," driving everything, built with Nuxt 3 and Tailwind. It worked. It just didn't say anything interesting about me.",{"type":19,"tag":20,"props":40,"children":41},{},[42],{"type":24,"value":43},"Here's how the new one came together.",{"type":19,"tag":45,"props":46,"children":48},"h2",{"id":47},"starting-with-the-right-questions",[49],{"type":24,"value":50},"Starting with the right questions",{"type":19,"tag":20,"props":52,"children":53},{},[54],{"type":24,"value":55},"Before writing a line of code, I spent time answering the hard questions: Who is this for? What does it need to do? What should it feel like?",{"type":19,"tag":20,"props":57,"children":58},{},[59],{"type":24,"value":60},"The answers shaped everything:",{"type":19,"tag":20,"props":62,"children":63},{},[64],{"type":24,"value":65},"The site has a dual job. It needs to convert hiring managers (scannable, credible, fast to the point) and build a presence for anyone who finds it organically. Those two goals can conflict. The resolution is to let the design and execution be the brand signal, while the information architecture stays recruiter-legible.",{"type":19,"tag":20,"props":67,"children":68},{},[69],{"type":24,"value":70},"The aesthetic direction came from a reference-gathering pass before touching any code. The process was simple: collect 15-20 portfolios that made me stop scrolling, then identify what they had in common.",{"type":19,"tag":20,"props":72,"children":73},{},[74],{"type":24,"value":75},"A few that stuck:",{"type":19,"tag":20,"props":77,"children":78},{},[79,92],{"type":19,"tag":80,"props":81,"children":82},"strong",{},[83],{"type":19,"tag":84,"props":85,"children":89},"a",{"href":86,"rel":87},"https://bradleyziffer.com",[88],"nofollow",[90],{"type":24,"value":91},"Bradley Ziffer",{"type":24,"value":93}," for the editorial confidence. Clean white space, strong type hierarchy, nothing decorative for its own sake. The kind of site where the restraint is the statement.",{"type":19,"tag":20,"props":95,"children":96},{},[97,107],{"type":19,"tag":80,"props":98,"children":99},{},[100],{"type":19,"tag":84,"props":101,"children":104},{"href":102,"rel":103},"https://www.iraklis.work",[88],[105],{"type":24,"value":106},"Irakli Sadgobelashvili",{"type":24,"value":108}," (found via Framer's community showcase) for the motion language. Scroll-driven reveals that feel inevitable rather than flashy. The cursor, the page transitions, the way each section earns attention rather than demands it.",{"type":19,"tag":20,"props":110,"children":111},{},[112,122],{"type":19,"tag":80,"props":113,"children":114},{},[115],{"type":19,"tag":84,"props":116,"children":119},{"href":117,"rel":118},"https://paco.me",[88],[120],{"type":24,"value":121},"Paco Coursey",{"type":24,"value":123}," for typographic discipline. Design engineer at Linear, formerly Vercel. His site has essentially no decoration — the type does everything. A useful counterweight when you're tempted to add one more visual element.",{"type":19,"tag":20,"props":125,"children":126},{},[127,137],{"type":19,"tag":80,"props":128,"children":129},{},[130],{"type":19,"tag":84,"props":131,"children":134},{"href":132,"rel":133},"https://rauno.me",[88],[135],{"type":24,"value":136},"Rauno Freiberg",{"type":24,"value":138}," for interaction detail. Staff design engineer at Vercel. Every hover state and transition feels considered. His writing on interaction design (\"Invisible Details of Interaction Design\") is worth reading before you touch a single GSAP tween.",{"type":19,"tag":20,"props":140,"children":141},{},[142,144,154],{"type":24,"value":143},"And yes, ",{"type":19,"tag":80,"props":145,"children":146},{},[147],{"type":19,"tag":84,"props":148,"children":151},{"href":149,"rel":150},"https://bruno-simon.com",[88],[152],{"type":24,"value":153},"Bruno Simon",{"type":24,"value":155}," is always on the reference list. A portfolio where you drive a toy car through a 3D world to see the work. Genuinely one of the most memorable things on the web. I would have done the same, but I don't have the gamedev skills.",{"type":19,"tag":20,"props":157,"children":158},{},[159],{"type":24,"value":160},"The pattern across the rest of them: light backgrounds, one strong accent used sparingly, large type with real weight, and animation that respects the reader's attention.",{"type":19,"tag":20,"props":162,"children":163},{},[164,166,172],{"type":24,"value":165},"The result for this site: white background, Syne for display type, Inter for body, one accent color (",{"type":19,"tag":31,"props":167,"children":169},{"className":168},[],[170],{"type":24,"value":171},"#0001dc",{"type":24,"value":173},"), and GSAP-driven reveals that earn attention rather than demand it.",{"type":19,"tag":45,"props":175,"children":177},{"id":176},"the-stack",[178],{"type":24,"value":179},"The stack",{"type":19,"tag":20,"props":181,"children":182},{},[183,188,190,196],{"type":19,"tag":80,"props":184,"children":185},{},[186],{"type":24,"value":187},"Nuxt 3",{"type":24,"value":189}," was already there and there was no reason to change it. Good SSG support, ",{"type":19,"tag":31,"props":191,"children":193},{"className":192},[],[194],{"type":24,"value":195},"@nuxt/content",{"type":24,"value":197}," for markdown, file-based routing that maps cleanly to the page structure I wanted.",{"type":19,"tag":20,"props":199,"children":200},{},[201,206,208,213,215,221],{"type":19,"tag":80,"props":202,"children":203},{},[204],{"type":24,"value":205},"GSAP + Lenis",{"type":24,"value":207}," handle all animation and scroll. Lenis gives the smooth scroll physics. GSAP drives the rest: staggered name reveal on the hero, strikethrough animation on the title, scroll-triggered section reveals, and the ",{"type":19,"tag":31,"props":209,"children":211},{"className":210},[],[212],{"type":24,"value":171},{"type":24,"value":214}," curtain wipe between pages. The cursor is a lagging circle built with GSAP's ",{"type":19,"tag":31,"props":216,"children":218},{"className":217},[],[219],{"type":24,"value":220},"quickTo",{"type":24,"value":222},".",{"type":19,"tag":20,"props":224,"children":225},{},[226,231],{"type":19,"tag":80,"props":227,"children":228},{},[229],{"type":24,"value":230},"Tailwind",{"type":24,"value":232}," for styling. The constraint of utility classes forces decisions that scoped CSS lets you defer. Combined with CSS custom properties for design tokens, changing the accent color is a one-line edit.",{"type":19,"tag":20,"props":234,"children":235},{},[236,244],{"type":19,"tag":80,"props":237,"children":238},{},[239],{"type":19,"tag":31,"props":240,"children":242},{"className":241},[],[243],{"type":24,"value":195},{"type":24,"value":245}," for blog posts and case studies. Markdown files in the repo, queried and rendered by Nuxt. No CMS dashboard, no external service. PostHog wrote a good piece on why GitHub makes a surprisingly good CMS. The short version: your content workflow is just git, which is a workflow you already know.",{"type":19,"tag":45,"props":247,"children":249},{"id":248},"the-content-structure",[250],{"type":24,"value":251},"The content structure",{"type":19,"tag":20,"props":253,"children":254},{},[255],{"type":24,"value":256},"The hybrid page structure (home highlights + deep-dive pages) was a deliberate choice over a single long scroll. It means:",{"type":19,"tag":258,"props":259,"children":260},"ul",{},[261,267,272],{"type":19,"tag":262,"props":263,"children":264},"li",{},[265],{"type":24,"value":266},"The home page is a strong first impression, not a CV dump",{"type":19,"tag":262,"props":268,"children":269},{},[270],{"type":24,"value":271},"Case studies get real estate to tell a story",{"type":19,"tag":262,"props":273,"children":274},{},[275],{"type":24,"value":276},"The blog has a home that isn't buried",{"type":19,"tag":20,"props":278,"children":279},{},[280,282,287,289,295],{"type":24,"value":281},"Structured data (experience, skills, education) lives in ",{"type":19,"tag":31,"props":283,"children":285},{"className":284},[],[286],{"type":24,"value":36},{"type":24,"value":288},". Long-form content (case studies, blog posts) lives in ",{"type":19,"tag":31,"props":290,"children":292},{"className":291},[],[293],{"type":24,"value":294},"content/",{"type":24,"value":296}," as markdown. The split made sense: tabular data is the right shape for JSON, documents are the right shape for markdown.",{"type":19,"tag":45,"props":298,"children":300},{"id":299},"what-id-do-differently",[301],{"type":24,"value":302},"What I'd do differently",{"type":19,"tag":20,"props":304,"children":305},{},[306,308,313,315,321],{"type":24,"value":307},"The ",{"type":19,"tag":31,"props":309,"children":311},{"className":310},[],[312],{"type":24,"value":195},{"type":24,"value":314}," query API has some rough edges around sorting. I ended up adding an explicit ",{"type":19,"tag":31,"props":316,"children":318},{"className":317},[],[319],{"type":24,"value":320},"order",{"type":24,"value":322}," field to frontmatter because alphabetical filename ordering isn't relevance ordering. Small thing, slightly annoying.",{"type":19,"tag":20,"props":324,"children":325},{},[326],{"type":24,"value":327},"The full mobile parity decision (vs graceful degradation for animations) added build time. Worth it for the end result, but not a decision to make casually.",{"type":19,"tag":45,"props":329,"children":331},{"id":330},"the-positioning-question",[332],{"type":24,"value":333},"The positioning question",{"type":19,"tag":20,"props":335,"children":336},{},[337],{"type":24,"value":338},"The most important work wasn't technical. It was figuring out what the site should actually say.",{"type":19,"tag":20,"props":340,"children":341},{},[342],{"type":24,"value":343},"The old site called me a \"Senior Fullstack Developer.\" That's accurate but inert. The new site calls me a \"Senior Product Engineer\" and the hero animation fades that back to reveal \"builder.\" The tagline is \"I build things that work — and prove that they do.\" A word and a sentence, both actually true about how I work.",{"type":19,"tag":20,"props":345,"children":346},{},[347],{"type":24,"value":348},"Getting that right took longer than the GSAP animations.",{"type":19,"tag":20,"props":350,"children":351},{},[352,354,361],{"type":24,"value":353},"The code is on ",{"type":19,"tag":84,"props":355,"children":358},{"href":356,"rel":357},"https://github.com/leonardo-p-miranda/portfolio",[88],[359],{"type":24,"value":360},"GitHub",{"type":24,"value":362}," if you want to poke around.",{"title":8,"searchDepth":364,"depth":364,"links":365},2,[366,367,368,369,370],{"id":47,"depth":364,"text":50},{"id":176,"depth":364,"text":179},{"id":248,"depth":364,"text":251},{"id":299,"depth":364,"text":302},{"id":330,"depth":364,"text":333},"markdown","content:blog:how-this-portfolio-was-built.md","content","blog/how-this-portfolio-was-built.md","blog/how-this-portfolio-was-built","md",{"_path":378,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":379,"description":380,"slug":381,"date":382,"summary":383,"readingTime":384,"body":385,"_type":371,"_id":525,"_source":373,"_file":526,"_stem":527,"_extension":376},"/blog/why-xp-still-matters","Why XP Still Matters (And Why Most Teams Get It Wrong)","Extreme Programming has a branding problem.","why-xp-still-matters","2025-01-15","Extreme Programming gets dismissed as a relic, but the teams I've worked with that actually practice it (really practice it) consistently ship better software. Here's what that actually looks like.","5 min",{"type":16,"children":386,"toc":520},[387,391,396,401,406,412,417,422,427,432,438,443,453,463,473,479,484,489,494,499,503],{"type":19,"tag":20,"props":388,"children":389},{},[390],{"type":24,"value":380},{"type":19,"tag":20,"props":392,"children":393},{},[394],{"type":24,"value":395},"The name sounds like something from a Mountain Dew ad circa 2001. When you tell people you practice XP, they either think you mean Windows XP or assume you're describing a watered-down version of Scrum with some pair programming bolted on.",{"type":19,"tag":20,"props":397,"children":398},{},[399],{"type":24,"value":400},"The teams I've seen do XP badly outnumber the ones doing it well by a wide margin. But the ones doing it well (genuinely well) are some of the most effective engineering environments I've been part of.",{"type":19,"tag":20,"props":402,"children":403},{},[404],{"type":24,"value":405},"Here's what actually separates the two.",{"type":19,"tag":45,"props":407,"children":409},{"id":408},"the-mistake-treating-xp-as-a-checklist",[410],{"type":24,"value":411},"The mistake: treating XP as a checklist",{"type":19,"tag":20,"props":413,"children":414},{},[415],{"type":24,"value":416},"Most teams that claim to do XP are actually doing a subset of the practices, divorced from the values that make them coherent. They do standups (communication ✓), they pair sometimes (pair programming ✓), they write tests after the fact (TDD ✓, sort of), and they call it done.",{"type":19,"tag":20,"props":418,"children":419},{},[420],{"type":24,"value":421},"This misses the point entirely.",{"type":19,"tag":20,"props":423,"children":424},{},[425],{"type":24,"value":426},"XP's practices are load-bearing against each other. TDD without pairing means the tests tell you one person's mental model of the code. Pairing without shared ownership means you're just watching each other write bad code. Continuous integration without small releases means you're integrating a big mess frequently.",{"type":19,"tag":20,"props":428,"children":429},{},[430],{"type":24,"value":431},"The practices are a system. Pull one out and the rest weaken.",{"type":19,"tag":45,"props":433,"children":435},{"id":434},"what-real-xp-actually-looks-like",[436],{"type":24,"value":437},"What real XP actually looks like",{"type":19,"tag":20,"props":439,"children":440},{},[441],{"type":24,"value":442},"The best XP team I worked with had a few things in common that I didn't fully appreciate until I'd been away from it for a while:",{"type":19,"tag":20,"props":444,"children":445},{},[446,451],{"type":19,"tag":80,"props":447,"children":448},{},[449],{"type":24,"value":450},"The feedback loops were genuinely short.",{"type":24,"value":452}," Not \"we ship every two weeks\" short. More like \"we know within an hour whether this was a good idea\" short. Tests ran in under a minute. Pairs rotated frequently enough that no single person could bury something. Code review happened in real time, not asynchronously.",{"type":19,"tag":20,"props":454,"children":455},{},[456,461],{"type":19,"tag":80,"props":457,"children":458},{},[459],{"type":24,"value":460},"Collective ownership was taken seriously.",{"type":24,"value":462}," Anyone could change any file. This sounds chaotic and initially feels that way. What it actually does is prevent the hoarding of context. Nobody becomes the sole owner of a module, which means nobody becomes the bottleneck for understanding it.",{"type":19,"tag":20,"props":464,"children":465},{},[466,471],{"type":19,"tag":80,"props":467,"children":468},{},[469],{"type":24,"value":470},"The simplest thing that could possibly work was the goal.",{"type":24,"value":472}," Not the most elegant thing. Not the most future-proof thing. The simplest thing. Then they'd look at it together and ask if it was actually too simple. Usually it wasn't.",{"type":19,"tag":45,"props":474,"children":476},{"id":475},"why-it-still-matters",[477],{"type":24,"value":478},"Why it still matters",{"type":19,"tag":20,"props":480,"children":481},{},[482],{"type":24,"value":483},"We live in an era of AI-assisted coding, enormous codebases, and distributed teams. The instinct is to reach for more tooling, more process, more documentation to deal with the complexity.",{"type":19,"tag":20,"props":485,"children":486},{},[487],{"type":24,"value":488},"XP's answer is the opposite: reduce the surface area. Write less code, but know it better. Ship sooner, but test more. Own the whole codebase, but together.",{"type":19,"tag":20,"props":490,"children":491},{},[492],{"type":24,"value":493},"That discipline (the refusal to let complexity accumulate) is harder than it sounds. It requires constant social negotiation, a shared commitment to quality that has to be actively maintained, and a willingness to slow down briefly in order to go faster for longer.",{"type":19,"tag":20,"props":495,"children":496},{},[497],{"type":24,"value":498},"Most teams don't want to pay that cost. The ones that do tend to build things that last.",{"type":19,"tag":500,"props":501,"children":502},"hr",{},[],{"type":19,"tag":20,"props":504,"children":505},{},[506],{"type":19,"tag":507,"props":508,"children":509},"em",{},[510,512,518],{"type":24,"value":511},"I've worked with XP teams at O Novo Mercado and FIT. Both experiences shaped how I think about software quality and delivery speed. I'm happy to talk shop. ",{"type":19,"tag":84,"props":513,"children":515},{"href":514},"mailto:me@leomiranda.dev",[516],{"type":24,"value":517},"Reach out",{"type":24,"value":519}," if you want to compare notes.",{"title":8,"searchDepth":364,"depth":364,"links":521},[522,523,524],{"id":408,"depth":364,"text":411},{"id":434,"depth":364,"text":437},{"id":475,"depth":364,"text":478},"content:blog:why-xp-still-matters.md","blog/why-xp-still-matters.md","blog/why-xp-still-matters",1780153358225]