# Accessibility for humans and not-so-humans.

June  1, 2026

## The many faces of accessibility

After a long period of, let's say, stability, where I did not really update or use this website for anything other than having the bare minimum online presence, I decided it was time to dust it off and start writing seriously.
While I am not exactly delusional and know very well that I am mostly writing for myself, there is still the possibility that someone else will end up here, either because they are a recruiter, a colleague, or just someone who clicked *next* on [Kagi's Smallweb](https://kagi.com/smallweb) one too many times.
One of the design principles I had originally when designing this website, other than being simple enough that I could make it not ugly, was to make it accessible to anyone; at the time, this mostly meant focusing on making sure it was very light on resources and degraded as gracefully as possible under the worst network conditions one could encounter.
But technical accessibility is just one axis, and arguably not the most important one as long as you make sure your website doesn't render everything as some inscrutable blob of JavaScript after pulling 12 MB of data from seven separate origins. And given that I am now spending a lot of time visiting my parents at the [INI](https://www.invalides.fr/), I am more aware than ever of the importance of accommodating the very varied needs of people. And let's be honest, I also wanted to have a green *100* on Google PageSpeed Insights.

## Accommodating humans

As a meatbag myself, I am keenly aware that I am biased toward human-centric accessibility.
Luckily, this was one of the easiest to pursue objectively, as not only does Google provide tools and metrics to measure it through Lighthouse, with a clearly defined list of [objectives](https://developer.chrome.com/docs/lighthouse/accessibility/scoring), but a significant amount of research on the subject exists with clear [guidelines](https://www.wcag.com/).
I then just had to carefully go through those guidelines and implement them diligently.

While I started my task of getting this website up to the standards of this century (and having it at least just *build*), I at first softly cursed past me for deciding to go with a niche Haskell static site generator, not only because past me did not keep learning Haskell so it looks mostly alien now, but also because [Hakyll](https://jaspervdj.be/hakyll/) isn't so much a static site generator as a framework of helpers and utilities over the incredible [Pandoc](https://pandoc.org/) to *build* your own static site generator, which involves, among other things, compiling the damn thing from scratch, slowly.

It, however, quickly became apparent that past me was a genius, and I couldn't have picked better. It *is* a framework to build your own generator! So any weird, nonstandard behavior you want is just one incantation of Haskell and an annoying compilation process away!

- Semantic landmarks like `header`, `nav`, `main`, `footer`? Just make sure to use a shared template and apply it to every page!
- `aria-current="page"` in nav? Use a Hakyll `Context` field to inspect the current route and inject `aria-current` automatically!

``` hs
navCurrentField :: String -> String -> Context a
navCurrentField name section = field ("nav-" ++ name ++ "-current") $ \item -> do
  path <- itemRoutePath item
  let sourcePath = toFilePath $ itemIdentifier item
  pure $
    if section `isPrefixOf` path || sourcePathMatches name sourcePath
      then " aria-current=\"page\""
      else ""
```

- Hidden `h1` on index/listing pages? Add a small boolean to the `Context` to let pages opt in or out!

``` hs
hiddenHeadingCtx :: Context String
hiddenHeadingCtx = boolField "hidden-heading" (const True)
```

And so many more things were made trivial with Hakyll and can be seen in the relatively simple `site.hs` [source](https://git.sr.ht/~aussetg/ausset.me/tree/master/item/site.hs).

## I like parsers too

But why stop at humans? Let's be honest, the most likely readers of this blog are Googlebot and his other search engine friends, so it might as well be *accessible* to them too.
Generating a `sitemap.xml` is just a matter of having Hakyll collect all the routes we generate and then create the XML file from it.

``` hs
sitemapStaticIdentifiers :: [Identifier]
sitemapStaticIdentifiers = -- fill in the blanks

sitemapUrls :: Compiler [String]
sitemapUrls = do
  posts <- getMatches blogPattern
  softwareProjects <- getMatches softwarePattern
  let identifiers = sitemapStaticIdentifiers ++ posts ++ softwareProjects
  mapMaybe id <$> mapM sitemapUrl identifiers

sitemapUrl :: Identifier -> Compiler (Maybe String)
sitemapUrl identifier = fmap absoluteUrl <$> getRoute identifier

renderSitemapXml :: [String] -> String
renderSitemapXml urls = -- fill in the blanks

renderSitemapUrl :: String -> String
renderSitemapUrl url = -- fill in the blanks

main :: IO ()
main = hakyllWith config $ do
  -- some routes
  create ["sitemap.xml"] $ do
    route idRoute
    compile $ do
      urls <- sitemapUrls
      makeItem $ renderSitemapXml urls
```

Similarly, gathering files from varied sources-the repositories that generate my CV or my thesis, for example-or my PGP key is just a matter of adding a `copyFileCompiler` route to `site.hs` with the illogical but mine destination I want.

``` hs
main :: IO ()
main = hakyllWith config $ do
  -- some routes
  match "thesis/*.pdf" $ do
    route idRoute
    compile copyFileCompiler

  match (fromList ["cv/cv.pdf", "cv/cv_compact.pdf"]) $ do
    let routeNoCV = gsubRoute "cv/" (const "")
    route routeNoCV
    compile copyFileCompiler
  -- more routes
```

I am sure those things are possible in many other static site generators, but I did not have to find out if my use case matched what a given SSG could be configured to do; I just wrote it that way.

Those are, however, extremely simple and frankly not particularly interesting examples, and they could have been solved just as easily, even if not as elegantly, by writing one or more scripts as a sidecar to any other SSG and wiring it up in the build process.
One thing that I think would have been a lot more annoying, or at least time-consuming, to implement as *just another script* is the ability to create and use [JSON-LD](https://json-ld.org/) structured data everywhere it makes sense.
As a heavy and dedicated user of [Obsidian](https://obsidian.md/), I am logically also in love with [Obsidian's Clipper](https://obsidian.md/clipper) to serve me as a bookmarking tool. One of Clipper's killer features is the ability to run templates based on URL patterns or the page's JSON-LD [Schema](https://schema.org/), and directly use the structured data contained in the schema to populate the note that will then be saved to the vault.
As a Clipper user (I actually "maintain" my own Clipper [fork](https://github.com/aussetg/obsidian-clipper), even though I have badly neglected it recently), I love when a page exposes structured data, and I selfishly wish every page on the web had the correct [Schema](https://schema.org/) to accompany it.
I may be selfish, but I am not hypocritical, and I would not allow myself to criticize others for not doing things I do not even do myself.
With a bit of LLM assistance to make up for all the Haskell I had now long forgotten, it was relatively straightforward to have Hakyll generate structured data for the blog, software, and publication pages, in addition to the standard structured data common to all pages. This made it possible to have entirely machine-parseable [`BlogPosting`](https://schema.org/BlogPosting), [`SoftwareSourceCode`](https://schema.org/SoftwareSourceCode), and [`ScholarlyArticle`](https://schema.org/ScholarlyArticle) schema on the matching pages, with the admittedly satisfyingly nerdy fact that the publication pages are generated directly from a `biblio.bib` file that is parsed directly in Haskell.

``` hs
blogPostStructuredDataField :: Context String
blogPostStructuredDataField = structuredDataField $ \item -> do
  let identifier = itemIdentifier item
  title <- metadataOr identifier "title" ""
  url <- itemCanonicalUrl item
  datePublished <- formatTime defaultTimeLocale "%Y-%m-%d" <$> getItemUTC defaultTimeLocale identifier
  pure
    [ schemaObject
        "BlogPosting"
        ( catMaybes
            [ jsonText "@id" url
            , jsonText "url" url
            , jsonText "mainEntityOfPage" url
            , jsonText "headline" title
            , jsonText "datePublished" datePublished
            , jsonValue "author" personReferenceJson
            ]
        )
    ]
-- etc.
```

At last, I am now free to judge others.

## Hello to the new netizens: agents

But there is one last public this site is now accessible to: agents, LLMs, clankers, "claws," or simply people who for some reason prefer to just `curl` pages.
Like many, I nowadays use LLMs as web-search interfaces (through [Kagi](https://kagi.com/), a fantastic web search engine, or directly via coding agents like [`pi`](www.pi.dev)), and I also have the sinking feeling that some people interested in me may just ask an LLM to read the site for them, not that I am judging.
Why serve messy HTML, especially after riddling it with `aria-` attributes, JSON-LD data, and other niceties like inlined, above-the-fold critical CSS, when they will try as hard as possible to convert it to the format they actually want: markdown?
This very post starts as a markdown file, so we are already pretty close to having the idea "agent-native" format. While this specific example isn't the best, as it is a purely self-contained markdown file, it is not the case for most of the pages that exist today on this site. A lot are constructed from templates and snippets of the form

``` html
<ul>
    $for(posts)$
        <li>
            <a href="$url$">$title$</a> - <time datetime="$date-iso$">$date$</time>
        </li>
    $endfor$
</ul>
```

which then get hydrated by Hakyll and passed to Pandoc to become the final HTML page.
But this doesn't have to be the case. Pandoc is a marvelous *everything to everything* text transformation tool (hence the name), and it can take almost any document format, transform it into its own intermediate representation, and then output it in any other format you want. Usually this means going from one format to a different one, but why not go from markdown (+ HTML-ish templates) to markdown?
This is exactly what I added to my website, and now every single page is available in both the `.html` and `.md` formats.

``` hs
main :: IO ()
main = hakyllWith config $ do
  -- some routes
  match "software/*.md" $ version agentMarkdownVersion $ do
    route idRoute
    compile $
      pandocMarkdown
      >>= loadAndApplyTemplate "templates/agent-software-project.md" softwareProjectCtx
```

From one of these formats, the other is automatically discoverable as a link, but I decided to go the extra mile and (in addition to the standard `rel="alternate"` link to help search engines) gently steer what I *know* are agents to the markdown format they will prefer.
Using Bunny's Edge Scripting, I detect the user agent of common LLM/Agent platforms, as well as of CURL, and serve them the markdown version of the page they are requesting:

``` js
const MARKDOWN_USER_AGENT_RE =
  /\b(ChatGPT-User|OAI-SearchBot|GPTBot|Claude-User|Claude-SearchBot|ClaudeBot|anthropic-ai|MistralAI-User|MistralAI-Index|Google-Agent|Google-NotebookLM|Perplexity-User|PerplexityBot|YouBot|DuckAssistBot|curl|Wget|HTTPie|httpie|python-requests|Python-urllib|aiohttp|httpx|Go-http-client|node-fetch|undici|axios)\b/i;
// ... Some important code
function markdownPathForHumanPath(pathname: string): string | null {
  // ... Some important case
  if (pathname.endsWith(".html")) {
    return pathname.slice(0, -".html".length) + ".md";
  }
  return null;
}
```

and because I believe *forcing* this redirection would be dishonest, agents are explicitly told they can just change their user agent or instead add `&format=html` to their request:

``` js
if (url.searchParams.get("format") === "html") {
  return null;
}
```

And with all this in place, I can proudly say that my website is *agent native*. I am, of course, open to investors and/or GPU grants to usher in a new age of ~~text files~~ agentic blogging.

## Closing Notes

While this post was written not so seriously, in large part to have some outlet to say *look! I did weird things with Hakyll!*, accessibility is an important consideration that I feel is too often overlooked.
I know, and have been guilty of that, that when designing software we often tell ourselves that either it's not important, not worth the effort, or that we will do it later and never do; but it is something we must collectively improve on and work toward in the content or software we produce.
It is easy to think that accessibility is just something for a minority; but not only is it forgetting that it improves the experience for everyone, but also that we all are beneficiaries of accessibility work done by someone else. The very font you are reading exists because someone worked hard to make something readable, the screen you are using is the result of decades of engineering prowess so that you can see better; the mouse and keyboard in front of you, or the UI of the OS you are using, are marvels of design with the sole goal of making it easier for you to use your computer. You are the beneficiary of untold efforts to make things more accessible to you, so just try to repay it even just a little bit; I promise it is easy and worth it.

If you are here and you think that actually some element on my website, the software I produced, or really anything, please do reach out and let me know. I can't promise I will be able to fix it, but I can promise I will try my best.

