Choosing a stack is a product decision before it is a technical one. This
article documents why this portfolio runs on Bun, Astro, and MDX — and,
more importantly, the reasoning that led me to turn down more sophisticated
alternatives. Nothing here is theory: every claim points to something
verifiable in this repository.
The problem
I needed a portfolio that could carry professional authority as a Software
Engineer going deeper into Applied AI Engineering. The requirements were
concrete: bilingual content (Portuguese and English), project cases and
articles with their own identity, solid SEO, high performance, genuine
accessibility, and maintenance simple enough that I would never have to
think about it twice. No backend, no platform team, no infrastructure
budget — just me, the code, and the content.
The question behind the architecture
The guiding question was simple: what is the simplest solution capable of
solving this problem? Not "which stack is trending," and not "which one
shows off the most skill." Technology chosen for hype often charges
interest in complexity. Every layer I added had to justify itself against this specific
problem — a mostly static content site maintained by one person.
Why Bun
Bun is this project's toolchain: package manager, runner for the
development scripts, and runtime for the validation tooling. The dev
server, build, artifact validation, asset measurement, and release-check
scripts are TypeScript executed directly by Bun, with no separate
transpilation layer for tooling. I make no comparative claims I haven't
measured here — the decision was about structural simplicity: one tool
where there would otherwise be several. What is observable is objective
but contextual: in this implementation, on the local environment, the full
build renders every page in about two seconds.
Why Astro
This site's content is predominantly static, so HTML is generated at build
time and served as files — no application server, no database, no
per-request runtime. Astro was chosen for exactly that model: it lets me
write composable editorial content while shipping almost no JavaScript to
the browser. The script embedded in the home page is 277 bytes (198
gzipped) and exists for exactly one job: the mobile menu. Zero bytes of
client-side framework — measured, not estimated. React was never installed
because no problem required React. This model also simplifies SEO: every
page arrives as complete HTML, with its own canonical and alternates.
Why MDX
Articles and cases live versioned alongside the code, in the same
repository, under the same review process. There is no CMS or backend to
maintain, update, or pay for — and no content outside version control. MDX
gives the content structure (schema-validated frontmatter requiring a
bilingual pair, slug, category, and review status), which makes every piece
of writing an artifact as reviewable as any other code. Publishing means
merging and building; rolling back means reverting a commit.
Why not Next.js, SPA, or CMS
Not because they are bad technologies — they are excellent for the right
problems. A full-stack framework would solve problems I don't have:
per-request rendering, API routes, global client state. A client-rendered
SPA would add JavaScript runtime and browser state to a site whose main
job is delivering ready-to-read content. A CMS would trade versioned files for an external dependency with logins,
backups, and a bill. None of that complexity was justified by the current
problem, so none of it got in.
Bilingualism as an architecture requirement
Bilingualism is not a plugin bolted on afterward — it shaped the
architecture. Portuguese lives at /, English at /en/, and every
published item is required to exist in both languages: the publication
model rejects at build time anything published without its reviewed
counterpart. Every page carries its own canonical and three alternates
(current language, opposite language, and x-default), and the artifact
validator checks the pairs across every generated page. Incomplete content
simply never reaches production: drafts are excluded from the production
build and included only in the preview build. The projects page
and the about page exist in both languages because the
system would accept nothing less.
Quality as part of the product
Here, quality is not a promise — it is a pipeline. Every change goes
through strict typechecking (zero errors), linting, verified formatting,
unit tests for the publication model (4/4), thirteen browser tests
(reciprocal PT/EN navigation, keyboard menu with Escape, no-JavaScript
navigation, localized 404, explicit external links, 320px reflow with
automated WCAG checks), and
artifact validation in both build modes. If something breaks, the build
says so — before any human needs to check.
CI/CD pipeline with GitHub Actions covering type checking, linting, unit
tests, Playwright E2E tests, artifact validation, and production builds.
Delivery follows a Continuous Delivery model: the same CI-approved
artifact is published after explicit manual approval, without rebuilding,
to a dedicated production branch and then deployed to Hostinger.
Measured performance
What follows are laboratory measurements, never real-user data. I ran
Lighthouse 13.4.1 in headless Chromium, mobile simulation (412×823
viewport, simulated network and CPU throttling), three runs per page,
serving the local static build — methodology recorded in
scripts/lighthouse.ts, results in reports/lighthouse-summary.json. On
the home page: performance 100 and accessibility 100 across all three
runs, LCP between roughly 1.5 s and 1.7 s, CLS around 0.0006, zero TBT,
and about 93.5 KB of initial transfer. The asset budget
(reports/assets.json) shows where the lightness comes from: roughly
6 KB of gzipped CSS, about 0.2 KB of inline JavaScript, and fonts totaling
around 48 KB. Lab numbers inform decisions; they prove nothing about real
user experience, and I would never present them as such.
A bug worth finding
The artifact validator flagged a broken link on the Portuguese 404 page:
/404/. The cause was in the header's language switcher, which used
Astro.url.pathname as the current-language link — and with trailing
slashes always on, that pathname renders as /404/. But the actual
artifact Astro generates for 404.astro is 404.html, a special 404
document for static hosting; the /404/ route never existed. The fix went
into the routing model (the 404 page now links its canonical
/404.html and /en/404/ paths), not into the test. Weakening the
validator would have hidden the symptom and destroyed its value. The
episode became a working rule: fix the publication model, never work
around the validator.
Trade-offs
Every choice has a cost, and these are mine, accepted: no backend, no
database, no CMS — any new content requires a commit, a build, and a
deploy. No React at launch — richer interactivity in the future will
require revisiting the decision. No analytics — there is no real-usage
telemetry, so the lab metrics above are the ceiling of what I can claim
today. And bilingualism costs double review: every piece must exist, make
sense, and be approved in both languages. A deliberate cost, because
international reach and consistency across languages are part of the
product I want to build.
Conclusion
Mature engineering is not about choosing the most sophisticated stack —
it is about choosing complexity proportional to the problem. This
portfolio could have been a full-stack monolith, an SPA, or a CMS
instance; those approaches could work, but they would introduce
capabilities and operational costs this project's requirements never asked
for. Technology starts from the human
problem, not from the code: understand what needs to change, choose
deliberately, and examine the outcome with evidence. FROM REAL PROBLEMS TO
INTELLIGENT PRODUCTS — including when building the showcase itself.
Top comments (2)
"Technology chosen for hype charges interest in complexity" is the line worth stealing, and the 277 bytes of client JS (measured, not estimated) is what makes the rest credible. The part I'd underline is bilingualism as a build-time gate rather than a plugin: rejecting a PT-only publication during the build is what stops the two trees from drifting, and no lint rule enforces that reliably.
I hit the failure mode in reverse in a small i18n parity test: the rule held, but the order of operations inside the build didn't. The canonical and hreflang alternates were emitted before the draft filter ran, so a page whose English pair was still a draft shipped with
x-defaultpointing at a URL that later 404'd, and the crawler kept the draft in the index for weeks. Same invariant, wrong position in the pipeline.On the artifact validator: does it verify hreflang reciprocity on the rendered HTML of every page (PT lists EN, EN lists PT, both list x-default), or only that the pairs exist in the publication model? The first catches template regressions, the second only catches missing content. And is the ~2s build measured across how many pages? The number only means something next to its page count.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.