If you edit your Hugo site through Sveltia CMS, you can add our extension to it with one line. The first thing it gives you is a Table option in the editor: paste the cells from Word or Excel, give the table a caption, and what comes out is HTML a screen reader can follow.

This page grows as the extension does. Tables come first because a markdown table arrives on the page bare.

What a plain markdown table is missing

Hugo turns a pipe table into a bare <table>. There is no caption, so a screen reader announces a table with no name. There is no scope on the header cells, so nothing says which heading belongs to which cell. There is no wrapper, so a wide table drags the page sideways on a phone.

Three files in your repository and one line in your admin page fix all of it.

Add the extension

Open admin/index.html and put this line after the Sveltia script:

<script src="https://api.statichost.uk/cms/statichost-cms.js"></script>

The order matters. Sveltia reads its editor components when it starts up, so ours has to be registered before that.

Add the Hugo files

layouts/_default/_markup/render-table.html

This replaces Hugo’s built-in table template, and it applies to every table on your site, including ones you wrote by hand.

{{- $caption := .Page.Store.Get "shukTableCaption" | default "" -}}
{{- $rowHeaders := .Page.Store.Get "shukTableRowHeaders" | default false -}}

{{- $hasHead := false -}}
{{- range .THead }}
  {{- range . }}
    {{- if trim (printf "%s" .Text) " " }}{{ $hasHead = true }}{{ end }}
  {{- end }}
{{- end }}

<div class="shuk-table" tabindex="0"{{ with $caption }} role="region" aria-label="{{ . }}"{{ end }}>
  <table>
    {{- with $caption }}
    <caption>{{ . }}</caption>
    {{- end }}
    {{- if $hasHead }}
    <thead>
      {{- range .THead }}
      <tr>
        {{- range . }}
        {{- $align := "" }}
        {{- with .Alignment }}{{ if ne . "left" }}{{ $align = printf " style=%q" (printf "text-align: %s" .) }}{{ end }}{{ end }}
        <th scope="col"{{ $align | safeHTMLAttr }}>{{ .Text }}</th>
        {{- end }}
      </tr>
      {{- end }}
    </thead>
    {{- end }}
    {{- if .TBody }}
    <tbody>
      {{- range .TBody }}
      <tr>
        {{- range $i, $cell := . }}
        {{- $align := "" }}
        {{- with $cell.Alignment }}{{ if ne . "left" }}{{ $align = printf " style=%q" (printf "text-align: %s" .) }}{{ end }}{{ end }}
        {{- if and $rowHeaders (eq $i 0) }}
        <th scope="row"{{ $align | safeHTMLAttr }}>{{ $cell.Text }}</th>
        {{- else }}
        <td{{ $align | safeHTMLAttr }}>{{ $cell.Text }}</td>
        {{- end }}
        {{- end }}
      </tr>
      {{- end }}
    </tbody>
    {{- end }}
  </table>
</div>

layouts/shortcodes/table.html

A markdown table cannot carry a caption, and it cannot say that the first column is a heading rather than data. This shortcode carries both.

{{- $caption := .Get "caption" | default "" -}}
{{- $rowHeaders := ne (.Get "rowheaders") "false" -}}

{{- .Page.Store.Set "shukTableCaption" $caption -}}
{{- .Page.Store.Set "shukTableRowHeaders" $rowHeaders -}}
{{- .Page.RenderString (dict "display" "block") .Inner -}}
{{- .Page.Store.Set "shukTableCaption" "" -}}
{{- .Page.Store.Set "shukTableRowHeaders" false -}}

static/statichost-table.css

Structure only. Colours come from your own theme.

.shuk-table {
  overflow-x: auto;
  max-width: 100%;
  margin: 1.5rem 0;
  outline-offset: 2px;
}

.shuk-table table {
  border-collapse: collapse;
  width: 100%;
  min-width: 30rem;
}

.shuk-table caption {
  text-align: left;
  font-weight: 600;
  padding-bottom: 0.5rem;
}

.shuk-table th,
.shuk-table td {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid currentColor;
  border-bottom-color: color-mix(in srgb, currentColor 20%, transparent);
  text-align: left;
  vertical-align: top;
}

.shuk-table thead th {
  border-bottom-width: 2px;
  font-weight: 600;
}

.shuk-table tbody th[scope='row'] {
  font-weight: 600;
}

Load it the way your theme loads any other stylesheet.

Adding a table

  1. In the body field, open the component menu and choose Table.
  2. Select the cells in Word or Excel, copy them, and paste them into Table.
  3. Fill in Caption.
  4. Check the two header toggles.
  5. Save.

Converting a table you already have

  1. In the body field, select the lines of the existing table and cut them.
  2. Insert a Table component where they were.
  3. Paste the lines into Table. A markdown table is accepted exactly as it is.
  4. Add a caption, check the two toggles, and save.

This is also the fix for a table that renders as lines of text rather than a table. A markdown table needs a divider row of dashes under the heading row, and without it Hugo treats the whole block as ordinary paragraph text. The widget writes that row for you.

The four fields

  • Table — the cells themselves. A spreadsheet paste, a CSV and a markdown table are all accepted.
  • Caption — what the table is called. A screen reader reads it out before the contents, so it is the difference between “table” and “table, plans compared”.
  • First row is a header — on unless your top row is data.
  • First column is a header — right for a comparison table, where the left column names the thing being compared. Turn it off for a table of figures.

The preview underneath shows what you will get. Save when it looks right.

What you get in your markdown is an ordinary table wrapped in a shortcode:

{{< table caption="Plans compared" >}}
| Feature | Basic | Pro |
| --- | --- | --- |
| Storage | 1 GB | 100 GB |
{{< /table >}}

You can edit that by hand afterwards, and reopening it in the editor puts it back in the widget.

Two things a markdown table cannot do

Merged cells

If your original has a cell spanning two columns, that structure is lost on the way in. The preview tells you when it spots it, and pads the short rows with blanks so you can put them right.

A line break inside a cell

A table row is one line of markdown, and the only way to break inside a cell is a literal <br>, which Hugo removes unless you have turned on unsafe HTML. Rather than write markup that quietly disappears, the editor turns the break into a space.

Tables written by hand

The render hook applies to every table on your site, so tables written by hand get the wrapper and the column headers as soon as you add the file. They get no caption and no row headers, because nothing in a plain markdown table says the first column is a heading. Wrap one in the shortcode and it gets both.

Planned next

An on-demand check of your page covering SEO and social metadata, headings, links, and images with no alt text. After that, help with rewriting and generating copy using your own AI provider key. Neither is built yet.

Questions to .