Building a Self-Hosted Portfolio: Zero CDNs, Zero Compromises
How this site was built — Next.js 15, Tailwind CSS v4, shadcn/ui, Framer Motion, self-hosted fonts, and a deliberate policy of no third-party requests.
This site is the first real product of the stack I reach for on client work at XAI.ma. It is fully self-hosted — no CDN fonts, no runtime Tailwind, no external icons, no analytics beacons phoning home. That sounds like a stylistic choice. It's actually an engineering one.
The stack
- Next.js 15 with the App Router, Server Components, and Server Actions
- TypeScript everywhere, strict mode on
- Tailwind CSS v4 with the new CSS-first
@themeconfiguration - shadcn/ui primitives backed by Radix UI
- Framer Motion for scroll reveals, the marquee, counters, and pill tabs
- TanStack Query, Zustand, React Hook Form, Zod for data fetching, state, forms, and validation
- Fonts — Fraunces, Inter, JetBrains Mono — self-hosted through
next/font
The remarkable thing is not that this stack exists; it's that all of it compiles to a few hundred kilobytes and renders static HTML for every route. The blog is MDX with a custom content layer. The projects are case studies in the same system. Nothing is fetched at runtime except the contact form.
Why no CDN?
Three reasons, and they're all practical rather than ideological:
Privacy. Every third-party script is a data leak. When a font loads from Google's servers, Google learns someone visited this site. When analytics loads from a SaaS, the SaaS builds a profile. On a personal site with zero tracking needs, the default should be no external requests, and you have to justify each exception rather than the reverse.
Reliability. A CDN link rots. A domain changes. A script tag breaks your page in a browser you didn't test. Every external dependency is a single point of failure that you don't control and that you'll discover on a Sunday. Self-hosting means the failure surface is exactly the surface you manage.
Speed. Fewer round trips. Fonts are bundled, hashed, and preloaded by Next at build time. No render-blocking stylesheet from another origin, no preconnect dance, no DNS lookup for a host that exists only to serve a font. The performance budget is enforced by construction, not by monitoring.
There's a fourth reason that matters for the business side of my work: clients notice. When I hand over a project and the audit says "zero third-party requests, all Core Web Vitals in the green," that is a feature they can put in a proposal. The discipline is the product.
The terminal, the marquee, and the little things
Most of the visible craft is in the details. The terminal — now living in the footer — is a real state machine, not a looping animation:
// type once, then hold — no infinite retype loop
const tick = () => {
setProgress((prev) => {
if (prev.done) return prev;
const current = LINES[prev.line];
if (prev.chars < current.length) return { ...prev, chars: prev.chars + 1 };
if (prev.line < LINES.length - 1) return { line: prev.line + 1, chars: 0 };
return { line: LINES.length, chars: 0, done: true };
});
};
It types the window title first, then the body, then stops with a blinking caret. That sequencing matters: an animation that respects the viewer's attention reads as deliberate. The macOS traffic-light dots in the terminal header are a running theme across the site — the CV modal, the code blocks, the terminal. Cohesion is underrated.
Reduced motion is respected everywhere via useReducedMotion. That isn't a checkbox; it's a test of whether the animation was built to serve the content or to serve itself.
Build decisions that mattered
A few choices shaped everything else, and they're worth writing down because they transfer directly to client work.
Content as code. Every blog post and project case study is an MDX file in the repository. Frontmatter drives the metadata — title, description, category, publication status — and a small content layer reads the filesystem at build time. The sitemap is generated from the same source of truth. This means SEO metadata, sitemaps, reading time, and the blog index can never drift out of sync, because they're all derived from one place.
// the content layer — one source of truth for posts and sitemaps
export function getAllPosts(): BlogPost[] {
return listMdx(BLOG_DIR)
.map((file) => {
const raw = fs.readFileSync(path.join(BLOG_DIR, file), "utf8");
const { data } = matter(raw);
return { ...(data as PostFrontmatter), slug, readingTime: readingTime(raw) };
})
.filter((p) => p.published)
.sort((a, b) => (a.date < b.date ? 1 : -1));
}
The blog index is a data product. Search, category filters, and reading-time chips run off a TanStack Query cache with the same initial data the server rendered. The page is instantly interactive and never flashes a loading state — progressively enhanced by design, not as an afterthought.
Syntax highlighting is a first-class feature. Code blocks in posts render as terminal windows — traffic-light dots, a filename label, dark background — with real syntax highlighting applied at build time. The highlight step is a remark/rehype plugin, so posts are authored in plain markdown and the prettiness is free.
The CV is a product too. The "View CV" button opens a modal with an embedded PDF preview — paginated, with download and close-on-Escape — rather than dumping a file in a new tab. Small touch, but it keeps the visitor inside the site's visual language.
SEO by default
Every page ships structured data, a canonical URL, and Open Graph tags. The robots file and sitemap are generated rather than hand-maintained. Performance budgets are enforced at the framework level: static rendering by default, no runtime data fetching, self-hosted fonts.
The reason this matters for a portfolio is the same reason it matters for clients: a site that ranks and loads fast is a site that gets the meeting. Good engineering and good SEO are the same discipline seen from two angles — both are about removing friction between the visitor and the answer.
What's next
The blog you're reading is the first of two content systems on this site. Each project gets a full case study page — goals, architecture, stack, and lessons learned. The writing goes beyond engineering: I've been publishing on use cases with founders, SEO-to-GEO, and how AI consulting actually works, because the best engineers are the ones who can explain the why as clearly as the how.
The site itself is a living artifact. It will keep changing — new projects, new posts, new experiments — but the constraints stay: fast, private, self-hosted, and built with a stack I'd put in front of any client.
If you're building something similar and want an opinionated take, get in touch. This is exactly the kind of work we ship at XAI.ma.