Most developer portals still run on a hosted CMS. Content lives in a vendor's database, the API reference is uploaded from an OpenAPI file by hand, and the only record of who changed what is an audit log inside someone else's dashboard. That arrangement holds until the API surface passes a few dozen endpoints, until three teams start editing the same guides, or until an auditor asks how a page reached production. Docs as code fixes those problems, but the migration turns on one decision that determines whether the rest of the work pays off: does the Git repository become the authoritative source of truth, or another mirror that a CMS syncs against? Content conversion, URL preservation, CI gates, and publishing permissions are all downstream of that single call.
TLDR:
- A dev portal migration succeeds when Git is authoritative and the CMS is retired, not when a sync layer is bolted onto the existing platform.
- Inventory first: pages, assets, custom components, gated content, and the full list of published URLs. The URL list is what protects your search rankings.
- Generate the API reference from the OpenAPI, AsyncAPI, or gRPC definition rather than converting it as prose, so reference content stops drifting from the API.
- CI gates replace the CMS review queue: spec validation, MDX syntax checks, Vale style linting, link checking, and named preview deployments.
- Non-technical contributors stay in the workflow through a WYSIWYG editor that commits to the same repository, not a parallel CMS.
- Fern fits portals that ship docs and SDKs together, generating the API reference, code snippets, and client libraries in 9 languages from one API definition held in Git.
What docs as code changes about a developer portal
Docs as code treats documentation as plain-text source: Markdown or MDX in version control, changed through pull requests, validated in CI, deployed by a pipeline. Applied to a developer portal, it changes four things a CMS cannot.
- Review becomes code review. A docs change ships in the same pull request as the API change that motivated it, with the same reviewers and merge protections.
- History becomes forensic.
git blameanswers "when did this parameter description change and who approved it" without a vendor support ticket. - The API reference becomes generated output. Endpoints, schemas, and error responses come from the spec on every build instead of being re-uploaded when someone remembers.
- The portal becomes reproducible. A site that builds from a commit is what makes staging environments, self-hosted deployments, and air-gapped installations possible at all.
The distinction that matters during migration is Git-native versus Git-synced. ReadMe offers bidirectional GitHub and GitLab sync through the rdme CLI, and GitBook offers bidirectional Git Sync with monorepo directory mapping. Both are real, and for writer-led teams that want a browser-first experience with a versioned backup, they work well. But in each case the rendered site is produced from a database that the repository mirrors, so two writable stores hold the same content and reconciliation is a feature rather than a property of the system. Git-native platforms have one writable store, so that class of conflict cannot occur.
| Property | CMS portal | CMS with Git sync | Git-native docs as code |
|---|---|---|---|
| Source of truth | Vendor database | Database, mirrored to Git | Repository |
| Docs and code in one PR | No | Partially | Yes |
| Local preview before publish | No | Rarely | Yes |
| CI gates on content | No | Limited | Full (lint, spec, links) |
| Conflict surface | None (single store) | Dashboard vs. branch | Standard Git merges |
| Reproducible from a commit | No | No | Yes |
Why teams migrate a developer portal
The trigger is rarely aesthetic. Four pressures drive dev portal migration in practice: reference drift, where the published reference no longer matches the deployed API because updating it is a manual step that competes with shipping; contribution scale, where a portal maintained by twelve engineers across four services needs branching and conflict resolution that a CMS does not provide; governance, where SOC 2, HIPAA, and internal change-management policies want an attributable trail for published content plus the option to host inside your own infrastructure; and AI consumption, where coding agents read docs as a live knowledge source and structured, generated content produces reliable llms.txt output that hand-maintained CMS pages do not.
Step 1: inventory what the portal actually contains
Migrations run over schedule because the inventory happens after the tooling is chosen. Do it first and produce five artifacts.
- A page manifest with every published page, its URL, owner, last-modified date, and 90-day traffic. Pages with no traffic and no owner are candidates for deletion, not conversion.
- A full URL list exported from your sitemap and analytics, not just the navigation, because orphaned pages still hold backlinks.
- An asset list covering images, videos, PDFs, and downloads, with the CDN paths they currently resolve from.
- A custom-component list of every proprietary widget, embedded form, and CSS override. These have no automatic equivalent and they set the real timeline.
- A gated-content list of what sits behind login, which partner tiers see what, and which endpoints are internal-only. Access rules are usually undocumented and encoded in CMS settings.
Handle the API reference separately. It should not appear in the manifest as prose, because it is going to be regenerated from the specification rather than converted.
Step 2: decide where the source of truth lives
This decision has three parts.
The API definition. OpenAPI 3.x is the default for REST, AsyncAPI covers event-driven and WebSocket APIs, OpenRPC covers JSON-RPC, and protobuf covers gRPC. Whichever applies, the file belongs in version control alongside the service that implements it, and its correctness belongs in CI. If the spec is hand-maintained and known to be inaccurate, fix it before the migration, because every downstream artifact inherits its errors.
The prose. Guides, tutorials, and changelogs move to MDX, in either a dedicated docs repo (simplest permissions, extra coordination when a code change needs a docs change) or a docs directory inside the monorepo holding the API (docs and code ship in one PR, at the cost of broader CODEOWNERS rules). Teams shipping API changes weekly usually benefit more from the monorepo layout.
The configuration. Navigation, theming, redirects, and versioning become files, not dashboard settings. Fern puts all of it in a fern/ directory: docs.yml for the site, generators.yml for generated artifacts, fern.config.json for the workspace, and an openapi/ or definition/ folder for the API. Because it is CLI and YAML driven with no database-backed CMS layer, there is no second store to reconcile.
Step 3: convert content to Markdown and MDX
Bulk conversion gets most of the way and then stalls on the parts that matter. Most CMS platforms export HTML or Markdown, and pandoc handles the mechanical transform. Fern's CLI ships direct converters: fern init --readme URL_TO_README_DOCS_SITE builds a Fern docs site from a published ReadMe portal, and fern init --mintlify PATH_TO_MINT_CONFIG converts a Mintlify docs folder from its config file.
The manual pass is where the estimate lives:
- Component mapping. CMS callouts, tabs, accordions, and step lists each map to an MDX component with different props. Build the mapping table once and apply it, rather than deciding per page.
- Asset rehoming. Rewrite every image and video reference to a repo-relative path and commit the files. Leaving assets on the old CDN guarantees a broken portal the day the subscription lapses.
- Snippet extraction. Content repeated across pages (auth setup, base URLs, rate-limit tables) becomes a reusable snippet imported everywhere, so one edit propagates.
- Code sample triage. Hand-written samples that duplicate the API reference should be deleted, not converted. Generated snippets stay correct; pasted ones do not.
- Frontmatter. Setting titles, descriptions, and slugs explicitly during conversion is the cheapest moment to fix thin meta descriptions across the portal.
Step 4: generate the API reference instead of converting it
The reference is the largest part of a developer portal and should require the least manual work. Point the platform at the API definition and let it produce endpoint pages, request and response schemas, error documentation, and an interactive explorer on every build.
Two details matter during migration. Tags in the spec drive navigation grouping, so tidy them before cutover rather than working around them in configuration. And if the old portal maintained separate internal and public sites, the x-fern-audiences extension lets one specification produce filtered views per audience, down to individual properties, removing the duplicate-spec maintenance the old setup required.
This is also where coupling docs to SDKs becomes concrete. If the portal documents client libraries maintained separately from the spec, the reference and the libraries drift independently, and docs as code without client libraries leaves the most-copied content on the site outside the automation.
Step 5: preserve every published URL
Skipping this step turns a good migration into a bad quarter. A developer portal accumulates backlinks from Stack Overflow answers, partner integration guides, support macros, and years of blog posts, and changing the URL structure without redirects discards that equity while breaking links inside your own product. Work from the URL list produced in Step 1, in this order:
- Preserve paths where possible. Configure slugs to match the old structure instead of accepting platform defaults. A migration with zero URL changes needs zero redirects.
- Map the unavoidable changes one to one. Do not collapse a set of removed pages onto the homepage; that reads as a soft 404 and helps no one.
- Use patterns for whole-section moves. Fern configures redirects in
docs.yml, with parameter and regex matching:
redirects:
- source: /docs/quickstart
destination: /learn/quickstart
- source: /reference/:slug*
destination: /api-reference/:slug*
permanent: trueRedirects evaluate top to bottom and the first match wins, so specific paths go above catch-all patterns. permanent defaults to true and issues a 308; set it to false for a temporary 307 during a phased cutover.
- Enforce coverage in CI.
fern checkincludes amissing-redirectsrule that flags published pages removed or moved without a redirect configured. That turns URL preservation from a checklist item somebody forgets into a merge blocker.
Keep the old portal serving 200s until redirects are verified in production, then re-crawl to confirm nothing resolves to a 404.
Step 6: build the CI pipeline that replaces the CMS review queue
A CMS enforces quality through a publishing workflow and an approval button. Docs as code enforces it through checks on every pull request. The replacement pipeline has five gates.
- Spec and configuration validation.
fern checkvalidates the API definition and Fern configuration, with--apito scope it to one definition and--warningsto surface non-fatal issues. This catches spec errors before they become broken reference pages. - Syntax validation.
fern docs md checkvalidates MDX and reports file paths and line numbers, so a malformed component fails the PR instead of the deployment. - Style linting. Vale enforces terminology and voice as code. Add a
.vale.iniat the repo root withmdx = mdso MDX parses correctly, then runvale fern/locally and through the Vale GitHub Action for inline PR comments. This replaces the style guide nobody reads. - Link checking.
fern docs link checkcrawls the published site for broken (404) and blocked (403) links, with--output jsonor--output csv. Run it on a schedule against production, not only on PRs, because external link rot is time-based. - Preview deployments.
fern docs devruns the site locally, andfern generate --docs --preview --id my-featurepublishes a stable named preview URL that updates in place on every push. Named previews are what make reviewer workflows work, since stakeholders bookmark one link instead of hunting for the newest build.
Publishing becomes a merge: a merged pull request triggers a GitHub Action that runs generation and deploys, and GitLab CI is supported for teams not on GitHub. Restrict production deploys to the default branch and treat the pipeline as the only path to publish.
Step 7: keep non-technical contributors in the workflow
The most common objection to dev portal migration is that product managers, support engineers, and technical writers will stop contributing once the workflow requires Git. The answer is a visual editor that writes to the same repository rather than a second database.
The Fern Editor is a WYSIWYG interface that opens pull requests through a GitHub app, toggles between visual and raw Markdown editing, and syncs bidirectionally with the repository so browser and IDE edits do not clobber each other. Per-page review requirements let a typo fix skip engineering approval while API reference changes still require it. Two limitations are worth planning around: there is no real-time collaborative editing with live presence, so two people on the same page can still collide at the PR stage, and inviting a collaborator into a session requires Git knowledge.
Access control moves into configuration too. Roles are declared in docs.yml and applied with a viewers property on pages, sections, and other navigation items, so one site serves public, partner, and internal audiences from a single source. Every visitor including unauthenticated ones carries the everyone role, and preview links include a role selector for viewing the site as a specific role. That last detail matters at cutover: gated content can be verified against each role before DNS changes, rather than discovered in production.
Step 8: run the cutover
Sequence the switch so every step is reversible until the last one.
- Build in parallel. Stand up the new portal on a staging subdomain with the full content set while the old portal stays authoritative.
- Freeze content. Announce a 24 to 72 hour freeze on the old CMS and re-run the conversion to capture last-minute edits.
- Verify. Crawl staging for broken links, check the redirect map against the URL list from Step 1, validate search, and test every gated page against each role.
- Switch DNS. Point the docs domain at the new site, keeping the old platform reachable on an internal hostname.
- Watch for two weeks. Monitor 404 logs, search-console coverage, and support tickets. Most missed redirects surface within ten days.
- Decommission. Cancel the old subscription only after the 404 rate returns to baseline and assets serve from the new origin.
What usually goes wrong
- Custom components are underestimated. Proprietary widgets have no automatic equivalent, and rebuilding them is engineering work, not writing work. Scope them in Step 1.
- Assets stay on the old CDN. The portal looks perfect at cutover and loses every image when the old contract ends.
- The spec is wrong. Generating a reference from an inaccurate specification publishes the inaccuracy faster and more visibly than the CMS did.
- Redirects are treated as a launch task. They are a design task. Decide the URL structure before converting content, not after.
- Both systems are kept "just in case." Running a CMS and a repository in parallel indefinitely recreates the dual-source-of-truth problem the migration was meant to solve. Set a decommission date and hold it.
- Nobody owns the pipeline. CI checks that fail constantly get bypassed. Assign an owner for the docs build the same way you would for any other build.
The docs as code tooling landscape
The category splits by what each tool optimizes for, and API portals need more than static-site rendering.
| Tool | Best fit | API reference from a spec | SDK generation |
|---|---|---|---|
| Fern | API portals shipping docs and client libraries together | Yes, from OpenAPI, AsyncAPI, OpenRPC, gRPC | Yes, 9 languages |
| Docusaurus | Product and OSS docs with heavy React customization | Via plugins | No |
| MkDocs (Material) | Python-ecosystem project docs | Via plugins | No |
| Sphinx | Python library reference from docstrings | Library-level, not HTTP | No |
| Antora | Multi-repo AsciiDoc documentation at enterprise scale | No | No |
| Mintlify | Fast, attractive developer docs sites | Yes | No, via Speakeasy or Stainless |
| GitBook | Writer-led docs with a Git mirror | Yes | No |
| ReadMe | Browser-first portals with usage analytics | Yes | No |
Static site generators like Docusaurus and MkDocs are genuinely good at docs as code and cost nothing in license fees, but they leave the API reference, interactive explorer, snippet generation, versioning, RBAC, and search as integration projects you own. That tradeoff is often worth it for a documentation site and rarely worth it for a portal with a large API surface.
Migrating to docs as code with Fern
Fern fits portals that produce both documentation and client libraries from the same contract. The repository holds the API definition and MDX content, the CLI validates and previews locally, and a merged pull request triggers the build, so Git stays the only writable store. From one API definition, Fern generates the interactive API reference with per-language code snippets and type-safe SDKs in TypeScript, Python, Go, Java, C#, PHP, Ruby, Swift, and Rust, published to npm, PyPI, Maven Central, NuGet, RubyGems, and Packagist. fern init --readme and fern init --mintlify handle the initial conversion from those platforms, docs.yml redirects and the missing-redirects rule protect existing URLs, and self-hosted and VPC deployments are available for teams that cannot run docs on vendor infrastructure. Over 150 companies including Square, Webflow, ElevenLabs, LaunchDarkly, and Intercom use Fern.
Final thoughts on migrating to docs as code
Every hard part of a dev portal migration resolves back to the same question: is the repository authoritative, or is it a copy? Teams that answer "authoritative" get docs and code in one review, an API reference that cannot drift from the spec, CI that catches broken links and style violations before readers do, and a portal that rebuilds from a commit. Teams that answer "a copy" get a version-controlled backup of a CMS and most of the original problems. Decide that first, then run the sequence: inventory, source of truth, conversion, generated reference, URL preservation, CI gates, contributor workflow, cutover.
Book a demo to see what your portal looks like generated from your existing OpenAPI spec, including the redirect map and the SDKs that come with it.
FAQ
How long does a developer portal migration to docs as code take?
Component complexity matters more than content volume. A portal of a few hundred prose pages with a clean OpenAPI spec and standard components typically converts in two to four weeks, most of which is review rather than conversion. Portals with proprietary widgets, multiple gated audiences, several API versions, or an inaccurate specification run longer, because each of those is engineering work bulk conversion tools cannot do.
What is the difference between docs as code and a CMS with Git sync?
Docs as code makes the repository the single writable source: the site builds from a commit, and there is nowhere else content can change. A CMS with Git sync keeps a vendor database as the rendering source and mirrors content into a repository, so two systems can both accept edits and can therefore disagree. Git sync suits writer-led teams, but it does not deliver the reproducibility, merge semantics, or single-PR review that engineering-driven portals migrate for.
Can technical writers work in docs as code without learning Git?
Yes, provided the platform offers a visual editor that commits to the same repository rather than a separate CMS. The Fern Editor gives non-technical contributors a WYSIWYG interface that opens pull requests automatically, toggles between visual and Markdown editing, and syncs bidirectionally with GitHub. Per-page review rules let business content and typo fixes bypass engineering approval while technical pages still require it.
How do you preserve SEO when migrating a developer portal?
Start from a complete URL export taken from the sitemap and analytics rather than the navigation, since orphaned pages still carry backlinks. Preserve paths wherever the information architecture allows it, write explicit one-to-one redirects for the rest, and use pattern redirects such as /reference/:slug* for whole-section moves. Enforce coverage in CI so a removed page without a redirect fails the build, keep the old site reachable until redirects are verified in production, and re-crawl after cutover to confirm nothing returns a 404.
Is docs as code a good fit for self-hosted or air-gapped documentation?
It is the only approach that makes self-hosting practical, because a site that builds reproducibly from a commit can be built inside your own infrastructure. Fern supports self-hosted and VPC deployments, GitHub Enterprise Server, GitLab, private package registries such as Artifactory, and pulling generator images from custom container registries via the image: { name, registry } field in generators.yml, which matters for organizations whose policies prohibit public registry access. A CMS-hosted portal cannot meet those requirements, since the rendering source lives on vendor infrastructure by design.