myco:myco-sh-docs-pipeline · git:20260613.ad8665d · 2026-06-13 · sha256 3088fadbf924e3f2

myco:myco-sh-docs-pipeline git:20260613.ad8665dA

Immutable. This exact content is served forever at /api/v1/blob/3088fadbf924e3f2.

---
name: myco:myco-sh-docs-pipeline
description: |
  Apply this skill when working on the myco.sh documentation site — including
  adding or updating guides, modifying the build pipeline, maintaining the
  GitHub Actions Pages deploy workflow, auditing SEO/AI-crawler discovery, or
  fixing rendering bugs in the static HTML output — even if the user doesn't
  explicitly ask about build infrastructure or deployment. Covers four
  interlocking procedures: (1) dual-surface architecture — keeping raw .md
  files for AI crawlers while generating parallel .html in docs/_site via
  docs/build.mjs; (2) build pipeline configuration using markdown-it + Shiki
  with the linkify fuzzyLink guard in docs/lib/render.mjs; (3) discovery layer
  maintenance — robots.txt, sitemap.xml, llms.txt, JSON-LD, and canonical
  tags; and (4) GitHub Pages workflow — matching upload-pages-artifact and
  deploy-pages version pairs to prevent silent deploy failures.
managed_by: myco
user-invocable: true
allowed-tools: Read, Edit, Write, Bash, Grep, Glob
---

# myco.sh Docs Site — Dual-Surface Build Pipeline, Pages Deployment, and Content Safety

The myco.sh docs site serves **two distinct audiences from one content base**: raw `.md` files for AI crawlers and LLM contexts (indexed via `llms.txt`, `sitemap.xml`, and `robots.txt`), and statically-rendered HTML pages for human readers. The build step (`npm run build` inside `docs/`) compiles everything into `docs/_site/` — both surfaces live there: copied raw `.md` files and generated `.html` files. That `_site/` directory is what gets deployed to GitHub Pages.

Changes to any of these areas are coupled — a new guide needs an entry in `docs/lib/nav.mjs` (the sidebar manifest), and a link in `llms.txt`. The `sitemap.xml` is auto-generated from the nav manifest. Understand the full picture before editing any one part.

---

## Prerequisites

- Node.js available; build runs as `npm run build` from inside the `docs/` directory (executes `docs/build.mjs`).
- Familiarity with `docs/` directory structure:
  - `docs/*.md` — source markdown guides (do not move or rename without updating `llms.txt` and the nav manifest)
  - `docs/lib/nav.mjs` — **sidebar nav manifest and single source of truth for guide ordering**; add new guides here
  - `docs/build.mjs` — build orchestrator; reads nav manifest, renders HTML, copies static files, generates sitemap
  - `docs/lib/render.mjs` — markdown-it renderer (Shiki theme + linkify guard)
  - `docs/lib/template.mjs` — HTML template with left-sidebar nav
  - `docs/lib/links.mjs` — link rewriter (`.md` → clean URL; out-of-tree → GitHub blob)
  - `docs/_site/` — build output directory (deployed to Pages); not committed to git
  - `.github/workflows/docs.yml` — Pages deploy on push to `main`
- Read `docs/lib/nav.mjs` before adding guides — guides not listed in the `NAV` array get no inbound navigation links and their source files are not picked up by the build.

---

## Procedure 1: Dual-Surface Architecture — Adding or Restructuring Guides

The fundamental constraint: **raw `.md` source files must stay at their paths under `docs/`**. The build copies them verbatim into `docs/_site/`, preserving the AI-crawler surface. Deleting or moving `.md` source files breaks `llms.txt` links and the LLM-facing surface even if the human-facing HTML is intact.

**How the dual surface works:**

```
docs/quickstart.md            ← Source (not deployed directly)
                                 ↓ build step
docs/_site/quickstart.md      ← AI crawlers, llms.txt links, sitemap (raw Markdown copy)
docs/_site/quickstart.html    ← Human readers (generated by build step)
/quickstart                   ← Clean URL served by GitHub Pages (maps to .html)
```

**Adding a new guide:**

1. Write the guide as `docs/<name>.md`. Follow the `write-myco-user-documentation` skill for content style.
2. Open `docs/lib/nav.mjs` and add a `{ slug: '<name>', title: '<Display Title>' }` entry to the appropriate group in the `NAV` array. The `NAV` array is an ordered list of `{ group, items }` objects; each item uses `slug` (filename without `.md`) and `title` (sidebar label). Omitting this step means the build ignores the guide entirely — no HTML generated, no sitemap entry, no inbound nav links.
3. Add a link to `llms.txt` under the appropriate section so LLM crawlers discover it. Use the `.md` URL (e.g., `https://myco.sh/<name>.md`), not the `.html` URL.
4. Run `npm run build` from inside `docs/` and verify `docs/_site/<name>.html` is generated without errors.
5. Smoke-test with a Pages-faithful local server (see Procedure 4) — confirm internal links resolve and no bare filenames were linkified (see Procedure 2).

**Note — sitemap is auto-generated.** The build script generates `docs/_site/sitemap.xml` from the `NAV` manifest on every build using today's date (or `DOCS_BUILD_DATE` env var). Do not manually edit a static `sitemap.xml` — it will be overwritten on the next build.

**Link rewriting rules (enforced by `docs/lib/links.mjs` via `docs/build.mjs`):**

| Link type in .md source | Rewritten to in .html output |
|---|---|
| `[text](other-guide.md)` | `/other-guide` (clean URL) |
| `[text](../../AGENTS.md)` | GitHub blob URL (out-of-tree) |
| `https://example.com` | Unchanged |

Do not write `.html` links in markdown source — the renderer handles the conversion.

---

## Procedure 2: Build Pipeline Configuration (`docs/lib/render.mjs`)

The renderer uses `markdown-it` with Shiki syntax highlighting. There are two non-obvious configuration decisions that must be preserved when modifying `render.mjs`.

**Shiki theme:** The dark theme is applied at build time (no client-side JS required). If you change the theme, rebuild and visually verify code blocks render correctly — Shiki theme names are not validated at import time.

**The `linkify` fuzzyLink guard (critical — do not remove):**

`markdown-it` with `linkify: true` auto-linkifies bare strings that look like domains. Because `.md`, `.sh`, `.ts`, `.io` are real ccTLDs, filenames like `CLAUDE.md`, `AGENTS.md`, and `install.sh` get converted to broken external links (`http://CLAUDE.md`) in rendered prose. Technical docs mention filenames constantly, so this produces broken links throughout the site.

The fix is already in place in `docs/lib/render.mjs` at lines 9–13 — **do not remove or change these lines**:

```js
const md = MarkdownIt({ html: true, linkify: true, typographer: true });
// Required: disables fuzzy (schemeless) linkification while keeping
// explicit-scheme URLs (https://...) auto-linked.
md.linkify.set({ fuzzyLink: false, fuzzyEmail: false, fuzzyIP: false });
```

This preserves `https://example.com` auto-linking while suppressing `CLAUDE.md` → `http://CLAUDE.md` conversion. Removing `fuzzyLink: false` re-introduces the bug across all rendered guides.

This bug is **not caught by unit tests on the render function** — it only surfaces when real prose with filenames is rendered and crawled. Always smoke-test with the link crawler (see Procedure 4) after render changes.

---

## Procedure 3: Discovery Layer Maintenance

The discovery layer (robots.txt / sitemap.xml / llms.txt / JSON-LD) is independently important from the HTML rendering. It must be kept current whenever guides are added, renamed, or removed.

**Component inventory and update triggers:**

| File | Purpose | Update when |
|---|---|---|
| `docs/robots.txt` | Explicit allow rules for crawlers + sitemap pointer | Rarely — only if adding new crawler allow rules |
| `docs/_site/sitemap.xml` | Auto-generated from NAV manifest on every build | Automatic — just keep `docs/lib/nav.mjs` current |
| `docs/llms.txt` | Curated LLM-readable entry point (Markdown) | Adding or significantly revising a guide |
| `docs/index.html` (head) | Canonical URL, robots meta, OG/Twitter tags, JSON-LD | Changing site name, URL, or description |

**`docs/robots.txt` — currently allows:**
- `GPTBot` (OpenAI training)
- `ClaudeBot` (Anthropic)
- `PerplexityBot`
- `OAI-SearchBot` (ChatGPT search)

Add new crawlers here as they emerge with stable user-agent strings. The `sitemap` directive must point to the live `https://myco.sh/sitemap.xml`.

**`docs/llms.txt` — keep concise.** It is not a full index — it is a curated entry point. Include the guide name, a one-line description, and the `.md` URL (not the `.html` URL — LLMs benefit from the raw Markdown). Do not link to GitHub URLs for docs that live on `myco.sh`; local links preserve topical authority. The build script verifies that all `myco.sh/*.md` links in `llms.txt` resolve to actual files in `_site/` — broken links will abort the build.

**`docs/index.html` JSON-LD schemas** — three schemas are present: `Organization`, `WebSite`, `SoftwareApplication`. Update the `SoftwareApplication` version string and `datePublished`/`dateModified` when releasing a new myco version. These fields feed structured-data cards in search results.

**SEO baseline for guides:** The build injects `<link rel="canonical">`, `<title>`, and `<meta name="description">` into each generated HTML page using the guide's slug, first H1, and opening paragraph. Verify these are populated correctly in the rendered output after adding a new guide.

---

## Procedure 4: GitHub Actions Pages Deployment Workflow

The deploy workflow (`.github/workflows/docs.yml`) triggers on push to `main` (when `docs/**` changes) and runs `npm run build` from inside the `docs/` directory before uploading `docs/_site` as the artifact and deploying to Pages.

**Version pairing rule — do not mix major versions:**

GitHub hard-deprecated `upload-pages-artifact@v3` + `deploy-pages@v4` on **2025-01-30**. The v3 artifact can no longer hand off to the deploy action — workflows using this pairing fail silently or with an opaque error at the deploy step (the upload step reports success, making the failure hard to diagnose).

Always use a matched pair. The current workflow uses v4:

```yaml
# Current (both @v4)
- uses: actions/upload-pages-artifact@v4
  with:
    path: docs/_site
- uses: actions/deploy-pages@v4

# If bumping to v5 (latest as of 2025) — bump BOTH at the same time:
- uses: actions/upload-pages-artifact@v5
  with:
    path: docs/_site
- uses: actions/deploy-pages@v5
```

**When bumping versions:** bump both actions in the same commit. A partial bump restores the mismatch problem. Check the [GitHub Actions changelog](https://github.blog/changelog/) for deprecation notices before merging workflow PRs.

**Workflow validation checklist before merging any `.github/workflows/docs.yml` change:**

1. `upload-pages-artifact` and `deploy-pages` are the same major version.
2. The `path:` passed to `upload-pages-artifact` is `docs/_site` (the build output directory — contains both `.md` and `.html` files for both surfaces).
3. The build step (`npm run build` from `working-directory: docs`) runs before the upload step.
4. Permissions block includes `pages: write` and `id-token: write`.

**Local smoke-test before pushing (Pages-faithful routing):**

GitHub Pages serves clean URLs by trying `path` → `path.html` → `path/index.html` → `404.html`. A plain `python3 -m http.server` does NOT replicate this — `.html` extension stripping won't work and internal clean-URL links will 404, giving false negatives.

Use a Pages-faithful preview server pointed at `docs/_site`:

```js
// /tmp/myco-docs-preview.mjs  (Node, run with: node /tmp/myco-docs-preview.mjs)
import http from 'http';
import fs from 'fs';
import path from 'path';

const ROOT = './docs/_site';
http.createServer((req, res) => {
  const tries = [
    path.join(ROOT, req.url),
    path.join(ROOT, req.url + '.html'),
    path.join(ROOT, req.url, 'index.html'),
    path.join(ROOT, '404.html'),
  ];
  for (const f of tries) {
    if (fs.existsSync(f) && fs.statSync(f).isFile()) {
      res.writeHead(200);
      return fs.createReadStream(f).pipe(res);
    }
  }
  res.writeHead(404); res.end('Not found');
}).listen(8080, () => console.log('http://localhost:8080'));
```

Then run a BFS link crawler against `http://localhost:8080` and assert all internal links return 200. Flag any external links that look like bare filenames (e.g., `http://CLAUDE.md`) — those indicate the `linkify` fuzzyLink guard was removed.

---

## Cross-Cutting Gotchas

**Adding a guide without updating `docs/lib/nav.mjs` means the build ignores it.** The guide won't be rendered to HTML, won't get a sitemap entry, and won't have inbound navigation links. The build throws an error if a NAV slug has no corresponding `.md` source file — but the reverse is silent: a `.md` file with no NAV entry is silently skipped.

**Do not serve docs with `python3 -m http.server` for testing.** It does not strip `.html` extensions, so clean-URL internal links (`/quickstart`, `/install`) will 404. Always test against `docs/_site` using the Pages-faithful preview server in Procedure 4.

**`sitemap.xml` is generated, not committed.** It lives in `docs/_site/sitemap.xml` after a build — do not create or maintain a static `docs/sitemap.xml`. Keeping guide URLs current in `sitemap.xml` is automatic once the NAV manifest is accurate.

**Link rewriting is path-relative.** If you restructure the `docs/` directory hierarchy (e.g., add subdirectories like `architecture/`), re-verify the out-of-tree link detection logic in `docs/lib/links.mjs`. Links currently classified as out-of-tree (rewritten to GitHub blob URLs) may become in-tree or vice versa.

**`llms.txt` links must point to `.md` URLs, not `.html` URLs.** LLMs benefit from the raw Markdown. The rendered HTML is for humans. Mixing this up undermines the dual-surface strategy. The build verifies `llms.txt` `.md` links resolve — broken ones abort the build.

**Both surfaces must be present in `docs/_site`.** The build copies raw `.md` files alongside generated `.html` files into `_site/` and runs a post-build guard to verify the copy succeeded. If the artifact path in the workflow is narrowed to only `.html` files, the raw `.md` files disappear from `myco.sh` and break the AI-crawler surface.