External links: screen-reader hint on links that leave the blog

External links in post bodies already opened in a new tab and showed an
icon, but the icon is invisible to screen readers, so nothing announced
that the link leaves the site.

- blogex: ExternalLinks.rewrite/1 appends
  <span class="blogex-external-hint"> (opens in a new tab)</span> as the
  last child of every external anchor. No aria-label, so the accessible
  name still contains the visible link text (WCAG 2.5.3); the icon colour
  and 2.5.3 both hold. Idempotent: a second pass adds no second hint.
- blogex: anchors are now matched together with their closing tag
  (the hint goes inside), so a stray unclosed <a> is left alone.
- app: sr-only rule for .blogex-external-hint in .blogex-post-body. The
  split stays as before: blogex marks up, the host app styles.
- tests: hint cases in blogex (appended, per link, after inner markup,
  idempotent, not on internal/mailto/anchor links) + anchors without href
  (Blogex.ExternalLinks now at 100% line coverage), and an app test that
  the hint reaches the rendered post page. Two exact-string expectations
  updated for the intended markup change.

make test: app 179, blogex 162. make verify: green.
This commit is contained in:
Willem van den Ende
2026-10-26 20:28:16 +00:00
parent 4b4d0c491c
commit e3fc734862
5 changed files with 105 additions and 12 deletions
+16
View File
@@ -219,6 +219,22 @@ body { font-family: 'Source Sans 3', sans-serif; }
-webkit-mask-size: contain;
}
/* Screen-reader hint injected by Blogex.ExternalLinks at the end of every
external link's text. Visually hidden (the icon above is the visual cue),
still announced: "Wikipedia, link, opens in a new tab". */
.blogex-post-body .blogex-external-hint {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border-width: 0;
}
.blogex-post-body blockquote { border-left: 3px solid oklch(var(--color-primary)); padding-left: 1rem; margin-top: 1rem; margin-bottom: 1rem; font-style: italic; opacity: 0.85; }
.blogex-tag-list {
@@ -79,6 +79,13 @@ defmodule FirehoseWeb.BlogTest do
assert body =~ "target=\"_blank\" rel=\"noopener\" class=\"blogex-external\""
end
test "rendered post page carries the screen-reader hint for external links", %{conn: conn} do
body =
conn |> get("/blog/engineering/delayed-gratification-in-ai-summer") |> html_response(200)
assert body =~ ~s|<span class="blogex-external-hint"> (opens in a new tab)</span>|
end
end
describe "input validation" do