Building a Powerful Docs Site for a Front‑End Graphics Engine Using Gatsby
This article walks through the challenges and solutions of creating an integrated, searchable, multilingual documentation website for the Oasis Engine graphics engine, covering the selection of static site generators, the use of Gatsby with TypeDoc and GraphQL, custom plugins for embedding demos, and implementing global search with Algolia DocSearch.
If you have watched the Vue.js documentary, you know that an open‑source product’s success depends not only on quality code but also on clear documentation, appealing design, continuous iteration, regular evangelism, and strong backing.
Writing code and unit tests guarantees functionality, but for users the most important thing is documentation. Just as a vacuum cleaner comes with a manual, software should provide clear docs instead of forcing users to read source code.
When we started operating the open‑source graphics engine Oasis Engine, we discovered that creating a clean, usable documentation site is far harder than expected.
Oasis Engine: https://oasisengine.cn/
Selection
Before open‑sourcing we stored docs on Yuque, a good knowledge‑management platform, but it cannot handle complex API docs or custom designs. An open‑source product without its own site feels incomplete.
Most well‑known open‑source projects host their sites on GitHub Pages. We researched static site generators that can be deployed on GitHub Pages:
Jekyll : Officially recommended but depends on Ruby, which is cumbersome on macOS.
Vuepress : A Vue‑based framework developed by a colleague, but its data source is limited to Markdown.
Dumi : A React‑based framework for component development; its dumi‑theme‑mobile is attractive, yet its demo preview is tightly coupled with components and its default style is rough.
Docsify : A lightweight site generator that can parse Markdown at runtime without a build step, offering Gitbook‑like features.
We ultimately chose Docsify . The site combines three pipelines:
Docsify for documentation (Markdown).
Typedoc for API documentation (TypeScript).
Demosify for demo pages (TypeScript).
Because the API, tutorials, and demos live in three separate GitHub repositories, each update required copying content across repos, leading to high maintenance cost and developer fatigue.
Original Intent
Two months after Oasis Engine was open‑sourced, the hype faded and users reported slow loading on the GitHub Pages site, especially for demo pages that were inaccessible without a VPN. We later mirrored the site on Gitee to improve access in China.
Looking at successful engine sites such as Unity, Unreal, Cocos, LayaAir, ThreeJS, and BabylonJS, we realized each presents information architecture and style according to its positioning and commercial strategy. Our goal is to make Oasis Engine’s site simple, reliable, and fast for front‑end developers.
We defined four core requirements:
Integration : Combine API docs (Typedoc), tutorial docs (Markdown), and demos (TypeScript) into a single site with global search.
Demo Embedding : Allow demos to be embedded in tutorials and link to online editors like CodePen.
Multi‑Version : Support simultaneous documentation for multiple engine versions with version switching.
Internationalization : Provide both Chinese and English content.
In essence, we wanted a site similar to Ant Design’s documentation.
Getting Started with Gatsby
The key difference of Gatsby is its GraphQL data layer. Any input that can be transformed into GraphQL can be queried and rendered as React components. For Oasis Engine we needed to convert Typedoc, Markdown, and TypeScript files into React components.
This decouples data from style: instead of compiling each format to HTML separately, a single tool transforms everything into React components, allowing unified styling.
Processing TypeDoc Data
TypeScript is the primary language for Oasis Engine. Typedoc reads TypeScript declaration data and generates HTML API docs, but it also offers a Node API for programmatic access.
<code>export const pageQuery = graphql
typedoc(typedocId: { eq: "default" }) {
internal {
content
}
}
export default function MyPage({ data: { typedoc } }) {
const typedDocContent = JSON.parse(typedoc?.internal.content);
// do something with that data...
}
</code>Typedoc defines many kinds (MODULE, ENUM, CLASS, etc.) which makes custom rendering complex:
<code>export enum Kinds {
MODULE = 1,
ENUM = 4,
CLASS = 128,
INTERFACE = 256,
TYPE_ALIAS = 4194304,
FUNCTION = 64,
PROPERTY = 1024,
CONSTRUCTOR = 512,
ACCESSOR = 262144,
METHOD = 2048,
GET_SIGNATURE = 524288,
SET_SIGNATURE = 1048576,
PARAMETER = 32768,
TYPE_PARAMETER = 131072,
}
</code>Steps to integrate Typedoc data:
Collect entry
index.tsfiles from each package in the monorepo and write them to a temporary
tsfiles.jsfile.
<code>const glob = require('glob');
const fs = require('fs');
glob(`${EngineRepoPath}/packages/**/src/index.ts`, { realpath: true }, function (er, files) {
var re = new RegExp(/([^test]+).ts/);
var tsFiles = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
var res = re.exec(file);
console.log('[Typedoc entry file]:', file);
if (!res) continue;
tsFiles.push(`"${file}"`);
}
fs.writeFile('./scripts/typedoc/tsfiles.js', `module.exports = [${tsFiles.join(',')}];`, function (err) {});
});
</code>Configure the gatsby-source-typedoc plugin in
gatsby-config.js.
<code>const DTS = require('./scripts/typedoc/tsfiles');
module.exports = {
plugins: [
{
resolve: "gatsby-source-typedoc",
options: {
src: DTS,
typedoc: {
tsconfig: `${typedocSource}/tsconfig.json`
}
}
}
]
};
</code>Verify data ingestion by opening
http://localhost:8000/___graphqland querying
typedoc { internal { content } }.
Create pages from the Typedoc data in
gatsby-node.js.
<code>async function createAPI(graphql, actions) {
const { createPage } = actions;
const apiTemplate = resolve(__dirname, '../src/templates/api.tsx');
const typedocquery = await graphql(`
{
typedoc {
internal { content }
}
}
`);
let apis = JSON.parse(typedocquery.data.typedoc.internal.content);
const packages = apis.children.map(p => ({ id: p.id, kind: p.kind, name: p.name.replace('/src', '') }));
if (apis) {
apis.children.forEach((node, i) => {
const name = node.name.replace('/src', '');
createPage({ path: `${version}/api/${name}/index`, component: apiTemplate, context: { node, type: 'package', packages } });
if (node.children) {
node.children.forEach(child => {
createPage({ path: `${version}/api/${name}/${child.name}`, component: apiTemplate, context: { node: child, type: 'module', packages, packageIndex: i } });
});
}
});
}
}
</code>The final site is reachable at https://oasisengine.cn/0.3/api/core/index . The custom styling makes the API docs far more readable than the default Typedoc output.
Embedding Demo in Markdown
We wanted a one‑line Markdown syntax to embed a demo, e.g.
<playground src="pbr-helmet.ts"></playground>. To achieve this we created a Gatsby remark plugin that extracts the
<playground>tag from the Markdown AST, reads the source file, and replaces the node with highlighted code using Prismjs.
<code>// `gatsby-remark-oasis` plugin:
// Extract <playground> from markdown AST and replace the content
const visit = require('unist-util-visit');
const fs = require('fs');
const Prism = require('prismjs');
module.exports = ({ markdownAST }, { api, playground, docs }) => {
visit(markdownAST, 'html', node => {
if (node.value.includes('<playground')) {
const src = /src="(.+)"/.exec(node.value);
if (src && src[1]) {
const name = src[1];
const path = `playground/${name}`;
const code = fs.readFileSync(`./${path}`, { encoding: 'utf8' });
node.value = `<playground name="${name}"><textarea>${code}</textarea>${Prism.highlight(code, Prism.languages.javascript, 'javascript')}</playground>`;
}
}
});
return markdownAST;
};
</code>Another plugin, gatsby-remark-component-parent2div , converts the custom
<playground>element into a React component using rehype‑react :
<code>import RehypeReact from "rehype-react";
import Playground from "../Playground";
const renderAst = new RehypeReact({
createElement: React.createElement,
components: { "playground": Playground }
}).Compiler;
export default class Article extends React.PureComponent {
render() { return renderAst(this.props.content.htmlAst); }
}
</code>In
gatsby-node.jswe also generate a virtual
Playgroundnode by transforming the demo TypeScript files with Babel:
<code>const babel = require("@babel/core");
exports.onCreateNode = async function onCreateNode({ node, loadNodeContent, actions, createNodeId, reporter, createContentDigest }) {
const { createNode } = actions;
const content = await loadNodeContent(node);
// Babel configuration omitted for brevity
const result = babel.transformSync(content, {/* ... */});
const playgroundNode = {
internal: { content: result.code, type: `Playground` }
};
playgroundNode.internal.contentDigest = createContentDigest(playgroundNode);
createNode(playgroundNode);
return playgroundNode;
};
</code>Additional features such as opening demos in CodePen, CodeSandbox, or Stackblitz were added to improve interactivity.
Global Search
Because the engine has a large API surface, a global search is essential. We integrated Algolia DocSearch, which crawls the site daily and provides a front‑end SDK for querying.
After obtaining an API key, we added a configuration file to the
docsearch-configsrepository:
<code>{
"start_urls": [
{
"url": "https://oasisengine.cn/(?P<version>.*?)/docs/.+?-cn",
"variables": { "version": ["0.3"] },
"tags": ["cn"]
}
],
"selectors": {
"lvl0": { "selector": ".docsearch-lvl0", "global": true, "default_value": "Documentation" },
"lvl1": "article h1",
"lvl2": "article h2",
"lvl3": "article h3",
"lvl4": "article h4",
"lvl5": "article h5",
"text": "article p, article li"
}
}
</code>The maintainer of the
docsearch-configsrepo responded quickly, making the integration smooth.
Conclusion
The documentation build process involved many detours—choosing a site generator, wiring Typedoc through GraphQL, writing custom Gatsby plugins, and adding Algolia search—but each step taught valuable lessons, especially the power of GraphQL. Oasis Engine’s documentation is still in its early stages, and we plan to keep iterating to provide developers with fast, reliable access to information.
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.