|
| 1 | +# ADR 0008 — URL structure: MyST flat slugs vs Hugo hierarchical paths |
| 2 | + |
| 3 | +Date: 2026-05-18 |
| 4 | +Status: Proposed |
| 5 | +Branch: lb/myst-migration |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +Hugo derives page URLs from the directory structure: `content/about/governance.md` |
| 10 | +→ `/about/governance/`. The directory hierarchy IS the URL, and `content/` is |
| 11 | +treated as the URL root — it is stripped from every path. |
| 12 | + |
| 13 | +MyST's default behaviour derives URLs from the filename only, ignoring the |
| 14 | +containing directory: `content/about/governance.md` → `/governance`. Files |
| 15 | +named `index.md` in subdirectories collide on the slug `index` and are |
| 16 | +deduplicated as `index-1`, `index-2`, etc.: `content/about/index.md` → |
| 17 | +`/index-1`. |
| 18 | + |
| 19 | +MyST does support directory-based URLs via `site.options.folders: true` in |
| 20 | +`myst.yml`. However, MyST has no concept of a "content root" — it has no |
| 21 | +config option equivalent to Hugo's `content/` stripping. MyST grew from the |
| 22 | +Jupyter notebook world where content lives at the project root alongside |
| 23 | +`myst.yml`; a top-level `content/` subdirectory is a Hugo convention, not a |
| 24 | +MyST one. With `folders: true` and content in `content/`, the output is |
| 25 | +`/content/about/governance/` — the `content/` prefix is preserved. |
| 26 | + |
| 27 | +Verified in MyST 1.9.0: no frontmatter field (`slug`, `url_path`, etc.) |
| 28 | +controls a page's URL. URL generation is internal to MyST. |
| 29 | + |
| 30 | +This was surfaced by `netlify-plugin-checklinks` during the first Netlify |
| 31 | +deploy of the PR. The build succeeds; the link checker finds 12 broken internal |
| 32 | +links because content was authored for Hugo-style hierarchical URLs |
| 33 | +(`/about/governance`, `/contributors/`, etc.) that do not exist under MyST's |
| 34 | +default flat-slug output. |
| 35 | + |
| 36 | +Affected link types: |
| 37 | + |
| 38 | +- Section index cards: `:link: /contributors/`, `:link: /documentation/` etc. |
| 39 | + in `content/index.md` |
| 40 | +- Inline markdown links: `[Role](/community/role)` in `content/community/index.md` |
| 41 | +- Sidebar navigation links generated by MyST from the toc (use actual slugs, |
| 42 | + so these are self-consistent but don't match the Hugo URL shape) |
| 43 | + |
| 44 | +## Options considered |
| 45 | + |
| 46 | +1. **Enable `site.options.folders: true` + move content to project root** |
| 47 | + - Enable `site.options.folders: true` in `myst.yml`. |
| 48 | + - Move all content from `content/<section>/` to `<section>/` at the project |
| 49 | + root (e.g. `content/about/governance.md` → `about/governance.md`). |
| 50 | + - Update all `toc` file paths in `myst.yml` accordingly. |
| 51 | + - Result: `about/governance.md` → `/about/governance/`, matching Hugo's |
| 52 | + URL structure exactly. |
| 53 | + - `<section>/index.md` files become section landing pages |
| 54 | + (`/contributors/`, `/about/`, etc.) — matching Hugo's behaviour. |
| 55 | + - Pros: follows MyST's documented design and conventional project structure; |
| 56 | + preserves URL continuity for external links; no SEO disruption; no |
| 57 | + redirects needed. |
| 58 | + - Cons: restructuring all file paths is a significant mechanical change; |
| 59 | + any tooling or scripts that reference `content/` must be updated. |
| 60 | + |
| 61 | +2. **Rename `index.md` to `section.md` + update all internal links to flat URLs** |
| 62 | + - Rename each `content/<section>/index.md` to `content/<section>/<section>.md` |
| 63 | + so MyST generates slug `/<section>` instead of `/index-N`. |
| 64 | + - Update all cross-page links in content to use flat URLs (`/governance` |
| 65 | + not `/about/governance`). |
| 66 | + - Add a Netlify `_redirects` file mapping old Hugo URLs to new flat URLs for |
| 67 | + external link / bookmark compatibility. |
| 68 | + - Pros: flat URLs are shorter; avoids project restructure. |
| 69 | + - Cons: large mechanical content churn; URL structure changes permanently; |
| 70 | + external links to the Hugo site break without redirects; flat URLs lose |
| 71 | + section context; SEO impact from changed URLs persists until redirects |
| 72 | + are indexed. |
| 73 | + |
| 74 | +3. **Netlify `_redirects` shim (keep Hugo-style links, redirect to flat URLs)** |
| 75 | + - Keep content links as-is; add a `_redirects` file that maps every |
| 76 | + hierarchical Hugo URL to the corresponding flat MyST slug. |
| 77 | + - Pros: no content changes; external links preserved. |
| 78 | + - Cons: fragile — `index-N` numbering shifts if toc order changes; |
| 79 | + maintenance burden every time a page is added; masks the underlying issue. |
| 80 | + |
| 81 | +4. **Disable `netlify-plugin-checklinks` failure / warn-only** |
| 82 | + - Configure or remove the plugin so broken links produce warnings, not a |
| 83 | + build failure. Defers URL fix to a follow-up issue. |
| 84 | + - Pros: unblocks the PR immediately. |
| 85 | + - Cons: leaves broken links in the deployed site; removes the link-integrity |
| 86 | + safety net established by ADR 0007. |
| 87 | + |
| 88 | +## Decision |
| 89 | + |
| 90 | +_Pending team discussion._ |
| 91 | + |
| 92 | +Recommendation: Option 1 (`folders: true` + move content to project root). It |
| 93 | +follows MyST's documented design and conventional project structure, preserves |
| 94 | +the existing URL shape without content link edits, and avoids the fragility of |
| 95 | +redirect shims. The file restructure is mechanical and one-time. |
| 96 | + |
| 97 | +## Consequences |
| 98 | + |
| 99 | +- Option 1 requires moving all content files out of `content/` to the project |
| 100 | + root and updating all `toc` entries in `myst.yml`. No content link edits |
| 101 | + are needed since URLs are preserved. |
| 102 | +- External links from the Hugo site (`/about/governance/`, `/contributors/`, |
| 103 | + etc.) continue to work unchanged — no redirects needed. |
| 104 | +- Until resolved, `netlify-plugin-checklinks` will fail on every Netlify deploy |
| 105 | + with 12 broken internal links. |
0 commit comments