diff --git a/shared/html.utils.spec.ts b/shared/html.utils.spec.ts
index fc590cb..b33d9df 100644
--- a/shared/html.utils.spec.ts
+++ b/shared/html.utils.spec.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { stripHtmlTags, decodeHtmlEntities, escapeHtml } from './html.utils';
+import { stripHtmlTags, decodeHtmlEntities, escapeHtml, ensureImageAlt } from './html.utils';
describe('stripHtmlTags', () => {
it('should return empty string for empty input', () => {
@@ -184,3 +184,42 @@ describe('escapeHtml and decodeHtmlEntities roundtrip', () => {
});
});
});
+
+describe('ensureImageAlt', () => {
+ it('adds an empty alt to a raw without alt', () => {
+ expect(ensureImageAlt('
')).toBe('
');
+ });
+
+ it('leaves an that already has an alt untouched', () => {
+ expect(ensureImageAlt('
')).toBe('
');
+ });
+
+ it('leaves an with an empty alt untouched', () => {
+ expect(ensureImageAlt('
')).toBe('
');
+ });
+
+ it('preserves a self-closing slash', () => {
+ expect(ensureImageAlt('
')).toBe('
');
+ });
+
+ it('only touches images without alt when several are present', () => {
+ const input = '




')).toBe('
');
+ });
+
+ it('is not fooled by an "alt" substring inside another attribute', () => {
+ expect(ensureImageAlt('
')).toBe('
');
+ });
+
+ it('leaves non-img content unchanged', () => {
+ expect(ensureImageAlt('link')).toBe('link');
+ });
+
+ it('returns an empty string unchanged', () => {
+ expect(ensureImageAlt('')).toBe('');
+ });
+});
diff --git a/shared/html.utils.ts b/shared/html.utils.ts
index 8aeb971..64bcdcd 100644
--- a/shared/html.utils.ts
+++ b/shared/html.utils.ts
@@ -35,3 +35,19 @@ export function escapeHtml(text: string): string {
.replace(//g, '>');
}
+
+/**
+ * Ensure every
+`;
+ const parser = new JekyllMarkdownParser(baseUrl, linkBasePath);
+ const result = parser.parse(input);
+
+ // Manual raw
+`;
+ const parser = new JekyllMarkdownParser(baseUrl, linkBasePath);
+ const result = parser.parse(input);
+
+ expect(result.html).toContain('alt="Photo"');
+ expect(result.html).not.toContain('alt=""');
+ });
+
it('should handle ./ prefix by stripping it', () => {
const input = `---
title: Test
diff --git a/shared/jekyll-markdown-parser.ts b/shared/jekyll-markdown-parser.ts
index 4a29c00..6d1a015 100644
--- a/shared/jekyll-markdown-parser.ts
+++ b/shared/jekyll-markdown-parser.ts
@@ -4,7 +4,7 @@ import { Marked, Renderer, Tokens } from 'marked';
import { markedHighlight } from 'marked-highlight';
import { gfmHeadingId, getHeadingList, resetHeadings } from './gfm-heading-id';
import hljs from 'highlight.js';
-import { escapeHtml } from './html.utils';
+import { escapeHtml, ensureImageAlt } from './html.utils';
// Precompiled regexes for performance
const PROTOCOL_REGEX = /^\w+:/;
@@ -322,7 +322,11 @@ export class JekyllMarkdownParser {
const html = this.marked.parse(processedMarkdown) as string;
const headingIds = getHeadingList().map(h => h.id);
const withImages = this.transformRelativeImagePaths(html);
- const finalHtml = this.transformRelativeLinks(withImages);
+ const withLinks = this.transformRelativeLinks(withImages);
+ // Late a11y pass on the FINAL html: images come both from Markdown (which already carries
+ // an alt via imageRenderer) and as manual raw