Polyglot Documentation Sites: Architecture and Search14 min read2,763 words

Polyglot Documentation Sites: Architecture and Search

Learn how to combine Doxygen, rustdoc, and Sphinx into one coherent docs site. Compare architectures, unify navigation and search, and choose a practical path.

polyglot documentation siteAPI documentation generatorsDoxygen and Sphinxrustdoc integrationdocumentation site search
Polyglot Documentation Sites: Architecture and Search
Polyglot Documentation Sites: Architecture and Search

How to Build a Cohesive Polyglot Documentation Site

A project that ships libraries in multiple programming languages usually ends up with multiple documentation systems, too. The C and C++ teams might use Doxygen, Rust teams might rely on rustdoc, and the product guides might live in Sphinx (or something similar). Each tool is great at understanding its own language. The problem is what happens when readers have to bounce between them.

The result often looks less like one product and more like a stack of unrelated sites: different navigation, separate search indexes, inconsistent visual styles, and links that break after a refactor or a path change. Rebuilding every API reference inside one generator can sound clean, but it can also mean throwing away language-specific features and maintaining a conversion layer forever.

There isn't a universal solution. Most teams end up choosing between transforming each generator's output into one system or publishing the generated sites separately and composing them into a single experience. This guide lays out the trade-offs, walks through how shared navigation and comprehensive search can work, and gives you a practical decision framework for an architecture your team can actually maintain.

Why multiple documentation generators create a site problem

A polyglot documentation site is an experience built from outputs produced by different generators. For example, a project might use Doxygen for C++, rustdoc for Rust, and Sphinx for guides, tutorials, and reference material that isn't tied to a single language.

Those generators aren't interchangeable. Each one has its own idea of source code, symbols, types, and what users expect to find. You can sometimes connect their outputs at the code level, but the documentation site still has to make them feel like one coherent place for readers.

That creates two different responsibilities:

  • Language-specific reference: get the API documented accurately and include the features each language's users expect.
  • Site-wide experience: keep navigation, search, styling, and cross-links consistent across everything.

The first part is where specialist generators shine. The second part is where multi-generator sites often struggle.

The mismatch is deeper than visual design

Different fonts and page layouts are noticeable, so teams often start by trying to standardize CSS. That can help, but it doesn't fix the core usability issues.

A reader lands on a Rust API page and can't easily find a way back to the main product docs. Search launched from a Sphinx page returns only Sphinx content, so relevant Doxygen and rustdoc pages become effectively invisible. Or a link between subsites relies on relative paths that silently break after someone changes a URL scheme or directory structure.

In other words, these aren't just "different themes." They're separate systems with different assumptions. Shared branding doesn't make them one site - what makes the experience cohesive is shared entry points and predictable behavior.

Why rebuilding everything in one generator is not free

It's tempting to treat your main documentation generator as the "single source of truth" and convert every API reference into its format. That can simplify the architecture, but it also shifts responsibility away from tools that already understand the language, and onto your conversion pipeline.

Take rustdoc: it understands Rust APIs and provides search behavior built around Rust concepts. Doxygen and Javadoc have their own conventions for how information is structured and presented. If your converter removes or flattens details, developers may lose features they were relying on - and it's hard to know what's safe to remove until someone actually needs it.

There's also a user-expectation cost. Developers familiar with rustdoc quickly recognize its conventions. Swapping those for a custom interface means asking them to relearn how to read the docs. A more uniform design can be nice, but uniformity isn't automatically an upgrade if it sacrifices useful functionality.

So the question isn't "Can we make everything look the same?" It's "Which parts of the experience should be unified, and which differences are worth keeping?"

Two architectures: transform or compose

At a high level, there are two approaches. You can transform each generator's output into content that your primary documentation system can consume, or you can publish the generated outputs as-is and connect them at the site level.

Neither approach is automatically "right." The real difference is the trade-off between the integration work you take on and the capabilities you preserve.

Option 1: transform generated reference content

In a transformation architecture, a pipeline reads the output from one API generator and converts it into something the main documentation system can include. For example, a Doxygen build can produce XML, and a Sphinx extension like Breathe can parse and render that as part of a Sphinx site.

This can produce a more unified structure. API reference material can live alongside guides, use the main system's navigation, and reuse the main system's search and theming - without stitching together multiple independent sites.

The cost is that you now own more of the "path" from source comments to published pages. A typical flow looks like this:

  1. Developers document code using the API generator's conventions.
  2. The generator produces structured output (for example, Doxygen XML).
  3. A conversion layer parses that output.
  4. The main docs build inserts the resulting reference content.
  5. The finished site is checked for missing or malformed pages.

Each step can fail. A developer can document a class correctly, but the page might not appear if a directive or inclusion step is wrong or missing. The failure can be quiet: the build may still pass, the source comments exist, but readers never see the reference.

Conversion can also affect build time. In the implementation described in the original case study, using Breathe was associated with a build time of roughly 90 seconds, compared with roughly 60 seconds after moving away from it. Those numbers are specific to that project, not a general benchmark - but they're a good reminder that integration work has an operational cost worth measuring.

Finally, transformation creates editorial decisions that need consistent rules. For example: should API entries be alphabetized, grouped by module, or inserted where they support a guide? Flexibility is useful, but when every contributor makes a different choice, navigation gets harder, not easier.

Transformation is a good fit when you need a tightly integrated content structure, the conversion layer is reliable, and you're ready to maintain it. It's a weaker fit when preserving generator-specific behavior matters more than rendering everything through one system.

Option 2: publish the generators' output and compose the site

In a composition architecture, each generator publishes its own output. Doxygen, rustdoc, and Sphinx can remain separate build products, and then a deployment or post-processing layer makes them available as parts of one documentation site.

This preserves each generator's behavior and interface. The trade-off is that you still have to solve site-wide concerns across boundaries: shared navigation, search, theme behavior, URL conventions, and links between sections.

A helpful way to think about composition is that you're creating several "apps" that share one public front door. They don't need the same implementation. They do need to agree on what a reader experiences when they move between them.

Composition reduces the risk of reimplementing language-aware features, but it doesn't eliminate integration work. Instead of content conversion, the work shifts into build orchestration and cross-site coordination. If you make those responsibilities explicit, this can be a reasonable compromise. If you leave it to ad hoc scripts and manual fixes, the site becomes fragile.

Composition is a good fit when you want to keep specialist generators doing what they do best, and you can invest in a shared site layer. It's less attractive when the product really needs a deeply unified page structure, or when deployment constraints make coordinated builds painful.

How to make separate sites feel connected

A composed site doesn't need every generator to behave identically. It needs to remove the places where users get stranded and where search results become incomplete. Practically, that means shared navigation, a shared search experience, and predictable build/link behavior.

Give every page the same route back

A universal header gives readers consistent access to top-level sections and search, no matter which generator produced the current page. Breadcrumbs can add helpful context, and synchronized theme settings prevent preferences from jumping around as users move between subsites.

One common pattern is to insert a marker into each generator's HTML output, then replace that marker during a later build step with shared header markup, styles, and scripts. In the Pigweed example described in the source, the Doxygen and rustdoc builds run before Sphinx. A Sphinx extension hooks into the completed build and replaces the marker across the generated pages.

The exact mechanism matters less than having one clear owner for the shared interface and a defined injection point. Without that, teams tend to copy a header into each site and then slowly accumulate differences.

Also: keep the shared header deliberately small. It should provide orientation and links to common destinations. If it starts trying to become a whole second application that runs on top of every generator, you'll eventually hit conflicts with generator-specific markup - and you'll end up maintaining compatibility work instead of shipping docs.

Build one search index from the finished output

Search is usually the biggest practical win, because it lets readers discover content across all generators through one interface. If you build a search index from the final generated HTML, it can include pages regardless of which tool produced them.

The case study uses Pagefind. It indexes a built output directory and gives you controls for including or excluding content. Its default web components are a solid starting point, and you can customize more when the default experience isn't enough.

The order matters. If you build the search index before every subsite has been generated and placed into the final output directory, the index will be incomplete. A robust pipeline should make the dependency explicit:

  1. Build each language-specific reference.
  2. Build guides and general documentation.
  3. Assemble the outputs at their intended paths.
  4. Add (or verify) shared navigation and other post-processing.
  5. Generate the search index from the assembled site.
  6. Test that results from each documentation source show up.

And don't blindly index every HTML file. Generated navigation fragments, duplicate pages, or low-value utility content can make results messy. Use allowlists or denylists, and check the actual results for representative queries. The goal isn't just "a big index." It's useful coverage.

Treat URLs and builds as shared contracts

A composed site works best when each component has a predictable public path on the same domain. For example, a project might publish general guides under /docs/, C++ reference under /reference/cpp/, and Rust reference under /reference/rust/. The exact structure is a product choice, but it should be stable enough that links don't depend on whatever directory layout happened to exist inside a build artifact.

Prefer canonical, root-relative links or a generated mapping over fragile relative paths that climb through multiple directories. Then test links across generator boundaries as part of the release process. This catches a common failure mode: each subsite looks correct when built alone, but the combined deployment has broken cross-links.

The same idea applies to deployment. Treat the assembled documentation site as one release artifact, even if multiple tools produced it. That gives the team a clear place to validate navigation, search, and paths before publishing.

A practical decision framework and rollout plan

Don't pick an architecture purely because it looks cleaner in a diagram. Start with what readers and maintainers actually need.

Choose based on what you cannot afford to lose

These questions make the trade-offs concrete:

  • Are generator-specific features important? If language-aware search or familiar API presentation matters, lean toward composing/publishing the generated output.
  • Does content need to be deeply interleaved? If API entries have to live inside one editorial structure, transformation may be worth the maintenance cost.
  • Can the team own a conversion layer? If nobody is responsible for correctness, don't build a conversion pipeline just to get visual uniformity.
  • Can the build assemble all outputs reliably? Composition depends on ordered, repeatable builds and a stable output layout.
  • What does failure look like? Prefer checks that catch missing pages, broken links, and incomplete search rather than relying on someone to notice after release.

A useful default for projects with multiple well-supported API generators is to keep their outputs and unify the site experience first. This preserves specialist behavior while fixing the most visible reader-facing problems: navigation and search. Consider transformation later if there's a real need for deeper integration - rather than just a preference for one template.

Roll out the smallest useful integration

A staged rollout limits risk and helps you find where the real pain lives:

  1. Inventory the outputs. Record each generator, build command, output directory, and public URL prefix.
  2. Define shared site rules. Agree on top-level navigation, canonical paths, breadcrumbs, and theme behavior before writing integration code.
  3. Assemble one deployment. Put all generated outputs into a predictable directory structure and verify the pages load under the intended paths.
  4. Add common navigation. Use a controlled injection or shared component mechanism, then test it against representative pages from every generator.
  5. Index the assembled HTML. Configure search inclusion deliberately and verify results from each source.
  6. Automate cross-site checks. Test important links, required pages, and search coverage in continuous integration or your release pipeline.
  7. Measure maintenance cost. Track build time, breakages, and contributor effort. If the shared layer becomes a frequent failure point, simplify it.

This order avoids a common trap: spending weeks polishing a unified visual theme before readers can navigate between reference sections or find relevant content via search. Start with the capabilities that affect everyone, then refine the look once the routes and content are solid.

Know when the compromise is no longer working

A composed site isn't a forever decision. Revisit it if generators change their HTML in ways that repeatedly break shared components, if search becomes hard to tune, or if readers need relationships between content that the separate outputs can't represent well.

That doesn't automatically mean rewriting all reference pages. It might be as simple as replacing a brittle injection mechanism, standardizing build contracts, or selectively transforming only the content that needs tighter integration. The architecture should follow the problem you actually observe - not an abstract preference for "one tool" versus "many."

Conclusion: unify the experience, not necessarily the tools

A multi-language project doesn't have to choose between a single "polished" site and useful language-specific reference tools. Transformation can give deeper integration, but it also means taking responsibility for conversion fidelity, build performance, and what content makes it into the final site. Composition keeps each generator's strengths, but it requires deliberate work on navigation, search, URL conventions, and deployment.

For many teams, a good starting point is to publish specialist outputs as-is and connect them with a shared header and a search index built from the complete site. Measure what's still difficult before adding a conversion layer.

Start by mapping your generators and their published paths. Then assemble one build, make it searchable, and test the experience from the reader's point of view. Keep the solution as simple as your constraints allow - and expand it only when you have evidence that you need to.

Frequently Asked Questions

It is a documentation site that brings together content generated by tools for multiple programming languages or content types. For example, one project might combine Doxygen for C++, rustdoc for Rust, and Sphinx for guides.
Not necessarily. A single generator can simplify navigation and publishing, but conversion may remove language-specific features and create maintenance work. Keep specialist generators when their capabilities matter, and unify the site experience around them where practical.
Yes, if the search tool indexes the assembled HTML output rather than only one generator’s pages. Build all subsites first, create the index afterward, and test queries that should return results from each source.
The shared header can become a fragile layer if it depends on generator-specific page structure or accumulates too much custom behavior. Keep it focused, define a consistent injection point, and test it against output from every generator.

Related articles