Personal portfolio and blog. Built with Next.js, self-hosted on a DigitalOcean VPS via Docker.
| Framework | Next.js 16 (App Router, Turbopack) |
| Runtime | Bun |
| Styling | Tailwind CSS v4 + CSS custom properties |
| Fonts | Space Grotesk, Inter, Caveat (next/font/google) |
| Blog | Fumadocs (fumadocs-mdx 14.2.7 + fumadocs-core/ui 16.6.0) |
| Syntax highlighting | Shiki — dual light/dark theme, compiled at build time |
| Animations | Framer Motion + GSAP + ScrollTrigger |
| Theme | next-themes (light / dark, persisted in localStorage) |
| Search | Fuse.js (fuzzy) + nuqs (URL state) |
| Icons | lucide-react (UI) + react-icons (brand/tech) |
| Auth | Clerk (@clerk/nextjs v7) — Google + GitHub sign-in |
| Database | Neon serverless Postgres (@neondatabase/serverless) |
| ORM | Drizzle (drizzle-orm + drizzle-kit) |
| Content moderation | leo-profanity (leetspeak-aware) + DB-level rate limiting |
| Webhook verification | svix (Clerk webhook signature verification) |
| Backend | Express.js on port 3001 — Spotify OAuth token exchange only |
| DevOps | Docker + GHCR + GitHub Actions → DigitalOcean VPS |
src/
├── app/
│ ├── (site)/ # All public pages — has navbar + footer layout
│ ├── (admin)/ # Admin panel — Clerk-gated, owner-only
│ ├── actions/ # Server actions (comments CRUD)
│ └── api/ # REST API routes + Clerk webhook
├── components/
│ ├── comments/ # CommentsSection, CommentCard, CommentComposer, CommentAvatar
│ └── admin/ # AdminDashboard, AdminEditor, AdminCommentsPanel
└── lib/
├── db.ts # Neon lazy connection (Drizzle proxy)
├── schema.ts # blog_posts, blog_comments, blog_comment_likes
└── assert-admin.ts # assertAdmin() — throws if not OWNER_CLERK_USER_ID
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
OWNER_CLERK_USER_ID= # your Clerk user ID — only this user gets admin access
DATABASE_URL= # Neon connection string
CLERK_WEBHOOK_SECRET= # from Clerk dashboard → Webhooks (set after first deploy)
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_REFRESH_TOKEN=
GITHUB_PAT=
WAKATIME_API_KEY=
NEXT_PUBLIC_OPENWEATHER_KEY=
bun run db:push # push schema to Neon (first-time setup or after schema changes)
bun run db:generate # generate migration files
bun run db:migrate # run migrations
bun run db:studio # open Drizzle Studio (visual DB browser)-
make basic structure
-
decide theme
-
think about markdown pages
-
first work on implementing basic functionality
- add listening now (spotify api)
- make the spotify thing pretty
- add online offline status (discord via lanyard)
- add keystrokes and mouse clicks counter and uptime
- add current project which you're working on
- add upcoming contests on leetcode, codechef and codeforces
- codeforces
- codechef (no public api)
- leetcode (no public api)
-
make about section
- add tech stack
-
make contact section
-
make projects section
- basic page structure
- some cool effects
- current working
- past projects
- dynamic fetching from github repos
-
make posts section (markdown)
-
make it pretty
-
hosting
- digital ocean $16 VPS
- bought domain for free from name.com
-
add live coding stats (Wakatime API)
- show time spent coding today
- show time spent coding all time
- show time spent coding this week
- show language breakdown (bar chart with per-language colors, last 7 days)
-
add weather widget (OpenWeather)
- fetch weather for Surat + Jodhpur
- display temp, condition + small icon
- fits the minimal theme
-
add AI-generated summaries for projects
- generate 1–2 line tagline from project description using API
- show tagline under each project card dynamically
-
integrate search in posts page
- client-side fuzzy + substring search using fuse.js (no server needed)
- searches title, description, and tags
-
add testimonials section (research other personal sites before proceeding)
-
add Resume section (pdf download + online view)
- hosted on Google Drive, embedded via iframe (no forced download)
- download button + /resume route + Resume nav link
-
light/dark theme
- toggle button to switch themes
- save preference in localStorage
-
github contribution graph
- real data via GitHub GraphQL API
- SVG heatmap with soft-royal-blue palette, hover tooltips, legend
- scrolls to most recent week, works on mobile
-
reading time on posts
- show "X min read" on post cards and at top of each post
- computed via remarkReadingTime plugin (prose-only, skips code blocks)
-
post tags + filter
- tags stored in YAML frontmatter, parsed by Fumadocs
- tag chips on post cards, filter on /blogs page, shown on post page
- horizontal scroll if too many tags
-
command palette (Cmd+K / Ctrl+K)
- fuzzy search across pages, posts, projects
- keyboard navigable, opens from anywhere on site
- actions: toggle theme, open spotify song, social links
- desktop only
-
geo-based links on gear page (US visitors get US links, India gets India links)
- set up geo-IP detection (geoip-lite or similar, self-hosted friendly)
- map each gear item to its US equivalent
-
/now page
- what i'm currently learning, reading, building, listening to
- updated manually, personal and casual tone
- linked from hero and command palette
-
visitor count per post
- stored in server/views.json on VPS
- localStorage-gated on frontend (one count per browser, forever)
- show "X reads" on post cards and post page
- total site visitor counter on home page
-
migrate to Next.js (from React + Vite + React Router)
- App Router, SSG, generateStaticParams, generateMetadata
- next-themes replacing custom useTheme hook
- useRouter/usePathname replacing react-router hooks
-
rename /posts to /blogs
-
switch to Bun as package manager + runtime
-
migrate blog to Fumadocs
- fumadocs-mdx 14.2.7 + fumadocs-core/ui 16.6.0 on Next.js 16
- MDX compiled at build time (no per-request rendering)
- Shiki dual-theme syntax highlighting (github-dark / github-light)
- remarkReadingTime plugin for accurate reading time
- all posts converted to .mdx with YAML frontmatter
- description field on posts
-
auto sitemap + robots.txt (Next.js metadata routes)
-
redirect shortcuts: /github, /linkedin, /twitter, /mail, /discord, /spotify
-
mouse glow effect following cursor
-
reading progress bar on blog posts (zero-lag, direct DOM writes)
-
scroll-driven CSS fade utility (.scroll-fade-x/y)
-
security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
-
fully dockerized + GitHub Actions CI/CD to DigitalOcean VPS
- 3 workflows: build + push image, deploy to VPS, dependabot
- standalone Next.js output, GHCR image registry
- docker-compose with named volume for views data
-
single-page scroll layout (Hero → About → Projects → Now → Writing → Contact)
- anchor nav links with smooth scrolling
- footer with all live widgets (Spotify, WakaTime, GitHub contributions, visitor count)
- blog teaser section on homepage (4 most recent posts)
- blocky/rectangular style (no rounded corners)
-
mobile responsive — animated nav dropdown, animated TOC
-
/projects page — dedicated page, scroll reveal on cards
-
Mark highlights, SectionDivider, SectionLabel components throughout site
-
consistent large h1 headers on /blogs, /resume, /projects
-
blogs page — tag filter, fuzzy search, pagination, per-page select, URL-synced via nuqs
-
Clerk auth for admin panel
-
src/proxy.ts— Clerk middleware (Next.js 16 convention) protecting/admin(.*) - admin layout checks
userId === OWNER_CLERK_USER_ID— non-owners see access denied screen -
/sign-inand/sign-uppages using Clerk's hosted<SignIn />/<SignUp />components -
<ClerkProvider>in root layout - admin logout via Clerk
signOut()replacing old cookie logout -
src/lib/assert-admin.ts—assertAdmin()server-side helper for server actions -
/api/adminblog CRUD route now uses Clerk auth — works in production (was local-only before) - Clerk webhook at
/api/webhooks/clerk— verified with svix, soft-deletes comments onuser.deleted - old cookie-based
admin-auth.ts+ADMIN_SECRETapproach fully replaced
-
-
comments on blog posts
- Neon serverless Postgres + Drizzle ORM
- schema:
blog_posts,blog_comments(withparent_idfor threading),blog_comment_likes -
drizzle.config.ts+db:push / generate / migrate / studioscripts in package.json - lazy DB connection — no build-time env var required (Proxy pattern in
src/lib/db.ts) -
src/app/actions/comments.ts— server actions: getComments, submitComment, deleteComment, toggleCommentLike, togglePinComment, getAllAdminComments - readers sign in with Clerk (Google / GitHub / email) to comment
- threaded replies up to 2 levels deep
- like / unlike comments (optimistic update)
- delete own comment; owner (
OWNER_CLERK_USER_ID) can delete any comment - owner can pin comments (pinned float to top, styled differently)
- full optimistic UI — comment appears instantly, confirmed or rolled back after server response
-
CommentsSectioninjected intoPostPagevia prop — server-fetched initial data, client-interactive - admin comments panel at
/admin/comments— grouped by post, search by name/content, filter pinned only, delete + pin/unpin
-
guestbook
-
/guestbookpublic page — sign in with Clerk, leave a short message (280 chars) -
guestbook_entries+guestbook_likestables in Neon schema - like / unlike entries (optimistic update)
- delete own entry; owner can delete any
- owner can pin entries
- rate limit: 3 entries per user per 10 minutes
- profanity filter + unicode normalization (same as comments)
- admin guestbook panel at
/admin/guestbook— search, filter pinned, delete, pin/unpin - Clerk webhook updated to also clean up guestbook on
user.deleted - Guestbook link added to navbar
-
-
comment security
- rate limiting — max 5 comments / 3 guestbook entries per user per 10 minutes, enforced in DB
- profanity filter —
leo-profanitywith leetspeak detection, rejects at server action level - unicode normalization —
String.normalize('NFKC')in sanitizer to block zero-width / homoglyph bypasses - XSS safe — comments stored as plain text, rendered via React (no dangerouslySetInnerHTML)
- min 2 / max 1000 character validation server-side
-
Perspective API content moderation (semantic spam + toxicity detection)
- Google Perspective API — free, no credit card, scores text for toxicity/spam/harassment
- add as second-pass check after leo-profanity (leo catches wordlist, Perspective catches context)
- fail-open on 429 (rate limit) — let comment through rather than blocking legit users
- one new env var:
PERSPECTIVE_API_KEY(Google Cloud project → enable Perspective API → copy key)
-
set up Clerk webhook in production
- add endpoint
https://xevrion.dev/api/webhooks/clerkin Clerk dashboard - select
user.deletedevent - copy signing secret → add
CLERK_WEBHOOK_SECRETto GitHub Actions secrets + VPS.env
- add endpoint
-
typing animation on hero (lowest priority)
- cycle through "developer", "designer", "pianist", "iit student"
-
dual favicons (light/dark SVG via prefers-color-scheme)
-
improve blog post formatting — inline code styling, image rounding
-
GSoC / Experience section
-
blog post editing workflow — decided to keep editing locally (VS Code → commit → push → CI deploys). Admin editor on prod works but requires a redeploy to reflect changes due to Fumadocs static generation at build time. For typo emergencies use GitHub web editor directly.
-
background moves with page scroll (not fixed)
-
/blog/:path*.mdx route (LLM-friendly raw MDX)
-
add testimonials section
-
geo-based links on gear page (US vs India links per item)