Your portfolio should boot like a terminal, not load like a Squarespace.
Write in Obsidian. Push to GitHub. Your site updates.
No database. No CMS. No writing in two places.
No local setup required. Click, paste, done.
| Step | Action | Time |
|---|---|---|
| 1 | Click Deploy with Vercel below β it forks the repo and creates your project | 30s |
| 2 | Vercel asks for 3 env variables β get them from resend.com (free tier) | 90s |
| 3 | Click Deploy β your site is live | 60s |
How to get the 3 env variables (2 minutes)
- Sign up at resend.com (free β 3,000 emails/month)
- RESEND_API_KEY β API Keys β Create API Key β copy
- RESEND_AUDIENCE_ID β Audiences β Create Audience β copy the ID
- NOTIFY_SECRET β Any random string. Generate one:
openssl rand -hex 32
After deploy, clone your fork and edit one file β content/site.ts β to make it yours. Push. Done.
Want a full walkthrough with screenshots? Read the step-by-step tutorial.
Most portfolio templates make you choose: look good or easy to maintain.
| Traditional portfolio | termfolio | |
|---|---|---|
| Write content | CMS dashboard | Obsidian / VS Code / any editor |
| Content storage | Database (Postgres, Notion, Contentful) | Plain .md files in your repo |
| Write in one place | No β CMS + code | Yes β just markdown |
| Vendor lock-in | Tied to CMS provider | Zero β it's just files |
| Works offline | No | Yes |
| Deploy | Complex pipeline | git push |
| Customize | Dig through 50+ files | Edit one file (content/site.ts) |
We obsessed over reading UX so your visitors actually finish your posts:
- 3 reading themes β Terminal (dark), Light, and Sepia. Readers switch mid-article without losing their place
- Adjustable font size β Small, medium, large. Readers pick what's comfortable
- Focus mode β Hides everything except the article. No nav, no sidebar, no distractions
- Reading progress bar β Shows how far through the post they are
- Table of contents β Auto-generated from headings, highlights current section as you scroll
- Estimated reading time β Shown before they start
- Syntax highlighting β Code blocks with proper language coloring
- Full RTL/Arabic support β Set
language: "ar"in frontmatter and the entire post flips β layout, fonts, everything
Your visitors won't just read β they'll play. The home page terminal is a real command parser:
| Command | What it does |
|---|---|
snake |
Classic Snake game β playable right in the terminal |
pokedex |
Browse Pokemon with stats, types, and pixel art |
typing-test |
Speed typing challenge with WPM tracking |
starmap |
Interactive constellation map based on your coordinates |
worldmap |
SVG world map highlighting your city |
json |
Paste and format/validate JSON instantly |
dashboard |
System dashboard with live clock and stats |
base64 |
Encode/decode Base64 strings |
wordcount |
Count words, characters, and lines |
epoch |
Convert Unix timestamps to human dates |
uuid |
Generate random UUIDs |
whoami |
Shows your identity (reads from config) |
skills |
Shows your skills (reads from config) |
theme |
Toggle light/dark mode |
Every command reads from your config β whoami outputs your name, starmap shows your sky.
Obsidian (write) β content/posts/*.md β git push β Live site
Your markdown files are the blog. No database. No CMS. No API calls to fetch content.
- Symlink your vault β posts update when you save in Obsidian
- GitHub Action included β auto-syncs from a separate vault repo on push
- Works with any editor β VS Code, Vim, iA Writer β if it saves
.mdfiles, it works
See Obsidian Integration for setup instructions.
Readers comment on your posts using their GitHub account. Comments live in your repo's Discussions tab β no database, no moderation dashboard, no third-party service.
Setup takes 2 minutes:
- Go to giscus.app
- Enter your repo name β it checks if Discussions are enabled
- Pick a category (use "Announcements")
- Copy the 4 values it gives you
- Add them to your
.env.local:
NEXT_PUBLIC_GISCUS_REPO=your-username/termfolio
NEXT_PUBLIC_GISCUS_REPO_ID=R_xxxxxxxxxx
NEXT_PUBLIC_GISCUS_CATEGORY=Announcements
NEXT_PUBLIC_GISCUS_CATEGORY_ID=DIC_xxxxxxxxxxComments appear at the bottom of every blog post. Moderate them from GitHub Discussions.
- Digital business card β Shareable
/cardpage with vCard download, QR code, WhatsApp, and Apple Wallet pass. Data lives in a single YAML file (content/card.md) β edit in Obsidian, no code changes needed - Newsletter β Email subscriptions + new-post notifications via Resend (free: 3,000 emails/month)
- Halftone image effect β Your profile photo renders as a canvas-based halftone
- Animated starfield β Star field background on the home page
- Writing heatmap β GitHub-style contribution graph for your blog activity
- Cal.com embed β Scheduling widget on the contact page
- Spotify widget β Links to your music profile
- SEO optimized β Dynamic OG images, sitemap, robots.txt, structured metadata
- Vercel Analytics β Built-in analytics and speed insights
- Security hardened β HSTS, CSP headers, rate limiting, timing-safe auth, input validation
Open content/site.ts and make it yours. This single file controls your entire site:
export const siteConfig = {
name: "Your Name", // site title, metadata, emails, footer
handle: "you", // terminal prompt, top bar
tagline: "your Β· tagline", // below ASCII art
email: "you@example.com", // contact page, comment fallback
siteUrl: "https://you.com", // metadata, sitemap, emails
// Terminal commands read from these
whoami: { focus: "what you do", status: "what you're up to" },
terminalSkills: ["skill1 // skill2 // skill3"],
// Your location powers the star map and world map
coordinates: { lat: 40.7128, lon: -74.0060, label: "New York" },
// Social links in the nav bar
socials: {
github: { url: "https://github.com/you", label: "GitHub", icon: "</>" },
linkedin: { url: "https://linkedin.com/in/you", label: "LinkedIn", icon: "[in]" },
},
// Generate ASCII art at patorjk.com/software/taag
asciiArt: { home: [...], about: [...] },
// About page
bio: [...], experience: [...], skills: [...], certifications: [...],
}Full field reference
| Field | Controls |
|---|---|
name |
Site title, metadata, email sender, footer |
handle |
Terminal prompt, top bar display |
title |
Meta title, about page header |
tagline |
Text below ASCII art on home page |
description |
Meta/OG description across all pages |
email |
Contact page, comment fallback |
siteUrl |
Metadata, sitemap, robots.txt, email links |
twitterHandle |
Twitter/X card metadata |
calUrl / calEmbedUrl |
Contact page calendar widget |
spotifyUrl |
Music widget link |
coordinates |
Star map location, world map pin |
asciiArt.home / asciiArt.about |
ASCII banners (generate here) |
socials |
Nav bar links (GitHub, LinkedIn, X, etc.) |
whoami |
Terminal whoami command output |
terminalSkills |
Terminal skills command output |
terminalPrompt |
Shell prompt (e.g. root@you:~) |
developedBy |
Footer attribution (links to this repo) |
customizedBy |
Your attribution β { name: "You", url: "..." } |
bio, stats, experience, skills, certifications, credentials |
About page content |
Create .md files in content/posts/. That's your entire CMS:
---
title: "My First Post"
date: "2026-01-15"
excerpt: "A brief description for cards and SEO"
tags: ["topic", "another"]
status: "published"
language: "en"
---
Your markdown content here. Supports GFM: tables, task lists,
strikethrough, footnotes, syntax-highlighted code blocks.| Field | Required | Notes |
|---|---|---|
title |
Yes | Post title |
date |
Yes | ISO date (YYYY-MM-DD) |
status |
Yes | "published" or "draft" (drafts are hidden) |
excerpt |
No | Used in cards, SEO, and email notifications |
tags |
No | Array of strings for filtering |
language |
No | "en" (default) or "ar" for full RTL Arabic |
coverImage |
No | Path relative to public/ |
author |
No | Defaults to siteConfig.name |
Option A: Copy files (simplest)
Copy .md files from your Obsidian vault to content/posts/.
Option B: Symlink (best for local dev)
Point content/posts/ at your vault. Posts update live when you save in Obsidian:
# macOS/Linux
ln -s ~/obsidian-vault/published content/posts
# Windows (PowerShell as Admin)
New-Item -ItemType SymbolicLink -Path content\posts -Target C:\Users\you\obsidian-vault\publishedOption C: GitHub Action (fully automated)
A ready-made workflow is included at .github/workflows/sync-obsidian.yml. It syncs posts from a separate Obsidian vault repo automatically.
Setup:
- Store your Obsidian posts in a separate GitHub repo (e.g.
your-username/obsidian-vault) - Create a Personal Access Token with
reposcope - In this repo β Settings β Secrets β Actions, add:
VAULT_REPOβyour-username/obsidian-vaultVAULT_PATβ your PATVAULT_PATHβ folder inside the vault repo (default:published)
- Runs daily at 6 AM UTC, or trigger manually from the Actions tab
Auto-trigger on vault push β add this workflow to your vault repo:
name: Trigger site sync
on:
push:
paths: ['published/**']
jobs:
sync:
runs-on: ubuntu-latest
steps:
- run: |
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${{ secrets.SITE_REPO_PAT }}" \
https://api.github.com/repos/YOUR_USERNAME/termfolio/dispatches \
-d '{"event_type":"sync-obsidian"}'Now every vault push auto-updates your site.
Obsidian frontmatter template
---
title: "{{title}}"
date: "{{date:YYYY-MM-DD}}"
author: "Your Name"
excerpt: ""
coverImage: ""
tags: []
status: "draft"
language: "en"
---| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY |
Yes | Resend API key (free: 3,000 emails/month) |
RESEND_AUDIENCE_ID |
Yes | Resend audience ID (Audiences β Create β copy ID) |
NOTIFY_SECRET |
Yes | Any random string β protects the notification endpoint |
RESEND_FROM_EMAIL |
No | Sender address (default: Name <noreply@yourdomain.com>) |
NEXT_PUBLIC_SITE_URL |
No | Override site URL (default from siteConfig.siteUrl) |
NEXT_PUBLIC_NASA_API_KEY |
No | NASA API key for APOD widget |
NEXT_PUBLIC_GISCUS_REPO |
No | Repo name for Giscus comments |
NEXT_PUBLIC_GISCUS_REPO_ID |
No | Giscus repo ID |
NEXT_PUBLIC_GISCUS_CATEGORY |
No | Giscus category (use "Announcements") |
NEXT_PUBLIC_GISCUS_CATEGORY_ID |
No | Giscus category ID |
WALLETWALLET_API_KEY |
No | WalletWallet API key for Apple Wallet passes on /card |
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| UI | React 19, Tailwind CSS 3, CSS design tokens (--term-*) |
| Content | Local Markdown with gray-matter β no database |
| Markdown | unified β remark-parse β remark-gfm β remark-rehype β rehype-slug β rehype-highlight β rehype-react |
| Resend API (newsletter + new-post notifications) | |
| Comments | Giscus (GitHub Discussions β no database) |
| Analytics | Vercel Analytics + Speed Insights |
| Fonts | IBM Plex Mono (UI), Source Serif 4 (reading), Noto Naskh Arabic (RTL) |
content/
site.ts β THE file. Your entire site config.
posts/*.md β Your blog posts. Drop markdown, done.
card.md β Digital business card data (YAML frontmatter).
app/
page.tsx Home (boot terminal, hero, recent posts)
about/page.tsx About/resume page
blog/page.tsx Journal (search, heatmap, tag filters)
blog/[slug]/ Blog post (3 themes, ToC, focus mode, comments)
contact/page.tsx Contact (Cal.com embed, newsletter)
card/page.tsx Digital business card (vCard, QR, WhatsApp)
api/subscribe/ Newsletter subscription endpoint
api/notify/ New-post notification endpoint
api/card/vcard/ vCard (.vcf) download endpoint
api/card/wallet/ Apple Wallet pass endpoint
components/ 30+ components including:
boot-terminal.tsx Interactive terminal with 14 commands
reading-controls.tsx 3 themes, font size, focus mode, progress bar
writing-heatmap.tsx GitHub-style contribution graph
halftone-image.tsx Canvas halftone image effect
star-map.tsx Interactive constellation renderer
world-map.tsx SVG world map with location pin
npm run dev # Development server
npm run build # Production build
npm run lint # ESLint
npm run typecheck # TypeScript check
npm run check # Lint + typecheck + buildContributions welcome:
- Fork the repo
- Create your branch (
git checkout -b feat/cool-feature) - Commit (
git commit -m 'feat: add cool feature') - Push (
git push origin feat/cool-feature) - Open a Pull Request
Report bugs or request features β
Keep the "developed by" footer link or add your own name next to it in content/site.ts:
customizedBy: { name: "Your Name", url: "https://your-site.com" }
// β "developed by waleed alhamed Β· customized by your name"MIT β use it however you want.
If this helped you build something cool, give it a β
Built by Waleed Alhamed




