A portfolio website and CMS platform built with Next.js, Convex, and a bespoke live visual editor. Features a block-based blog engine powered by Tiptap with Tailwind class assignment on any content block.
- Framework: Next.js (App Router, standalone output)
- Backend: Convex (self-hosted compatible)
- Styling: Tailwind CSS v4
- Content Editor: Tiptap (block-based, with custom extensions)
- UI Primitives: @base-ui/react, lucide-react
- Animations: Anime.js, Three.js (WebGL shaders)
- Carousel: Embla Carousel
- Node.js 18+
- A Convex instance (cloud or self-hosted)
npm installCopy .env.local.example (or create .env.local) with the following:
# Convex
NEXT_PUBLIC_CONVEX_URL=https://your-convex-instance.example.com
NEXT_PUBLIC_CONVEX_SITE_URL=https://your-convex-site.example.com
CONVEX_SELF_HOSTED_URL=https://your-convex-instance.example.com # only for self-hosted
CONVEX_SELF_HOSTED_ADMIN_KEY=your-admin-key # only for self-hosted
# Site
NEXT_PUBLIC_SITE_URL=https://your-domain.com
# Contact form (Brevo / Sendinblue)
BREVO_API_KEY=your-brevo-api-key
BREVO_SENDER_EMAIL=you@example.com
BREVO_RECIPIENT_EMAIL=inbox@example.comConvex environment variable (server-side):
Set the admin password on your Convex instance for CMS authentication:
npx convex env set ADMIN_PASSWORD your-secure-passwordnpx convex devThis syncs your convex/ functions and schema to the Convex instance.
npm run devThe predev script auto-generates a Tailwind safelist from published blog posts before starting.
npm run build
npm startDefined in convex/schema.ts:
| Table | Purpose |
|---|---|
| posts | Blog posts with Tiptap JSON content, cover images, SEO fields, tags, draft/published status |
| files | File library — uploaded images and assets stored in Convex storage |
| sessions | Admin authentication sessions (7-day expiry) |
The CMS uses a simple password-based auth flow (convex/auth.ts):
- Admin navigates to
/nexus/login - Password is validated against the
ADMIN_PASSWORDenv var on the Convex server - A session token is created (valid for 7 days) and stored in
localStorage - All admin mutations/queries require a valid session token
convex/posts.ts — Blog post CRUD:
| Function | Type | Auth | Description |
|---|---|---|---|
list |
query | public | All published posts |
listRecent |
query | public | Recent posts (default 6) |
getBySlug |
query | public | Single post by URL slug |
listAll |
query | admin | All posts (draft + published) |
getById |
query | admin | Single post by ID |
create |
mutation | admin | Create a new draft post |
update |
mutation | admin | Update post content/metadata |
publish |
mutation | admin | Set post status to published |
unpublish |
mutation | admin | Revert to draft |
remove |
mutation | admin | Delete post and cover image |
generateUploadUrl |
mutation | admin | Get a Convex storage upload URL |
allUsedClasses |
query | public | Collect Tailwind classes from all posts (for safelist) |
convex/files.ts — File library management
convex/auth.ts — Login, session validation, logout
Blog posts can use arbitrary Tailwind classes on any content block. To ensure these classes are included in the production CSS build:
npm run generate:safelistThis script (scripts/generate-safelist.mjs) queries all published posts via allUsedClasses, extracts every Tailwind class from the Tiptap JSON, and writes a safelist file that Tailwind picks up during build. This runs automatically before dev and build.
The admin interface lives at /nexus and provides:
- Dashboard — List of all posts with status indicators
- Post Editor — Full Tiptap block editor with:
- Slash command menu (
/) for inserting blocks - Multi-column layouts
- Styled containers with arbitrary Tailwind classes
- Image and video embedding with file picker
- Code blocks with syntax highlighting
- Drag-and-drop block reordering
- Node inspector for assigning Tailwind classes to any block
- Slash command menu (
- File Library (
/nexus/files) — Upload and manage media assets - SEO Fields — Per-post title and meta description with character counts
- Tags — Multi-tag system
- Draft/Publish workflow — Preview drafts before going live
A bespoke development tool with two modes — Edit mode for applying DOM patches, and Annotate mode for leaving visual feedback that Claude agents resolve autonomously. This is not the blog content editor — it's a developer tool for tweaking the site's own UI.
| Shortcut | Mode | Purpose |
|---|---|---|
E E |
Edit | Select elements, modify classes/styles/props/text, persist patches |
A A |
Annotate | Click any element, leave feedback — Claude agents pick it up and implement changes |
- Select: Click any element on the page to inspect it
- Edit: Use the inspector panel to modify classes, styles, props, or text
- Persist: Patches are saved to
localStorageand synced to a JSON file via API - Commit: Optionally commit patches directly to source files (uses
ts-morphfor AST manipulation)
The editor creates reversible "patches" — each targets a CSS selector on a specific page and applies one or more operations:
| Operation | Description |
|---|---|
text |
Change text content |
addClass |
Add a CSS/Tailwind class |
removeClass |
Remove a class |
setStyle |
Set an inline CSS property |
removeStyle |
Remove an inline CSS property |
hide |
Set display: none |
show |
Remove display: none |
setProp |
Change a JSX prop value |
- Click any element — a popup appears with an intent picker (fix / change / question / approve)
- Describe what you want changed in plain language
- The annotation is synced to the local annotation server (port 4747)
- The orchestrator automatically spawns a Claude agent in an isolated git worktree
- The agent reads the source file, implements the change, and commits
- Progress and agent replies stream back to the browser in real-time via SSE
- Reply to an annotation thread to give follow-up instructions — the agent re-spawns with the new context
Annotations include rich element context: source file location (via React fiber traversal), CSS classes, computed styles, nearby text, accessibility info, and the React component stack.
src/editor/
├── index.tsx # DevEditor entry point & loader
├── DevEditorLoader.tsx # Dynamic import guard (dev-only)
├── EditorProvider.tsx # React context — state, modes, patch/annotation management, SSE
├── EditorPanel.tsx # Inspector panel (classes, styles, props, text, meta)
├── EditorOverlay.tsx # Visual selection overlay & bounding box (capture-phase listeners)
├── EditorToolbar.tsx # Bottom HUD — mode toggles, patch count, annotation controls
├── AnnotationOverlay.tsx # Annotation UI — hover highlight, create/reply popups, markers
├── types.ts # Patch, Annotation, ThreadMessage, Intent, Severity, Status types
├── constants.ts # Shared constants (data attributes, API paths, debounce values)
├── editor.raw.css # Editor styles injected as raw string (avoids Tailwind conflicts)
│
├── engine/
│ ├── fiber.ts # React fiber traversal for source locations & component props
│ ├── selector.ts # CSS selector generation & validation
│ ├── patches.ts # PatchStore — dual persistence (localStorage + file API)
│ ├── applicator.ts # PatchApplicator — apply/revert patches, MutationObserver re-apply
│ ├── annotations.ts # Annotation persistence, element identification helpers
│ ├── output.ts # Markdown output generator (compact/standard/detailed/forensic)
│ └── sync.ts # HTTP client — syncs annotations to port 4747, SSE subscription
│
├── panels/
│ ├── ClassEditor.tsx # Tailwind class editor with autocomplete
│ ├── StyleEditor.tsx # Inline style editor
│ ├── TextEditor.tsx # Text content editor
│ ├── PropsEditor.tsx # React props editor
│ └── MetaInfo.tsx # Element metadata display
│
├── tailwind/
│ └── class-index.ts # Runtime Tailwind class lookup & autocomplete index
│
└── server/ # Annotation server (standalone Node.js process)
├── index.ts # Entry point — starts HTTP + MCP + orchestrator
├── http.ts # REST API + SSE server on port 4747
├── store.ts # In-memory annotation store + EventBus (1000-event replay)
├── mcp.ts # MCP server (stdio) — editor_watch, editor_resolve, etc.
├── orchestrator.ts # Multi-agent orchestrator — auto-spawns Claude per annotation
├── prompt.ts # Agent prompt builders (initial + follow-up)
└── agent-types.ts # AgentInfo interface
The annotation server (src/editor/server/) is a standalone Node.js process that runs alongside Next.js during development. It has three layers:
| Endpoint | Method | Description |
|---|---|---|
/sessions |
POST | Create a session (keyed to a page URL) |
/sessions/:id/annotations |
POST | Add an annotation |
/annotations/:id |
PATCH | Update annotation status/intent |
/annotations/:id/thread |
POST | Add a thread message |
/pending |
GET | List all unresolved annotations |
/sessions/:id/pending |
GET | Unresolved annotations for a session |
/events |
GET | SSE stream (all events) |
/sessions/:id/events |
GET | SSE stream (session-scoped) |
/agents |
GET | List running agents |
/agents/:id/abort |
POST | Abort a running agent |
/annotations/:id/respawn |
POST | Re-trigger agent for an annotation |
Exposes the editor-annotations MCP server for Claude Code integration. Tools:
| Tool | Description |
|---|---|
editor_list_sessions |
List active annotation sessions |
editor_get_pending |
Get unresolved/unclaimed annotations |
editor_acknowledge |
Mark annotation as acknowledged |
editor_resolve |
Mark as resolved with summary |
editor_dismiss |
Dismiss with reason |
editor_reply |
Add agent message to thread |
editor_watch |
Block until new annotations or replies arrive (loop-based) |
In --no-agents mode, Claude Code uses these MCP tools directly to process annotations manually via the editor_watch loop.
When a new annotation arrives, the orchestrator automatically:
- Creates a git worktree at
.claude/worktrees/agent/{annotationId}on a new branch - Spawns a Claude agent (
claude-sonnet-4-6, max 50 turns) via@anthropic-ai/claude-agent-sdk - The agent reads the source file, implements the requested change, and commits
- Progress streams back over SSE — the browser shows a live "Claude thinking..." indicator
- On success, the annotation is marked
resolvedand a summary is posted to the thread
Limits: max 6 concurrent agents. Follow-up thread replies abort the current agent and re-spawn with updated context.
npx tsx src/editor/server/index.ts # HTTP + MCP + agents (default)
npx tsx src/editor/server/index.ts --mcp-only # MCP only (for Claude Code)
npx tsx src/editor/server/index.ts --no-agents # HTTP + MCP, no auto-spawn
npx tsx src/editor/server/index.ts --port 5000 # Custom port| Endpoint | Method | Description |
|---|---|---|
/api/editor-patches |
GET | Fetch all patches |
/api/editor-patches |
POST | Create or update a patch |
/api/editor-patches/commit |
POST | Commit a patch to the source file via AST |
- The live editor is a development-only tool — it is disabled in production builds
- Patches live in a JSON file and
localStorage; they don't affect the production build until committed to source - The "Commit to Source" feature uses
ts-morphto modify the actual.tsxfiles, adding/removing classes or changing props directly in the AST - Annotations are transient (in-memory on the server, localStorage in the browser with 7-day retention) — they are developer feedback, not persistent data
- The orchestrator's spawned agents are explicitly forbidden from using the
editor-annotationsMCP tools to prevent recursive annotation loops
├── convex/ # Convex backend
│ ├── schema.ts # Database schema (posts, files, sessions)
│ ├── posts.ts # Post CRUD functions
│ ├── files.ts # File library functions
│ └── auth.ts # Authentication (login, sessions)
│
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Homepage
│ │ ├── (convex)/ # Routes using Convex provider
│ │ │ ├── blog/ # Public blog
│ │ │ │ ├── page.tsx # Blog listing
│ │ │ │ └── [slug]/ # Dynamic blog post pages
│ │ │ └── nexus/ # Admin CMS
│ │ │ ├── login/ # Admin login
│ │ │ ├── page.tsx # Dashboard
│ │ │ ├── posts/ # Post editor (new, edit, preview)
│ │ │ └── files/ # File library
│ │ ├── work/ # Portfolio project pages
│ │ │ ├── solitonic/
│ │ │ ├── flux/
│ │ │ ├── postlight/
│ │ │ ├── airbit/
│ │ │ └── wadi-grocery/
│ │ ├── api/
│ │ │ ├── editor-patches/ # Live editor patch API
│ │ │ └── og/ # Dynamic OG image generation
│ │ ├── actions/
│ │ │ └── contact.ts # Contact form server action
│ │ ├── robots.ts # robots.txt generation
│ │ └── sitemap.ts # sitemap.xml generation
│ │
│ ├── components/
│ │ ├── ui/ # Reusable UI components (Button, Input, Dialog, etc.)
│ │ ├── admin/ # CMS admin components
│ │ │ ├── PostForm.tsx # Blog post editor form
│ │ │ ├── PostList.tsx # Posts dashboard
│ │ │ ├── FileLibrary.tsx # File manager
│ │ │ └── editor/ # Tiptap editor & extensions
│ │ │ ├── TiptapEditor.tsx
│ │ │ └── extensions/ # Custom Tiptap extensions
│ │ │ ├── ColumnsExtension.ts
│ │ │ ├── StyledBlockExtension.ts
│ │ │ ├── ImageExtension.ts
│ │ │ ├── VideoExtension.ts
│ │ │ ├── LogoDividerExtension.ts
│ │ │ ├── StylableNodesExtension.ts
│ │ │ └── SlashCommand.ts
│ │ ├── cms/ # Blog rendering
│ │ │ ├── TiptapRenderer.tsx # Tiptap JSON → React
│ │ │ └── renderers/ # Block-specific renderers
│ │ ├── home/ # Homepage sections
│ │ └── blog/ # Blog-specific components
│ │
│ ├── editor/ # Live visual editor & annotation system (dev tool)
│ │ ├── index.tsx # Entry point
│ │ ├── EditorProvider.tsx # State management (edit + annotate modes)
│ │ ├── EditorPanel.tsx # Inspector panel
│ │ ├── EditorOverlay.tsx # Selection overlay
│ │ ├── AnnotationOverlay.tsx # Annotation UI (create, reply, markers)
│ │ ├── EditorToolbar.tsx # Bottom HUD
│ │ ├── engine/ # Core engine (fiber, selector, patches, applicator, annotations, sync)
│ │ ├── panels/ # Editor panel sub-components
│ │ ├── tailwind/ # Tailwind class index
│ │ └── server/ # Annotation server (HTTP + MCP + orchestrator)
│ │
│ ├── hooks/ # Custom React hooks
│ │ ├── useAdminSession.ts # Admin auth state
│ │ └── useTheme.ts # Dark/light mode
│ │
│ └── lib/ # Utilities
│ ├── convex.ts # Convex client setup
│ ├── admin-auth.ts # Token management
│ ├── tw-classes.ts # Tailwind class utilities
│ ├── tw-arbitrary.ts # Arbitrary class CSS generation
│ └── utils.ts # General helpers
│
├── scripts/
│ └── generate-safelist.mjs # Tailwind safelist generator
│
├── public/ # Static assets
├── next.config.ts # Next.js config (standalone, image remotes)
├── postcss.config.mjs # PostCSS config (Tailwind v4)
├── tsconfig.json # TypeScript config
└── package.json
| Script | Description |
|---|---|
npm run dev |
Start Next.js dev server + annotation server (auto-generates Tailwind safelist) |
npm run build |
Production build (auto-generates safelist before build) |
npm start |
Run production server |
npm run generate:safelist |
Manually regenerate the Tailwind safelist from published posts |
npx convex dev |
Start Convex dev sync |
Browser Annotation Server (4747) Git / Claude Agent
─────── ──────────────────────── ──────────────────
Press AA → annotate mode
Click element → popup
Submit comment ───────────────→ POST /sessions/:id/annotations
│
├─→ EventBus emits annotation.created
│
└─→ Orchestrator receives event
│
├─→ Creates git worktree
│ (.claude/worktrees/agent/{id})
│
└─→ Spawns Claude agent (sonnet) ──→ Reads source file
│ Implements change
│ Commits to branch
│
SSE ← agent.progress ←─────── EventBus ←────── Agent streams progress
SSE ← thread.message ←─────── EventBus ←────── Agent posts summary
SSE ← annotation.updated ←─── EventBus ←────── Marks resolved
Click element → EditorPanel opens
Modify classes/styles/text
│
├─→ Live DOM preview (PatchApplicator)
├─→ localStorage (immediate)
└─→ /api/editor-patches (debounced 500ms) → JSON file
"Commit to Source" → /api/editor-patches/commit → ts-morph AST edit → .tsx file updated
MIT