How I Built Wikibeem: Turning ClickUp Docs into Professional Documentation Sites

Published: (December 24, 2025 at 08:45 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

A solo dev journey from frustration to product launch

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:

https://doc.clickup.com/d/2kxuepwx-192

That ugly, branded, unprofessional URL. Clients would ask: “Why are we using ClickUp? Can you put this on your website?”

I spent hours:

  • Exporting to PDF (formatting breaks)
  • Copy‑pasting to Notion (links break)
  • Rebuilding in GitBook (context lost)

Workarounds like Cloakist only proxied the ClickUp page, so clients could still click around and see internal stuff.

The problem is common

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.

Introducing Wikibeem

One night I thought: What if I could solve this for myself and for them?
That’s how Wikibeem was born.

Tech stack

ComponentTechnology
Frontend / ServerNext.js 16 (App Router), React 19, Tailwind CSS 4
Data layerPostgreSQL + Prisma ORM
HostingVercel (edge functions)
AuthNextAuth v5 (beta) – credentials + OAuth
PaymentsPaddle (handles global taxes)
ClickUp integrationClickUp API v3 (OAuth 2.0)
HTTP clientAxios
Sync engineCustom logic for nested pages, wikis, doc hierarchies
Markdown ↔ HTMLMarked, Cheerio, Turndown
SearchFuse.js (client‑side fuzzy search)
SEOPer‑site & per‑document SEO, auto‑generated sitemaps
DomainsVercel SDK (programmatic custom domain setup, automatic SSL)
Internationalisationnext‑intl (9 languages: EN, FR, DE, ES, PT, IT, RU, AR, ZH)

How Wikibeem works (under the hood)

┌─────────────────┐     OAuth      ┌─────────────────┐
│   ClickUp API   │◄──────────────►│    Wikibeem     │
│   (Your Docs)   │                │    (Next.js)    │
└─────────────────┘                └────────┬────────┘

                    ┌───────────────────────┼───────────────────────┐
                    │                       │                       │
              ┌─────▼─────┐          ┌──────▼──────┐         ┌──────▼──────┐
              │ PostgreSQL │          │   Vercel    │         │   Paddle    │
              │  (Prisma)  │          │  (Domains)  │         │ (Payments) │
              └───────────┘          └─────────────┘         └─────────────┘

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

Sync engine details

ClickUp’s API returns docs that can contain:

  • Nested pages
  • Wikis with their own structure
  • Content in various formats (JSON blocks, Markdown, HTML)

The engine must:

  1. Fetch all docs from a workspace.
  2. Recursively process pages and their children.
  3. Convert content to clean HTML.
  4. Build a hierarchy with parent‑child relationships.
  5. Generate unique slugs for URLs.
  6. Handle updates without creating duplicates.

Handling root‑doc duplication

// 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
}

Optimising slug checks

The first version queried the database for each slug, resulting in 100+ round trips for 100 pages.

// In‑memory slug tracking
const existingSlugs = new Set()

// Instead of: await prisma.document.findUnique(...)
// Now:
if (existingSlugs.has(slug)) {
  // generate a new slug
}
existingSlugs.add(newSlug) // Track immediately

Sync time dropped from 30 + seconds to under 10 seconds.

Programmatic domain provisioning (Vercel SDK)

import { Vercel } from '@vercel/sdk'

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 the docs go live on their custom domain with HTTPS automatically.

Future roadmap (features in progress)

  • Real‑time sync via ClickUp webhooks
  • Analytics – see which docs people read
  • Password‑protected docs – for private client portals
  • Additional themes
  • API access – for power users

Get started

Wikibeem is live at wikibeem.com.

  1. Connect your ClickUp workspace.
  2. Sync your docs.
  3. Add your custom domain.

Under 5 minutes to a professional documentation site.

Feedback

I’m building this in public and genuinely want your input. What features would make this useful for you? What’s missing?

Reach out on Twitter/X or LinkedIn. Let’s build this together.

Back to Blog

Related posts

Read more »