Add homepage tag discovery matrix
Deploy production / deploy (push) Successful in 21s

This commit is contained in:
2026-08-23 16:51:54 -07:00
parent fe6025fa63
commit 623686590c
6 changed files with 244 additions and 5 deletions
+34
View File
@@ -112,6 +112,40 @@ prefers reduced motion. When hero styles or behavior change, increment the
corresponding `style.css` or `section-heroes.js` cache-busting version in
`templates/base.html`.
## Homepage tag discovery
The homepage ends with an **Explore the Signal** topic matrix immediately after
Recent Activity and Featured Projects and before the site footer. It is a
server-rendered discovery route over the canonical child records under
`content/tags/`; it does not maintain a separate topic list. Each topic is a
normal link to its dedicated `/tags/<slug>/` page and includes its current
published usage count.
Counts use the same discovery scope as the tag directory and dedicated tag
pages: projects, project devlog entries, blog entries, and articles. The matrix
uses four restrained visual levels for counts of 01, 23, 47, and 8 or more.
The displayed count remains exact while the highest visual level is capped, so
a frequently used tag cannot dominate the layout. Tag records and content
changes update the matrix automatically on the next normal project-aware build.
`assets/static/tags.js` progressively updates the Tag Summary readout when a
visitor hovers or focuses a topic. The summary is optional enhancement only:
every link remains visible, keyboard accessible, touch friendly, and usable
with JavaScript disabled. Styling lives in `assets/static/style.css`, uses the
existing Labyricorn gold treatment, wraps without horizontal scrolling, and
removes the small focus/hover lift for reduced-motion users. When these assets
change, increment their cache-busting versions in `templates/base.html`.
After changing the homepage matrix:
1. Run `python scripts/build_with_projects.py --output-path build` with the
Lektor runtime documented below.
2. Confirm the generated homepage topic count matches the tracked registry and
every topic links to its existing tag page with the expected usage count.
3. Check desktop and narrow layouts, keyboard focus, the summary enhancement,
normal link navigation, and the transition into the footer.
4. Run `python -m unittest discover -s tests -v` and `git diff --check`.
## Routine content workflow
Edit in a normal Git checkout, review the changes, and push them to `main`.
+108
View File
@@ -780,6 +780,106 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.featured-projects-empty { margin: 0; padding: 24px 0; color: var(--muted); }
.featured-projects-link { display: inline-block; margin-top: 22px; color: var(--teal); font: 500 10px var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.homepage-tag-section {
border-top: 1px solid var(--rule);
padding: 52px 0 58px;
background: linear-gradient(180deg, rgba(201,168,76,.025), transparent 72%);
}
.homepage-tag-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 28px 48px;
align-items: end;
}
.homepage-tag-header .eyebrow { margin-bottom: 10px; color: var(--mustard); }
.homepage-tag-header h2 { margin: 0; font-size: clamp(27px, 3vw, 36px); line-height: 1.2; }
.homepage-tag-header > div > p:last-child { max-width: 680px; margin: 12px 0 0; color: var(--muted); }
.homepage-tag-readout {
display: flex;
align-items: center;
gap: 9px;
margin: 0 0 4px;
color: var(--dim);
font: 500 9px var(--mono);
letter-spacing: .1em;
white-space: nowrap;
text-transform: uppercase;
}
.homepage-tag-readout span {
width: 6px;
height: 6px;
border: 1px solid var(--mustard);
background: rgba(201,168,76,.28);
box-shadow: 0 0 8px rgba(201,168,76,.25);
}
.tag-matrix { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 30px; }
.tag-matrix-link {
--tag-border: rgba(201,168,76,.26);
--tag-copy: #b7b3aa;
--tag-node: rgba(201,168,76,.42);
--tag-fill: rgba(201,168,76,.018);
display: inline-flex;
min-height: 42px;
align-items: center;
gap: 9px;
border: 1px solid var(--tag-border);
background: var(--tag-fill);
padding: 9px 12px;
color: var(--tag-copy);
font: 500 11px/1.2 var(--mono);
letter-spacing: .055em;
text-transform: uppercase;
transition: border-color .16s ease, background-color .16s ease, color .16s ease, transform .16s ease;
}
.tag-matrix-link::before {
content: "";
width: 5px;
height: 5px;
flex: 0 0 auto;
border: 1px solid var(--tag-node);
background: var(--tag-node);
}
.tag-matrix-level-2 {
--tag-border: rgba(201,168,76,.38);
--tag-copy: #c6c1b5;
--tag-node: rgba(201,168,76,.58);
--tag-fill: rgba(201,168,76,.028);
}
.tag-matrix-level-3 {
--tag-border: rgba(201,168,76,.53);
--tag-copy: #d8d2c4;
--tag-node: rgba(201,168,76,.76);
--tag-fill: rgba(201,168,76,.045);
}
.tag-matrix-level-4 {
--tag-border: rgba(201,168,76,.7);
--tag-copy: var(--ink);
--tag-node: var(--mustard);
--tag-fill: rgba(201,168,76,.07);
}
.tag-matrix-link:hover,
.tag-matrix-link:focus-visible {
border-color: var(--mustard);
background: rgba(201,168,76,.1);
color: var(--ink);
transform: translateY(-1px);
}
.tag-matrix-link:focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; }
.tag-matrix-count { color: var(--mustard); font-size: 9px; letter-spacing: .02em; opacity: .78; }
.tag-matrix-summary {
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
gap: 18px;
align-items: start;
min-height: 62px;
margin-top: 26px;
padding-top: 18px;
border-top: 1px solid rgba(39,48,57,.72);
}
.tag-matrix-summary-label { color: var(--mustard); font: 500 9px var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.tag-matrix-summary p { display: grid; gap: 3px; margin: 0; color: var(--muted); font-size: 13px; }
.tag-matrix-summary strong { color: var(--ink); font: 500 11px var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.card-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
.entry-card { min-height: 245px; padding: 28px; background: var(--bg); }
.entry-tags { display: flex; flex-wrap: wrap; gap: 7px; margin: 16px 0; }
@@ -1247,6 +1347,11 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
.circuit-replay { left: 16px; bottom: 13px; }
h1 { font-size: 30px; }
.homepage-discovery-grid { grid-template-columns: 1fr; gap: 48px; }
.homepage-tag-header { grid-template-columns: 1fr; gap: 16px; }
.homepage-tag-readout { white-space: normal; }
.tag-matrix { gap: 7px; margin-top: 24px; }
.tag-matrix-link { min-height: 44px; padding-inline: 11px; }
.tag-matrix-summary { grid-template-columns: 1fr; gap: 7px; }
.featured-project-card { padding: 20px; }
.card-grid { grid-template-columns: 1fr; }
.tag-directory { grid-template-columns: 1fr; }
@@ -1315,6 +1420,9 @@ h1 { max-width: 880px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
}
@media (prefers-reduced-motion: reduce) {
.tag-matrix-link { transition: none; }
.tag-matrix-link:hover,
.tag-matrix-link:focus-visible { transform: none; }
.circuit-status span,
.circuit-node-main i,
.section-hero,
+17
View File
@@ -54,4 +54,21 @@
const requestedTag = new URL(window.location.href).searchParams.get("tag") || "";
applyFilter(availableTags.has(requestedTag) ? requestedTag : "", false);
}
for (const matrix of document.querySelectorAll("[data-tag-matrix]")) {
const summaryTitle = matrix.querySelector("[data-tag-summary-title]");
const summaryText = matrix.querySelector("[data-tag-summary-text]");
if (!summaryTitle || !summaryText) continue;
for (const link of matrix.querySelectorAll("[data-tag-title]")) {
const showSummary = () => {
summaryTitle.textContent = link.dataset.tagTitle;
summaryText.textContent = link.dataset.tagSummary || `Open the ${link.dataset.tagTitle} archive.`;
};
link.addEventListener("mouseenter", showSummary);
link.addEventListener("focus", showSummary);
}
}
})();
+2 -2
View File
@@ -10,8 +10,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Inter:wght@400;500&family=Playfair+Display:ital,wght@0,600;0,700;1,500;1,600&display=swap" rel="stylesheet">
<link rel="icon" href="{{ '/static/favicon.svg'|url }}" type="image/svg+xml">
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=23">
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=24">
<script src="{{ '/static/tags.js'|url }}?v=2" defer></script>
<script src="{{ '/static/activity.js'|url }}?v=1" defer></script>
<script src="{{ '/static/hero-circuit.js'|url }}?v=2" defer></script>
<script src="{{ '/static/section-heroes.js'|url }}?v=1" defer></script>
+47
View File
@@ -181,3 +181,50 @@
</div>
</div>
</section>
{% set tracked_tags = site.get('/tags').children %}
<section class="homepage-tag-section" aria-labelledby="homepage-tag-heading" data-tag-matrix>
<div class="shell">
<div class="homepage-tag-header">
<div>
<p class="eyebrow">Topic map</p>
<h2 id="homepage-tag-heading">Explore the Signal</h2>
<p>A map of the topics that keep surfacing across Labyricorn. Pick a signal and follow it deeper into the site.</p>
</div>
<p class="homepage-tag-readout"><span aria-hidden="true"></span>{{ tracked_tags.count() }} tracked topics across Labyricorn</p>
</div>
<nav class="tag-matrix" aria-label="Explore content by topic">
{% for tag in tracked_tags %}
{% set usage = namespace(count=0) %}
{% for section_id in ['projects', 'blog', 'articles'] %}
{% for item in site.query('/' ~ section_id) %}
{% set item_tags = item.taxonomy_tags if item._model == 'project' else item.tags %}
{% if tag._id in item_tags %}{% set usage.count = usage.count + 1 %}{% endif %}
{% endfor %}
{% endfor %}
{% for project in site.query('/projects') %}
{% if project._model == 'project' %}
{% set devlog = site.get(project.path ~ '/devlog') %}
{% if devlog %}
{% for entry in devlog.children %}
{% if tag._id in entry.topic_tag_slugs %}{% set usage.count = usage.count + 1 %}{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{% set weight = 4 if usage.count >= 8 else (3 if usage.count >= 4 else (2 if usage.count >= 2 else 1)) %}
<a class="tag-matrix-link tag-matrix-level-{{ weight }}" href="{{ tag|url }}" rel="tag" data-tag-title="{{ tag.title }}" data-tag-summary="{{ tag.summary|striptags|trim }}">
<span class="tag-matrix-name">{{ tag.title }}</span>
<span class="tag-matrix-count" aria-hidden="true">{{ usage.count }}</span>
<span class="visually-hidden"> — {{ usage.count }} published {% if usage.count == 1 %}entry{% else %}entries{% endif %}</span>
</a>
{% endfor %}
</nav>
<div class="tag-matrix-summary" aria-live="polite" aria-atomic="true">
<span class="tag-matrix-summary-label">Tag Summary</span>
<p><strong data-tag-summary-title>Preview a topic</strong><span data-tag-summary-text>Hover or focus a topic to preview it. Select a topic to open its archive.</span></p>
</div>
</div>
</section>
+36 -3
View File
@@ -5,10 +5,14 @@ import unittest
class HomeTemplateTests(unittest.TestCase):
def test_recent_activity_adds_only_latest_devlog_from_each_project(self) -> None:
def setUp(self) -> None:
site_root = Path(__file__).resolve().parents[1]
template = (site_root / "templates" / "home.html").read_text(encoding="utf-8")
recent_activity = template.split(
self.template = (site_root / "templates" / "home.html").read_text(
encoding="utf-8"
)
def test_recent_activity_adds_only_latest_devlog_from_each_project(self) -> None:
recent_activity = self.template.split(
'<section class="homepage-panel featured-projects-panel"', 1
)[0]
@@ -19,6 +23,35 @@ class HomeTemplateTests(unittest.TestCase):
self.assertIn("entries.append(latest_devlog_entry)", recent_activity)
self.assertNotIn("for entry in devlog.children", recent_activity)
def test_tag_matrix_is_last_homepage_section_and_uses_canonical_tags(self) -> None:
matrix_position = self.template.index("{% set tracked_tags")
featured_position = self.template.index(
'class="homepage-panel featured-projects-panel"'
)
self.assertGreater(matrix_position, featured_position)
self.assertIn("site.get('/tags').children", self.template[matrix_position:])
self.assertIn('href="{{ tag|url }}"', self.template[matrix_position:])
self.assertNotIn("'/tags/' ~ tag", self.template[matrix_position:])
def test_tag_matrix_counts_all_discovery_content_and_caps_weighting(self) -> None:
matrix = self.template.split('class="homepage-tag-section"', 1)[1]
self.assertIn("['projects', 'blog', 'articles']", matrix)
self.assertIn("entry.topic_tag_slugs", matrix)
self.assertIn("usage.count >= 8", matrix)
self.assertIn("usage.count >= 4", matrix)
self.assertIn("usage.count >= 2", matrix)
self.assertIn("tag-matrix-level-{{ weight }}", matrix)
def test_tag_matrix_keeps_links_semantic_and_summary_optional(self) -> None:
matrix = self.template.split('class="homepage-tag-section"', 1)[1]
self.assertIn('aria-label="Explore content by topic"', matrix)
self.assertIn('rel="tag"', matrix)
self.assertIn("data-tag-summary", matrix)
self.assertIn("published {% if usage.count == 1 %}entry", matrix)
if __name__ == "__main__":
unittest.main()