This commit is contained in:
@@ -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 0–1, 2–3, 4–7, 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`.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user