diff --git a/board/_theme.py b/board/_theme.py
index 5c8a2ac..792a48c 100644
--- a/board/_theme.py
+++ b/board/_theme.py
@@ -760,6 +760,13 @@ def mark(key):
background:var(--warn)}
a.go:hover{opacity:.9;text-decoration:none}
.mdsrc{font-size:.85em}
+.board-source-link{display:inline-flex;align-items:center;justify-content:center;flex:none;width:44px;height:44px;border-radius:6px;color:var(--muted);text-decoration:none}
+.board-source-link:hover{color:var(--accent);background:var(--btn)}
+.board-source-link:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
+.board-source-link svg{width:18px;height:18px;pointer-events:none}
+p:has(>.board-source-link){display:flex;align-items:center;gap:.5rem;max-width:none}
+p>.board-source-link{order:1;margin-left:auto}
+.board-section>summary>.board-source-link{margin-left:auto}
"""
# One organ chip colour per scheme, appended to the sheet so the family
@@ -1244,8 +1251,9 @@ def portable_prompt(payload):
def section_layout(page, summaries=None):
"""Compose an owner's rendered board into the shared disclosure layout.
- Preserve the owner's HTML verbatim, including IDs and copy payloads. Only
- major heading groups are wrapped; existing details and card headings are
+ Preserve owner IDs, destinations and copy payloads. Markdown source links
+ become accessible icons; major heading groups are wrapped, while existing
+ details and card headings are
left alone. Optional summaries map section IDs to owner-supplied count,
status and label fields. This helper never infers health from prose.
"""
@@ -1296,6 +1304,41 @@ def handle_endtag(self, tag):
break
tree = Tree(page)
+ # Normalize the existing source-link convention once for every consumer.
+ # Match the complete label, never arbitrary links to Markdown documents.
+ source_edits = []
+ for node in tree.nodes:
+ if node['tag'] != 'a' or not node['end'] or not node['attrs'].get('href'):
+ continue
+ label = _html.unescape(_re.sub('<[^>]*>', '', page[node['inner']:node['close']])).strip()
+ if label.lower() != 'markdown version':
+ continue
+ attrs = dict(node['attrs'])
+ attrs['class'] = (attrs.get('class', '') + ' board-source-link').strip()
+ attrs['title'] = attrs['aria-label'] = 'Markdown version'
+ attributes = ''.join(' ' + key + (f'="{_html.escape(value, quote=True)}"'
+ if value is not None else '') for key, value in attrs.items())
+ icon = ('')
+ start, end = node['start'], node['end']
+ parent = node['parent']
+ if parent and parent['tag'] == 'p':
+ # Page-level links share a utility row; its separator is redundant
+ # once the source link moves to the far right.
+ trailing = _re.match(r'\s*·\s*', page[end:parent['close']])
+ preceding = _re.search(r'\s*·\s*$', page[parent['inner']:start])
+ if trailing:
+ end += trailing.end()
+ elif preceding:
+ start = parent['inner'] + preceding.start()
+ source_edits.append((start, end, '' + icon + ''))
+ for start, end, replacement in reversed(source_edits):
+ page = page[:start] + replacement + page[end:]
+ if source_edits:
+ tree = Tree(page)
summaries = summaries or {}
def classes(node):
@@ -1387,8 +1430,15 @@ def headings(node):
badges += (f''
+ _html.escape(str(info.get('label') or status)) + '')
title = page[heading['start']:heading['end']]
+ sources = [n for n in tree.nodes if n['tag'] == 'a' and n['end']
+ and 'board-source-link' in classes(n)
+ and heading['inner'] <= n['start'] < heading['close']]
+ source_links = ''.join(page[n['start']:n['end']] for n in sources)
+ for node in reversed(sources):
+ left, right = node['start'] - heading['start'], node['end'] - heading['start']
+ title = title[:left] + title[right:]
content = page[start:heading['start']] + page[heading['end']:end]
- replacement = ('' + title + badges
+ replacement = ('' + title + badges + source_links
+ '
' + content + '
')
edits.append((start, end, replacement))
covered_until = end
diff --git a/docs/board-navigation.md b/docs/board-navigation.md
index 6a0c3e1..b872ef9 100644
--- a/docs/board-navigation.md
+++ b/docs/board-navigation.md
@@ -108,3 +108,13 @@ synthetic test data, not current operational state.


+
+### Markdown source links
+
+The shared section layout renders links labelled “markdown version” as small
+document icons, preserving the owner’s URL. Section icons sit at the right of
+the summary after its count/status badges; page-level source icons sit at the
+right of their utility row. Each link has a “Markdown version” tooltip and
+accessible name, visible keyboard focus and a 44px click target. Following the
+link does not toggle the disclosure. Other Markdown links retain their labels.
+This presentation is applied when a consuming board is next regenerated.
diff --git a/tests/test_board.py b/tests/test_board.py
index 5cecd62..4edf620 100644
--- a/tests/test_board.py
+++ b/tests/test_board.py
@@ -543,7 +543,7 @@ def test_html_is_self_contained_with_one_tap_payloads(tmp_path):
assert 'data-cmd="Use the intake skill."' in page
# The header line every sibling board carries: the markdown twin and the
# way back to the repository front door on github.com.
- assert 'markdown version' in page
+ assert '') in page
diff --git a/tests/test_intake_dashboard.py b/tests/test_intake_dashboard.py
index f45307b..0672ec4 100644
--- a/tests/test_intake_dashboard.py
+++ b/tests/test_intake_dashboard.py
@@ -623,8 +623,8 @@ def test_html_sections_link_their_markdown_source(tmp_path):
"repos.yaml": REPOS_YAML})
html = _html(mind)
for src in ("active.md", "epics.md", "planned.md"):
- assert f'/blob/main/{src}">markdown version' in html, src
- assert '/tree/main/draft">markdown version' in html
+ assert f'/blob/main/{src}" title="Markdown version" aria-label="Markdown version">