Confluence to nextjs
Personal collection of agent skills for use with Claude Code and other LLM agents.
npx -y skills add andreab67/agent-skills --skill confluence-to-nextjsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Convert Atlassian Confluence pages to native Next.js App Router pages — fetch content via Confluence REST API v2, parse storage-format HTML into semantic JSX, build a knowledge base with table of contents, anchor navigation, and matching design system. Use this skill whenever the user mentions Confluence migration, converting Confluence docs to Next.js, building a KB site from Confluence content, or replacing Atlassian-hosted pages with a self-hosted knowledge base — even if they just say "move our docs off Confluence" or "build a KB app".
SKILL.md
12.2 KB, as published. Nobody here has run it
confluence-to-nextjs
Migrate Atlassian Confluence documentation to a self-hosted Next.js knowledge base. Fetches pages via the Confluence REST API, converts HTML storage format to semantic JSX, and wires up anchor navigation and a sticky table of contents.
When to use
- Replacing external Confluence URLs in a product site with internal
/kbroutes - Building a public knowledge base from Confluence content
- Porting support docs, deployment guides, or product documentation to Next.js
Do NOT use for:
- Wiki content that changes frequently and needs real-time sync (use Confluence embed instead)
- Non-Confluence CMS sources (use a different migration approach)
Step 1: Fetch Confluence content
Use scripts/fetch-page.sh rather than hand-rolling the curl call — it pins body-format=storage (never view — see Anti-pattern 1), checks the HTTP status, and fails loudly with the response body on 401/403/404 instead of silently writing an error payload to disk:
[email protected] CONFLUENCE_TOKEN=ATATT3x... \
./scripts/fetch-page.sh your-org PAGE_ID page.json
The storage format returns HTML with Confluence-specific tags (<ac:structured-macro>, <ac:parameter>, <ri:attachment>) that must be stripped and converted.
Step 2: Storage format → JSX conversion rules
| Confluence HTML | JSX equivalent |
|---|---|
<h1>, <h2>, <h3> with text | <h1 id="slug">, <h2 id="slug"> (slugify for anchor nav) |
<ac:structured-macro ac:name="info"> | <div className="info-callout"> |
<table> | <table className="kb-table"> |
<ac:link> internal links | Remove or replace with external link |
<strong>, <em>, <code> | Pass through as-is |
<ul>, <ol>, <li> | Pass through as-is |
Slug generation for heading IDs — use scripts/slugify.mjs rather than reimplementing it per migration; it also dedupes collisions (Anti-pattern 6) via dedupeSlugs():
node scripts/slugify.mjs "Standard Support Contract" # -> standard-support-contract
node scripts/slugify.mjs --file headings.txt # dedup a whole page's headings in order
Step 3: Page structure
Each knowledge base page follows this layout:
// app/support-maintenance/page.tsx
import type { Metadata } from "next";
import { TableOfContents } from "@/components/toc";
export const metadata: Metadata = {
title: "Support & Maintenance",
description: "SLA tiers, response times, and maintenance windows.",
};
const tocItems = [
{ id: "definitions", label: "Definitions", level: 2 },
{ id: "standard-support-contract", label: "Standard Support", level: 2 },
{ id: "gold-support-contract", label: "Gold Support", level: 2 },
];
export default function SupportMaintenancePage() {
return (
<section className="section inner-hero">
<div className="container section">
<div className="article-layout">
<div className="article-content">
<h2 id="definitions">Definitions</h2>
<p>...</p>
<table className="kb-table">
<thead><tr><th>Priority</th><th>Description</th></tr></thead>
<tbody>...</tbody>
</table>
</div>
<TableOfContents items={tocItems} />
</div>
</div>
</section>
);
}
Step 4: Table of contents component
Sticky sidebar with IntersectionObserver for active heading tracking:
"use client";
import { useEffect, useState } from "react";
interface TocItem { id: string; label: string; level: number; }
export function TableOfContents({ items }: { items: TocItem[] }) {
const [active, setActive] = useState<string>("");
useEffect(() => {
const observers = items.map(({ id }) => {
const el = document.getElementById(id);
if (!el) return null;
const obs = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) setActive(id); },
{ rootMargin: "-20% 0px -70% 0px" }
);
obs.observe(el);
return obs;
});
return () => observers.forEach((o) => o?.disconnect());
}, [items]);
return (
<nav className="toc-sidebar" aria-label="On this page">
<p className="toc-title">On this page</p>
<ul>
{items.map(({ id, label, level }) => (
<li key={id} className={`toc-item level-${level} ${active === id ? "active" : ""}`}>
<a href={`#${id}`}>{label}</a>
</li>
))}
</ul>
</nav>
);
}
Step 5: Update source URLs
Replace Confluence links across the product site with KB routes:
// Before (in site-data.ts, pricing.ts, etc.)
discoverUrl: "https://your-org.atlassian.net/wiki/spaces/SPACE/pages/12345"
// After
discoverUrl: `${process.env.NEXT_PUBLIC_KB_URL ?? "https://kb.example.com"}/deployment-motions#docker`
Anchor IDs must match the id attributes on the converted headings.
Step 6: KB app scaffold
Minimal Next.js app for the knowledge base:
apps/kb/
├── app/
│ ├── layout.tsx # SiteHeader + SiteFooter + GA script
│ ├── page.tsx # Article index / home
│ ├── robots.ts
│ ├── sitemap.ts
│ └── <article-slug>/
│ └── page.tsx # One file per Confluence page
├── components/
│ ├── site-header.tsx
│ ├── site-footer.tsx
│ └── toc.tsx # TableOfContents
├── lib/
│ └── kb-data.ts # Article metadata array
├── __tests__/ # Vitest + RTL tests per page
├── Dockerfile
└── package.json
Add the app to CI and K8s following the nextjs-monorepo-ci and k8s-nextjs-deploy skills.
Testing converted pages
Each page needs a Vitest test checking:
- H1 heading renders
- Key section headings exist with correct anchor IDs
- Tables contain expected content
- TOC renders
import { render, screen } from "@testing-library/react";
import SupportMaintenancePage from "@/app/support-maintenance/page";
vi.mock("@/components/toc", () => ({
TableOfContents: () => <nav data-testid="toc" />,
}));
it("renders standard support anchor", () => {
render(<SupportMaintenancePage />);
const h = screen.getByRole("heading", { name: /Standard Support Contract/ });
expect(h.id).toBe("standard-support-contract");
});
When a heading appears at multiple levels (h2 + h3 with similar text), use { level: 2 } in the query to avoid Found multiple elements errors.
Anti-patterns
These look reasonable during a Confluence migration but cause broken pages or stale content:
- Fetching in
body-format=viewinstead ofbody-format=storage—viewreturns rendered HTML with session-relative image URLs and Confluence CDN paths that are unauthenticated and will 404 in production. Always usestorageformat and handle the<ac:*>macros explicitly. - Using heading text as anchor IDs without slugification — Confluence heading anchors can contain spaces, special characters, and accents. Using heading text directly as
id=attributes breaks#fragmentlinks. Always run the text throughslugify()and store the mapping. - Assuming the
subUUID in headings is stable across page versions — Confluence internally uses UUID-based heading IDs in some contexts; these change when content is edited. Base your anchor IDs on the visible heading text, not on any Confluence internal identifier. - Building the Table of Contents from static render rather than
IntersectionObserver— a static list of links scrolls fine but shows no active state. TheIntersectionObserverapproach in Step 4 is non-negotiable for UX parity with Confluence's native sidebar. - Migrating pages with
<ac:structured-macro name="include">transclusion — Confluence's include macro pulls content from other pages at render time. In a static Next.js migration, the included content must be inlined at build time or fetched as a separate API call. Treating it as a simple HTML element will produce a broken macro tag in production. - Not deduplicating
idattributes across a page — when Confluence content has two headings with the same text (e.g., two "Overview" sections in a long doc), both will getid="overview", breaking anchor navigation. Append a numeric suffix on collision:overview,overview-2, etc. - Forgetting to update
next-sitemap.config.js— migrated KB pages won't appear in Google's index until the sitemap lists them. Add the new routes explicitly or ensure the dynamic route is covered by the sitemap generation logic.
Error Handling
Realistic failure modes when running a Confluence migration, how to detect them, and how to recover:
- 401/403 from the REST API — invalid or expired personal API token, or the page lives in a restricted space. Detect: the response body is
{"statusCode":401,...}or403. Recovery: regenerate the token in Atlassian account settings, or get a space admin to grant read access before retrying. - 404 on
pages/PAGE_ID— wrong page ID, or the ID belongs to a different Confluence site/cloud instance. Detect: JSON body{"statusCode":404}. Recovery: re-derive the page ID from "..." → "Page information" (pageId=in that URL), not from the pretty URL slug. - 429 rate limiting on bulk exports — hit when migrating many pages in a tight loop. Detect: HTTP 429 with a
Retry-Afterheader. Recovery: back off per the header and batch requests in small groups instead of fetching the whole space at once. - Unmapped
<ac:structured-macro>types — the conversion table in Step 2 only coversinfo. Macros likenote,warning,expand,code, orjirapass through as raw<ac:structured-macro>tags. Detect: build fails on an unknown tag, or the macro renders as literal markup in the browser. Recovery: extend the conversion table per macro name before converting the page — don't blind-strip unfamiliar macros. - Broken image/attachment links —
<ri:attachment>references Confluence-hosted binaries that the page-body API doesn't return inline. Detect:<img>404s against the Confluence CDN in production (unauthenticated). Recovery: fetch attachments separately via/wiki/api/v2/pages/{id}/attachments, download intopublic/, and rewritesrcto the local path. - Malformed storage HTML breaks JSX parsing — unclosed tags or stray entities from Confluence's rich text editor aren't valid JSX. Detect: build-time JSX syntax error pointing at the converted file. Recovery: normalize the storage HTML (balance tags, escape entities) before hand-authoring the
.tsx, rather than pasting raw storage output directly. - Internal
<ac:link>targets not yet migrated — linking to a Confluence page that hasn't been converted produces a dead/kb/...route. Detect: 404 when clicking through during review. Recovery: keep a migration tracking table (Confluence page ID → KB route) and point unconverted links at the original Confluence URL until that page ships.
Example prompts
- "I want to replace our Confluence KB with a self-hosted Next.js site. Where do I start?"
- "How do I fetch a Confluence page's content via the REST API?"
- "Show me how to convert a Confluence
<ac:structured-macro name='info'>callout to JSX." - "Generate the TableOfContents component with IntersectionObserver for active-section tracking."
- "Write a Vitest test for the support-maintenance KB page checking anchor IDs."
- "How do I replace Confluence URLs across the product site with internal KB routes?"
Related skills
nextjs-monorepo-ci— add theapps/kbNext.js app to the CI pipelinek8s-nextjs-deploy— deploy the KB app to Kubernetes