Fixing HTML Errors in Static Site Generators
Static site generators such as Hugo, Jekyll, Eleventy and Astro turn templates and Markdown into plain HTML files before anyone visits. That is good news for validation: every page exists as a file you can check before it goes live.
The errors that do appear come from a few predictable places: a layout that wraps content in the wrong element, a shortcode that outputs a block inside a paragraph, raw HTML that the Markdown converter handles differently from what you expected, and summaries cut off mid-tag. This guide shows how to validate the build output, trace errors to their source file and fix each pattern.
Where static site errors come from
A static site page is assembled from three layers, and each produces its own kind of error:
- Layouts and partials, written in Go templates for Hugo, Liquid for Jekyll, Nunjucks,
Liquid or others for Eleventy, and
.astrocomponents for Astro. Errors here repeat on every page that uses the layout. - Markdown conversion, done by Goldmark in Hugo, kramdown in Jekyll by default, markdown-it in Eleventy and remark in Astro. Each follows its own rules for raw HTML, blank lines and indentation, and those rules decide whether your HTML survives intact.
- Shortcodes, includes and components dropped into content. They are written by developers but placed by authors, often in positions the developer did not expect, such as the middle of a paragraph.
Because the output is generated, the fix is never to edit the HTML file in the output folder. It will be overwritten on the next build. Every fix goes into a layout, a partial, a content file or the generator's configuration.
| Generator | Output folder | Markdown converter | Raw HTML in Markdown |
|---|---|---|---|
| Hugo | public/ | Goldmark | Omitted by default and replaced with
an HTML comment; enabled with the unsafe renderer setting |
| Jekyll | _site/ | kramdown | Passed through; Markdown inside HTML
blocks is not processed unless the block has markdown="1" |
| Eleventy | _site/ | markdown-it | Passed through by default; Markdown files are also run through a template language first |
| Astro | dist/ | remark and rehype | Passed through in .md;
.mdx treats HTML as JSX and is stricter |
Validating the build output
Run the HTML Checker against a few deployed pages of each type to see the errors as visitors get them. To check every page at once, validate the output folder locally with a command-line validator.
# html-validate (Node)
npm install --save-dev html-validate
npx html-validate "public/**/*.html"
# The Nu HTML Checker, the engine behind the W3C validator (Java)
java -jar vnu.jar --skip-non-html --errors-only _site/
html-validate reads its rules from a .htmlvalidate.json file. Start with its recommended
preset and turn off rules that do not reflect real problems on your site, rather than ignoring the whole
report. The Nu checker gives the same messages as the W3C online validator, which makes it easier to
compare with the HTML Checker results.
Turn off minification while you debug
Minified output puts a whole page on one line, so every error is reported at line 1 and a column number
in the tens of thousands. Build without minification when validating: in Hugo, leave off the --minify
flag; in Eleventy and Astro, disable the minifying transform or integration for the debug build. Minifiers
also legitimately remove optional end tags and attribute quotes, which is valid HTML but can confuse
stricter lint rules.
Layout and partial errors
Content wrapped in a paragraph
The most common layout error is a template that wraps the rendered content in an element that cannot hold it. Rendered Markdown contains paragraphs, lists, headings and tables, so it needs a block container.
{{/* Hugo: invalid, because .Content contains its own paragraphs */}}
<p class="lead">{{ .Content }}</p>
{{/* Valid */}}
<div class="prose">{{ .Content }}</div>
The same applies to Jekyll's {{ content }}, Eleventy's {{ content | safe }}
and an Astro <slot /> placed inside a <p> or <span>.
Front matter descriptions rendered through Markdown have the same problem, because the converter adds
its own paragraph; render them as plain text or use an inline Markdown function such as Hugo's markdownify
or .RenderString, which avoid the wrapper for short text.
Partials that open what another closes
A header partial that opens <main> and a footer partial
that closes it will break the day a layout uses one without the other. Keep each partial self-contained,
and let the base layout own the page skeleton: doctype, html, head, body
and main.
Layouts nested twice
In Jekyll and Eleventy, a layout can itself use another layout. If both include the full skeleton,
the output contains two html and body elements and a stray doctype. Only the
outermost layout should contain the skeleton; inner layouts output a fragment.
Escaped output
If the content shows literal tags such as <p> on the page, the template
language is escaping HTML it should trust. In Nunjucks use the safe filter on rendered content;
in Hugo, use safeHTML only for HTML you control; in Astro use the set:html
directive. Escaped output is not a validation error, but the fix is often made wrongly by pasting HTML
into a place that then nests badly.
Markdown and shortcode quirks
Shortcodes and components inside paragraphs
Authors often place a shortcode on the same line as text, or directly under a line of text with no
blank line in between. The converter then treats it as part of the paragraph, and a shortcode that outputs
a figure, div or iframe ends up inside a <p>.
Put block shortcodes on their own line with a blank line above and below. In Hugo, note the difference
between the two call styles: output from the {{< >}} form is inserted as it is, while
output from the {{% %}} form is passed through the Markdown converter, which can add paragraph
tags around it.
Indented HTML becomes a code block
In Markdown, a line indented by four spaces is code. Raw HTML in a Markdown file that is neatly indented
for readability can be turned into an escaped code block halfway through, leaving an opening <div>
without its closing tag. Keep raw HTML blocks flush with the left margin, or move complex HTML into a
shortcode or component.
Blank lines end HTML blocks
Under CommonMark rules, which Goldmark, markdown-it and remark follow, a raw HTML block ends at the
first blank line. Text after the blank line is treated as Markdown again. That can be useful, but it
also means a blank line inside a <table> or <details> written by
hand can split the HTML so that part of it is wrapped in paragraphs.
Hugo's raw HTML omitted comment
In Hugo, raw HTML in Markdown is removed by default and replaced with the comment <!-- raw HTML omitted -->.
If only the opening tag of an element was on its own line, the result can be a closing tag with nothing
to close. Either enable raw HTML deliberately with markup.goldmark.renderer.unsafe = true
in the site configuration, or replace raw HTML in content with shortcodes.
Jekyll and markdown inside HTML
kramdown does not convert Markdown inside an HTML block unless the block carries markdown="1".
Authors who expect Markdown to work inside a div sometimes add HTML tags by hand to compensate
and leave some unclosed. The markdown="1" attribute is removed from the output by kramdown,
so it does not become a validation error itself.
The fix, step by step
Step 1
Validate an unminified build of the site
Build the site locally with minification turned off, so the output has readable line
numbers. Then run html-validate or the Nu checker over the output folder, public/, _site/
or dist/ depending on the generator. Compare a few results with the HTML Checker on the
live site to confirm you are seeing the same errors, and export the report so you can sort messages by
message type and by file.
Step 2
Map each error to a layout, partial or content file
If the same error appears on every page using one layout, the cause is in that layout or a partial it includes. If it appears on a single page, the cause is in that page's Markdown or a shortcode used there. Search the source for a class name or text near the error to find the file. Never fix the generated HTML in the output folder, because the next build overwrites it and the error returns.
Step 3
Fix element wrappers in layouts and partials
Change any p or span that wraps rendered content, a slot
or a Markdown field into a div or a suitable sectioning element. Make each partial open
and close its own elements, and make sure only the outermost layout contains the doctype, html,
head and body. Check layouts that inherit from other layouts, because doubling
up the page skeleton produces stray doctype and duplicate body errors on every page.
Step 4
Stop block output landing inside paragraphs
Find shortcodes, includes and components that output block elements such as figure,
div, iframe or table, and check where authors have placed them.
Move each call onto its own line with a blank line above and below. In Hugo, use the angle-bracket call
style for shortcodes that output finished HTML. Add a note to the shortcode documentation explaining
that it must stand alone, so new content follows the same rule.
Step 5
Set how the Markdown renderer treats raw HTML
Decide whether authors may write raw HTML in Markdown. If yes, enable it explicitly,
for example with the Goldmark unsafe setting in Hugo, and keep raw HTML flush left without
blank lines inside it. If not, replace existing raw HTML with shortcodes or components and remove the
leftover tags. In Jekyll, add markdown="1" to HTML blocks whose contents should be converted
instead of hand-writing tags around them.
Step 6
Repair truncated summaries and excerpts
Check list pages, feeds and cards that show a shortened version of each post. Filters that cut rendered HTML by character or word count, such as a truncate filter applied to post content, can stop in the middle of an element and leave it open. Strip the HTML before truncating, use the generator's built-in summary or excerpt feature, or give each post a plain-text description in front matter and use that instead.
Step 7
Fail the build when new errors appear
Add the validator to your continuous integration pipeline so it runs after every build and fails the job on errors. Commit the validator configuration to the repository so everyone uses the same rules. Once the existing errors are cleared, keep the rule set strict, and rerun the HTML Checker on the deployed site from time to time to catch problems introduced by scripts or third-party embeds that the build folder does not contain.
A build check you can adapt
This GitHub Actions workflow builds a Hugo site and validates the output. The same pattern works for Jekyll, Eleventy or Astro by changing the build command and folder.
name: Validate HTML
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: peaceiris/actions-hugo@v3
with:
hugo-version: 'latest'
extended: true
- run: hugo --environment production
- run: npx --yes html-validate "public/**/*.html"
If the site is large, validate a sample of pages on every pull request and the whole site on a schedule.
Pin the validator version in package.json so an upgrade with new rules does not unexpectedly
fail an unrelated change.
Edge cases
- Astro scoped styles add
data-astro-cidattributes to elements. These are valid data attributes and not an error. - Astro islands render framework components, and a React component inside an island can produce the same nesting errors covered in the React and Next.js guide.
- MDX treats HTML as JSX, so it fails loudly on unclosed tags at build time. That is stricter, and usually helpful.
- Eleventy preprocessing. Markdown files are run through a template language before
Markdown conversion. Characters such as
{{in content can be misread as template syntax and break the surrounding HTML. - Generated heading anchors can produce duplicate IDs when two headings share the same text. The duplicate ID guide covers that separately.
- Gatsby is React-based, so its HTML errors follow the React patterns rather than the Markdown ones here.
1. Why is my Hugo page showing a raw HTML omitted comment?
Since Hugo switched to the Goldmark Markdown renderer, raw HTML in Markdown files is removed by default for safety and replaced with that comment. If you trust your content authors, set markup.goldmark.renderer.unsafe to true in the site configuration and raw HTML will pass through. Otherwise, replace the HTML with a shortcode that outputs it. Check for leftover closing tags afterwards, because removing only part of an element's markup can leave a stray end tag.
2. Why does my shortcode output end up inside a p tag?
Because the Markdown converter sees the shortcode as part of a paragraph. That happens when the call sits on the same line as text or directly under a line of text without a blank line between them. Put block-level shortcodes on their own line, with a blank line above and below. In Hugo, also check the call style: the percent form sends the shortcode's output through the Markdown renderer, which can add paragraph tags around it.
3. Should I fix errors in the public or _site folder?
No. Those folders are rebuilt from your source files every time the generator runs, so any change you make there is lost on the next build. Use the generated files to find errors, then trace each one back to the layout, partial, shortcode or Markdown file that produced it and fix it there. It is worth adding the output folder to your editor's excluded files so nobody edits it by mistake.
4. Which validator should I use for a static site?
Either works well. html-validate runs on Node, is easy to add to projects that already use npm, and has configurable rules. The Nu HTML Checker, available as vnu.jar, is the engine behind the W3C online validator and gives the same messages, which makes its results easy to compare with the HTML Checker. Many teams run html-validate on every pull request and use the HTML Checker on the deployed site for the full picture.
5. Why is indented HTML in my Markdown showing as code?
In Markdown, any line indented by four or more spaces starts a code block. If you indent raw HTML to make it readable, part of it can be turned into escaped code, and the rest remains real HTML, leaving an element opened but never closed. Keep raw HTML blocks aligned with the left margin in Markdown files, or move complex markup into a shortcode, include or component where indentation does not matter.
6. Does minified HTML cause validation errors?
Usually not. Good minifiers only remove things HTML allows you to omit, such as optional end tags, some attribute quotes and whitespace. The result is valid, though some strict lint rules may complain about the style. The practical problem is that minified pages are one long line, so error locations become impossible to read. Validate an unminified build while you are fixing errors, and minify only the production build that you deploy.
7. Why do excerpts on my blog list page break the layout?
The excerpt was probably made by cutting the rendered HTML of each post to a fixed length. If the cut falls inside a link, list or emphasis element, the opening tag is kept but its closing tag is lost, and everything after it on the list page is swallowed into that element. Strip HTML before truncating, use the generator's own summary or excerpt feature, or write a short plain-text description in each post's front matter.
The HTML Checker validates your markup against W3C standards and reports errors and warnings covering the doctype, encoding, tag nesting, duplicate IDs, ARIA, forms and the language attribute, so you can fix each layout and partial at its source.
Open HTML Checker Fixes โ