<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Willen Gitonga</title>
    <description>The latest articles on DEV Community by Willen Gitonga (@willen-gitonga).</description>
    <link>https://dev.to/willen-gitonga</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4198647%2F8cad35ee-457f-40b6-ba02-45649c0b2acb.jpg</url>
      <title>DEV Community: Willen Gitonga</title>
      <link>https://dev.to/willen-gitonga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/willen-gitonga"/>
    <language>en</language>
    <item>
      <title>Build and publish a developer docs site with Thally</title>
      <dc:creator>Willen Gitonga</dc:creator>
      <pubDate>Wed, 21 Oct 2026 15:01:02 +0000</pubDate>
      <link>https://dev.to/willen-gitonga/build-and-publish-a-developer-docs-site-with-thally-efi</link>
      <guid>https://dev.to/willen-gitonga/build-and-publish-a-developer-docs-site-with-thally-efi</guid>
      <description>&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;p&gt;Thally is a documentation platform that builds and hosts docs sites from a GitHub repository. In this article you'll publish a documentation site for your own project with Thally: an introduction, a quickstart, a practical guide and an API reference, live at a &lt;code&gt;thally.app&lt;/code&gt; address, with its source in a GitHub repository you own.&lt;/p&gt;

&lt;p&gt;I built mine for a small FastAPI helper that sends M-Pesa STK Push payment prompts. You can see the result at &lt;a href="https://mpesa-stk-helper.thally.app/" rel="noopener noreferrer"&gt;mpesa-stk-helper.thally.app&lt;/a&gt; and its source at &lt;a href="https://github.com/willen-gitonga/mpesa-stk-service" rel="noopener noreferrer"&gt;github.com/willen-gitonga/mpesa-stk-service&lt;/a&gt;. Setup and publishing took me about an hour and a half, and most of the effort went into writing and checking content, not configuring Thally.&lt;/p&gt;

&lt;p&gt;Along the way I hit a few obstacles. Each one is described where it happened, with how I resolved it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Thally Cloud account.&lt;/strong&gt; A new workspace gets a one-time trial that covers up to three sites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GitHub account&lt;/strong&gt; with permission to install the Thally GitHub App on the account that will own the docs repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A project to document&lt;/strong&gt;, ideally in its own GitHub repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git and Node.js&lt;/strong&gt; for editing and previewing the site locally. You don't need them to publish Thally's starter site. I used Node.js v22.23.3.
You'll also want a plain text file open for notes. Writing down each problem as it happens makes the troubleshooting sections much easier to write later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Create the site in Thally Cloud
&lt;/h2&gt;

&lt;p&gt;Thally's recommended path creates the site, its GitHub repository and the first deployment for you, with no local setup.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://app.thally.io" rel="noopener noreferrer"&gt;app.thally.io&lt;/a&gt;, sign in, and select &lt;strong&gt;Create a site&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a site name and choose your &lt;code&gt;thally.app&lt;/code&gt; address. Thally checks that the address is available.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Create a new docs repository&lt;/strong&gt;. When GitHub asks you to install the Thally GitHub App, the quickstart tells you to grant access to &lt;strong&gt;All repositories&lt;/strong&gt;, so the app can set up the repository it creates. If your organization only allows access to selected repositories, the quickstart describes an alternative using an existing Thally repository.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Product sources&lt;/strong&gt;, connect your project's repository or choose &lt;strong&gt;I'll do this later&lt;/strong&gt;. Skip &lt;strong&gt;Knowledge surfaces&lt;/strong&gt; for now.&lt;/li&gt;
&lt;li&gt;Review the details and select &lt;strong&gt;Publish site&lt;/strong&gt;. When the Home page shows the site as &lt;strong&gt;Live&lt;/strong&gt;, open &lt;strong&gt;View live site&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Obstacle: I expected Thally to write my docs
&lt;/h3&gt;

&lt;p&gt;I connected my project's repository as a product source, so I assumed the published site would contain documentation about my project. It showed Thally's starter pages instead, and I thought I'd missed a step.&lt;/p&gt;

&lt;p&gt;Product sources feed Thally Track, which watches your project for merged changes and suggests documentation updates. When I connected my helper repo, Track also generated a first draft from a snapshot of it: Thally's GitHub app opened a pull request ("Generate product documentation") in my docs repo, with 15 changed files and a deploy preview.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I resolved it:&lt;/strong&gt; I treated the starter site as a working shell and the generated pull request as a first draft to check, not finished documentation. If you connected a product source, look for the same pull request in your docs repository and open its deploy preview from the Thally bot comment. Then either merge it and correct the pages afterwards, as I did, or check out its branch and fix the pages before merging. Step 3 covers what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configure the site locally
&lt;/h2&gt;

&lt;p&gt;Open the repository shown in your site summary, clone it, and start the local preview:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/YOUR-ACCOUNT/YOUR-DOCS-REPO.git
&lt;span class="nb"&gt;cd &lt;/span&gt;YOUR-DOCS-REPO
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three files control almost everything:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/content/*.mdx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Each page's content, with its title and description in frontmatter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docs.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tabs, sidebar groups and page order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/data/site.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Product name, description and links&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set your product name and links in &lt;code&gt;site.ts&lt;/code&gt; first. Then plan the sidebar in &lt;code&gt;docs.json&lt;/code&gt;. Each page is listed by its path inside &lt;code&gt;src/content/&lt;/code&gt;, without the &lt;code&gt;.mdx&lt;/code&gt; extension, and the groups sit inside the &lt;code&gt;tabs&lt;/code&gt; entry. Here's the structure I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"groups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Getting Started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"introduction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quickstart"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Guides"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"guides/sandbox-callbacks"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API Reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api/stk-push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api/payment-status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api/callbacks"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"configuration"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order follows how a reader moves through the docs: get started, complete a real task, then look up details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Obstacle: edits to pages didn't hot reload
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;npm run dev&lt;/code&gt; running, saving a change to an &lt;code&gt;.mdx&lt;/code&gt; page didn't update the preview. I had to stop and restart the dev server to see each change.&lt;/p&gt;

&lt;p&gt;I didn't find the cause within my time budget. Restarting the server worked as a workaround. If you hit this, check whether your project folder is synced by iCloud, OneDrive or Dropbox, since file watchers often miss changes in synced folders. Moving the project outside the synced folder is the usual fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write the content
&lt;/h2&gt;

&lt;p&gt;Plan your pages around what a new reader needs, in the order they need it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Introduction:&lt;/strong&gt; what the project does, who it's for, and what it doesn't do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quickstart:&lt;/strong&gt; the shortest path to a real result, with the expected output after each step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guide:&lt;/strong&gt; one complete task, with an example and troubleshooting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference:&lt;/strong&gt; the details readers look up later, such as endpoints and settings.
Each page is an MDX file with frontmatter at the top:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
title: Quickstart
description: Run the helper in mock mode and send your first payment prompt.
---

In this quickstart you'll run the helper locally and send an STK Push.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For sequences, Thally's &lt;code&gt;Steps&lt;/code&gt; component numbers each step and keeps its commands together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Steps&amp;gt;
  &amp;lt;Step title="Start the server"&amp;gt;
    Run `uvicorn app.main:app --reload` from the project root.
  &amp;lt;/Step&amp;gt;
&amp;lt;/Steps&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MDX treats curly braces and angle brackets in normal text as code, so keep things like &lt;code&gt;{PUBLIC_BASE_URL}&lt;/code&gt; inside backticks or code blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Obstacle: the generated draft was confidently wrong
&lt;/h3&gt;

&lt;p&gt;The pages Thally generated from my repository were well structured and mostly accurate. But when I checked each claim against the code, I found problems a reader would have hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A description saying tokens refresh automatically. The code only fetches a new token when the cached one is close to expiring.&lt;/li&gt;
&lt;li&gt;A dependency table with a wrong FastAPI version, duplicating &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Setup steps that named the virtual environment &lt;code&gt;virtual&lt;/code&gt;, which &lt;code&gt;.gitignore&lt;/code&gt; didn't ignore, so readers would have committed it.&lt;/li&gt;
&lt;li&gt;Expected server output that didn't match what the server actually prints.&lt;/li&gt;
&lt;li&gt;A claim that valid Kenyan numbers starting with &lt;code&gt;2546&lt;/code&gt; were rejected, and an unverified claim that Safaricom retries callbacks.&lt;/li&gt;
&lt;li&gt;No practical guide at all, only reference pages.
&lt;strong&gt;How I resolved it:&lt;/strong&gt; In a follow-up pull request, I rewrote the introduction and quickstart, moved the sandbox setup into a new guide, and deleted the generated changelog. Track's draft also included a reference page for each API endpoint, and I kept those after checking them against the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing my own commands caught one more problem. I ran &lt;code&gt;uvicorn main:app&lt;/code&gt; out of habit and got &lt;code&gt;Could not import module "main"&lt;/code&gt;, because the app lives in &lt;code&gt;app/main.py&lt;/code&gt;. The correct command is &lt;code&gt;uvicorn app.main:app&lt;/code&gt;, run from the project root. I added the error to the quickstart's troubleshooting table, since readers will make the same mistake.&lt;/p&gt;

&lt;p&gt;The lesson: treat generated docs as a first draft, and only publish output you've seen in your own terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Publish your changes
&lt;/h2&gt;

&lt;p&gt;Thally publishes whatever is on your production branch, so work on a separate branch and merge through a pull request.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a branch and commit each logical change separately:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; rewrite-docs
   git add src/content/introduction.mdx
   git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Rewrite introduction for accuracy and audience"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Before pushing, validate the site and make sure it builds:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npx &lt;span class="nt"&gt;--yes&lt;/span&gt; @thallylabs/cli@latest check
   npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check catches problems such as a page listed in &lt;code&gt;docs.json&lt;/code&gt; that doesn't exist.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push the branch and open a pull request into your production branch:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin rewrite-docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Before merging, open the deploy preview. Thally posts it as a bot comment on the pull request, and it's easy to miss under the CI checks. I missed it, and the comment's link was removed once I merged, so I checked my changes locally with &lt;code&gt;npm run dev&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;Merge the pull request. Thally builds and publishes the new version, and the deployment appears in your site's deployment history. My live site updated after the merge.
Finally, open your live site and click through every page and link once more.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd tell you before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep notes as you go.&lt;/strong&gt; Every obstacle in this article came from a note I wrote in the moment. They became my troubleshooting rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test your quickstart in a fresh folder.&lt;/strong&gt; Reading it isn't enough. Running the commands myself is how I caught the import error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it small.&lt;/strong&gt; An introduction, one quickstart and one guide are enough for a useful site. Add reference pages only where readers will look things up.
You now have a live documentation site and a workflow for updating it: edit on a branch, check, preview and merge. My finished site is at &lt;a href="https://mpesa-stk-helper.thally.app/" rel="noopener noreferrer"&gt;mpesa-stk-helper.thally.app&lt;/a&gt; if you want to compare.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>documentation</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>technicalwriting</category>
    </item>
  </channel>
</rss>
