This commit is contained in:
@@ -52,9 +52,12 @@ making changes; it is the authoritative operations and deployment runbook.
|
||||
- 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.
|
||||
- Tag-directory counts and dedicated tag pages must include projects, blog
|
||||
entries, and articles in the same discovery feed.
|
||||
- For remote projects, keep repository language labels and project-declared
|
||||
technology labels separate from their derived controlled tag slugs. Approved
|
||||
labels link to tag pages; unapproved labels remain visible, non-clickable, and
|
||||
mustard-colored but do not affect tag feeds or counts.
|
||||
- When `README.md` changes, keep the README-derived example blog entry aligned
|
||||
with the runbook content it demonstrates.
|
||||
|
||||
@@ -75,8 +78,9 @@ making changes; it is the authoritative operations and deployment runbook.
|
||||
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.
|
||||
record, tag counts match rendered project/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.
|
||||
|
||||
@@ -144,18 +144,27 @@ 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.
|
||||
`/tags/` lists every approved tag and counts its usage across projects, blog
|
||||
entries, and articles. Dedicated tag URLs combine matching records from all
|
||||
three sections in one chronological discovery feed. Tag links are normal links
|
||||
and work without JavaScript. On the Projects, Blog, and Articles listing pages,
|
||||
`assets/static/tags.js` progressively adds instant filtering and keeps the
|
||||
selected filter in the `tag` query parameter.
|
||||
|
||||
Remote-project languages and declared technologies are normalized to slugs and
|
||||
matched only against this controlled vocabulary. Their approved matches become
|
||||
clickable tags and a deduplicated `taxonomy_tags` field used by filters, counts,
|
||||
and tag pages. A label that has no approved record remains visible and
|
||||
non-clickable in mustard, produces a build warning, and is excluded from tag
|
||||
discovery. Remote data never creates a tag record automatically.
|
||||
|
||||
After taxonomy or filtering changes:
|
||||
|
||||
1. Run `python scripts/build_with_projects.py --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.
|
||||
2. Verify `/tags/`, at least one dedicated tag URL, and all three section listings.
|
||||
3. Confirm tag counts cover projects, blog entries, and articles; every stored
|
||||
entry slug has a matching tag record; and unapproved project labels are not
|
||||
counted.
|
||||
4. Test filtering with JavaScript enabled and confirm tag links remain usable
|
||||
without JavaScript.
|
||||
5. Check keyboard operation plus desktop and narrow layouts.
|
||||
@@ -225,7 +234,9 @@ revisions, metadata digests, synchronization status, and synchronization time.
|
||||
The project card combines repository-owned content with public provider and Git
|
||||
metadata: source and README links, default branch, latest commit, license,
|
||||
languages, open issues, stars, forks, latest release, imported revision, and
|
||||
sync time. API metadata failure uses cached metadata and is identified in the
|
||||
sync time. Approved language and technology labels link into the shared tag
|
||||
taxonomy, allowing a project to lead visitors to related writing. API metadata
|
||||
failure uses cached metadata and is identified in the
|
||||
sync status; it never causes an unvalidated new snapshot to replace a working
|
||||
one.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
--dim: #666b70;
|
||||
--rule: #273039;
|
||||
--teal: #51b6a5;
|
||||
--mustard: #c9a84c;
|
||||
--magenta: #d26186;
|
||||
--violet: #8c78d8;
|
||||
--mono: "IBM Plex Mono", ui-monospace, monospace;
|
||||
@@ -212,14 +213,18 @@ h1 { max-width: 650px; margin: 0; font-size: clamp(30px, 4vw, 45px); font-style:
|
||||
.repository-counts dd { font: 600 22px/1 var(--serif); }
|
||||
|
||||
.technology-list { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.technology-list span {
|
||||
.technology-list span,
|
||||
.technology-list a {
|
||||
border: 1px solid var(--teal);
|
||||
padding: 3px 7px;
|
||||
color: var(--teal);
|
||||
font: 500 9px var(--mono);
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.technology-list a { color: var(--teal); }
|
||||
.technology-list span { border-color: var(--mustard); color: var(--mustard); }
|
||||
.technology-list a:hover,
|
||||
.technology-list a:focus-visible { background: var(--teal); color: var(--bg); }
|
||||
.project-technologies { border-block: 1px solid var(--rule); padding: 36px 0 42px; background: var(--panel); }
|
||||
.project-technology-list { padding-top: 24px; }
|
||||
.project-devlog-preview { padding: 52px 0 80px; }
|
||||
|
||||
@@ -161,18 +161,19 @@ 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.
|
||||
`/tags/` lists every approved tag and counts its usage across projects, blog
|
||||
entries, and articles. Dedicated tag URLs combine matching records from all
|
||||
three sections. Tag links are normal links and work without JavaScript. On the
|
||||
Projects, 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.
|
||||
1. Run `python scripts/build_with_projects.py --output-path build`.
|
||||
2. Verify `/tags/`, at least one dedicated tag URL, and all three section listings.
|
||||
3. Confirm tag counts cover projects, blog entries, and articles and that every
|
||||
stored 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.
|
||||
|
||||
@@ -4,4 +4,4 @@ title: Tags
|
||||
---
|
||||
eyebrow: Taxonomy
|
||||
---
|
||||
summary: Browse the controlled vocabulary used across blog entries and articles.
|
||||
summary: Browse the controlled vocabulary used across projects, blog entries, and articles.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: HTML
|
||||
---
|
||||
summary: Semantic markup and document structure for the web.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: JavaScript
|
||||
---
|
||||
summary: Web and application development using JavaScript.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Local-first
|
||||
---
|
||||
summary: Software that keeps user data and primary operation on the local device.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Provenance
|
||||
---
|
||||
summary: Inspectable records of origin, transformation, and creative process.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: React
|
||||
---
|
||||
summary: User interfaces and application architecture built with React.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Rust
|
||||
---
|
||||
summary: Systems and application development using the Rust language.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: SQLite
|
||||
---
|
||||
summary: Embedded relational data storage powered by SQLite.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Tauri
|
||||
---
|
||||
summary: Native desktop applications built with the Tauri framework.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: TypeScript
|
||||
---
|
||||
summary: Statically typed JavaScript development with TypeScript.
|
||||
@@ -0,0 +1,5 @@
|
||||
_model: tag
|
||||
---
|
||||
title: Writing
|
||||
---
|
||||
summary: Tools, workflows, and ideas for writing and editorial work.
|
||||
@@ -58,6 +58,18 @@ type = strings
|
||||
label = Imported technologies
|
||||
type = strings
|
||||
|
||||
[fields.technology_labels]
|
||||
label = Technology display labels
|
||||
type = strings
|
||||
|
||||
[fields.technology_tag_slugs]
|
||||
label = Approved technology tags
|
||||
type = strings
|
||||
|
||||
[fields.taxonomy_tags]
|
||||
label = Unified taxonomy tags
|
||||
type = strings
|
||||
|
||||
[fields.body]
|
||||
label = Exhibition narrative
|
||||
type = markdown
|
||||
@@ -106,6 +118,14 @@ type = string
|
||||
label = Languages
|
||||
type = strings
|
||||
|
||||
[fields.repository_language_tags]
|
||||
label = Approved language tags
|
||||
type = strings
|
||||
|
||||
[fields.repository_language_labels]
|
||||
label = Language display labels
|
||||
type = strings
|
||||
|
||||
[fields.repository_open_issues]
|
||||
label = Open issues
|
||||
type = integer
|
||||
|
||||
@@ -321,6 +321,50 @@ def parse_record(data: bytes, path: str) -> tuple[dict[str, str], str]:
|
||||
return values, text
|
||||
|
||||
|
||||
def load_tag_vocabulary(site_root: Path) -> set[str]:
|
||||
tags_root = site_root / "content" / "tags"
|
||||
vocabulary: set[str] = set()
|
||||
if not tags_root.is_dir():
|
||||
raise ProjectSourceError("controlled tag vocabulary is missing")
|
||||
for record_path in tags_root.glob("*/contents.lr"):
|
||||
slug = record_path.parent.name
|
||||
if not SLUG_RE.fullmatch(slug):
|
||||
raise ProjectSourceError(f"invalid controlled tag slug: {slug}")
|
||||
record, _text = parse_record(record_path.read_bytes(), str(record_path))
|
||||
if record.get("_model") != "tag" or not record.get("title"):
|
||||
raise ProjectSourceError(f"invalid controlled tag record: {record_path}")
|
||||
vocabulary.add(slug)
|
||||
if not vocabulary:
|
||||
raise ProjectSourceError("controlled tag vocabulary is empty")
|
||||
return vocabulary
|
||||
|
||||
|
||||
def normalize_tag_value(value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "-", value.casefold()).strip("-")
|
||||
|
||||
|
||||
def approved_tag_slugs(
|
||||
project_id: str, source_name: str, values: list[str], vocabulary: set[str]
|
||||
) -> list[str]:
|
||||
approved: list[str] = []
|
||||
for value in values:
|
||||
slug = normalize_tag_value(value)
|
||||
if slug not in vocabulary:
|
||||
log(f"{project_id}: ignoring unapproved {source_name} tag {value!r}")
|
||||
continue
|
||||
if slug not in approved:
|
||||
approved.append(slug)
|
||||
return approved
|
||||
|
||||
|
||||
def taxonomy_label_items(values: list[str], approved_slugs: list[str]) -> list[str]:
|
||||
approved = set(approved_slugs)
|
||||
return [
|
||||
f"{normalize_tag_value(value) if normalize_tag_value(value) in approved else '-'}\t{value.replace(chr(9), ' ')}"
|
||||
for value in values
|
||||
]
|
||||
|
||||
|
||||
def validate_image(path: str, data: bytes) -> None:
|
||||
suffix = PurePosixPath(path).suffix.lower()
|
||||
signatures = {
|
||||
@@ -529,12 +573,33 @@ def materialize(
|
||||
metadata: dict[str, Any],
|
||||
synchronized_at: str,
|
||||
sync_status: str,
|
||||
tag_vocabulary: set[str],
|
||||
) -> None:
|
||||
project_root = destination_root / "content" / "projects" / source["project_id"]
|
||||
if project_root.exists():
|
||||
raise ProjectSourceError(f"refusing to overwrite existing project path: {project_root}")
|
||||
(project_root / "devlog").mkdir(parents=True)
|
||||
|
||||
technology_values = split_list(
|
||||
records[".labyricorn/project/contents.lr"]["tags"]
|
||||
)
|
||||
language_values = list(metadata["languages"])
|
||||
technology_tag_slugs = approved_tag_slugs(
|
||||
source["project_id"],
|
||||
"technology",
|
||||
technology_values,
|
||||
tag_vocabulary,
|
||||
)
|
||||
repository_language_tags = approved_tag_slugs(
|
||||
source["project_id"],
|
||||
"language",
|
||||
language_values,
|
||||
tag_vocabulary,
|
||||
)
|
||||
taxonomy_tags = list(
|
||||
dict.fromkeys(technology_tag_slugs + repository_language_tags)
|
||||
)
|
||||
|
||||
project_fields = {
|
||||
"kicker": "Project",
|
||||
"date": records[".labyricorn/project/contents.lr"]["started"],
|
||||
@@ -554,7 +619,16 @@ def materialize(
|
||||
"repository_latest_release_url": metadata["latest_release_url"],
|
||||
"synchronized_at": synchronized_at,
|
||||
"sync_status": sync_status,
|
||||
"technology_tags": split_list(records[".labyricorn/project/contents.lr"]["tags"]),
|
||||
"technology_tags": technology_values,
|
||||
"technology_labels": taxonomy_label_items(
|
||||
technology_values, technology_tag_slugs
|
||||
),
|
||||
"technology_tag_slugs": technology_tag_slugs,
|
||||
"repository_language_labels": taxonomy_label_items(
|
||||
language_values, repository_language_tags
|
||||
),
|
||||
"repository_language_tags": repository_language_tags,
|
||||
"taxonomy_tags": taxonomy_tags,
|
||||
}
|
||||
|
||||
for source_path, data in files.items():
|
||||
@@ -592,6 +666,7 @@ def metadata_digest(metadata: dict[str, Any]) -> str:
|
||||
|
||||
def sync_projects(site_root: Path, destination_root: Path, cache_root: Path) -> dict[str, Any]:
|
||||
sources = load_registry(site_root)
|
||||
tag_vocabulary = load_tag_vocabulary(site_root)
|
||||
cache_root.mkdir(parents=True, exist_ok=True)
|
||||
state = load_state(cache_root)
|
||||
next_projects_state = dict(state["projects"])
|
||||
@@ -644,6 +719,7 @@ def sync_projects(site_root: Path, destination_root: Path, cache_root: Path) ->
|
||||
metadata,
|
||||
synchronized_at,
|
||||
sync_status,
|
||||
tag_vocabulary,
|
||||
)
|
||||
public_metadata = {key: value for key, value in metadata.items() if key != "mirror_path"}
|
||||
next_projects_state[project_id] = {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<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=5">
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}?v=6">
|
||||
<script src="{{ '/static/tags.js'|url }}?v=1" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,3 +4,13 @@
|
||||
{% if tag_record %}<a href="{{ tag_record|url }}" rel="tag">{{ tag_record.title }}</a>{% else %}<span>{{ tag }}</span>{% endif %}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro taxonomy_labels(items) -%}
|
||||
{% for item in items %}
|
||||
{% set parts = item.split('\t', 1) %}
|
||||
{% if parts[0] != '-' %}
|
||||
{% set tag_record = site.get('/tags/' ~ parts[0]) %}
|
||||
{% if tag_record %}<a href="{{ tag_record|url }}" rel="tag">{{ parts[1] }}</a>{% else %}<span class="non-taxonomy-label">{{ parts[1] }}</span>{% endif %}
|
||||
{% else %}<span class="non-taxonomy-label">{{ parts[1] }}</span>{% endif %}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/tag-links.html" import tag_links, taxonomy_labels with context %}
|
||||
{% block body %}
|
||||
{% set devlog = site.get(this.path ~ '/devlog') %}
|
||||
<article class="project-page">
|
||||
@@ -48,10 +49,10 @@
|
||||
<dd>{{ this.repository_license }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
{% if this.repository_languages %}
|
||||
{% if this.repository_language_labels %}
|
||||
<div class="post-info-row">
|
||||
<dt>Languages</dt>
|
||||
<dd class="technology-list">{% for language in this.repository_languages %}<span>{{ language }}</span>{% endfor %}</dd>
|
||||
<dd class="technology-list">{{ taxonomy_labels(this.repository_language_labels) }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="repository-pair repository-counts">
|
||||
@@ -71,11 +72,11 @@
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{% if this.technology_tags %}
|
||||
{% if this.technology_labels %}
|
||||
<section class="project-technologies">
|
||||
<div class="shell">
|
||||
<div class="section-heading"><span>Built with</span><span>{{ this.technology_tags|length }} technologies</span></div>
|
||||
<div class="technology-list project-technology-list">{% for tag in this.technology_tags %}<span>{{ tag }}</span>{% endfor %}</div>
|
||||
<div class="section-heading"><span>Built with</span><span>{{ this.technology_labels|length }} technologies</span></div>
|
||||
<div class="technology-list project-technology-list">{{ taxonomy_labels(this.technology_labels) }}</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
{% from "macros/tag-links.html" import tag_links with context %}
|
||||
{% block body %}
|
||||
{% set filter_tags = namespace(values=[]) %}
|
||||
{% if this._id in ['blog', 'articles'] %}
|
||||
{% if this._id in ['blog', 'articles', 'projects'] %}
|
||||
{% for item in this.children %}
|
||||
{% for tag in item.tags %}
|
||||
{% set item_tags = item.taxonomy_tags if item._model == 'project' else item.tags %}
|
||||
{% for tag in item_tags %}
|
||||
{% if tag not in filter_tags.values %}{% set filter_tags.values = filter_tags.values + [tag] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
@@ -30,13 +31,13 @@
|
||||
{% endif %}
|
||||
<div class="card-grid">
|
||||
{% for item in this.children %}
|
||||
<article class="entry-card accent-{{ item.kicker|lower }}"{% if filter_tags.values %} data-filter-item data-tags="{{ item.tags|join(' ') }}"{% endif %}>
|
||||
{% set item_tags = item.taxonomy_tags if item._model == 'project' else item.tags %}
|
||||
<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 %}
|
||||
{% if item._model == 'project' %}<div class="technology-list entry-tags">{% for tag in item.technology_tags %}<span>{{ tag }}</span>{% endfor %}</div>
|
||||
{% else %}<div class="entry-tags">{{ tag_links(item.tags) }}</div>{% endif %}
|
||||
{% if item_tags %}
|
||||
<div class="entry-tags{% if item._model == 'project' %} technology-list{% endif %}">{{ tag_links(item_tags) }}</div>
|
||||
{% endif %}
|
||||
<a class="read-more" href="{{ item|url }}">{% if item._model == 'project' %}View project{% else %}Read entry{% endif %} →</a>
|
||||
</article>
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
<div class="tag-directory">
|
||||
{% for tag in this.children %}
|
||||
{% set usage = namespace(count=0) %}
|
||||
{% for section_id in ['blog', 'articles'] %}
|
||||
{% for section_id in ['projects', 'blog', 'articles'] %}
|
||||
{% for item in site.query('/' ~ section_id) %}
|
||||
{% if tag._id in item.tags %}{% set usage.count = usage.count + 1 %}{% endif %}
|
||||
{% 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 %}
|
||||
<a class="tag-directory-card" href="{{ tag|url }}">
|
||||
|
||||
+7
-5
@@ -2,9 +2,10 @@
|
||||
{% from "macros/tag-links.html" import tag_links with context %}
|
||||
{% block body %}
|
||||
{% set matches = namespace(items=[]) %}
|
||||
{% for section_id in ['blog', 'articles'] %}
|
||||
{% for section_id in ['projects', 'blog', 'articles'] %}
|
||||
{% for item in site.query('/' ~ section_id) %}
|
||||
{% if this._id in item.tags %}{% set matches.items = matches.items + [item] %}{% endif %}
|
||||
{% set item_tags = item.taxonomy_tags if item._model == 'project' else item.tags %}
|
||||
{% if this._id in item_tags %}{% set matches.items = matches.items + [item] %}{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<section class="hero compact">
|
||||
@@ -23,17 +24,18 @@
|
||||
{% if matches.items %}
|
||||
<div class="card-grid">
|
||||
{% for item in matches.items|sort(attribute='date', reverse=true) %}
|
||||
{% set item_tags = item.taxonomy_tags if item._model == 'project' else item.tags %}
|
||||
<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>
|
||||
{% if item_tags %}<div class="entry-tags">{{ tag_links(item_tags) }}</div>{% endif %}
|
||||
<a class="read-more" href="{{ item|url }}">{% if item._model == 'project' %}View project{% else %}Read entry{% endif %} →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No blog entries or articles use this tag yet.</p>
|
||||
<p class="empty-state">No projects, blog entries, or articles use this tag yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -17,6 +17,7 @@ from project_sources import ( # noqa: E402
|
||||
metadata_digest,
|
||||
parse_record,
|
||||
split_list,
|
||||
taxonomy_label_items,
|
||||
validate_image,
|
||||
validate_url,
|
||||
)
|
||||
@@ -61,6 +62,12 @@ class ProjectSourceTests(unittest.TestCase):
|
||||
with self.assertRaises(RepositoryNotPublicError):
|
||||
fetch_json("https://git.example.test/api/repo", require_public=True)
|
||||
|
||||
def test_unapproved_labels_remain_visible_without_becoming_tags(self) -> None:
|
||||
items = taxonomy_label_items(
|
||||
["TypeScript", "Unlisted Tool"], ["typescript"]
|
||||
)
|
||||
self.assertEqual(items, ["typescript\tTypeScript", "-\tUnlisted Tool"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user