‹ all posts

Erik Rekola · updated 2026-09-27

HTML and Markdown can disagree

2026-09-12

A Markdown version can leave out part of a page or send a reader to a different link. I built markdown-parity-check to compare what the two versions contain.

My own tools page gave the checker something to report. A check recorded on 11 September 2026 found a Related heading and four links in its Markdown version that the HTML page left out. The comparison returned five errors. That dated result is recorded in the tool's README.

The difference was structural rather than missing content. The HTML page linked to the same four destinations from its tool cards, so nothing was unreachable, but the Markdown reader got a Related section the HTML reader never saw. Neither version looked broken on its own.

Which differences actually matter?

Notation is not the difference worth chasing. A heading can use an HTML element in one version and a hash mark in the other and still say the same thing. The difference that matters is in the content itself.

HTML carries the structure a browser renders, often with navigation and controls around the main text. Markdown expresses text structure with lighter notation.

The problem starts when the content changes with the format. A condition disappears from a service description. A table keeps an old value. A link has the same label but points to another address.

These are examples of differences worth checking, not findings about anyone else's website. A client using Markdown receives the content in that response. If the response omits a condition, that condition is absent from the material the client was given, regardless of how clearly it appears in a browser.

Serving a Markdown response and keeping it consistent with the HTML are separate things to verify.

What does the checker compare?

It compares the main content of the two versions block by block. The checks cover headings and paragraphs, list items, tables, code blocks and links, and every reported difference points back to the text it came from.

I built markdown-parity-check for this comparison. It extracts the main content from HTML and lines it up with the Markdown.

Missing or added blocks are errors. Changed wording and changed numbers can fail the comparison too, and so can a link that keeps its label but points to a different address. Changes in heading level or order are reported as warnings, as are changes in case or punctuation.

A warning still needs reading. The order of instructions can matter even when the words match. The tool's severity is a starting point for review.

How do I run it on my own page?

With Node.js 22 or newer available, this command requests both representations of my tools page and prints the comparison:

npx --yes markdown-parity-check --url https://turva.dev/tools

It sends one request with the header Accept: text/html and another with Accept: text/markdown. Replace the address with your own public page. When Markdown lives at a separate address, supply it with --markdown-url. The usage documentation also covers comparing two local files.

In URL mode the delivery check runs first. An HTTP error or an HTML response in place of Markdown fails that check. The content comparison stops there.

For a build pipeline, --format json --output report.json writes a structured report, and --strict makes warnings reject the run too. Exit code 1 means the completed check found a rejecting difference. Exit code 2 means an input or execution problem prevented a reliable comparison. Both deserve attention, for different reasons.

Choose what you compare

The HTML container matters. By default, the tool looks for main, then article, then an element with role=main, and falls back to the page body. That fallback lets navigation and other surrounding content enter the comparison, and it produces a warning. Use --selector when the intended content needs a more precise boundary.

Markdown front matter is kept by default. If that metadata has no HTML counterpart, it can fail the comparison on its own. Use --front-matter strip when front matter is outside the content you intend to compare, and keep that choice visible in the command.

I would start with the page a buyer relies on to understand a service. Reading that report is what tells me which differences belong in the source. Then I would keep the same comparison in the release checks. A shorter report is useful only if it still covers the content that matters.

Can I run the check in a browser?

The browser version checks published pages on turva.dev. It refuses addresses on other sites. Use the command-line package for your own public site or a pair of local files.

The hosted page pins a release and applies smaller limits. Its report includes a toolVersion field, which matters when comparing results from the browser and the command line. The npm package can be newer than the hosted release.

JavaScript is not executed by the checker. It compares the HTML as received, so content added later in a browser needs a separate check. Block matching is heuristic too. A clean report means the implemented checks found nothing to reject. It does not prove that the two versions mean the same thing, or that either version is factually correct.

The Related section on my tools page was the kind of finding I want this to expose: a difference I could locate in both versions and decide about. I decided to show the section on the HTML page too.

Frequently asked

Does a clean report mean the two versions say the same thing?

No. It means the implemented checks found nothing to reject in the blocks that were extracted. Block matching is heuristic, JavaScript is not executed, and nothing in the tool reads meaning. A plain diff would report every notation difference instead, which is why this comparison aligns blocks first.

Can I run it against a site I do not own?

Yes, from the command line. The package takes any public address with --url and compares what that address returns for Accept: text/html and for Accept: text/markdown. The hosted page on this site is the exception. It checks turva.dev pages only and answers 403 for any other host.

What should I fix first when the report is long?

The differences that change what a reader is told: a condition that is missing, a number that moved, a link whose target is not the one in the other version. I read the ordering and punctuation warnings too, but they rarely change a release. The tool reports quantities and does not decide which difference matters to your business.

Corrected 2026-09-25. This post used my tools page as the example of what the checker finds. A run on 25 September over every page the hosted check accepts, 71 of them, failed all 71 with 836 findings. Of those, 552 came from the page frame: the back link, the date line, the contents list and the next-step box sat inside the main element, which the checker reads as the content. The frame now sits outside it, and the contents list is a navigation element. After that change the same run passes 51 pages and reports 271 findings: 166 on the hand-built service and company pages, where cards, calls to action and email links differ from the Markdown, 71 on the two index pages, 23 on the two sample reports and 11 on seven articles, ten of which are links the HTML makes from a bare address that the Markdown leaves as plain text.

Corrected 2026-09-26. The same run now passes 59 of the 71 pages and reports 208 findings. In six articles the Markdown now links the addresses that the HTML already linked. The email address in the Markdown is a normal link on eight pages, where it used to show as mailto text. The company and contact pages have a space between each label and its value in the HTML text, and the tools page shows its Related section. What still fails stays for a stated reason. Each index page shows a summary card for every guide or post, and the Markdown keeps a plain list. The two sample reports repeat each wide table in a second, stacked view for narrow screens, and the Markdown carries the table once. The home page has a score board and a worked example, and it sets each step title apart where the Markdown runs it into the paragraph. Two pages carry an image whose description the Markdown does not repeat. The service pages have buttons that repeat an address or a link the text already gives, and they turn a few labels into headings. One guide names a protocol identifier that the HTML does not link on purpose.

Corrected again 2026-09-26. The same run now passes 61 of the 71 pages and reports 199 findings. Nine button rows on five pages are now read from a paragraph of links in the Markdown, so both versions carry the same links in the same order. The company and audit pages now pass. Three rows stay in the HTML only, because their first button points to a place on the same page, and the Markdown view has no anchor for it.

Corrected a third time 2026-09-26. The same run now passes 63 of the 71 pages and reports 188 findings. The badge preview and the Signal code are image lines in the Markdown, a label over a list is a heading in both versions, and the blog index Markdown links its RSS feed. Most of what still fails is the checker's own reading. The largest cause is that it takes a row of card links as one block. It also matches a link by its text alone, so the same address under a different label reads as lost on one side and added on the other.

Corrected a fourth time 2026-09-26. The same run now passes 67 of the 71 pages and reports 151 findings. The two sample reports pass, and so do the Shopify check page and the commerce guide. A wide table used to appear twice. The second copy was a list for narrow screens, and now every table turns into cards on a narrow screen instead, so its text is on the page once, as it is in the Markdown. Both versions now write the JSON excerpts and the AP2 address as code, which neither links. The three rows that stayed in the HTML only now point at a heading both versions carry. Four pages still fail, the home page, the services page, the blog index and the guides index, and most of their findings come from cards.

Corrected 2026-09-27, a fifth time. Two more releases on 2026-09-26 took the same run to 71 of 71 pages with no errors and no warnings, and My own site is my proof of work tells how. Since then every page in the sitemap has to pass this check with no error and no warning before my site can ship.