Starting point
Resume
Runtime
TanStack + Nitro
Publishing
Custom admin
The goal was not to build an online resume with better colors. The goal was to make the resume behave like a small product: navigable, editable, opinionated, and specific enough that the implementation itself says something about the engineer behind it.
Decision 01
Let the resume stay honest
The page still has to answer ordinary resume questions: what have I done, what can I build, what do I care about, and how do you reach me. The trick is to stop treating those answers like a document export. Each section should make one claim and then back it up with visible evidence.
What this creates
The homepage becomes a sequence of chapters instead of a single scroll of credentials.
Decision 02
Use TanStack Start because the site is small, not because it is simple
A portfolio can look static while still needing real application boundaries. TanStack Start gives the site file routes, loaders, server functions, and typed links without splitting the work into a separate API project. That keeps the architecture legible.
What this creates
Public content is server loaded into the initial HTML, while private editing stays behind a custom admin.
Decision 03
Use SQLite before reaching for heavier machinery
The site needs durable content, drafts, ordering, contact submissions, sessions, and migrations. It does not need a hosted database dashboard or a distributed system. SQLite gives the project a real data layer while keeping the operational shape small.
What this creates
Content lives outside the code checkout in production, so deploys can change the app without replacing the data.
Decision 04
Keep Markdown fast, but do not let it own every idea
Most writing should be easy to publish. Some pieces need layout, rhythm, code panels, diagrams, or interactive sections. The system supports both by letting a post choose Markdown or a registered custom component.
What this creates
The same article route can render a plain field note or a designed essay without forking navigation and metadata.
Decision 05
Make deployment boring on purpose
A personal site is still production software once people can visit it. The deploy path runs checks, builds the Nitro output, applies migrations, and restarts a Node process on a custom Linux server. The server is where the app runs, not where the app is designed.
What this creates
GitHub Actions owns verification and release steps; the host receives a known build and keeps persistent data in one place.
Reading model
A portfolio has to explain what kind of attention it wants.
Someone scanning a resume is usually looking for keywords. Someone reading a portfolio is looking for judgment. The page needs enough structure to let both people move quickly without flattening the work into bullet points.
The page should teach the reader how to read you.
A strong portfolio creates a small language: chapter labels, repeated card shapes, a consistent accent color, and a rhythm that turns experience into evidence.
Architecture
The app structure is part of the argument.
The folder layout keeps the public experience, private publishing tools, custom field notes, and server-only data access easy to find. That is intentional. The site should be small enough to understand and structured enough to grow without becoming a pile of exceptions.
Folder structure
src/
components/
custom-admin/
home/
field-notes/
custom/
PortfolioPageLesson.tsx
index.tsx
metadata.ts
routes/
__root.tsx
index.tsx
field-notes.tsx
field-notes/
$slug.tsx
custom-admin/
server/
content.server.ts
posts.functions.ts
posts.server.ts
db/
migrations/Server-rendered route data
export const Route = createFileRoute('/')({
loader: async () => {
const [coreStack, fieldNotes, footerLinks] = await Promise.all([
listCoreStackItemsFn(),
listPublishedPostsFn(),
listFooterLinksFn(),
])
return { coreStack, fieldNotes, footerLinks }
},
component: Home,
})Server function wrapper
export const listPublishedPostsFn = createServerFn({
method: 'GET',
}).handler(async () => {
return listPublishedPosts()
})Post content mode migration
ALTER TABLE posts
ADD COLUMN content_type TEXT NOT NULL DEFAULT 'markdown'
CHECK (content_type IN ('markdown', 'custom'));
ALTER TABLE posts
ADD COLUMN custom_component TEXT;Custom post rendering
if (post.contentType === 'custom') {
const CustomFieldNote = customFieldNoteComponents[post.customComponent]
return <CustomFieldNote post={post} />
}
return <article dangerouslySetInnerHTML={{ __html: post.html }} />The database stores the editorial choice. The code owns the experience.
The custom admin does not need a visual page builder. It only needs to know whether a post is Markdown or custom, and which component key to use when it is custom. That keeps authorship simple while still leaving space for one-off designed pieces like this one.
Frontend system
The front end should feel authored, not decorated.
The homepage uses motion, chapter labels, dense cards, and a tight accent palette to create a recognizable system. The work is not in adding more effects; it is in making the effects behave well on real screens and letting the content stay readable.
Use responsive type and spacing so the page feels composed instead of merely squeezed.
Reduce background work on small and touch-first screens where decorative motion can become expensive.
Keep shared section patterns stable so custom article moments have somewhere to stand.
Use code snippets only when they reveal a decision the prose cannot explain as clearly.
The deployment pipeline is part of the craft.
The production setup keeps the SQLite database in persistent server storage, outside the repository checkout. That choice matters: content can survive deploys, migrations can run deliberately, and the server process can restart without treating the repo as the source of truth for everything.
GitHub Actions outline
name: Deploy
on:
push:
branches: [main]
jobs:
ship:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm buildResources
The best resume-based portfolio does not hide the resume. It gives the resume an interface.
Portfolio Page Lesson