Your own data API — now-playing, projects, and blog posts as clean JSON — plus a thin portfolio frontend that consumes it. Content lives as markdown + JSON on disk, so publishing a post is just adding a file.
pip install -r requirements.txt
uvicorn app.main:app --reload # http://localhost:8000 (portfolio + /docs)| Route | Returns |
|---|---|
GET /api/profile |
name, headline, links |
GET /api/now |
now-playing / current activity (wire to Spotify/Last.fm in prod) |
GET /api/projects |
project list |
GET /api/posts |
post metadata (title, date, summary, tags); ?tag= to filter |
GET /api/posts/{slug} |
full post incl. body |
GET /api/tags |
every tag with its post count, most-used first |
GET /feed.xml |
an RSS 2.0 feed of all posts |
The single-post route reads exactly one file and rejects unsafe slugs (no path traversal). /feed.xml XML-escapes every field, so titles with & / < can't break the feed.
Drop a markdown file in content/posts/ with front matter:
---
title: My new post
date: 2026-06-01
summary: One-line teaser.
tags: [typescript, react]
---
Post body here.Posts are returned newest-first automatically.
The content layer (app/content.py) is a set of pure functions over a directory — front-matter parsing, post loading, profile loading — so it's tested against a fixture folder without spinning up the server. The frontend is intentionally "dumb": it just fetches the API and renders.
python -m pytest -q # 19 tests (content parsing, tag/RSS helpers, traversal safety + API endpoints)MIT
