Don't Replace Doxygen
The C++ ecosystem has spent twenty years trying to replace Doxygen. The escape was always inside the toolchain. Cut at the joint.
Doxygen is the only piece of software the C++ ecosystem hates and cannot leave.
A huge amount of serious C++ still ships it. Every serious developer has complained about its output. Replacement attempts have either failed, gone commercial, or quietly evaporated. Twenty-six years on, the position is exactly what it was a decade ago: the tool is in the repo, the Doxyfile is checked in, the /// blocks are above every declaration, and nobody chose this. It is what was running when you got there.
And it works. The output looks like 1998, the search is bad, the navigation reads like a man page rendered by a Java applet, and the thing keeps running. The build is fast; under a minute on projects where Sphinx takes thirty-one. The parser handles macros, templates, friend declarations, ADL, namespace pollution, and partial specialisations without complaining. It handles inheritance graphs, call graphs, and the cross-references that anything purpose-built for documentation usually gets wrong, because most replacements had to rebuild a C++ frontend before they could even start.
That is the problem. The reason nobody has replaced Doxygen is not that the C++ community is reactionary. It is that Doxygen does something genuinely hard, and it does it well, and the people most qualified to replace it know exactly how hard the part they would need to rebuild is.
The escapes that failed
Several teams have tried.
hdoc wrote a Clang-based parser, shipped a polished output, and moved commercial. Viable for projects that can absorb the licensing change; not a path most maintainers can take without legal review. Standardese went the same route, built an impressive C++ frontend from scratch, then stalled when its primary author moved on. DoxyPress modernised Doxygen’s internals while staying loyal to the same output model, which means it inherits the problem it was supposed to solve. doxygen-awesome-css took the pragmatic angle: restyle the existing HTML, ship a CSS theme, and let 1.3K projects star it because their site looks better the morning after they install it. That popularity is also evidence of how much demand exists for a real upgrade, and how partial each existing solution is.
The strongest production answer is Breathe with Sphinx, often with Exhale on top. Take Doxygen’s XML, transform it into reStructuredText, render through Sphinx. Microsoft recommends this chain. The Slint team evaluated every available option and settled on exactly this stack. It works. The cost is four tools to maintain, a build time that balloons from one minute to thirty or more, and a configuration surface that none of those tools own end to end. OpenCV tried Sphinx for v3, watched their docs build go from one minute to thirty-one, and reverted to Doxygen.
Each escape route has a tax. Rewrite the parser, and you spend years closing a gap you may never close. Wrap the parser in something heavier, and the build slows down while the configuration surface fragments across tools. Restyle the output, and the skeleton underneath is still 1998.
The position has become widely held and quietly accepted: Doxygen is what we have; the C++ community has made peace with it.
The category error
That peace is built on a category error.
Doxygen is not one tool. It is two tools that ship together, share a binary, and have been treated as a single object for the entire history of the project.
The first tool is the parser. It reads C++, builds a structured model of your codebase, and emits that model to disk. This is the part with twenty-six years of accumulated edge cases solved. This is the part nobody can credibly rebuild without years of work nobody will fund.
The second tool is the renderer. It reads the model and writes HTML pages. It was state of the art when the browser wars were still being fought. It has not meaningfully moved since.
The parser is excellent. The renderer is the problem.
Cut at the joint.
Every escape attempt aimed at the whole tool when only half of it had aged. The seam was there the entire time, hidden inside the binary, hidden inside the assumption that you were always choosing or rejecting Doxygen in one motion.
The XML was always the seam
Doxygen has emitted a complete structured XML representation of your codebase since the early 2000s. Set GENERATE_XML = YES in your Doxyfile and you get every class, every member, every file, every group, every namespace, with cross-references, type information, inheritance relationships, file locations, and the comments you wrote against each declaration. The entire model the parser built, in a stable schema, on disk, ready for anything to consume.
This is what Breathe consumes. It is what doxygen2docusaurus consumes. It is what Doxybook consumed. It is what proves the parser and renderer were always separable, and what the rest of the world has been quietly building against for years while the renderer kept shipping pages that look like Java 1.4 documentation.
If the XML is the contract, the renderer is replaceable. Anything that can read the XML can take its place. The chain shortens. The parser stays where it is. The output stops being a 1990s artefact.
Moxygen is the smallest bridge across the seam. It reads Doxygen XML and writes Markdown. From Markdown, anything modern renders: static site generators, Docusaurus, MkDocs, Sourcey. Three steps. One parser, one converter, one renderer; each replaceable, none of them re-implementing C++.
Icey, in public
The proof is running.

Icey is a C++ networking library. WebRTC, signalling, TURN relay, and media encoding in one stack. Real users, real shipping code, the kind of C++ that exercises every feature Doxygen is famous for getting confused about: templates, partial specialisations, multi-module dependency graphs, the works.
Its Doxyfile is unremarkable. PROJECT_NAME = "icey". INPUT = ./src. GENERATE_HTML = YES. GENERATE_XML = YES. The kind of configuration that has been working for two decades, untouched.
What changes is what happens to the XML.
The pipeline that produces the icey docs at 0state.com/icey/docs is short. Doxygen writes XML to ./build/doxygen/xml. Icey’s sourcey.config.ts declares a Doxygen tab pointing at that path with index: "rich". Sourcey reads the XML natively through moxygen.generate() in memory, alongside the project’s hand-written guides in concepts/, recipes/, modules/, run/, and operate/. No intermediate Markdown mirror on disk, no second source of truth to drift. The build ships a static site that loads in milliseconds, indexes for search, exposes a single canonical URL per symbol, emits llms.txt and llms-full.txt for agents, and reads on a phone.
Same Doxyfile every other C++ project ships. Same /// blocks above every declaration. Different renderer.
The CI has no four-tool chain. The build does not run for thirty minutes. The parser nobody wants to rebuild is doing the work it was always doing. What ships at the end of the chain is what a modern web property ships: owned URLs, search, agent-readable surfaces, and design that doesn’t apologise for itself.
What this means
If you maintain a C++ project that runs Doxygen, you already have everything you need. The XML is one config line away. Everything downstream of it is a swap.
You keep your Doxyfile. You keep the comment conventions your team has built up over years. The build stays fast. The parser stays where it is, handling the edge cases that make C++ documentation hard.
The migration is not a rewrite. It is a pipeline change. The output stops being the place a project’s docs go to age, and starts being where they are owned, indexed, and built to be read by the people and the agents that will actually read them.
The escape was always inside the toolchain. The C++ ecosystem agreed years ago that there was no way out, then stopped looking.
There is. It runs through the XML.