A solo dev journey from frustration to product launch
The Frustration That Started It All
I love ClickUp. It's where my team lives — tasks, docs, wikis, everything connected.
But every time I needed to share documentation with a client, I hit the same wall:
doc.clickup.com/d/2kxuepwx-192
That URL. That ugly, branded, unprofessional URL.
Clients would ask: "Why are we using ClickUp? Can you put this on your website?"
So I'd spend hours:
- Exporting to PDF (formatting breaks)
- Copy-pasting to Notion (links break)
- Rebuilding in GitBook (context lost)
I found workarounds like Cloakist, but they just proxy the ClickUp page — clients could still click around and find internal stuff.
Then I went digging.
I searched Reddit, ClickUp forums, and feedback boards. Turns out I wasn't alone:
"Shared doc.clickup.com with a prospect. They thought we use ClickUp internally and bailed." — r/clickup
"Want to share the onboarding section. Can't without exposing the entire task roadmap." — feedback.clickup.com
"Docs export breaks embeds, bullets vanish. Recreating in Notion costs 4 hours of work." — Agency PM on Reddit
Hundreds of people had the same frustration. Agencies, SaaS teams, freelancers — all stuck with the same problem.
One night, I thought: What if I could solve this for myself AND for them?
That's how Wikibeem was born.
The Tech Stack
I built Wikibeem as a solo developer over several months. Here's what powers it:
Frontend
- Next.js 16 with App Router
- React 19
- Tailwind CSS 4
- Server and Client Components for optimal performance
Backend
- Next.js API Routes — no separate backend needed
- PostgreSQL with Prisma ORM
- Vercel for hosting and edge functions
Authentication & Payments
- NextAuth v5 (beta) — credentials + OAuth ready
- Paddle — for subscriptions (handles global taxes automatically)
The ClickUp Integration
- ClickUp API v3 — OAuth 2.0 flow
- Axios for API calls
- Custom sync engine that handles nested pages, wikis, and doc hierarchies
Content Processing
- Marked — Markdown to HTML conversion
- Cheerio — HTML manipulation and cleaning
- Turndown — HTML to Markdown when needed
Search & SEO
- Fuse.js — client-side fuzzy search
- Custom SEO per site and per document
- Auto-generated sitemaps
Domain Management
- Vercel SDK — programmatic custom domain setup
- Automatic SSL certificates
Internationalization
- next-intl — 9 languages supported
- English, French, German, Spanish, Portuguese, Italian, Russian, Arabic, Chinese
The Architecture
Here's how Wikibeem works under the hood:
┌─────────────────┐ OAuth ┌─────────────────┐
│ ClickUp API │◄──────────────►│ Wikibeem │
│ (Your Docs) │ │ (Next.js) │
└─────────────────┘ └────────┬────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ PostgreSQL │ │ Vercel │ │ Paddle │
│ (Prisma) │ │ (Domains) │ │ (Payments) │
└───────────┘ └─────────────┘ └─────────────┘
The Data Model
User
└── Workspace (ClickUp connection)
└── Site (your docs website)
├── Documents (synced from ClickUp)
│ └── Children (nested pages)
├── Domain (custom domain)
├── Theme (colors, logo)
└── SEO Settings
The Hardest Part: Syncing ClickUp Docs
ClickUp's API is... interesting. Docs can have:
- Nested pages
- Wikis with their own structure
- Content in different formats (JSON blocks, Markdown, HTML)
My sync engine had to:
- Fetch all docs from a workspace
- Recursively process pages and their children
- Convert content to clean HTML
- Build a hierarchy with parent-child relationships
- Generate unique slugs for URLs
- Handle updates without creating duplicates
The trickiest bug? ClickUp sometimes returns a page that's actually a root doc. Took me days to figure out why I had duplicate content everywhere.
// The fix: Track root doc IDs and filter them out
const rootDocIds = new Set(docs.map(d => d.id))
// When processing pages, skip if it's actually a root doc
if (rootDocIds.has(page.id)) {
continue // This page is a doc, not a child page
}
Performance Optimization
The first version was slow. Each document checked slug uniqueness with a database query.
For 100 pages = 100+ database round trips. 💀
The fix: In-memory slug tracking.
const existingSlugs = new Set<string>()
// Instead of: await prisma.document.findUnique(...)
// Now: existingSlugs.has(slug)
existingSlugs.add(newSlug) // Track immediately
Sync time dropped from 30+ seconds to under 10.
Custom Domains: The Magic
This was the feature I was most excited about.
Using Vercel's SDK, I can programmatically:
- Add a domain to the project
- Return DNS records for the user to configure
- Verify domain ownership
- Auto-provision SSL certificates
const vercel = new Vercel({ accessToken: process.env.VERCEL_TOKEN })
// Add domain
await vercel.projects.addProjectDomain({
idOrName: projectId,
requestBody: { name: 'docs.yourcompany.com' }
})
// Get verification records
const config = await vercel.domains.getDomainConfig({
domain: 'docs.yourcompany.com'
})
The user adds a CNAME record, clicks "Verify," and boom — their docs are live on their domain with HTTPS.
Things I'd Do Differently
1. Write More Tests
The sync logic is complex. I've caught bugs manually that tests would have caught faster.
2. Ship Faster
I spent too long on "perfect" features. Should have launched with 50% of what I have now.
What's Next
- Real-time sync — webhook integration with ClickUp
- Analytics — see which docs people read
- Password-protected docs — for private client portals
- More themes
- API access — for power users
Try It Yourself
Wikibeem is live at wikibeem.com.
Connect your ClickUp workspace, sync your docs, add your domain.
Under 5 minutes to a professional documentation site.
I'm building this in public and genuinely want feedback. What features would make this useful for you? What's missing?
DM me on Twitter/X or LinkedIn.
Let's build this together.
Top comments (0)