Automata Portfolio | Automata
sections (10)
$ cd .. // back to projects
automata@latam: ~/projects/automata-portfolio
PRODUCT · slug: automata-portfolio ·2026 · 4 min read · 911 words

> Automata Portfolio

// This site. Astro 5 static portfolio + 9 case studies with a terminal aesthetic, live Mermaid diagrams, Cmd+K search, server-side syntax highlighting and a single translations.ts as the data source.

Astro 5TypeScriptTailwind CSS 4MermaidShikiGSAPLenisPlaywright
forks last commit
Automata Portfolio
▸ role
Design · FE · Infra · Content
▸ timeline
~1 week of sprints (LLM-paired)
▸ team
Solo
▸ status
online
// section 01 · discovery

$ cat ./discovery.md

▸ overview

Portfolio + case-study site for Automata. Every project lives as one object in src/i18n/translations.ts; the [slug].astro route renders the 10-section template from that data, so adding a project is a typed edit, not a new file. Mermaid diagrams render lazily from a CDN with a custom theme that matches the rest of the site. Code blocks are highlighted server-side with Shiki (zero client JS for highlighting). Bilingual: EN at /, ES at /es. The site is a static build deployed via GitHub Actions FTP.

▸ problem

Notion-as-portfolio and Webflow templates look the same as everyone else's, and Linktree does not show technical depth. The point is to be evidence of craft, not a list of skills — and to add a new project by editing one typed record, not by setting up a CMS.

▸ audience

Potential clients (CTOs, founders) evaluating Automata, and technical recruiters who need to see real depth in under five minutes.

// section 02 · design

$ cat ./design.md

▸ tools
Figma (mockups)VS Code (design-in-code)Excalidraw (C4 sketches)
▸ design system
  • · JetBrains Mono everywhere, ligatures on
  • · Palette: #000 / #0D1117 panel / #4ADE80 green / #06B6D4 cyan / #FBBF24 amber
  • · Prompt grammar: $ ▸ ○ // as visual punctuation
  • · Window-chrome dots (● ● ●) on each panel
  • · Border-color as hover state — no shadows, no gradients
// section 03 · architecture

$ cat ./architecture.md

// section 04 · infrastructure

$ cat ./infrastructure.md

▸ services
provider: Astro 5 SSG + edge CDN
  • astro build (Sharp for image optimization)
  • Static export: HTML + assets, no Node runtime in production
  • Hosting: any static (FTP to shared host today; Vercel/CF Pages-compatible)
  • Plausible analytics (cookieless)
  • Formspree for the contact form
  • GitHub API fetched client-side for stars/forks/last commit on project headers
  • Mermaid lazy-loaded from esm.sh; Shiki runs at build time
// section 05 · implementation

$ cat ./implementation.md

▸ frontend
  • · Astro 5.16 (islands, SSG)
  • · TypeScript (strict)
  • · Tailwind CSS 4
  • · GSAP + ScrollTrigger
  • · Lenis (smooth scroll)
▸ backend
  • ·
  • · (static, no runtime)
▸ data
  • · translations.ts (single in-memory record, EN + ES)
  • · GitHub API (live fetch)
  • · Formspree (form submissions)
▸ ai
  • · Shiki (syntax highlighting, server-side)
  • · Mermaid (live diagram rendering)
  • · Claude Code (LLM pair on every session)
▸ devops
  • · GitHub Actions (CI: typecheck + build + Playwright)
  • · GitHub Actions FTP deploy on push to main
  • · i18n audit script (tools/i18n-audit.mjs)
// section 06 · technical challenges

$ cat ./challenges/*.md

// 4 technical problems solved

01 / 04
challenge-01.md · astro · compiler · patterns
▸ problem

Astro rejects <slot name={dynamic} /> inside a .map() — slot names must be literal at parse time.

constraint: MasterDetail needs to render a different pane per item, but Astro will not compile a dynamic named slot.

▸ approach

Invert the contract: MasterDetail exposes one default slot; callers tag each pane with data-md-key. A small script in MasterDetail toggles visibility based on that attribute. Compile-time error gone, caller-side stays explicit.

src/components/MasterDetail.astro astro
<!-- Generic: does not know what goes in each pane -->
<div class="md-grid" data-masterdetail>
  <nav>{items.map((it, i) => (
    <button data-md-pick={it.key} data-active={i === 0}>{it.label}</button>
  ))}</nav>
  <div><slot /></div> <!-- caller drops <div data-md-key="..."> here -->
</div>

<!-- Caller: explicit, type-safe -->
<MasterDetail items={levels}>
  <div data-md-key="context"><MermaidViewer ... /></div>
  <div data-md-key="container" class="hidden"><MermaidViewer ... /></div>
</MasterDetail>
challenge-02.md · frontend · scroll · css
▸ problem

Carousel built on scroll-snap fights Lenis smooth-scroll and snaps back to slide 0.

constraint: Wanted native horizontal scroll, perfect snap and Lenis compatibility. Cannot have all three.

▸ approach

Drop scroll-snap entirely. The track translates via transform: translateX(-N * 100%). The wrapper is overflow:hidden — there is no actual scroll, so nothing for Lenis to hijack. Arrows, dots and arrow keys all update the same index.

src/components/Carousel.astro typescript
const update = (i: number) => {
    current = Math.max(0, Math.min(slides.length - 1, i));
    track.style.transform = `translateX(-${current * 100}%)`;
    counter.textContent = String(current + 1).padStart(2, "0");
    dots.forEach((d, di) => d.setAttribute("data-active", String(di === current)));
    if (prev) prev.disabled = current === 0;
    if (next) next.disabled = current === slides.length - 1;
};
challenge-03.md · css · grid · shiki
▸ problem

A sticky-sidebar + code-block grid blows out the viewport horizontally.

constraint: Default min-width:auto on grid items lets them grow to their content. Long Shiki <pre> blocks force the grid wider than 1fr.

▸ approach

Change grid-template-columns from [200px 1fr] to [200px minmax(0, 1fr)], add min-w-0 to the content item. Code blocks scroll internally, grid keeps its width.

src/components/ProjectDetail.astro html
<!-- Before: blows out -->
<div class="lg:grid lg:grid-cols-[200px_1fr]">
  <aside>...</aside>
  <div>...code blocks overflow grid...</div>
</div>

<!-- After: contained -->
<div class="lg:grid lg:grid-cols-[200px_minmax(0,1fr)]">
  <aside>...</aside>
  <div class="min-w-0">...code blocks scroll inside...</div>
</div>
challenge-04.md · mermaid · theming
▸ problem

Mermaid C4 diagrams arrive with hardcoded blue/grey fills that no themeCSS overrides.

constraint: C4Context/Container/Component paint shapes with inline SVG attributes (fill='#08427B'). Neither themeCSS nor CSS classes touch them.

▸ approach

Migrate every C4 diagram from C4Context syntax to plain flowchart TD + classDef. classDef gives total control over fill, stroke and color per node, while still letting the diagram show the same visual primitives (person, boundary, container).

src/components/MermaidViewer.astro javascript
// Before: C4Context (paints with inline SVG fills)
C4Context
  Person(user, "Visitor")
  System(sys, "automata.pe")

// After: flowchart + classDef (themeable)
flowchart TD
  user(((Visitor)))
  sys["automata.pe"]
  user --> sys

  classDef person fill:#0D1117,stroke:#4ADE80,color:#E5E7EB
  classDef system fill:#0D1117,stroke:#06B6D4,color:#E5E7EB
  class user person
  class sys system
// section 07 · testing & ci

$ cat ./testing.md

▸ strategy

Typecheck on every PR (astro check). Playwright smoke test for the critical flow (Cmd+K → pick a project → carousel + lightbox work). Lighthouse CI runs but is soft-failing. i18n audit script asserts EN + ES have the same projects with the same field sets.

▸ tools
astro check (TypeScript)PlaywrightLighthouse CItools/i18n-audit.mjs
// section 09 · results

$ cat ./results.md

01 /
168
commits (and counting)
02 /
12+
reusable components (MasterDetail, Carousel, MermaidViewer, CommandPalette, Lightbox, ...)
03 /
0 KB
client JS on pages without interactivity
04 /
9
case studies, all from one translations.ts
▸ outcomes

The site is the pitch, the portfolio and the sandbox at once. Every component (MasterDetail, Carousel, MermaidViewer, CommandPalette, Lightbox) is reusable in other projects. Adding a case study is one typed object, in EN and ES, audited by a script.

// section 10 · lessons learned

$ cat ./lessons.md

// if I did it again

  • 01 /

    Starting from the aesthetic shaped the whole code API

    The first commit was 'palette + JetBrains Mono'. That decision propagated to CSS variable names (--green, --line), button names (btn-cli), hover style (border-color instead of shadow), even section headings (// section 03). What looked like branding turned out to be the architecture.

  • 02 /

    Server-side highlights beat client-side libraries

    Shiki at build time outputs static HTML — zero client JS. Mermaid cannot be precompiled (it is interactive), so it is lazy-loaded from a CDN. Rule of thumb: if it does not need runtime interactivity, do it server-side.

  • 03 /

    LLM pair-programming changed the explore/build ratio

    Trying four layouts (stacked → carousel → tabs → master-detail) took as long as trying one by hand. The unexpected consequence: granular commits became a discipline, otherwise auditing what got approved becomes impossible.

// next step

$ automata deploy --your-operation

// Let's talk about adapting this to your case.

./let-s-talk.sh