<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Favour Bamgboye · Developer Experience]]></title><description><![CDATA[Favour Bamgboye · Developer Experience]]></description><link>https://favourbamgboye.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 14:08:24 GMT</lastBuildDate><atom:link href="https://favourbamgboye.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Build a docs site for your CLI with Thally: from zero to published, plus what the docs found in my code]]></title><description><![CDATA[I maintain PyDoctor CLI, a Python CLI that scans a project for vulnerable, outdated, conflicting, and unused dependencies. I rebuilt its documentation on…]]></description><link>https://favourbamgboye.hashnode.dev/build-cli-docs-site-with-thally</link><guid isPermaLink="true">https://favourbamgboye.hashnode.dev/build-cli-docs-site-with-thally</guid><category><![CDATA[documentation]]></category><category><![CDATA[Python]]></category><category><![CDATA[devtools]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[thally]]></category><category><![CDATA[cli]]></category><dc:creator><![CDATA[Favour Bamgboye]]></dc:creator><pubDate>Sat, 24 Oct 2026 15:45:33 GMT</pubDate><content:encoded><![CDATA[<p>I maintain <a href="https://github.com/iamAgbaCoder/pydoctor-cli">PyDoctor CLI</a>, a Python CLI that scans a project for vulnerable, outdated, conflicting, and unused dependencies. I rebuilt its documentation on <a href="https://thally.io">Thally</a> and published it at <a href="https://pydoctor-docs.thally.app">pydoctor-docs.thally.app</a>. This article walks you through doing the same for your own tool: setup, configuration, writing content, and publishing. It ends with the obstacles I hit and how I resolved them, because most of them weren't Thally problems. Writing accurate docs exposed bugs in the tool itself.</p>
<p>You should be able to follow along in under an hour for a small CLI.</p>
<h2>What you'll build</h2>
<ul>
<li>A live documentation site at <code>https://YOUR-SITE.thally.app</code></li>
<li>A GitHub repository you own, containing the site as MDX pages</li>
<li>An introduction, a working quickstart, a practical guide, and a troubleshooting page</li>
<li>Automatic redeploys whenever you push to the production branch</li>
</ul>
<h2>Prerequisites</h2>
<ul>
<li><strong>A Thally Cloud account</strong> at <a href="https://app.thally.io">app.thally.io</a>. A new workspace gets a one-time 7-day trial. It starts when you first publish, not when you browse the setup wizard, so check the dates before your site needs to stay up.</li>
<li><strong>A GitHub account</strong> where you can install the Thally GitHub App.</li>
<li><strong>Node.js and npm</strong> for editing and checking pages locally. Publishing the starter site in Step 1 doesn't need them.</li>
<li><strong>The tool you're documenting, installed and working.</strong> You will need to run every command you document. More on that below.</li>
</ul>
<h2>Step 1: Create the site in Thally Cloud</h2>
<ol>
<li><p>Go to <a href="https://app.thally.io">app.thally.io</a>, sign in, and select <strong>Create a site</strong>.</p>
</li>
<li><p>Enter a site name, a short description, and the <code>thally.app</code> address you want. Thally checks the address is free before you continue.</p>
</li>
<li><p>At <strong>Docs source</strong>, choose <strong>Create a new docs repository</strong>. Thally creates a GitHub repository from its starter template and manages the first deployment.</p>
<p>When the GitHub App asks for access, this path needs <strong>All repositories</strong>, because the app has to initialise a repository that doesn't exist yet. If your organisation only allows selected-repository access, you'll need an existing Thally repository instead.</p>
</li>
<li><p>At <strong>Product sources</strong>, you can connect the repository of the tool you're documenting so Thally Track can watch it for merged changes. I connected <code>pydoctor-cli</code>. If you'd rather write everything by hand, choose <strong>I'll do this later</strong>.</p>
</li>
<li><p>Review the settings and select <strong>Publish site</strong>. When the workspace Home page shows the site as <strong>Live</strong>, open it from the site summary.</p>
</li>
</ol>
<p>You now have a live site full of starter content, and a repository containing it.</p>
<h2>Step 2: Clone the repository and learn the three files that matter</h2>
<p>Clone the repository Thally created and install its dependencies. Replace <code>YOUR-GITHUB-USERNAME</code> and <code>YOUR-DOCS-REPO</code> with the values shown in your site summary.</p>
<pre><code class="language-bash">git clone https://github.com/YOUR-GITHUB-USERNAME/YOUR-DOCS-REPO.git
cd YOUR-DOCS-REPO
npm ci
</code></pre>
<p>Most of your work happens in three places:</p>
<table>
<thead>
<tr>
<th>File</th>
<th>What it controls</th>
</tr>
</thead>
<tbody><tr>
<td><code>src/content/*.mdx</code></td>
<td>The pages: frontmatter plus MDX content</td>
</tr>
<tr>
<td><code>docs.json</code></td>
<td>Tabs, sidebar groups, and page order</td>
</tr>
<tr>
<td><code>src/data/site.ts</code></td>
<td>Product name, description, and links</td>
</tr>
</tbody></table>
<p>A page's path in <code>docs.json</code> is its file path under <code>src/content/</code> without the extension. This is the navigation for my site:</p>
<pre><code class="language-json">{
  "tabs": [
    {
      "tab": "Documentation",
      "groups": [
        { "group": "Getting Started", "pages": ["introduction", "quickstart"] },
        { "group": "Reference", "pages": ["cli/commands", "configuration", "scanners"] },
        {
          "group": "Guides",
          "pages": ["guides/ci-integration", "guides/fixing-issues", "guides/troubleshooting"]
        },
        { "group": "Release Notes", "pages": ["changelog"] }
      ]
    }
  ]
}
</code></pre>
<p>Set your product name and description in <code>src/data/site.ts</code> so the header and metadata don't say "Documentation".</p>
<h2>Step 3: Write the content</h2>
<h3>Generate a first draft (optional)</h3>
<p>Because I'd connected <code>pydoctor-cli</code> as a product source, I used Thally's <strong>Generate product documentation</strong> to produce a first draft from the code. It arrived as a pull request from Thally's bot that replaced the starter pages with an introduction, quickstart, CLI reference, configuration page, scanner reference, and two guides.</p>
<p>The draft was a good starting point. Exit codes, environment variables, and the health-score bands all matched the source. But a generated draft only describes what the code says. It can't tell you what the tool actually does on a real machine, and that's where the important gaps turned out to be (see Obstacles).</p>
<h3>Verify every command by running it</h3>
<p>This is the step that matters most. For every command, flag, and code sample:</p>
<ol>
<li>Run it, in a fresh virtual environment.</li>
<li>Compare the real output with what the page says.</li>
<li>Try the failure cases: no network, no virtual environment, a project with real problems.</li>
</ol>
<pre><code class="language-bash">python -m venv .venv &amp;&amp; source .venv/bin/activate
pip install pydoctor-cli
pydoctor --version
pydoctor diagnose --json &gt; report.json
echo $?
</code></pre>
<h3>Use the components for the reader's job</h3>
<p>Thally's MDX components cover what technical docs need. I used three:</p>
<ul>
<li><p><code>&lt;Steps&gt;</code> / <code>&lt;Step&gt;</code> for the quickstart, so each action is numbered and self-contained.</p>
</li>
<li><p><code>&lt;Warning&gt;</code> for things that will hurt the reader if they miss them, such as installing the wrong package:</p>
<pre><code class="language-mdx">&lt;Warning title="Install pydoctor-cli, not pydoctor"&gt;
  The PyPI package `pydoctor` is a different project. Both install a
  `pydoctor` command, so installing both breaks whichever came first.
&lt;/Warning&gt;
</code></pre>
</li>
<li><p><code>&lt;AccordionGroup&gt;</code> / <code>&lt;Accordion&gt;</code> for the troubleshooting page. Give each accordion an <code>id</code> and it gets a stable link that opens that entry directly, which is ideal for linking from error messages and other pages:</p>
<pre><code class="language-mdx">&lt;AccordionGroup&gt;
  &lt;Accordion id="scan-incomplete" title="SEC_SCAN_INCOMPLETE: security scan incomplete"&gt;
    **Symptom:** ...
    **Cause:** ...
    **Fix:** ...
  &lt;/Accordion&gt;
&lt;/AccordionGroup&gt;
</code></pre>
<p><code>https://pydoctor-docs.thally.app/guides/troubleshooting#scan-incomplete</code> opens that entry already expanded.</p>
</li>
</ul>
<p>I wrote every troubleshooting entry as symptom, cause, fix. Readers arrive with a symptom, so that's what the titles describe.</p>
<h3>Validate before you push</h3>
<p>Run Thally's content checker and a production build:</p>
<pre><code class="language-bash">npm run check
npm run build
</code></pre>
<p><code>npm run check</code> is defined in the starter and runs Thally's CLI checker for you, so you don't need to install anything else. The checker lints your content against Thally's rules, and the build catches MDX that won't compile.</p>
<h2>Step 4: Publish</h2>
<p>Commit and push to the production branch you chose in the wizard:</p>
<pre><code class="language-bash">git add docs.json src/content
git commit -m "docs: add troubleshooting guide"
git push
</code></pre>
<p>Thally Cloud builds and deploys the new version automatically, and the deployment history records the commit. Open the live URL and check the sidebar, a code block, and any links with <code>#</code> anchors.</p>
<h2>Obstacles, and how I resolved them</h2>
<p>Thally itself was the easy part. The obstacles came from verifying the docs against the real tool.</p>
<p><strong>1. The tool's name collides with another package.</strong> Searching for "pydoctor" returns Twisted's API documentation generator, which is also on PyPI as <code>pydoctor</code>. I installed both in one environment to see what happens. They write into the same <code>pydoctor/</code> package directory and both provide a <code>pydoctor</code> command, so one silently breaks the other. <em>Resolution:</em> a <code>&lt;Warning&gt;</code> in the quickstart's install step and a troubleshooting entry with the clean-reinstall commands.</p>
<p><strong>2. The security scan reported "healthy" when it hadn't run.</strong> I ran the quickstart on a machine with no access to the OSV vulnerability database. The scan reported "No issues detected" with a score of 100/100. Network errors were being swallowed. Documenting that honestly would have meant writing "a clean result might mean nothing", so I fixed the tool instead. <em>Resolution:</em> the scan now fails closed with a <code>SEC_SCAN_INCOMPLETE</code> error and exit code 1 (pydoctor-cli 2.0.1), and the docs explain how to diagnose network and proxy problems.</p>
<p><strong>3. CI failed on one runner out of twelve.</strong> After the fix, the project's own CI failed only on macOS with Python 3.13. The tool was right: that runner had a pre-installed <code>certifi</code> with a fresh advisory, which pip left in place because it already satisfied the requirements. The same log showed a second bug. One advisory listed under two IDs (GHSA and PYSEC) was scored twice, costing an extra 25 points. <em>Resolution:</em> advisories are de-duplicated by alias, CI installs dependencies with <code>--upgrade-strategy eager</code>, and the troubleshooting page now has an entry for runner-to-runner differences.</p>
<p><strong>4. <code>fix</code> said "fixed" when nothing changed.</strong> Testing the fixing guide on a real Django project, <code>pydoctor fix</code> printed "✔ pillow fixed" dozens of times, but nothing was upgraded. <code>uv add --upgrade pillow</code> keeps an existing pin like <code>pillow==12.2.0</code> and still exits successfully. The same run flagged packages such as <code>whitenoise</code> and <code>django-cors-headers</code> as unused, because Django loads them from strings in <code>settings.py</code>. <em>Resolution:</em> <code>fix</code> now targets a version, verifies the installed version afterwards, and asks once per package. The unused scanner understands import-name mappings, module paths in strings, and Procfiles (pydoctor-cli 2.1.0). The fixing guide documents exactly what <code>fix</code> runs and what it prints when an upgrade is blocked.</p>
<p>The pattern across all four: <strong>the docs didn't just describe the tool, they tested it.</strong> A page that tells someone to run a command is a promise that the command works. Keeping that promise meant changing the code, then documenting the new behaviour.</p>
<h2>Takeaways</h2>
<ul>
<li><strong>Getting a site live is fast.</strong> The Cloud wizard gives you a live site and a repository in minutes, without a local setup.</li>
<li><strong>A generated draft is a starting point, not the finish.</strong> It's accurate about what the code says, but only running the tool tells you what it does.</li>
<li><strong>Write troubleshooting as symptom → cause → fix,</strong> and give each entry an anchor you can link to.</li>
<li><strong>Run <code>check</code> and <code>build</code> before every push.</strong> They're fast and they catch the mistakes that would otherwise ship.</li>
</ul>
<p>The finished site is at <a href="https://pydoctor-docs.thally.app">pydoctor-docs.thally.app</a>, and its source is at <a href="https://github.com/iamAgbaCoder/pydoctor-docs">github.com/iamAgbaCoder/pydoctor-docs</a>.</p>
<h2>Next steps</h2>
<ul>
<li><a href="https://docs.thally.io/guides/writing-content">Write and format pages</a> covers every MDX component the starter supports.</li>
<li><a href="https://docs.thally.io/guides/configuring-navigation">Organize the navigation</a> explains tabs and sidebar groups in more depth.</li>
<li><a href="https://docs.thally.io/guides/branding-and-theming">Customize the site</a> shows how to set colors, logos, and fonts.</li>
</ul>
]]></content:encoded></item></channel></rss>