This commit is contained in:
@@ -35,6 +35,26 @@ making changes; it is the authoritative operations and deployment runbook.
|
||||
- Make focused changes. Avoid opportunistic refactors, design changes, or
|
||||
content rewrites outside the requested task.
|
||||
|
||||
## Tags and filtering
|
||||
|
||||
- Treat the child records under `content/tags/` as the single controlled tag
|
||||
vocabulary. Their lowercase, hyphenated directory names are the canonical
|
||||
slugs stored in entry `tags` fields and used in public tag URLs.
|
||||
- Keep the entry model's checkbox choices dynamically sourced from
|
||||
`content/tags/`; do not duplicate the vocabulary in model configuration or
|
||||
introduce free-form spelling and capitalization variants.
|
||||
- Add a tag record before assigning its slug to an entry. When renaming or
|
||||
retiring a tag, update all references deliberately and verify that no orphan
|
||||
slug or broken tag URL remains.
|
||||
- Preserve normal links to dedicated tag pages as the no-JavaScript baseline.
|
||||
Client-side filtering must remain a progressive enhancement with semantic
|
||||
buttons, keyboard support, and an announced result count.
|
||||
- Tag-directory counts and dedicated tag pages must include both blog entries
|
||||
and articles, but not project records unless the documented product scope is
|
||||
explicitly changed.
|
||||
- When `README.md` changes, keep the README-derived example blog entry aligned
|
||||
with the runbook content it demonstrates.
|
||||
|
||||
## Validation
|
||||
|
||||
- Inspect the existing implementation before editing; do not infer that a file,
|
||||
@@ -46,6 +66,9 @@ making changes; it is the authoritative operations and deployment runbook.
|
||||
- When template changes rely on updated CSS or other static assets, increment
|
||||
the relevant cache-busting version in the template and verify that generated
|
||||
pages reference the new asset URL.
|
||||
- For taxonomy changes, verify every entry tag has a matching `content/tags/`
|
||||
record, tag counts match rendered blog/article usage, dedicated tag URLs
|
||||
build, and listing filters work with valid, absent, and unknown query values.
|
||||
- Run `git diff --check` and review the exact diff before committing.
|
||||
- If a push was requested, verify the Gitea Action result, deployed revision,
|
||||
and site health instead of treating a successful `git push` as completion.
|
||||
|
||||
@@ -59,9 +59,10 @@ server is not part of production serving; nginx serves generated files only.
|
||||
```text
|
||||
Labyricorn.lektorproject Lektor project definition
|
||||
content/ Editable site content (`contents.lr` files)
|
||||
content/tags/ Controlled tag vocabulary and dedicated tag routes
|
||||
models/ Lektor content models
|
||||
templates/ Jinja templates
|
||||
assets/static/ CSS, favicon, and other static source assets
|
||||
assets/static/ CSS, filtering JavaScript, favicon, and static assets
|
||||
.gitignore Excludes generated and local files
|
||||
README.md This runbook
|
||||
```
|
||||
@@ -99,6 +100,56 @@ Do not edit generated files under `/srv/labyricorn/current` or
|
||||
`/srv/labyricorn/releases`. They will be replaced by deployment and are not
|
||||
source-controlled.
|
||||
|
||||
## Tags and filtering workflow
|
||||
|
||||
`content/tags/` is the single source of truth for the controlled tag
|
||||
vocabulary. Each child directory is a normal Lektor record whose directory name
|
||||
is the canonical lowercase, hyphenated slug used in entry data and URLs:
|
||||
|
||||
```text
|
||||
content/tags/deployment/contents.lr -> /tags/deployment/
|
||||
```
|
||||
|
||||
The tag record supplies the editor label and public description:
|
||||
|
||||
_model: tag
|
||||
---
|
||||
title: Deployment
|
||||
---
|
||||
summary: Build, release, rollout, verification, and rollback workflows.
|
||||
|
||||
The entry model reads its checkbox choices directly from these records. Blog
|
||||
entries and articles store the selected slugs as a comma-separated `tags`
|
||||
value:
|
||||
|
||||
```text
|
||||
tags: lektor, deployment, operations
|
||||
```
|
||||
|
||||
To add a tag, create its `content/tags/<slug>/contents.lr` record, run a build,
|
||||
and then select it on the relevant entries. Do not add a slug directly to an
|
||||
entry before its tag record exists. To rename or retire a tag, update every
|
||||
referencing entry deliberately and verify that no old slug remains before
|
||||
renaming or removing its record.
|
||||
|
||||
`/tags/` lists every approved tag and counts its usage across blog entries and
|
||||
articles. Dedicated tag URLs list matching entries from both sections. Tag
|
||||
links are normal links and work without JavaScript. On the Blog and Articles
|
||||
listing pages, `assets/static/tags.js` progressively adds instant filtering and
|
||||
keeps the selected filter in the `tag` query parameter.
|
||||
|
||||
After taxonomy or filtering changes:
|
||||
|
||||
1. Run `lektor build --output-path build`.
|
||||
2. Verify `/tags/`, at least one dedicated tag URL, and both section listings.
|
||||
3. Confirm tag counts cover blog entries and articles and that every entry slug
|
||||
has a matching tag record.
|
||||
4. Test filtering with JavaScript enabled and confirm tag links remain usable
|
||||
without JavaScript.
|
||||
5. Check keyboard operation plus desktop and narrow layouts.
|
||||
6. If filtering JavaScript or tag styles changed, increment the corresponding
|
||||
cache-busting version in `templates/base.html`.
|
||||
|
||||
## Local development
|
||||
|
||||
Install Lektor in an isolated Python environment. For example:
|
||||
|
||||
+60
-8
@@ -59,6 +59,63 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
|
||||
.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; }
|
||||
.entry-tags a,
|
||||
.post-tags a,
|
||||
.post-tags span {
|
||||
border: 1px solid var(--teal);
|
||||
padding: 3px 7px;
|
||||
color: var(--teal);
|
||||
font: 500 9px var(--mono);
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.entry-tags a:hover,
|
||||
.entry-tags a:focus-visible,
|
||||
.post-tags a:hover,
|
||||
.post-tags a:focus-visible { border-color: var(--ink); color: var(--ink); }
|
||||
.tag-filter { display: none; flex-wrap: wrap; gap: 8px; padding: 22px 0; }
|
||||
.js .tag-filter { display: flex; }
|
||||
.tag-filter-button {
|
||||
border: 1px solid var(--rule);
|
||||
background: transparent;
|
||||
padding: 7px 10px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
font: 500 9px var(--mono);
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tag-filter-button:hover,
|
||||
.tag-filter-button:focus-visible,
|
||||
.tag-filter-button.is-active { border-color: var(--teal); color: var(--teal); }
|
||||
.filter-status {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
.filter-empty,
|
||||
.empty-state { margin: 28px 0 0; color: var(--muted); }
|
||||
.tag-directory { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; border: 1px solid var(--rule); background: var(--rule); }
|
||||
.tag-directory-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 10px 24px;
|
||||
min-height: 170px;
|
||||
padding: 28px;
|
||||
background: var(--bg);
|
||||
}
|
||||
.tag-directory-card:hover,
|
||||
.tag-directory-card:focus-visible { background: var(--panel); }
|
||||
.tag-directory-name { font: 600 21px/1.3 var(--serif); }
|
||||
.tag-directory-count { color: var(--teal); font: 500 9px var(--mono); letter-spacing: .1em; text-transform: uppercase; }
|
||||
.tag-directory-summary { grid-column: 1 / -1; color: var(--muted); }
|
||||
.content-section { padding: 55px 0 80px; }
|
||||
.prose { max-width: 720px; }
|
||||
.prose p { color: #c8c5bc; }
|
||||
@@ -109,14 +166,6 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
}
|
||||
.post-info dd { margin: 0; color: var(--ink); font-size: 14px; }
|
||||
.post-tags { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.post-tags span {
|
||||
border: 1px solid var(--teal);
|
||||
padding: 3px 7px;
|
||||
color: var(--teal);
|
||||
font: 500 9px var(--mono);
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.article-info .post-info-title { color: var(--teal); }
|
||||
.publication-links { display: grid; gap: 9px; margin: 0; padding: 0; list-style: none; }
|
||||
.publication-links a {
|
||||
@@ -143,6 +192,9 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
.hero, .hero.compact { padding-block: 44px; }
|
||||
h1 { font-size: 30px; }
|
||||
.card-grid { grid-template-columns: 1fr; }
|
||||
.tag-directory { grid-template-columns: 1fr; }
|
||||
.tag-filter { padding-block: 18px; }
|
||||
.tag-directory-card { min-height: 0; }
|
||||
.blog-post-layout,
|
||||
.article-post-layout {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
(() => {
|
||||
document.documentElement.classList.add("js");
|
||||
|
||||
for (const filter of document.querySelectorAll("[data-tag-filter]")) {
|
||||
const section = filter.closest(".activity-section");
|
||||
const cards = [...section.querySelectorAll("[data-filter-item]")];
|
||||
const buttons = [...filter.querySelectorAll("[data-tag-value]")];
|
||||
const count = section.querySelector("[data-entry-count]");
|
||||
const status = section.querySelector("[data-filter-status]");
|
||||
const empty = section.querySelector("[data-filter-empty]");
|
||||
const availableTags = new Set(buttons.map((button) => button.dataset.tagValue));
|
||||
|
||||
const applyFilter = (tag, updateUrl) => {
|
||||
let visibleCount = 0;
|
||||
|
||||
for (const card of cards) {
|
||||
const tags = new Set((card.dataset.tags || "").split(" ").filter(Boolean));
|
||||
const visible = !tag || tags.has(tag);
|
||||
card.hidden = !visible;
|
||||
if (visible) visibleCount += 1;
|
||||
}
|
||||
|
||||
for (const button of buttons) {
|
||||
const active = button.dataset.tagValue === tag;
|
||||
button.classList.toggle("is-active", active);
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
}
|
||||
|
||||
const entryLabel = visibleCount === 1 ? "entry" : "entries";
|
||||
count.textContent = `${visibleCount} ${entryLabel}`;
|
||||
status.textContent = tag
|
||||
? `Showing ${visibleCount} ${entryLabel} tagged ${buttons.find((button) => button.dataset.tagValue === tag)?.textContent.trim()}.`
|
||||
: `Showing all ${visibleCount} ${entryLabel}.`;
|
||||
empty.hidden = visibleCount !== 0;
|
||||
|
||||
if (updateUrl) {
|
||||
const url = new URL(window.location.href);
|
||||
if (tag) url.searchParams.set("tag", tag);
|
||||
else url.searchParams.delete("tag");
|
||||
window.history.replaceState({}, "", url);
|
||||
}
|
||||
};
|
||||
|
||||
for (const button of buttons) {
|
||||
button.addEventListener("click", () => applyFilter(button.dataset.tagValue, true));
|
||||
button.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
applyFilter(button.dataset.tagValue, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const requestedTag = new URL(window.location.href).searchParams.get("tag") || "";
|
||||
applyFilter(availableTags.has(requestedTag) ? requestedTag : "", false);
|
||||
}
|
||||
})();
|
||||
@@ -8,12 +8,7 @@ updated: 2026-08-11
|
||||
---
|
||||
author: Example Article Author
|
||||
---
|
||||
tags:
|
||||
|
||||
Lorem Ipsum
|
||||
Information Card
|
||||
Field Mapping
|
||||
Example
|
||||
tags: lorem-ipsum, information-card, field-mapping, example
|
||||
---
|
||||
kicker: Article
|
||||
---
|
||||
@@ -38,7 +33,7 @@ follows:
|
||||
- **Updated** is read from the optional `updated` field.
|
||||
- **Reading time** is calculated automatically from the words in the `body`
|
||||
field, using approximately 200 words per minute.
|
||||
- **Topics** are read from the multiline `tags` field.
|
||||
- **Topics** are read from the controlled checkbox `tags` field.
|
||||
- **Published online** loops over every item in the multiline `published_urls`
|
||||
field.
|
||||
|
||||
|
||||
@@ -6,12 +6,7 @@ date: 2026-08-11
|
||||
---
|
||||
author: Example Blog Author
|
||||
---
|
||||
tags:
|
||||
|
||||
Lektor
|
||||
Deployment
|
||||
Operations
|
||||
Example
|
||||
tags: lektor, deployment, operations, example
|
||||
---
|
||||
kicker: Blog
|
||||
---
|
||||
@@ -27,7 +22,7 @@ back to this record's `contents.lr` file:
|
||||
|
||||
- **Author** is read from the `author` field.
|
||||
- **Published** is read from the `date` field.
|
||||
- **Tags** are read from the multiline `tags` field.
|
||||
- **Tags** are read from the controlled checkbox `tags` field.
|
||||
|
||||
The title, summary, and article body are supplied by `title`, `summary`, and
|
||||
`body`, respectively, but those values appear outside the information card.
|
||||
@@ -93,9 +88,10 @@ server is not part of production serving; nginx serves generated files only.
|
||||
```text
|
||||
Labyricorn.lektorproject Lektor project definition
|
||||
content/ Editable site content (`contents.lr` files)
|
||||
content/tags/ Controlled tag vocabulary and dedicated tag routes
|
||||
models/ Lektor content models
|
||||
templates/ Jinja templates
|
||||
assets/static/ CSS, favicon, and other static source assets
|
||||
assets/static/ CSS, filtering JavaScript, favicon, and static assets
|
||||
.gitignore Excludes generated and local files
|
||||
README.md This runbook
|
||||
```
|
||||
@@ -133,6 +129,56 @@ Do not edit generated files under `/srv/labyricorn/current` or
|
||||
`/srv/labyricorn/releases`. They will be replaced by deployment and are not
|
||||
source-controlled.
|
||||
|
||||
## Tags and filtering workflow
|
||||
|
||||
`content/tags/` is the single source of truth for the controlled tag
|
||||
vocabulary. Each child directory is a normal Lektor record whose directory name
|
||||
is the canonical lowercase, hyphenated slug used in entry data and URLs:
|
||||
|
||||
```text
|
||||
content/tags/deployment/contents.lr -> /tags/deployment/
|
||||
```
|
||||
|
||||
The tag record supplies the editor label and public description:
|
||||
|
||||
_model: tag
|
||||
---
|
||||
title: Deployment
|
||||
---
|
||||
summary: Build, release, rollout, verification, and rollback workflows.
|
||||
|
||||
The entry model reads its checkbox choices directly from these records. Blog
|
||||
entries and articles store the selected slugs as a comma-separated `tags`
|
||||
value:
|
||||
|
||||
```text
|
||||
tags: lektor, deployment, operations
|
||||
```
|
||||
|
||||
To add a tag, create its `content/tags/<slug>/contents.lr` record, run a build,
|
||||
and then select it on the relevant entries. Do not add a slug directly to an
|
||||
entry before its tag record exists. To rename or retire a tag, update every
|
||||
referencing entry deliberately and verify that no old slug remains before
|
||||
renaming or removing its record.
|
||||
|
||||
`/tags/` lists every approved tag and counts its usage across blog entries and
|
||||
articles. Dedicated tag URLs list matching entries from both sections. Tag
|
||||
links are normal links and work without JavaScript. On the Blog and Articles
|
||||
listing pages, `assets/static/tags.js` progressively adds instant filtering and
|
||||
keeps the selected filter in the `tag` query parameter.
|
||||
|
||||
After taxonomy or filtering changes:
|
||||
|
||||
1. Run `lektor build --output-path build`.
|
||||
2. Verify `/tags/`, at least one dedicated tag URL, and both section listings.
|
||||
3. Confirm tag counts cover blog entries and articles and that every entry slug
|
||||
has a matching tag record.
|
||||
4. Test filtering with JavaScript enabled and confirm tag links remain usable
|
||||
without JavaScript.
|
||||
5. Check keyboard operation plus desktop and narrow layouts.
|
||||
6. If filtering JavaScript or tag styles changed, increment the corresponding
|
||||
cache-busting version in `templates/base.html`.
|
||||
|
||||
## Local development
|
||||
|
||||
Install Lektor in an isolated Python environment. For example:
|
||||
|
||||
@@ -6,11 +6,7 @@ date: 2026-08-11
|
||||
---
|
||||
author: Labyricorn
|
||||
---
|
||||
tags:
|
||||
|
||||
Lektor
|
||||
Git
|
||||
Publishing
|
||||
tags: lektor, git, publishing
|
||||
---
|
||||
kicker: Blog
|
||||
---
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
_model: tag-index
|
||||
---
|
||||
title: Tags
|
||||
---
|
||||
eyebrow: Taxonomy
|
||||
---
|
||||
summary: Browse the controlled vocabulary used across blog entries and articles.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Deployment
|
||||
---
|
||||
summary: Build, release, rollout, verification, and rollback workflows.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Example
|
||||
---
|
||||
summary: Demonstration content that explains how the publishing system works.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Field Mapping
|
||||
---
|
||||
summary: Connections between Lektor content fields and their rendered output.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Git
|
||||
---
|
||||
summary: Version control, review, and repository-centered publishing.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Information Card
|
||||
---
|
||||
summary: Metadata presentation for posts and long-form articles.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Lektor
|
||||
---
|
||||
summary: Content modeling, authoring, and static-site generation with Lektor.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Lorem Ipsum
|
||||
---
|
||||
summary: Placeholder prose used to exercise article layouts and metadata.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Operations
|
||||
---
|
||||
summary: Hosting, service management, observability, and production procedures.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Publishing
|
||||
---
|
||||
summary: Editorial workflows, content delivery, and production releases.
|
||||
+4
-1
@@ -20,7 +20,10 @@ type = string
|
||||
|
||||
[fields.tags]
|
||||
label = Tags
|
||||
type = strings
|
||||
type = checkboxes
|
||||
source = site.query('/tags')
|
||||
item_label = {{ this.title }}
|
||||
description = Select tags from the controlled vocabulary in content/tags.
|
||||
|
||||
[fields.kicker]
|
||||
label = Type
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[model]
|
||||
name = Tag Index
|
||||
label = {{ this.title }}
|
||||
|
||||
[children]
|
||||
order_by = title
|
||||
|
||||
[fields.title]
|
||||
label = Title
|
||||
type = string
|
||||
|
||||
[fields.eyebrow]
|
||||
label = Eyebrow
|
||||
type = string
|
||||
|
||||
[fields.summary]
|
||||
label = Summary
|
||||
type = text
|
||||
@@ -0,0 +1,11 @@
|
||||
[model]
|
||||
name = Tag
|
||||
label = {{ this.title }}
|
||||
|
||||
[fields.title]
|
||||
label = Display Name
|
||||
type = string
|
||||
|
||||
[fields.summary]
|
||||
label = Summary
|
||||
type = text
|
||||
+3
-1
@@ -9,7 +9,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=3">
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=4">
|
||||
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
@@ -42,6 +43,7 @@
|
||||
<span>© 2026 Labyricorn — GENERATED FROM GIT, NEVER HAND-EDITED</span>
|
||||
<nav aria-label="Footer navigation">
|
||||
<a href="mailto:[email protected]">Contact</a>
|
||||
<a href="{{ '/tags'|url }}">Tags</a>
|
||||
<a href="https://github.com/">GitHub</a>
|
||||
<a href="https://git.labyricorn.com/">Gitea</a>
|
||||
<a href="https://ko-fi.com/">Ko-fi</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/tag-links.html" import tag_links with context %}
|
||||
{% block body %}
|
||||
{% if this.parent._id == 'blog' %}
|
||||
<article class="blog-post-layout shell">
|
||||
@@ -22,7 +23,7 @@
|
||||
<div class="post-info-row">
|
||||
<dt>Tags</dt>
|
||||
<dd class="post-tags">
|
||||
{% for tag in this.tags %}<span>{{ tag }}</span>{% endfor %}
|
||||
{{ tag_links(this.tags) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@@ -70,7 +71,7 @@
|
||||
<div class="post-info-row">
|
||||
<dt>Topics</dt>
|
||||
<dd class="post-tags">
|
||||
{% for tag in this.tags %}<span>{{ tag }}</span>{% endfor %}
|
||||
{{ tag_links(this.tags) }}
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{% macro tag_links(tags) -%}
|
||||
{% for tag in tags %}
|
||||
{% set tag_record = site.get('/tags/' ~ tag) %}
|
||||
{% if tag_record %}<a href="{{ tag_record|url }}" rel="tag">{{ tag_record.title }}</a>{% else %}<span>{{ tag }}</span>{% endif %}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
+23
-3
@@ -1,5 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/tag-links.html" import tag_links with context %}
|
||||
{% block body %}
|
||||
{% set filter_tags = namespace(values=[]) %}
|
||||
{% if this._id in ['blog', 'articles'] %}
|
||||
{% for item in this.children %}
|
||||
{% for tag in item.tags %}
|
||||
{% if tag not in filter_tags.values %}{% set filter_tags.values = filter_tags.values + [tag] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<section class="hero compact">
|
||||
<div class="shell narrow">
|
||||
<p class="eyebrow">{{ this.eyebrow }}</p>
|
||||
@@ -8,18 +17,29 @@
|
||||
</section>
|
||||
<section class="activity-section">
|
||||
<div class="shell">
|
||||
<div class="section-heading"><span>{{ this.title }}</span><span>{{ this.children.count() }} entries</span></div>
|
||||
<div class="section-heading"><span>{{ this.title }}</span><span data-entry-count>{{ this.children.count() }} entries</span></div>
|
||||
{% if filter_tags.values %}
|
||||
<div class="tag-filter" data-tag-filter aria-label="Filter {{ this.title }} by tag">
|
||||
<button class="tag-filter-button is-active" type="button" data-tag-value="" aria-pressed="true">All</button>
|
||||
{% for tag in filter_tags.values|sort %}
|
||||
{% set tag_record = site.get('/tags/' ~ tag) %}
|
||||
{% if tag_record %}<button class="tag-filter-button" type="button" data-tag-value="{{ tag }}" aria-pressed="false">{{ tag_record.title }}</button>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="filter-status" data-filter-status aria-live="polite"></p>
|
||||
{% endif %}
|
||||
<div class="card-grid">
|
||||
{% for item in this.children %}
|
||||
<article class="entry-card accent-{{ item.kicker|lower }}">
|
||||
<article class="entry-card accent-{{ item.kicker|lower }}"{% if filter_tags.values %} data-filter-item data-tags="{{ item.tags|join(' ') }}"{% endif %}>
|
||||
<div class="entry-meta"><span class="label">{{ item.kicker }}</span><time>{{ item.date|dateformat('YYYY.MM.dd') }}</time></div>
|
||||
<h2><a href="{{ item|url }}">{{ item.title }}</a></h2>
|
||||
<p>{{ item.summary }}</p>
|
||||
{% if item.tags %}<div class="entry-tags">{{ tag_links(item.tags) }}</div>{% endif %}
|
||||
<a class="read-more" href="{{ item|url }}">Read entry →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if filter_tags.values %}<p class="filter-empty" data-filter-empty hidden>No entries use this tag.</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<section class="hero compact">
|
||||
<div class="shell narrow">
|
||||
<p class="eyebrow">{{ this.eyebrow }}</p>
|
||||
<h1>{{ this.summary }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="activity-section">
|
||||
<div class="shell">
|
||||
<div class="section-heading"><span>{{ this.title }}</span><span>{{ this.children.count() }} approved tags</span></div>
|
||||
<div class="tag-directory">
|
||||
{% for tag in this.children %}
|
||||
{% set usage = namespace(count=0) %}
|
||||
{% for section_id in ['blog', 'articles'] %}
|
||||
{% for item in site.query('/' ~ section_id) %}
|
||||
{% if tag._id in item.tags %}{% set usage.count = usage.count + 1 %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<a class="tag-directory-card" href="{{ tag|url }}">
|
||||
<span class="tag-directory-name">{{ tag.title }}</span>
|
||||
<span class="tag-directory-count">{{ usage.count }} {% if usage.count == 1 %}entry{% else %}entries{% endif %}</span>
|
||||
<span class="tag-directory-summary">{{ tag.summary }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/tag-links.html" import tag_links with context %}
|
||||
{% block body %}
|
||||
{% set matches = namespace(items=[]) %}
|
||||
{% for section_id in ['blog', 'articles'] %}
|
||||
{% for item in site.query('/' ~ section_id) %}
|
||||
{% if this._id in item.tags %}{% set matches.items = matches.items + [item] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<section class="hero compact">
|
||||
<div class="shell narrow">
|
||||
<p class="eyebrow">Tag</p>
|
||||
<h1>{{ this.title }}</h1>
|
||||
<p class="dek">{{ this.summary }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="activity-section">
|
||||
<div class="shell">
|
||||
<div class="section-heading">
|
||||
<span>Tagged {{ this.title }}</span>
|
||||
<span>{{ matches.items|length }} {% if matches.items|length == 1 %}entry{% else %}entries{% endif %}</span>
|
||||
</div>
|
||||
{% if matches.items %}
|
||||
<div class="card-grid">
|
||||
{% for item in matches.items|sort(attribute='date', reverse=true) %}
|
||||
<article class="entry-card accent-{{ item.kicker|lower }}">
|
||||
<div class="entry-meta"><span class="label">{{ item.kicker }}</span><time>{{ item.date|dateformat('YYYY.MM.dd') }}</time></div>
|
||||
<h2><a href="{{ item|url }}">{{ item.title }}</a></h2>
|
||||
<p>{{ item.summary }}</p>
|
||||
{% if item.tags %}<div class="entry-tags">{{ tag_links(item.tags) }}</div>{% endif %}
|
||||
<a class="read-more" href="{{ item|url }}">Read entry →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No blog entries or articles use this tag yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user