A personal website built with Next.js using a file-based content management approach. No database, no CMS—just markdown files and CSV data processed at build time.
- Next.js 16 with App Router and
output: 'standalone'for self-contained server deployment - TypeScript for type safety
- Tailwind CSS with
@tailwindcss/typographyfor styling - React 19 for the UI layer
The site uses a file-based content system with two main content types:
-
Records (
content/records/): Blog posts and articles stored as markdown files with frontmatter- Frontmatter includes:
title,date,slug,summary,status(published/draft/hidden) - Processed at build time using
gray-matterandremark/rehypepipelines - Dynamically routed via
[slug]dynamic routes with static generation
- Frontmatter includes:
-
Pages (
content/pages/): Static pages likeabout.md,home.md,now.md- Same markdown processing pipeline as records
- Rendered server-side in their respective page components
Bookmarks and bookshelf can come from Supabase (no rebuild to update) or CSV fallback:
- With Supabase: Set
NEXT_PUBLIC_SUPABASE_URLandSUPABASE_ANON_KEYin.env.local. The app fetches from thebookmarksandbookstables. Use the Supabase dashboard or API to edit data; changes show after revalidation (no deploy). See.env.example. - Without Supabase (or if either env var is missing): The app reads from
data/bookmarks.csvanddata/books.csv. Edit the CSVs and redeploy to update.
Other data remains file-based:
bookmarks.csv/books.csv: Used when Supabase is not configured (see above).music-rotation.csv: Currently listening trackslife-in-weeks.json: Life calendar visualization data
The src/lib/ directory contains utilities for content processing:
records.ts: Record-specific processing with markdown-to-HTML conversion using remark/rehypedata-source.ts: Fetches bookmarks and books from Supabase (when env is set) or falls back to CSV; uses Next.js cache with revalidation.bookmarks.ts: Loads bookmarks via data-source (date-sorted).book-csv.ts: Book/bookshelf lookups via data-source; current book from Supabaseis_currentorconfig.currentBookIdwhen using CSV.music-rotation.ts: Track data processing from CSVgithub.ts: Fetches latest GitHub activity via RSS feed parsing
/: Home page/records: List of all published records/records/[slug]: Individual record pages (statically generated)/records/feed.xml: RSS feed (rewritten from/records/feedroute handler)/about: About page from markdown/now: "Now" page combining markdown content with dynamic data (current book, GitHub activity, latest bookmark, music)/bookmarks: Bookmark listing with pagination/bookshelf: Book archive/life-in-weeks: Life calendar visualization
To use Supabase for bookmarks and bookshelf:
- Create a Supabase project and run the migration in
supabase/migrations/001_bookmarks_and_books.sql(Table Editor > SQL orsupabase db pushif using Supabase CLI). - Add
NEXT_PUBLIC_SUPABASE_URLandSUPABASE_ANON_KEYto.env.local(see.env.example). - Import data: use the Table Editor CSV import for
bookmarksandbooks, or runscripts/import-csv-to-supabase.ts. Setis_current = trueon one book to show it as "currently reading" on the Now page.
npm run dev # Start development server
npm run build # Build for production (outputs standalone bundle)
npm run start # Start production server (not used in prod — see Deployment)
npm run lint # Run ESLintDeploys are automated via GitHub Actions on every push to main. The workflow:
- Builds the app on GitHub's runners (avoids memory constraints of the production VPS)
- Packages the
standaloneoutput with static assets,public/,content/, anddata/ - rsyncs the bundle to the server over SSH (port 2222)
- Restarts the pm2 process (
edc) on the server
The server runs the app with:
pm2 start ~/emdecr.com/server.js --name edcRequired GitHub Actions secrets:
| Secret | Description |
|---|---|
DEPLOY_KEY |
SSH private key for the deploy user |
SERVER_HOST |
Server IP or hostname |
SERVER_USER |
SSH username |
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL (build-time) |
SUPABASE_ANON_KEY |
Supabase anon key (build-time) |
Server requirements:
- Node.js (via nvm) and pm2 installed
.env.localpresent at~/emdecr.com/.env.local(never overwritten by deploys)- SSH access on port 2222
- Content Processing:
gray-matter,remark,remark-html,rehype-raw - Data Parsing:
csv-parsefor CSV files - External APIs:
rss-parserfor GitHub activity feed,@supabase/supabase-jsfor optional database - Styling:
tailwindcss,@tailwindcss/typography