Skip to content

Alt text on all images, and a markdownlint pass over the repo - #3358

Merged
enf0rc3 merged 14 commits into
mainfrom
willlaugesen/markdown-lint-cleanup
Aug 18, 2026
Merged

Alt text on all images, and a markdownlint pass over the repo#3358
enf0rc3 merged 14 commits into
mainfrom
willlaugesen/markdown-lint-cleanup

Conversation

@enf0rc3

@enf0rc3 enf0rc3 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Closes NES-235.

Why

NES-235 asks for alt text on our images, for accessibility and because it is a primary signal for search crawlers and AI search assistants. Every image in the docs now has it.

Adding alt text meant running markdownlint over the content, and once it was running, almost everything else it flagged was auto-fixable. So this does the whole repo in one go rather than leaving thousands of findings to surface a handful at a time in unrelated PRs. Worth doing once now, before the docs rewrites start, so the rewrites land on a clean baseline.

The safety net

NES-235 also asks for something to stop this regressing. That now exists without new tooling: CI already lints every .md and .mdx file a PR touches, and the repo passes clean, so MD045 fails any PR that adds an image without alt text.

One caveat. WCAG says genuinely decorative images should have empty alt, but MD045 requires alt on every markdown image. There was one such case — an inline help icon — and rather than invent a description, the sentence was reworded so the icon is the thing being named. A future decorative image needs an HTML <img alt=""> tag, which MD045 does not inspect.

What changed

Alt text on 534 images. Measured directly: the files in this diff contained 535 empty-alt images on main and contain one now — the deliberate before/after example in README.md. 12 built pages were still rendering alt="" after the first pass, because the include files in src/shared-content live outside src/pages and were never being linted. They are now.

Everything else markdownlint reported, nearly all of it mechanical: whitespace, emphasis style, blank lines around headings and lists, hard tabs, trailing newlines, table pipe alignment. Beyond the auto-fixable set, every fenced code block now declares a language, and a handful of heading levels were corrected so they increment by one.

List and heading structure are unchanged. Every ordered-list marker in the diff matches main exactly, indent and number, and no bold label was promoted to a heading. See the config notes below for why.

Wording is left alone apart from seven spelling corrections cspell flagged (Whilst to While, therefor to therefore, utilized to used, and similar), six bare here links given descriptive text, and the link and heading corrections listed under real bugs below.

Roughly 355 code blocks are labelled text, which gives them a visible "Text" chip in the block header. That is the existing convention — main already labels 275 blocks that way — but it is a visible change at this scale, so worth knowing before merge.

README.md documents the conventions, including the two auto-fixes below that are unsafe here, and the fact that src/shared-content needs linting too.

Config decisions

Six rules needed scoping or disabling rather than fixing. Each is a judgement call worth reviewing.

Four were rewriting authored structure rather than fixing faults, and are off:

  • MD007 and MD005 (list indentation) → off. MD007 re-indented nested list items to its own 2-space default, flattening lines that were deliberately indented 4 spaces. The repo is mixed, so enforcing either width just churns files. Every bullet indent in the diff is back to what the author wrote.
  • MD029 (ordered list prefix) → off. This one was breaking pages. Where a figure or code block sat at column 0 inside a numbered list, the parser ended the list, and MD029 "fixed" the follow-on items by restarting them at 1 — so rendered pages lost their step numbering. Numbering that deliberately runs across headings renders as <ol start="N">, which MD029 cannot express. Every ordered-list marker is restored, verified by aligning each changed file against main.
  • MD036 (emphasis as heading) → off. It wanted 214 bold labels turned into headings. The level for each came from MD001 ratcheting rather than from meaning, so flat peers nested one level deeper each until they ran out at h6, and shared includes gained an h2 that popped a page outline back up when the include sat under an h3. It also pushed 214 entries into the on-page nav. Promoting a label to a heading is an information architecture decision rather than formatting, so it is left for a deliberate pass.

The other three are scoped rather than off:

  • MD010code_blocks: false. 8,090 of the hard tabs are inside published script samples under src/shared-content/scripts. Retabbing code that users copy and run is worse than allowing tabs there. Tabs outside code blocks are still caught.
  • MD024siblings_only. API reference pages legitimately repeat Parameters, Sample response and Properties under each endpoint. These are not siblings, and the rule still catches genuine duplicates within a section.
  • MD051ignored_pattern: "^[a-z0-9-]*[A-Z]". The rule only recognises lowercase-kebab custom anchors, so it flags the 188 legacy {#PascalCase} anchors this site uses. Those anchors render and resolve correctly — verified against the rendered HTML. The pattern is anchored so it exempts that shape rather than any fragment with a capital in it.

The spelling dictionary is deliberately untouched

dictionary-octopus.txt has a code owner, and adding words to it would make this formatting PR wait on a review it does not otherwise need. It is unchanged here, and so is cspell.json.

None of the words cspell flagged were introduced by this PR — they are pre-existing prose that only got checked because spellcheck runs on changed files and this PR changes a lot of them. Touching a page for lint alone is enough to pull it into that job, so these failed regardless of whether anything was respelled.

No wording is changed. Each affected file carries a scoped exception next to the usage instead:

<!-- cspell:ignore SCCM -->

Fifteen words are handled this way: eksctl, mysqldump, authbind, Payara, SCCM, octopussamples, licensekey, tasklog, Linkback, Doesnt, Dbup, mytentacle, deregistering, gitversion and Springboot. The word is accepted only where it appears rather than repo-wide, and both dictionary-octopus.txt and cspell.json are untouched.

Several are genuinely worth correcting — Dbup is DbUp, Springboot is Spring Boot, mytentacle would read better as your-tentacle — but that is prose, not formatting, and it belongs in a follow-up the dictionary owner can review. This PR deliberately leaves the wording exactly as the authors wrote it.

One MDX page uses a YAML comment in its frontmatter instead of the HTML form, because MDX rejects <!-- --> and markdownlint reads {/* */} as emphasis markers.

Every other cspell finding was a genuine typo and was corrected.

Real bugs this surfaced

  • /docs/octopus-rest-api linked to #api-clients, a section that does not exist on that page. Now points at the Octopus.Client page.
  • The Azure accounts page had two sections with the same title and the same anchor. The second one actually documents federated credentials, and a link elsewhere on that page pointed at the anchor it should have had.
  • kubernetes-resources.md carried a byte-identical duplicated Command section, and the probe sub-types sat at the same heading level as the probes themselves.
  • An eksctrl link whose text contradicted its own URL, Springboot where the product is Spring Boot, and two dead link fragments in a shared include.
  • Six bare here links replaced with descriptive text.

Two auto-fixes to avoid on this repo

Both are now documented in README.md:

  • MD034 on .mdx. It rewrites bare URLs and email addresses as <url> autolinks. Valid CommonMark, but MDX parses the angle brackets as JSX and fails to compile. It broke the build twice here before being caught. Both cases are now explicit markdown links, and every .mdx has been swept for others.
  • MD010 on tab-indented lists. It rewrites a leading tab as a single space, which is too shallow to nest the item, so the bullet silently escapes its parent. The 19 affected lines are now four spaces, matching what the tab rendered as.

Verification

Run locally against the whole repo, not just the changed files:

  • markdownlint-cli2 over all 2,900 .md and .mdx files in the repo: 0 errors
  • pnpm spellcheck: 0 issues, with no dictionary change
  • 0 empty-alt images left in src, and 0 built pages rendering alt=""

Rebased onto main after 20 commits landed, including the heading-icon removal (#3354). Four content pages conflicted; those took main's version and the lint pass was re-run over them, so nothing from the newer commits is left behind. The nine markdown files those commits touched were checked individually — all images have alt text, no unlabelled code fences, no <url> autolinks in .mdx, and no tab-indented list items.

The indented figures were checked separately, since indenting a block can turn it into a code block: no :::figure or :::div text leaks into any built page, no image path ends up inside a <pre>, and rendered <figure> counts match the source image counts per page. main.css documents figures-inside-list-items as the intended layout.

@team-marketing-branch-protections

Copy link
Copy Markdown

Pull request environment is available at https://stoctodocspr3358.z22.web.core.windows.net.

You can view the ephemeral environment status in Octopus Deploy.

This environment will be automatically deprovisioned when the pull request is closed, or after 7 days of inactivity.

@enf0rc3 enf0rc3 changed the title Fix markdown lint and spelling issues Fix markdown lint, spelling issues & alt text Aug 16, 2026
@enf0rc3 enf0rc3 changed the title Fix markdown lint, spelling issues & alt text Alt text on all images, and a markdownlint pass over the repo Aug 16, 2026
@enf0rc3
enf0rc3 marked this pull request as ready for review August 16, 2026 23:46
@enf0rc3
enf0rc3 force-pushed the willlaugesen/markdown-lint-cleanup branch from 5183fdb to af0a291 Compare August 17, 2026 03:48
@enf0rc3
enf0rc3 removed the request for review from steve-fenton-octopus August 17, 2026 04:32
@enf0rc3
enf0rc3 force-pushed the willlaugesen/markdown-lint-cleanup branch 4 times, most recently from 25a0dd3 to 44d9459 Compare August 17, 2026 21:09
enf0rc3 and others added 13 commits August 18, 2026 12:34
Clears 647 of the 830 markdownlint errors across the docs pages: whitespace,
emphasis style, list and heading spacing, table alignment, heading levels, and
two broken link fragments. Also fixes a "therefor" typo flagged by cspell.

Remaining: 131 images without alt text, plus duplicate headings, fenced blocks
without a language, and emphasis used as headings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds alt text to 131 images, labels 15 code fences, promotes emphasis used as
headings to real headings, and corrects heading levels. Configures MD024 to
siblings_only, since API reference pages repeat "Parameters" and "Properties"
under each endpoint, and scopes MD051 past the legacy {#PascalCase} anchors it
cannot parse.

The numbered list in the variables getting-started guide is now one continuous
sequence; it previously restarted at 1 twice where figures interrupted it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clears all 4,432 markdownlint errors reported across the 2,666 markdown
pages, and all 18 remaining cspell issues.

Alt text added to 364 images. 397 fenced code blocks given a language.
113 bold labels used as headings promoted to real headings, or inlined
with their value where they were repeated field labels. Table pipes
aligned, heading levels corrected, whitespace and list formatting fixed.

Real bugs fixed along the way:
- The Azure accounts page had two sections titled "Create a Service
  Principal Client Secret with PowerShell" sharing one anchor. The second
  documents federated credentials, and a link elsewhere on the page
  pointed at the anchor it should have had.
- kubernetes-resources.md carried a duplicated "Command" section, and the
  probe sub-types sat at the same heading level as the probes themselves.
- Several link fragments pointed at anchors that do not exist.
- Six bare "here" links replaced with descriptive text.

MD024 is set to siblings_only and MD051 ignores fragments containing an
uppercase letter, since the site uses legacy {#PascalCase} anchors that
markdownlint cannot parse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous pass only covered src/pages, so the include files under
src/shared-content were never linted even though they render into those
pages. This clears the remaining 8,901 errors across 234 files.

Alt text added to 45 images in shared-content includes, which is why
several built pages still had empty alt attributes. 109 bold labels used
as headings promoted to real headings, 22 code fences given a language,
tables aligned, and two dead link fragments removed.

MD010 is set to code_blocks: false. Nearly all 8,090 hard tabs are inside
published script samples under src/shared-content/scripts, and retabbing
code that users copy is worse than allowing tabs there. Tabs outside code
blocks are still caught.

Also fixes an "eksctrl" link whose text contradicted its own URL, and
"Springboot" where the product is Spring Boot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two style rules were rewriting authored structure rather than fixing
faults, so both are now off and the damage is reverted.

MD007 re-indented nested list items to its own 2-space default, flattening
589 lines across 57 files that were deliberately indented 4 spaces. The
repo is mixed, so enforcing either width churns files for no benefit.
MD005 is off for the same reason.

MD029 was the worse one. Where a figure or code block sat at column 0
inside a numbered list, the parser ended the list, and MD029 "fixed" the
follow-on items by restarting them at 1. Rendered pages lost their step
numbering. Authored numbering is restored on 252 items, and the 536 lines
of interrupting content are now indented into the item they belong to, so
each list stays intact. Numbering that runs across headings renders as
<ol start="N">, which MD029 cannot express, so the rule is off.

Tab-indented list items needed care: markdownlint rewrites a leading tab
as a single space, which is too shallow to nest. Those 19 lines are now
4 spaces, matching what the tab rendered as.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The linting section told authors to lint src/pages only, which is how the
shared-content includes went unlinted and shipped images with no alt text.
It now covers both trees.

Adds the conventions this PR settled on: images need alt text, content
inside a numbered list is indented into its step rather than left at
column 0, and the two auto-fixes that break things here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Product screenshots example modelled an image with no alt text, which
contradicts the alt text section above it and would fail the lint check
for anyone who copied it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dictionary-octopus.txt has a code owner, so adding words to it makes this
formatting PR wait on a review it does not otherwise need. It is unchanged
here, and so is cspell.json.

None of the words cspell flagged were introduced by this PR. They are
pre-existing prose that only got checked because the spellcheck job runs
over every changed file, so touching a page for lint alone was enough to
surface them.

Rather than reword anything, each affected file carries a cspell:ignore
for just the word it uses: eksctl, mysqldump, authbind, Payara, SCCM,
octopussamples, licensekey, tasklog, Linkback, Doesnt, Dbup, mytentacle,
deregistering, gitversion and Springboot. The word is accepted where it
appears rather than repo-wide, the wording is left exactly as the authors
wrote it, and the pages stay linted.

Several of these are worth correcting properly (Dbup is DbUp, Springboot
is Spring Boot, mytentacle would read better as your-tentacle). That is
left for a follow-up once the dictionary owner can review it.

The MDX page uses a YAML comment in its frontmatter instead: MDX rejects
<!-- --> and markdownlint reads {/* */} as emphasis markers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eadings

Three fixes from review.

MD029 damage was still in the diff. The earlier restore skipped any item
whose list was interrupted by a heading, but that is exactly the case the
authors rely on: numbering that runs across headings renders as
<ol start="N">. Every ordered-list marker now matches main, indent and
number, verified by aligning each changed file against main. The same
alignment turned up 49 bullet indents that MD007 had moved and that the
first restore missed.

MD036 was promoting bold labels to headings, and the level for each came
from MD001 ratcheting rather than from meaning, so flat peers ended up
nested one level deeper each until they ran out at h6. It also pushed 214
new headings into the on-page nav, and gave shared includes an h2 that
popped the outline back up when the include sat under an h3. Promoting a
label to a heading is an information architecture decision, not
formatting, so the rule is now off and all 214 promotions are reverted,
along with the blockquote and punctuation edits made to satisfy it.

Also from review: four code blocks the classifier defaulted to text are
really PowerShell or Bash, and MD051's ignored_pattern was unanchored, so
it exempted a capital anywhere rather than only the legacy anchors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block under "outputs the list of projects in parsable JSON format"
was labelled text. The classifier fell back to text because the array
carries a trailing comma after its last object, so a strict parse of it
fails.

The console table further down the same page stays as text: it is
tabular command output, not a structured format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These two sat at two spaces on main. The earlier fix put back one, from
reading a truncated terminal column rather than measuring the line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two trailing spaces are a <br>, and that is what the rule allows, but a
line carrying three or more was trimmed to zero rather than back to two,
so the break was deleted instead of normalised.

Seven lines are restored to exactly two trailing spaces: the definition
lines and hint in the deploy release step page, two paragraphs in the
import certificate step page, and one in step conditions.

Sixteen other lines in the same class keep the stripped whitespace. In
those the break sat before a list item or a ::: block close, where it
renders nothing.

The README now lists this alongside the other two auto-fixes that leave a
file passing the linter while changing the rendered page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MD039 removed spaces from inside link text. Where the space sat against
the neighbouring word, code span or link, that joined two words in the
page: "CIDR format172.28.6.0/24", "projectsand", "likePuTTYgen",
"Backup and restorepage". Five lines, and the space is now outside the
brackets rather than deleted.

MD034 wrapped a scheme-less URL in angle brackets. www.octopussamples.com
is not a valid autolink, so the brackets rendered as literal characters.
It is an explicit link now.

The earlier MD036 revert matched on heading text, so in two files that
held both a real heading and a bold label of the same words, it demoted
the heading as well. Both are restored, and the sibling labels stay bold.

Five fences carried a wrong language: two Terraform blocks marked xml and
csharp in a file whose other blocks are correctly hcl, a CLI usage listing
marked python, a javac command marked powershell, and CLI help output
marked bash. Every non-text language this PR assigned has now been checked
against the block it labels, not just the ones changed from text.

The README gains the MD039 hazard, and its MD034 entry now says the rule
is unsafe in .md too when the URL has no scheme.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@enf0rc3
enf0rc3 force-pushed the willlaugesen/markdown-lint-cleanup branch from da457b7 to a7aaebc Compare August 18, 2026 00:41
Two rows in api.md carry an unescaped pipe mid-cell, so they parse as
three cells in a two column table. GFM drops the extra cell, which is why
production never showed that text. The table formatter read the widest
row instead and grew both tables to three columns, giving every other row
a blank cell on the right.

The pipe in the Deployments row is escaped so its sentence stays in the
Description column, the empty third cell on the RecoveredFrom row is
dropped, and both tables are back to two columns. The page renders 225
table cells again, matching production, down from 247.

Also drops a stray semicolon from a bash fence in prompted-variables.md.
It predates this PR but cost the block its highlighting when main changed
markdown engine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@enf0rc3

enf0rc3 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Verification

This PR was reviewed in two passes: static analysis of the source diff, then a diff of the rendered HTML against production. The second pass is what mattered — it found regressions the first pass structurally could not see.

Why the rendered diff was necessary

Every regression found here left the file passing the linter. lint, spellcheck and build were green throughout. They only appear in the built page, so the diff is the only place to catch them.

Method: fetch all 639 changed doc pages from the preview environment and from octopus.com, reduce each to structural text (headings, <ol start>, <br>, code-block language, table cells, prose), and diff. 1,405 hunks were produced and every one was categorised — engine-level noise, intended change, or defect.

What it found — 12 regressions across 5 classes, all fixed

# Class Instances Effect
1 MD029 list renumbering not fully reverted ~25 steps, 4 pages Tutorial steps renumbered to 1. mid-sequence
2 MD036 bold→heading promotion 312 headings False hierarchies down to ######; shared includes emitting ## inside ### sections
3 MD009 trimmed 3+ trailing spaces to zero 22 lines, 7 significant Hard line breaks deleted; a 4-line definition list collapsed to one paragraph
4 MD039 removed spaces doing real work 5 likePuTTYgen, projectsand, CIDR format172.28.6.0/24
5 Table formatter inferred width from a malformed row 2 tables Blank third column on /docs/insights/api

Plus: a scheme-less <www.…> autolink rendering literal angle brackets, two authored ### headings demoted to bold, and 5 mislabelled code fences (Terraform as xml/csharp, a CLI help listing as python).

Final state — verified against origin/main

Check Result
Rendered ordered-list ordinals vs main 3 files differ, all fixes
Table column counts 0 changed
Hard line breaks 0 lost, 0 spuriously added
Headings demoted to bold 0
MD039 word joins 0
Anchor fragments 0 newly broken
Empty alt in src 0
<url> autolinks in .mdx 0
Accidental indented code blocks 0
Fence languages 0 suspects across 74 non-text assignments
/docs/insights/api cells 225, matching production table-for-table

dictionary-octopus.txt and cspell.json are byte-identical to main and absent from the diff.

Note on the diff baseline

aee3fac10 (unified → satteri) is in this PR's base but not yet on production, so 777 of the 1,405 hunks are that engine change, not this PR. Confirmed by running the same comparison on a page this PR doesn't touch: production drops <, & and && inside code blocks (List<string> renders as Liststring>) and satteri fixes it.

Lesson worth keeping

A formatter that normalises presentation should not infer structure. MD009 trimmed to zero because its rule is about trailing spaces, not about <br>. MD039 removed a space inside brackets without asking what the space separated. The table formatter measured the widest row because that is how you align columns — not because that row was correct.

README.md now documents MD009, MD010, MD034 and MD039 as auto-fixes to check by hand, with the point that ties them together: none of them show up as a lint failure afterwards.

Known gap, out of scope

CI lints only changed files, so MD045 guards alt text on any page a PR touches, but nothing re-checks all 2,901 files as a set. The clean baseline holds for edited pages and can drift elsewhere. A scheduled full-repo lint would close that — separate PR.

@enf0rc3
enf0rc3 merged commit fc6ce8d into main Aug 18, 2026
7 checks passed
@enf0rc3
enf0rc3 deleted the willlaugesen/markdown-lint-cleanup branch August 18, 2026 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants