<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Daniel Gustafsson</title>
    <description>The latest articles on Forem by Daniel Gustafsson (@labontese).</description>
    <link>https://forem.com/labontese</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3705767%2Fd08dd2dc-081a-4fed-baa5-88008ab4fc33.jpeg</url>
      <title>Forem: Daniel Gustafsson</title>
      <link>https://forem.com/labontese</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/labontese"/>
    <language>en</language>
    <item>
      <title>One Redis Instance, Three Jobs: DevOps for AI Agents Without the Overkill</title>
      <dc:creator>Daniel Gustafsson</dc:creator>
      <pubDate>Sat, 14 Mar 2026 17:51:47 +0000</pubDate>
      <link>https://forem.com/labontese/one-redis-instance-three-jobs-devops-for-ai-agents-without-the-overkill-gm7</link>
      <guid>https://forem.com/labontese/one-redis-instance-three-jobs-devops-for-ai-agents-without-the-overkill-gm7</guid>
      <description>&lt;p&gt;I've built my share of microservices over the years. It usually ends with an architecture where every service has its own database, its own cache, its own queue, and a 200 line YAML file just to hold everything together in Docker Compose.&lt;/p&gt;

&lt;p&gt;When I started experimenting with AI agents, I expected the same story. A vector database here, a message queue there, a cache service, a state store. But it turned out that Redis Stack handles all of it. And it simplifies operations more than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The entire infrastructure: one docker compose yaml
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis/redis-stack:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8001:8001"&lt;/span&gt;  &lt;span class="c1"&gt;# RedisInsight&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis_data:/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;11434:11434"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama_models:/root/.ollama&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/api/tags&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama_models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. Two containers. No Postgres, no Pinecone, no RabbitMQ, no separate Memcached. Redis handles all three jobs and Ollama runs models locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Redis actually does (and why it is elegant)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Conversation memory (checkpointer)
&lt;/h3&gt;

&lt;p&gt;LangGraph needs somewhere to persist conversation state, such as which messages were sent, which tools were called, and what the results were. &lt;code&gt;RedisSaver&lt;/code&gt; handles that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;RedisSaver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_conn_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_react_agent&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each thread (&lt;code&gt;thread_id&lt;/code&gt;) gets its own history. Restart the agent and reconnect to the same thread? It picks up right where it left off. No manual serialization, no migrations.&lt;/p&gt;

&lt;p&gt;From an ops perspective: these are just regular Redis keys. You can inspect them in RedisInsight, set TTL to automatically clean up old conversations, and monitor memory usage with &lt;code&gt;INFO memory&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Long term memory (vector index)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;RedisStore&lt;/code&gt; with a vector index gives you semantic search. Data is stored with embeddings and can be queried based on &lt;em&gt;meaning&lt;/em&gt;, not exact string matching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;RedisStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_conn_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dims&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;distance_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point from a DevOps perspective: you do not need to run a separate vector database. No Milvus, no Qdrant, no Weaviate. Redis Stack includes RediSearch out of the box, and it is more than sufficient for this kind of workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Semantic cache
&lt;/h3&gt;

&lt;p&gt;The same question phrased slightly differently, like "what is WCAG?" versus "explain WCAG to me", produces the same answer. Instead of sending both through the LLM, we cache responses based on vector proximity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redisvl.extensions.llmcache&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SemanticCache&lt;/span&gt;

&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redis_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://localhost:6379&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;distance_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;distance_threshold&lt;/code&gt; of 0.1 means queries with cosine distance ≤0.1 get cached responses. In practice, this means very similar questions. Where exactly to set this threshold depends on your embedding model (different models spread their vectors differently), so experiment. TTL of 3600 seconds automatically cleans up stale data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three completely different use cases. Same &lt;code&gt;redis://localhost:6379&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  No state in the application
&lt;/h2&gt;

&lt;p&gt;This is the biggest win for operations. The agent itself is &lt;em&gt;stateless&lt;/em&gt;. All state lives in Redis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conversation history: Redis (checkpointer)&lt;/li&gt;
&lt;li&gt;Saved memories: Redis (vector index)&lt;/li&gt;
&lt;li&gt;Cached responses: Redis (semantic cache)&lt;/li&gt;
&lt;li&gt;Scan history: Redis (vector index)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restart the agent without losing anything&lt;/li&gt;
&lt;li&gt;Run multiple instances behind a load balancer&lt;/li&gt;
&lt;li&gt;Scale horizontally without shared state in memory&lt;/li&gt;
&lt;li&gt;Deploy new versions with zero downtime (rolling update)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice: &lt;code&gt;docker compose restart agent&lt;/code&gt; and the user notices nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama as a local inference server
&lt;/h2&gt;

&lt;p&gt;Ollama abstracts away model management. You pull a model once, then it is exposed as an HTTP API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen3.5:4b      &lt;span class="c"&gt;# 2.5 GB, requires ~4 GB VRAM&lt;/span&gt;
ollama pull nomic-embed-text  &lt;span class="c"&gt;# 274 MB, for embeddings&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the agent, it looks like any other API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOllama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen3.5:4b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://ollama:11434&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Docker service name
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key management. No rate limiting. No cost per token. Models run locally on your GPU.&lt;/p&gt;

&lt;p&gt;Want to swap models? Change an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CHAT_MODEL=qwen3.5:4b
EMBEDDING_MODEL=nomic-embed-text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No code changes. Ollama handles the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring without extra tooling
&lt;/h2&gt;

&lt;p&gt;Redis has built in monitoring that goes a long way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Memory usage&lt;/span&gt;
redis-cli INFO memory | &lt;span class="nb"&gt;grep &lt;/span&gt;used_memory_human

&lt;span class="c"&gt;# Key count&lt;/span&gt;
redis-cli DBSIZE

&lt;span class="c"&gt;# Live command stream&lt;/span&gt;
redis-cli MONITOR

&lt;span class="c"&gt;# Slow queries&lt;/span&gt;
redis-cli SLOWLOG GET 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RedisInsight (port 8001 in the docker compose) gives you a web UI to inspect keys, run queries, and view memory graphs. It is included in the &lt;code&gt;redis-stack&lt;/code&gt; image.&lt;/p&gt;

&lt;p&gt;For Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Which models are loaded?&lt;/span&gt;
curl http://localhost:11434/api/tags

&lt;span class="c"&gt;# How much VRAM is being used?&lt;/span&gt;
nvidia-smi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need more? A Prometheus exporter exists for Redis (&lt;code&gt;redis_exporter&lt;/code&gt;) and is straightforward to set up. But for most use cases, the built in tools are enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost model is different
&lt;/h2&gt;

&lt;p&gt;With cloud AI (OpenAI, Claude, etc) you pay per token. That makes costs unpredictable, as an agent that makes many tool calls can get expensive.&lt;/p&gt;

&lt;p&gt;With Ollama locally, the cost is fixed to your hardware. A machine with an RTX 4070 (12 GB VRAM) costs around $1,500 and runs &lt;code&gt;qwen3.5:4b&lt;/code&gt; fast enough for production.&lt;/p&gt;

&lt;p&gt;Rough math:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud (e.g. Qwen 3 72b via OpenRouter): ~$0.005 per scan. 200 scans per day = $30 per month&lt;/li&gt;
&lt;li&gt;Local (Qwen 3.5 4b): ~$0 per scan. Unlimited.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference becomes dramatic at volume. And the local model works well enough for tasks with clear context.&lt;/p&gt;

&lt;p&gt;You can also mix them. Run locally for 90% of traffic and fall back to a cloud model for complex queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backup and persistence
&lt;/h2&gt;

&lt;p&gt;Redis Stack with &lt;code&gt;appendonly yes&lt;/code&gt; (default in redis stack) gives you AOF persistence. Every write is logged to disk. On restart, everything is restored.&lt;/p&gt;

&lt;p&gt;Backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Snapshot&lt;/span&gt;
redis-cli BGSAVE
&lt;span class="nb"&gt;cp&lt;/span&gt; /data/dump.rdb /backup/redis.rdb

&lt;span class="c"&gt;# Or copy AOF&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /data/appendonly.aof /backup/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama models are cached in &lt;code&gt;/root/.ollama&lt;/code&gt;. Mount it as a Docker volume and models survive container restarts without needing to be downloaded again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently in production
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Set &lt;code&gt;maxmemory&lt;/code&gt; and eviction policy:&lt;/strong&gt; Redis without a memory limit on a shared machine is a ticking time bomb. &lt;code&gt;maxmemory-policy allkeys-lru&lt;/code&gt; automatically evicts the oldest entries.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;TTL on everything that does not need to live forever:&lt;/strong&gt; Cached LLM responses: 1 hour. Conversation history: 7 days. Scan history: keep.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Separate Redis instances per environment:&lt;/strong&gt; Dev, staging, prod should not share data. Use key prefixes (&lt;code&gt;dev:&lt;/code&gt;, &lt;code&gt;staging:&lt;/code&gt;, &lt;code&gt;prod:&lt;/code&gt;) or ideally separate Redis instances entirely. Avoid logical databases (&lt;code&gt;/0&lt;/code&gt;, &lt;code&gt;/1&lt;/code&gt;, &lt;code&gt;/2&lt;/code&gt;). RediSearch and other modules only work on database 0, and clustering does not support them either.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Health checks in docker compose:&lt;/strong&gt; Already included in the example above. If you add an agent service, use &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; so it does not start before Redis and Ollama are ready.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Log token usage:&lt;/strong&gt; Even with local models, you want to know how much inference you are running. It helps with capacity planning.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;What makes this stack attractive for DevOps is its simplicity. Two containers. No external state. Standard monitoring tools. Predictable cost.&lt;/p&gt;

&lt;p&gt;Redis is no longer just a cache. With the Stack distribution, it replaces three or four services that would otherwise require separate operations. LangGraph abstracts away agent orchestration. Ollama turns LLM inference into a local service.&lt;/p&gt;

&lt;p&gt;Bottom line: less to operate, less that can break, easier to debug.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stack: Redis Stack, LangGraph, Ollama. Everything runs in Docker. You need a GPU with ≥8 GB VRAM for local models, or point Ollama at a cloud endpoint.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Published: March 2026 | Daniel Gustafsson&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>devops</category>
      <category>redis</category>
    </item>
    <item>
      <title>The Human Side of Web Accessibility, Beyond Compliance Checklists</title>
      <dc:creator>Daniel Gustafsson</dc:creator>
      <pubDate>Tue, 13 Jan 2026 12:58:56 +0000</pubDate>
      <link>https://forem.com/labontese/the-human-side-of-web-accessibility-beyond-compliance-checklists-1hfg</link>
      <guid>https://forem.com/labontese/the-human-side-of-web-accessibility-beyond-compliance-checklists-1hfg</guid>
      <description>&lt;h1&gt;
  
  
  The Human Side of Web Accessibility: Beyond Compliance Checklists
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;1.3 billion people&lt;/strong&gt; live with some form of disability worldwide. That's not a statistic - that's your users, your customers, your colleagues, and eventually, all of us.&lt;/p&gt;

&lt;p&gt;When we talk about accessibility in tech, we often jump straight to WCAG criteria, automated scanners, and compliance audits. But before we dive into the code, let's talk about &lt;em&gt;why&lt;/em&gt; this matters so deeply.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Temporary Nature of Ability
&lt;/h2&gt;

&lt;p&gt;Here's something that changed how I think about accessibility:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Disability is a mismatch between a person and their environment."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You don't need a permanent condition to experience this mismatch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Holding a baby&lt;/strong&gt; with one arm - temporarily one-handed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bright sunlight on your screen&lt;/strong&gt; - temporarily low vision
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loud environment&lt;/strong&gt; - temporarily deaf&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken arm&lt;/strong&gt; - temporarily motor impaired&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aging&lt;/strong&gt; - gradually everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you build for accessibility, you're building for &lt;em&gt;everyone&lt;/em&gt; - including your future self.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Stories, Real Impact
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Blind Developer Who Tested My Code
&lt;/h3&gt;

&lt;p&gt;I once shipped a "beautiful" dashboard with drag-and-drop widgets. A blind developer on our team tried to use it. His screen reader announced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Button. Button. Button. Clickable. Image. Button."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;40 minutes of his day - wasted. Not because he couldn't do his job, but because I hadn't done mine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The CEO With RSI
&lt;/h3&gt;

&lt;p&gt;A company executive developed repetitive strain injury. Suddenly, using a mouse was painful. She needed to navigate everything with a keyboard. Our application - which we thought was modern and polished - became unusable for her.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Parent in a Meeting
&lt;/h3&gt;

&lt;p&gt;A developer told me: "I need captions on your tutorial videos. Not because I'm deaf - because my toddler is sleeping in the next room."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Business Case (Because Sometimes You Need It)
&lt;/h2&gt;

&lt;p&gt;Let's be honest: sometimes empathy alone doesn't get budget approval. Here's what does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Numbers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Global disabled population&lt;/td&gt;
&lt;td&gt;1.3 billion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spending power in the US alone&lt;/td&gt;
&lt;td&gt;$490 billion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target.com accessibility lawsuit&lt;/td&gt;
&lt;td&gt;$6 million settlement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domino's Supreme Court case&lt;/td&gt;
&lt;td&gt;Company lost, forced to remediate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Accessibility isn't charity. It's market access.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  From Empathy to Action: The Technical Implementation
&lt;/h2&gt;

&lt;p&gt;Now that we understand &lt;em&gt;why&lt;/em&gt;, let's talk &lt;em&gt;how&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;At &lt;a href="https://wiki.holmdigital.se" rel="noopener noreferrer"&gt;Holm Digital&lt;/a&gt;, we built an open-source ecosystem that bridges the gap between technical WCAG validation and legal compliance.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Automated Scanning That Speaks Human
&lt;/h3&gt;

&lt;p&gt;Most scanners tell you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X WCAG 1.4.3 violation: contrast ratio 3.2:1 &amp;lt; 4.5:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our scanner tells you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X Low contrast makes this text hard to read

Legal: EN 301 549 Clause 9.1.4.3 | Swedish DOS-lagen 12 P
Risk: HIGH - This affects users with low vision
Fix: Darken the text color from #767676 to #595959

Impact: ~8% of your users may struggle to read this
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The difference?&lt;/strong&gt; Context. Risk. Human impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Components That Don't Let You Fail
&lt;/h3&gt;

&lt;p&gt;Instead of teaching every developer ARIA patterns, we built React components that are accessible by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FormField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Dialog&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@holmdigital/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// This automatically handles:&lt;/span&gt;
&lt;span class="c1"&gt;// - Correct ARIA attributes&lt;/span&gt;
&lt;span class="c1"&gt;// - Focus management  &lt;/span&gt;
&lt;span class="c1"&gt;// - Keyboard navigation&lt;/span&gt;
&lt;span class="c1"&gt;// - 44px touch targets (not 32px!)&lt;/span&gt;
&lt;span class="c1"&gt;// - Color contrast&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormField&lt;/span&gt; 
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Email Address"&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;
  &lt;span class="na"&gt;helpText&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"We'll never share your email."&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No special knowledge required. Just use the component.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Regulatory Mapping for Non-Technical Stakeholders
&lt;/h3&gt;

&lt;p&gt;Legal teams don't care about aria-labelledby. They care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are we compliant with the European Accessibility Act?&lt;/li&gt;
&lt;li&gt;What's our exposure under Section 508?&lt;/li&gt;
&lt;li&gt;Can we prove due diligence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our standards database maps every technical rule to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WCAG 2.1 criteria&lt;/strong&gt; (A, AA, AAA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EN 301 549 clauses&lt;/strong&gt; (EU standard)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;National laws&lt;/strong&gt; (DOS-lagen, Section 508, AODA, BITV, RGAA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk levels&lt;/strong&gt; (critical, high, medium, low)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now your compliance report speaks their language.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Practical Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Start With Automated Checks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add to your CI pipeline&lt;/span&gt;
npx hd-a11y-scan https://your-staging-url.com &lt;span class="nt"&gt;--ci&lt;/span&gt; &lt;span class="nt"&gt;--lang&lt;/span&gt; en
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches ~30% of issues automatically. Not everything, but a solid baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use Accessible Components
&lt;/h3&gt;

&lt;p&gt;Don't build buttons, forms, and dialogs from scratch. Use libraries that handle accessibility for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test With Real Assistive Technology
&lt;/h3&gt;

&lt;p&gt;Once a month, try this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate your app with only a keyboard&lt;/li&gt;
&lt;li&gt;Turn on VoiceOver (Mac) or NVDA (Windows) and close your eyes&lt;/li&gt;
&lt;li&gt;Zoom to 400% and try to complete a task&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Include Disabled Users in Testing
&lt;/h3&gt;

&lt;p&gt;Invite people who use assistive technology daily to test your product. Pay them - their expertise is valuable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Objections (And Counter-Arguments)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "We don't have disabled users"
&lt;/h3&gt;

&lt;p&gt;You don't &lt;em&gt;know&lt;/em&gt; you have disabled users. If your analytics show none, it's likely they &lt;em&gt;left&lt;/em&gt; because they couldn't use your product.&lt;/p&gt;

&lt;h3&gt;
  
  
  "We'll add accessibility later"
&lt;/h3&gt;

&lt;p&gt;Retrofitting accessibility is 10x more expensive than building it in.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Automated tools say we're fine"
&lt;/h3&gt;

&lt;p&gt;Automated tools catch 30% of issues. The other 70% require human judgment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://wiki.holmdigital.se" rel="noopener noreferrer"&gt;Accessibility Wiki&lt;/a&gt;&lt;/strong&gt; - Deep dives on regulations and compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/engine" rel="noopener noreferrer"&gt;@holmdigital/engine&lt;/a&gt;&lt;/strong&gt; - Regulatory scanning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/components" rel="noopener noreferrer"&gt;@holmdigital/components&lt;/a&gt;&lt;/strong&gt; - Accessible React components&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://webaim.org/" rel="noopener noreferrer"&gt;WebAIM&lt;/a&gt;&lt;/strong&gt; - Excellent tutorials&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Ask
&lt;/h2&gt;

&lt;p&gt;Next time you're building a feature, take 5 extra minutes to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add proper labels to your form fields&lt;/li&gt;
&lt;li&gt;Test keyboard navigation&lt;/li&gt;
&lt;li&gt;Check color contrast&lt;/li&gt;
&lt;li&gt;Add alt text to images&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Accessibility is not about perfect compliance. It's about gradually making things better, one commit at a time.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Connect:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://holmdigital.se" rel="noopener noreferrer"&gt;HolmDigital&lt;/a&gt; - Accessibility consulting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://wiki.holmdigital.se" rel="noopener noreferrer"&gt;Accessibility Wiki&lt;/a&gt; - Free documentation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/holmdigital/a11y-hd" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; - Open source tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Have you encountered accessibility challenges in your projects? I'd love to hear your stories in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>inclusivedesign</category>
      <category>ux</category>
    </item>
    <item>
      <title>Building a Regulatory-Compliant Accessibility Scanner: From WCAG to Legal Compliance</title>
      <dc:creator>Daniel Gustafsson</dc:creator>
      <pubDate>Sun, 11 Jan 2026 20:45:57 +0000</pubDate>
      <link>https://forem.com/labontese/building-a-regulatory-compliant-accessibility-scanner-from-wcag-to-legal-compliance-38an</link>
      <guid>https://forem.com/labontese/building-a-regulatory-compliant-accessibility-scanner-from-wcag-to-legal-compliance-38an</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s9cv7ii3pop6ve63v80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s9cv7ii3pop6ve63v80.png" alt="accessibility ecosystem that bridges technical validation" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Regulatory-Compliant Accessibility Scanner: From WCAG to Legal Compliance
&lt;/h2&gt;

&lt;p&gt;Most accessibility tools tell you &lt;em&gt;what's wrong&lt;/em&gt;. But when regulators come knocking, they don't ask "Did you fix the color contrast?" — they ask "Are you compliant with EN 301 549 clause 9.1.4.3?"&lt;/p&gt;

&lt;p&gt;This guide walks you through three open-source packages I built to bridge that gap: from technical WCAG validation to legal compliance reporting, with ready-to-use React components that are accessible by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem We're Solving
&lt;/h2&gt;

&lt;p&gt;Here's a typical accessibility tool output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ color-contrast: Elements must have sufficient color contrast ratio
   Expected: 4.5:1, Actual: 3.2:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's what a compliance auditor needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ WCAG 2.1 Success Criterion 1.4.3 (Level AA)
   EN 301 549 Reference: 9.1.4.3
   Swedish DOS-lagen: 12 § Lag (2018:1937)
   Risk Level: HIGH
   Remediation: Increase foreground/background contrast to minimum 4.5:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's build this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Packages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@holmdigital/engine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accessibility scanner + CLI&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/engine" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fnpm%2Fv%2F%40holmdigital%2Fengine.svg" alt="npm" width="80" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@holmdigital/standards&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Regulatory mapping database&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/standards" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fnpm%2Fv%2F%40holmdigital%2Fstandards.svg" alt="npm" width="80" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@holmdigital/components&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accessible React components&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/components" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fnpm%2Fv%2F%40holmdigital%2Fcomponents.svg" alt="npm" width="80" height="20"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 1: The Quick Win — CLI Scanning
&lt;/h2&gt;

&lt;p&gt;Install and scan any website in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hd-a11y-scan https://your-website.com &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔍 Scanning https://your-website.com...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  HolmDigital Regulatory Accessibility Report
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Score: 72/100
  Violations: 4 critical, 8 serious, 12 moderate

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  CRITICAL: Missing form labels
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  📋 WCAG 2.1: 1.3.1 (Level A)
  📋 EN 301 549: 9.1.3.1
  📋 Section 508: 1194.22(n)

  ⚠️  Risk: CRITICAL

  💡 Fix: Associate each input with a &amp;lt;label&amp;gt; element using 
     the 'for' attribute matching the input's 'id'.

  📍 Elements affected:
     - &amp;lt;input type="email" class="newsletter-input"&amp;gt;
     - &amp;lt;input type="text" class="search-box"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CLI Options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate PDF report&lt;/span&gt;
npx hd-a11y-scan https://example.com &lt;span class="nt"&gt;--pdf&lt;/span&gt; report.pdf

&lt;span class="c"&gt;# Mobile viewport&lt;/span&gt;
npx hd-a11y-scan https://example.com &lt;span class="nt"&gt;--viewport&lt;/span&gt; mobile

&lt;span class="c"&gt;# Swedish language output&lt;/span&gt;
npx hd-a11y-scan https://example.com &lt;span class="nt"&gt;--lang&lt;/span&gt; sv

&lt;span class="c"&gt;# JSON for CI/CD pipelines&lt;/span&gt;
npx hd-a11y-scan https://example.com &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 2: Programmatic Integration
&lt;/h2&gt;

&lt;p&gt;For deeper integration, use the scanner programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RegulatoryScanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLanguage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@holmdigital/engine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Set language context (affects rule descriptions)&lt;/span&gt;
&lt;span class="nf"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;auditWebsite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegulatoryScanner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;failOnCritical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Throws if critical issues found&lt;/span&gt;
    &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
      ✅ Accessibility Score: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100
      📊 Total Issues: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
      ⚠️  Critical: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Group by WCAG criterion&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byWcag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wcagCriterion&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;byWcag&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Critical accessibility violations found!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="nf"&gt;auditWebsite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-site.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Real-World Example: GitHub Actions Integration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Accessibility Audit&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;a11y-scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install &amp;amp; Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm ci&lt;/span&gt;
          &lt;span class="s"&gt;npm run build&lt;/span&gt;
          &lt;span class="s"&gt;npm run start &amp;amp;&lt;/span&gt;
          &lt;span class="s"&gt;sleep 5&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Accessibility Scan&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx hd-a11y-scan http://localhost:3000 --ci --lang en&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails the PR if any &lt;strong&gt;critical&lt;/strong&gt; accessibility violations are found.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Standards Database
&lt;/h2&gt;

&lt;p&gt;The real magic is in the regulatory mapping. Here's how it works under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;getEN301549Mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;getRulesByLanguage&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@holmdigital/standards&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Get the EN 301 549 mapping for a WCAG criterion&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getEN301549Mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.4.3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   wcagCriterion: '1.4.3',&lt;/span&gt;
&lt;span class="c1"&gt;//   wcagLevel: 'AA',&lt;/span&gt;
&lt;span class="c1"&gt;//   en301549Clause: '9.1.4.3',&lt;/span&gt;
&lt;span class="c1"&gt;//   description: 'Contrast (Minimum)',&lt;/span&gt;
&lt;span class="c1"&gt;//   risk: 'high',&lt;/span&gt;
&lt;span class="c1"&gt;//   remediation: {&lt;/span&gt;
&lt;span class="c1"&gt;//     description: 'Ensure text has a contrast ratio of at least 4.5:1',&lt;/span&gt;
&lt;span class="c1"&gt;//     technicalGuidance: 'Use CSS to adjust foreground or background colors...'&lt;/span&gt;
&lt;span class="c1"&gt;//   }&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;

&lt;span class="c1"&gt;// Get all rules for a specific language/region&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;swedenRules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRulesByLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usRules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRulesByLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-us&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Each rule includes national law references&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;swedenRules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;nationalLaw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   name: 'DOS-lagen',&lt;/span&gt;
&lt;span class="c1"&gt;//   reference: 'Lag (2018:1937) 12 §',&lt;/span&gt;
&lt;span class="c1"&gt;//   description: 'Lagen om tillgänglighet till digital offentlig service'&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Supported Regulations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Regulation&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EN 301 549, WCAG 2.1&lt;/td&gt;
&lt;td&gt;EU Generic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en-us&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Section 508, ADA&lt;/td&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en-ca&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AODA&lt;/td&gt;
&lt;td&gt;Canada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;en-gb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PSBAR&lt;/td&gt;
&lt;td&gt;United Kingdom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DOS-lagen&lt;/td&gt;
&lt;td&gt;Sweden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;de&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BITV 2.0&lt;/td&gt;
&lt;td&gt;Germany&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RGAA&lt;/td&gt;
&lt;td&gt;France&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Digitoegankelijk&lt;/td&gt;
&lt;td&gt;Netherlands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;es&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UNE 139803&lt;/td&gt;
&lt;td&gt;Spain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 4: Accessible React Components
&lt;/h2&gt;

&lt;p&gt;Stop reinventing the wheel. These components handle ARIA, focus management, and keyboard navigation for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;FormField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="nx"&gt;Heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SkipLink&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@holmdigital/components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ContactForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Skip link for keyboard users */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SkipLink&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#main-content"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Skip to main content
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SkipLink&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"main-content"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Semantic headings with enforced hierarchy */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Heading&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Contact Us&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Heading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Auto-generated labels, error states, ARIA */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormField&lt;/span&gt;
            &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Email Address"&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
            &lt;span class="na"&gt;required&lt;/span&gt;
            &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
            &lt;span class="na"&gt;helpText&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"We'll never share your email."&lt;/span&gt;
            &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormField&lt;/span&gt;
            &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Message"&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"textarea"&lt;/span&gt;
            &lt;span class="na"&gt;required&lt;/span&gt;
            &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Accessible button with loading state */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; 
            &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt; 
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;
            &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSubmitting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Send Message
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Focus-trapped modal dialog */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;
          &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Message Sent"&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Thank you! We'll respond within 24 hours.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Close
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What's Handled Automatically
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;WCAG Criterion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Focus trapping&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dialog&lt;/code&gt;, &lt;code&gt;Modal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2.4.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escape to close&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dialog&lt;/code&gt;, &lt;code&gt;Modal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2.1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Label association&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FormField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error announcement&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FormField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heading hierarchy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Heading&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skip navigation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SkipLink&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible focus&lt;/td&gt;
&lt;td&gt;All components&lt;/td&gt;
&lt;td&gt;2.4.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color contrast&lt;/td&gt;
&lt;td&gt;All components&lt;/td&gt;
&lt;td&gt;1.4.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Development Story
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why I Built This
&lt;/h3&gt;

&lt;p&gt;I was working with a Swedish agency that needed to prove compliance with DOS-lagen (Sweden's digital accessibility law). Standard tools gave them WCAG violations, but auditors wanted EN 301 549 clause references with Swedish legal context.&lt;/p&gt;

&lt;p&gt;Existing solutions were either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too technical&lt;/strong&gt; (WCAG-only, no legal mapping)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too expensive&lt;/strong&gt; (enterprise SaaS pricing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too manual&lt;/strong&gt; (consultants doing spreadsheet mappings)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture Decisions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Monorepo Structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
├── engine/      # Scanner + CLI (depends on standards)
├── standards/   # Regulatory database (no deps)
└── components/  # React UI library (no deps)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each package is independently publishable but designed to work together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Build Tooling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;tsup&lt;/code&gt; for building because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single config for CJS, ESM, and DTS&lt;/li&gt;
&lt;li&gt;Fast builds (Rollup under the hood)&lt;/li&gt;
&lt;li&gt;Tree-shakeable output
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tsup.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;esm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;dts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Exports Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lesson learned the hard way — always put &lt;code&gt;types&lt;/code&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;types&lt;/code&gt; comes after &lt;code&gt;import&lt;/code&gt;, some bundlers won't find your TypeScript definitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. i18n Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All user-facing strings are externalized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/i18n/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../locales/en.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sv&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../locales/sv.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ... other locales&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;currentLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentLang&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# CLI scanning (no install needed)&lt;/span&gt;
npx hd-a11y-scan https://your-site.com

&lt;span class="c"&gt;# For programmatic use&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @holmdigital/engine

&lt;span class="c"&gt;# For React components&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @holmdigital/components

&lt;span class="c"&gt;# For regulatory database only&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @holmdigital/standards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quick Integration Checklist
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add CI scanning&lt;/strong&gt; to catch regressions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace form components&lt;/strong&gt; with accessible versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add skip links&lt;/strong&gt; for keyboard navigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate PDF reports&lt;/strong&gt; for compliance documentation&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Browser extension for on-page scanning&lt;/li&gt;
&lt;li&gt;[ ] VS Code extension for inline warnings&lt;/li&gt;
&lt;li&gt;[ ] More national regulations (Australia, India, Japan)&lt;/li&gt;
&lt;li&gt;[ ] Automated fix suggestions with code generation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/holmdigital/a11y-hd" rel="noopener noreferrer"&gt;github.com/holmdigital/a11y-hd&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NPM&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/engine" rel="noopener noreferrer"&gt;@holmdigital/engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/standards" rel="noopener noreferrer"&gt;@holmdigital/standards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@holmdigital/components" rel="noopener noreferrer"&gt;@holmdigital/components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built with ❤️ by &lt;a href="https://wiki.holmdigital.se" rel="noopener noreferrer"&gt;Holm Digital&lt;/a&gt; — Making accessibility compliance actually achievable.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Did you find this useful?&lt;/strong&gt; Drop a comment below or connect with me on &lt;a href="https://linkedin.com/in/digitaltillg%C3%A4nglighet" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. I'd love to hear about your accessibility challenges!&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>typescript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
