CMS for technical documentation - front end build

Using Our CMS for Technical Documentation Part 3 – Front End Build

The final part of the ‘CMS for Technical Documentation’ series looks at how we connected to TerminusCMS, fetched data and built the front end. If you missed the other two, part one explains the schema model for the static content, and part two looks at how we parsed JS and Python code into TerminusCMS for our client developer guides.

For the website, we used Next.js and styled it with the Tailwind CSS framework & Flowbite which is a library of components built on top of Tailwind to support JS integration.

Why NextJS

We could have taken many paths when building the new documentation website. We could use one of the many static site generators and fetch that content as JSON. The disadvantage of this approach is that if we ever need to do something on the server side, we would have to use another framework to choose it manually. NextJS allows us to create API endpoints that can be called by the front end within the same framework.

Another NextJS advantage is that it is built on React, meaning we have a vast amount of ready-made React components to use. As our developers are familiar with React there is only a little learning curve. As a bonus, if we decide to create React components ourselves, they benefit the TerminusDB community too. Everyone wins.

Connecting to TerminusCMS and Fetching Data

As covered in part one of the CMS for tech docs series, we created a schema for the documentation structure and added this, along with the content, to a TerminusCMS data product called terminusCMS_docs.

We then created a page in NextJS: A JavaScript or TypeScript file that renders a certain page. This page can be rendered through different routes or a single route, depending on how it is configured. In our case, the route is dynamic since it depends on the slug that is used inside the document we fetch.

The getStaticPaths function fetches every possible route to generate. It uses the TerminusDB client to fetch all the possible pages and extracts all slugs from them, so we know which pages need to be rendered later. We make three exceptions: the Python, OpenAPI and JavaScript slugs are skipped (they are the pages parsed from code covered in part two). They have their own page renderer since they are not written in Markdown. If the page contains no slug at all, we skip it as there is no way to render a page without a proper URL that points to it.

				
					export async function getStaticPaths() {
  // Connect and configure the TerminusClient
  const client = new TerminusClient.WOQLClient(
    "https://cloud.terminusdb.com/TerminatorsX",
    {
      user: "name@terminusdb.com",
      organization: "TerminatorsX",
      db: "terminusCMS_docs",
      token: process.env.TERMINUSDB_API_TOKEN,
    }
  )
  const docs = await client.getDocument({ "@type": "Page", as_list: true })
  const exceptions = ["python", "openapi", "javascript"]
  const paths = docs
    .filter((x) => {
      return typeof x["slug"] !== "undefined" && !exceptions.includes(x["slug"])
    })
    .map((x) => "/" + x["slug"])
  return { paths: paths, fallback: false }
}
				
			
In the getStaticPageProps function of the page, all the required static content is fetched from TerminusDB. The TerminusDB JavaScript client is used to fetch the data
				
					export async function getStaticProps({ params }) {
  // Connect and configure the TerminusClient
  const client = new TerminusClient.WOQLClient(
    "https://cloud.terminusdb.com/TerminatorsX",
    {
      user: "name@terminusdb.com",
      organization: "TerminatorsX",
      db: "terminusCMS_docs",
      token: process.env.TERMINUSDB_API_TOKEN,
    }
  )
 const query = {
    "@type": "Page",
    slug: params["name"],
  }
  const docs = await client.getDocument({
    "@type": "Page",
    as_list: true,
    query: query,
  })
  const docResult = docs[0]
  let html = ""
  if (typeof docResult["body"] !== "undefined") {
    html = await renderMarkdown(docResult["body"]["value"])
  }
  const entry = { html: html, document: docResult }
  return { props: { entry, menu } }
}
				
			

The rendered page looks like:

				
					export default function Doc(
  props: JSX.IntrinsicAttributes & { menu: any[]; entry: any[] }
) {
  let html = getHtml(props.entry)
  let displayElement = <div dangerouslySetInnerHTML={{ __html: html }} />
  if (typeof props.entry.document.body === "undefined") {
    displayElement = defaultDoc(props.entry.document, props.menu)
  }
  return (
    <Layout
      menu={props.menu}
      entry={props.entry}
      displayElement={displayElement}
      heading={props.entry.document.title.value}
      subtitle={getSubTitle(props.entry.document)}
      seo_metadata={props.entry.document.seo_metadata}
    />
  )
}
				
			

The rendered HTML is passed to the template and rendered inside a Div. We use the dangerousSetInnerHTML to set the rendered HTML inside the div. We pass all the properties to a Layout component which renders the different pages.

If you want to see more of the code to get a picture of the full build, check out our static docs repository on GitHub: https://github.com/terminusdb-labs/terminusdb-docs-static

Lightning Quick – Speed Tricks

We recently published the website’s speed improvements (92% faster!) compared to our old Gitbook docs site, we did a couple of things to make fast, it hindsight it would be good to test the speed without these to see TerminusCMS’ naked performance. 

As the content is statically generated, it can be hosted on a range of CDNs and be served around the world on different edges.

NextJS also provides a <Link> component. If a link points to another NextJS page, it will prefetch the static JSON data in advance. This speeds the site up quite a lot, as all the necessary HTML, JavaScript and CSS content do not need to be fetched again. Instead, it only fetches the static JSON that is needed to render the page.

Deploy and Updates

The site is deployed on Cloudflare Pages, as it provides easy Git integration. Whenever we push to the main branch, CloudFlare Pages recompile the documentation site.

To ensure changes in the CMS get deployed correctly, we run a cron every few hours that updates the site. We are working on a way to trigger a webhook after a commit so that the changes are nearly instantaneous.

CloudFlare Pages do not offer a way to host the page in a subfolder (e.g. terminusdb.com/docs/). By default, it only allows hosting on a subdomain. We therefore had to create a CloudFlare Worker which proxies the requests to terminusdb.com/docs/ and fetches them from the CloudFlare Pages domain.

Conclusion

This was a fun project for the team. It meant we got to use TerminusCMS for our own project and helped us improve the product and iron out bugs. When we have a little more time we will use TerminusCMS for the main website too, but as we’re busy helping customers and developing new functionality, WordPress will live another day.

To conclude, look at the average page loading speeds compared to Gitbook! We’re very happy.