Tagger på innlegg med tagg-sider
Kommaseparert tagg-felt i editoren, lagret som liste i frontmatter (dedup case-ufølsomt). /tag/<slug>/ lister alle innlegg med taggen, også gjesters; /tag/ lister alle tagger med antall. Tagger vises som piller på innlegg og i lister, som <category> i feeden og i sitemap. «tag» er reservert som slug/brukernavn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR
This commit is contained in:
parent
259465342b
commit
f0fcb875c5
11 changed files with 109 additions and 6 deletions
|
|
@ -27,6 +27,8 @@ data/
|
||||||
«Historikk»-seksjon i editoren med diff per endring; hele loggen ligger under Innstillinger
|
«Historikk»-seksjon i editoren med diff per endring; hele loggen ligger under Innstillinger
|
||||||
(eller `git -C data/content log`).
|
(eller `git -C data/content log`).
|
||||||
* **Gjester** ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester.
|
* **Gjester** ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester.
|
||||||
|
* **Tagger** settes kommaseparert i editoren. `/tag/<tagg>/` lister alle innlegg med taggen (også
|
||||||
|
gjesters), `/tag/` alle tagger. Tagger får slug på samme måte som titler.
|
||||||
* **Signatur**: innlegg avsluttes med «– Visningsnavn», evt. med e-post som mailto-lenke. Begge
|
* **Signatur**: innlegg avsluttes med «– Visningsnavn», evt. med e-post som mailto-lenke. Begge
|
||||||
settes under Profil i admin og slås opp ved bygging, så en endring gjelder alle innlegg.
|
settes under Profil i admin og slås opp ved bygging, så en endring gjelder alle innlegg.
|
||||||
|
|
||||||
|
|
@ -115,7 +117,7 @@ cp soapbox/themes/green/static/style.css data/content/themes/skog/static/ # re
|
||||||
Et tema kan være **delvis**: bare filene som finnes overstyrer det innebygde `green`-temaet.
|
Et tema kan være **delvis**: bare filene som finnes overstyrer det innebygde `green`-temaet.
|
||||||
Vil du endre malene, legg `templates/base.html`, `index.html` eller `post.html` i temamappen.
|
Vil du endre malene, legg `templates/base.html`, `index.html` eller `post.html` i temamappen.
|
||||||
Malene får `site`, `posts`/`post`, `html`, `source_url` og `rel(sti)` (relativ lenke fra
|
Malene får `site`, `posts`/`post`, `html`, `source_url` og `rel(sti)` (relativ lenke fra
|
||||||
gjeldende side), samt filtrene `date` og `excerpt(n)`. `static/` havner under `/theme/`.
|
gjeldende side), samt filtrene `date`, `excerpt(n)` og `tagslug`. `static/` havner under `/theme/`.
|
||||||
|
|
||||||
## Lisens
|
## Lisens
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ from .content import (
|
||||||
existing_slugs,
|
existing_slugs,
|
||||||
get_post,
|
get_post,
|
||||||
list_posts,
|
list_posts,
|
||||||
|
parse_tags,
|
||||||
post_dir,
|
post_dir,
|
||||||
save_post,
|
save_post,
|
||||||
split_id,
|
split_id,
|
||||||
|
|
@ -63,7 +64,7 @@ bp = Blueprint(
|
||||||
USERNAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
|
USERNAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
|
||||||
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||||
# Reserverte navn som ville kollidert med URL-strukturen
|
# Reserverte navn som ville kollidert med URL-strukturen
|
||||||
RESERVED = {"admin", "theme", "feed.xml", "posts", "guests", "index.html"}
|
RESERVED = {"admin", "theme", "tag", "feed.xml", "sitemap.xml", "posts", "guests", "index.html"}
|
||||||
|
|
||||||
|
|
||||||
def _cfg():
|
def _cfg():
|
||||||
|
|
@ -261,6 +262,7 @@ def edit(post_id: str):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
title = request.form.get("title", "").strip() or "Uten tittel"
|
title = request.form.get("title", "").strip() or "Uten tittel"
|
||||||
body = request.form.get("body", "").replace("\r\n", "\n")
|
body = request.form.get("body", "").replace("\r\n", "\n")
|
||||||
|
post.tags = parse_tags(request.form.get("tags", ""))
|
||||||
action = request.form.get("action", "save")
|
action = request.form.get("action", "save")
|
||||||
was_draft = post.draft
|
was_draft = post.draft
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ from . import db
|
||||||
from .config import Config, load_site_settings
|
from .config import Config, load_site_settings
|
||||||
from .content import IMAGE_EXT, Post, list_posts
|
from .content import IMAGE_EXT, Post, list_posts
|
||||||
from .render import excerpt, relative_to, render_markdown
|
from .render import excerpt, relative_to, render_markdown
|
||||||
|
from .slug import slugify
|
||||||
|
|
||||||
SOURCE_URL = "https://kode.naiv.no/olemd/soapbox"
|
SOURCE_URL = "https://kode.naiv.no/olemd/soapbox"
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ def _env(cfg: Config) -> Environment:
|
||||||
)
|
)
|
||||||
env.filters["date"] = lambda d, fmt="%d.%m.%Y": d.strftime(fmt)
|
env.filters["date"] = lambda d, fmt="%d.%m.%Y": d.strftime(fmt)
|
||||||
env.filters["excerpt"] = excerpt
|
env.filters["excerpt"] = excerpt
|
||||||
|
env.filters["tagslug"] = slugify
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,8 +97,27 @@ def build_site(cfg: Config) -> int:
|
||||||
render("index.html", f"{guest}/", posts=gposts, heading=gposts[0].author_name or guest),
|
render("index.html", f"{guest}/", posts=gposts, heading=gposts[0].author_name or guest),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Tagger: /tag/<slug>/ per tag (alle brukeres innlegg), /tag/ som oversikt.
|
||||||
|
# Slug-en er nøkkel så «Skog» og «skog» havner på samme side.
|
||||||
|
tags: dict[str, dict] = {}
|
||||||
|
for p in posts:
|
||||||
|
for t in p.tags:
|
||||||
|
entry = tags.setdefault(slugify(t), {"name": t, "posts": []})
|
||||||
|
entry["posts"].append(p)
|
||||||
|
for tslug, entry in tags.items():
|
||||||
|
_write(
|
||||||
|
tmp / "tag" / tslug / "index.html",
|
||||||
|
render(
|
||||||
|
"index.html",
|
||||||
|
f"tag/{tslug}/",
|
||||||
|
posts=entry["posts"],
|
||||||
|
heading=f"Tagg: {entry['name']}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_write(tmp / "tag" / "index.html", render("tags.html", "tag/", tags=tags))
|
||||||
|
|
||||||
_write(tmp / "feed.xml", _feed(cfg, site, posts[:20]))
|
_write(tmp / "feed.xml", _feed(cfg, site, posts[:20]))
|
||||||
_write(tmp / "sitemap.xml", _sitemap(cfg, posts, sorted(guests)))
|
_write(tmp / "sitemap.xml", _sitemap(cfg, posts, sorted(guests), sorted(tags)))
|
||||||
|
|
||||||
# Statiske filer lagvis: innebygd først, eget tema oppå (overskriver).
|
# Statiske filer lagvis: innebygd først, eget tema oppå (overskriver).
|
||||||
for d in reversed(cfg.theme_dirs):
|
for d in reversed(cfg.theme_dirs):
|
||||||
|
|
@ -137,7 +158,7 @@ def _resolve_authors(cfg: Config, posts: list[Post]) -> None:
|
||||||
p.author_email = user.email if user else ""
|
p.author_email = user.email if user else ""
|
||||||
|
|
||||||
|
|
||||||
def _sitemap(cfg: Config, posts: list[Post], guests: list[str]) -> str:
|
def _sitemap(cfg: Config, posts: list[Post], guests: list[str], tags: list[str]) -> str:
|
||||||
"""sitemap.xml med forsiden, alle innlegg og gjesteoversiktene."""
|
"""sitemap.xml med forsiden, alle innlegg og gjesteoversiktene."""
|
||||||
base = cfg.site_url + "/"
|
base = cfg.site_url + "/"
|
||||||
|
|
||||||
|
|
@ -150,6 +171,7 @@ def _sitemap(cfg: Config, posts: list[Post], guests: list[str]) -> str:
|
||||||
entries = [url("", newest)]
|
entries = [url("", newest)]
|
||||||
entries += [url(p.url_path, p.updated or p.date) for p in posts]
|
entries += [url(p.url_path, p.updated or p.date) for p in posts]
|
||||||
entries += [url(f"{g}/") for g in guests]
|
entries += [url(f"{g}/") for g in guests]
|
||||||
|
entries += [url(f"tag/{t}/") for t in tags]
|
||||||
return (
|
return (
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
||||||
|
|
@ -171,7 +193,8 @@ def _feed(cfg: Config, site: dict, posts: list[Post]) -> str:
|
||||||
f"<pubDate>{format_datetime(p.date)}</pubDate>"
|
f"<pubDate>{format_datetime(p.date)}</pubDate>"
|
||||||
+ (f"<dc:creator>{escape(p.author_name)}</dc:creator>" if p.author_name else "")
|
+ (f"<dc:creator>{escape(p.author_name)}</dc:creator>" if p.author_name else "")
|
||||||
+ f"<description>{escape(excerpt(p.body))}</description>"
|
+ f"<description>{escape(excerpt(p.body))}</description>"
|
||||||
"</item>"
|
+ "".join(f"<category>{escape(t)}</category>" for t in p.tags)
|
||||||
|
+ "</item>"
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class Post:
|
||||||
author: str = "" # brukernavn (eldre innlegg kan ha visningsnavn her)
|
author: str = "" # brukernavn (eldre innlegg kan ha visningsnavn her)
|
||||||
author_name: str = "" # visningsnavn, fylles ut ved bygging fra brukertabellen
|
author_name: str = "" # visningsnavn, fylles ut ved bygging fra brukertabellen
|
||||||
author_email: str = ""
|
author_email: str = ""
|
||||||
|
tags: list[str] = field(default_factory=list) # visningsform, f.eks. "Friluftsliv"
|
||||||
guest: str | None = None # None for hovedbruker, ellers gjestens brukernavn
|
guest: str | None = None # None for hovedbruker, ellers gjestens brukernavn
|
||||||
path: Path | None = None
|
path: Path | None = None
|
||||||
images: list[str] = field(default_factory=list)
|
images: list[str] = field(default_factory=list)
|
||||||
|
|
@ -107,6 +108,9 @@ def load_post(path: Path, guest: str | None) -> Post:
|
||||||
updated=_parse_dt(meta.get("updated")),
|
updated=_parse_dt(meta.get("updated")),
|
||||||
draft=bool(meta.get("draft", False)),
|
draft=bool(meta.get("draft", False)),
|
||||||
author=meta.get("author", ""),
|
author=meta.get("author", ""),
|
||||||
|
tags=[t for t in meta.get("tags", []) if isinstance(t, str) and t.strip()]
|
||||||
|
if isinstance(meta.get("tags"), list)
|
||||||
|
else [],
|
||||||
guest=guest,
|
guest=guest,
|
||||||
path=path,
|
path=path,
|
||||||
images=images,
|
images=images,
|
||||||
|
|
@ -153,6 +157,7 @@ def save_post(cfg: Config, post: Post) -> Path:
|
||||||
"date": post.date.isoformat(timespec="seconds"),
|
"date": post.date.isoformat(timespec="seconds"),
|
||||||
"updated": post.updated.isoformat(timespec="seconds") if post.updated else None,
|
"updated": post.updated.isoformat(timespec="seconds") if post.updated else None,
|
||||||
"author": post.author,
|
"author": post.author,
|
||||||
|
"tags": post.tags or None,
|
||||||
"draft": True if post.draft else None,
|
"draft": True if post.draft else None,
|
||||||
}
|
}
|
||||||
path = d / "index.md"
|
path = d / "index.md"
|
||||||
|
|
@ -173,6 +178,16 @@ SLUG_RE = re.compile(r"^[\w-]+$") # samme alfabet som slugify() produserer
|
||||||
GUEST_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
|
GUEST_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tags(raw: str) -> list[str]:
|
||||||
|
"""Kommaseparert input → unike tagger, første forekomst vinner (case-ufølsomt)."""
|
||||||
|
seen: dict[str, str] = {}
|
||||||
|
for part in raw.split(","):
|
||||||
|
tag = " ".join(part.split())
|
||||||
|
if tag and tag.casefold() not in seen:
|
||||||
|
seen[tag.casefold()] = tag
|
||||||
|
return list(seen.values())
|
||||||
|
|
||||||
|
|
||||||
def split_id(post_id: str) -> tuple[str | None, str]:
|
def split_id(post_id: str) -> tuple[str | None, str]:
|
||||||
"""``"_owner/slug"`` → (None, slug); ``"gjest/slug"`` → ("gjest", slug).
|
"""``"_owner/slug"`` → (None, slug); ``"gjest/slug"`` → ("gjest", slug).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label class="grow">Tittel <input name="title" value="{{ post.title }}" required autofocus></label>
|
<label class="grow">Tittel <input name="title" value="{{ post.title }}" required autofocus></label>
|
||||||
|
<label class="grow">Tagger <span class="hint" id="tags-hint">Kommaseparert, f.eks. «friluftsliv, skog»</span>
|
||||||
|
<input name="tags" value="{{ post.tags|join(', ') }}" aria-describedby="tags-hint" autocapitalize="none"></label>
|
||||||
<span class="meta">
|
<span class="meta">
|
||||||
{% if post.draft %}<span class="badge">utkast</span> Publiseres på <code>{{ cfg.site_url }}/{{ post.url_path }}</code> (følger tittelen)
|
{% if post.draft %}<span class="badge">utkast</span> Publiseres på <code>{{ cfg.site_url }}/{{ post.url_path }}</code> (følger tittelen)
|
||||||
{% else %}Publisert på <a href="{{ url_for('public', path=post.url_path) }}">{{ cfg.site_url }}/{{ post.url_path }}</a>
|
{% else %}Publisert på <a href="{{ url_for('public', path=post.url_path) }}">{{ cfg.site_url }}/{{ post.url_path }}</a>
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ h2 { font-size: 1.4rem; line-height: 1.25; margin: 2rem 0 .5rem; }
|
||||||
.summary .meta { margin: 0; }
|
.summary .meta { margin: 0; }
|
||||||
.summary .excerpt { margin: .5rem 0 0; color: var(--fg); }
|
.summary .excerpt { margin: .5rem 0 0; color: var(--fg); }
|
||||||
.body p, .body ul, .body ol { margin: 0 0 1.1rem; }
|
.body p, .body ul, .body ol { margin: 0 0 1.1rem; }
|
||||||
|
.tags { list-style: none; padding: 0; margin: .75rem 0 0; display: flex; flex-wrap: wrap; gap: .4rem; }
|
||||||
|
.tags a {
|
||||||
|
display: inline-block; padding: .2rem .6rem; border: 1px solid var(--border); border-radius: 999px;
|
||||||
|
background: var(--surface); text-decoration: none; font-size: .85rem; min-height: 32px; line-height: 1.6;
|
||||||
|
}
|
||||||
|
.tags a:hover, .tags a:focus-visible { border-color: var(--accent); }
|
||||||
|
.tag-cloud { margin-top: 1rem; } .tag-cloud a { font-size: 1rem; }
|
||||||
.signature { margin: 1.5rem 0 0; color: var(--muted); font-style: italic; }
|
.signature { margin: 1.5rem 0 0; color: var(--muted); font-style: italic; }
|
||||||
.body img { max-width: 100%; height: auto; display: block; margin: 1.5rem 0; border-radius: 4px; }
|
.body img { max-width: 100%; height: auto; display: block; margin: 1.5rem 0; border-radius: 4px; }
|
||||||
.body pre { background: var(--code-bg); padding: 1rem; overflow-x: auto; border-radius: 4px; }
|
.body pre { background: var(--code-bg); padding: 1rem; overflow-x: auto; border-radius: 4px; }
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
</main>
|
</main>
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<nav aria-label="Bunnmeny">
|
<nav aria-label="Bunnmeny">
|
||||||
<a href="{{ rel('') }}">Forsiden</a><a href="{{ rel('feed.xml') }}">RSS</a><a href="{{ rel('admin/') }}">Logg inn</a>
|
<a href="{{ rel('') }}">Forsiden</a><a href="{{ rel('tag/') }}">Tagger</a><a href="{{ rel('feed.xml') }}">RSS</a><a href="{{ rel('admin/') }}">Logg inn</a>
|
||||||
</nav>
|
</nav>
|
||||||
{# AGPL §13: brukere av tjenesten skal kunne hente kildekoden. #}
|
{# AGPL §13: brukere av tjenesten skal kunne hente kildekoden. #}
|
||||||
<p class="colophon">Drevet av <a href="{{ source_url }}">Soapbox</a> (AGPL-3.0)</p>
|
<p class="colophon">Drevet av <a href="{{ source_url }}">Soapbox</a> (AGPL-3.0)</p>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@
|
||||||
{% if post.guest %} · <a href="{{ rel(post.guest ~ '/') }}">{{ post.author_name or post.guest }}</a>{% endif %}
|
{% if post.guest %} · <a href="{{ rel(post.guest ~ '/') }}">{{ post.author_name or post.guest }}</a>{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% set text = post.body|excerpt(220) %}{% if text %}<p class="excerpt">{{ text }}</p>{% endif %}
|
{% set text = post.body|excerpt(220) %}{% if text %}<p class="excerpt">{{ text }}</p>{% endif %}
|
||||||
|
{% if post.tags %}
|
||||||
|
<ul class="tags" aria-label="Tagger">
|
||||||
|
{% for t in post.tags %}<li><a href="{{ rel('tag/' ~ (t|tagslug) ~ '/') }}">{{ t }}</a></li>{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@
|
||||||
<div class="body">
|
<div class="body">
|
||||||
{{ html|safe }}
|
{{ html|safe }}
|
||||||
</div>
|
</div>
|
||||||
|
{% if post.tags %}
|
||||||
|
<ul class="tags" aria-label="Tagger">
|
||||||
|
{% for t in post.tags %}<li><a href="{{ rel('tag/' ~ (t|tagslug) ~ '/') }}">{{ t }}</a></li>{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
{% if post.author_name %}
|
{% if post.author_name %}
|
||||||
<p class="signature">– {{ post.author_name }}{% if post.author_email %}
|
<p class="signature">– {{ post.author_name }}{% if post.author_email %}
|
||||||
· <a href="mailto:{{ post.author_email }}">{{ post.author_email }}</a>{% endif %}</p>
|
· <a href="mailto:{{ post.author_email }}">{{ post.author_email }}</a>{% endif %}</p>
|
||||||
|
|
|
||||||
11
soapbox/themes/green/templates/tags.html
Normal file
11
soapbox/themes/green/templates/tags.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Tagger – {{ site.title }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Tagger</h1>
|
||||||
|
{% if not tags %}<p>Ingen tagger ennå.</p>{% endif %}
|
||||||
|
<ul class="tags tag-cloud">
|
||||||
|
{% for tslug, t in tags|dictsort(by='key') %}
|
||||||
|
<li><a href="{{ rel('tag/' ~ tslug ~ '/') }}">{{ t.name }} <span class="meta">({{ t.posts|length }})</span></a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -339,3 +339,34 @@ def test_signature_from_profile(client, cfg):
|
||||||
).status_code
|
).status_code
|
||||||
== 200
|
== 200
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tags(client, cfg):
|
||||||
|
login(client)
|
||||||
|
token = csrf(client)
|
||||||
|
|
||||||
|
def publish(title, tags):
|
||||||
|
r = client.post("/admin/new", data={"csrf": token})
|
||||||
|
pid = r.headers["Location"].split("/admin/edit/")[1]
|
||||||
|
client.post(
|
||||||
|
f"/admin/edit/{pid}",
|
||||||
|
data={"csrf": token, "title": title, "body": "x", "tags": tags, "action": "publish"},
|
||||||
|
)
|
||||||
|
|
||||||
|
publish("En", "Friluftsliv, skog ,skog, Skog")
|
||||||
|
publish("To", "friluftsliv")
|
||||||
|
publish("Tre", "")
|
||||||
|
md = (cfg.content_dir / "posts/en/index.md").read_text()
|
||||||
|
assert 'tags: ["Friluftsliv", "skog"]' in md # dedup, case-ufølsomt, trimmet
|
||||||
|
assert "tags:" not in (cfg.content_dir / "posts/tre/index.md").read_text()
|
||||||
|
|
||||||
|
assert 'href="../tag/friluftsliv/"' in client.get("/en/").text
|
||||||
|
page = client.get("/tag/friluftsliv/").text
|
||||||
|
assert "tagg: friluftsliv" in page.lower() and ">En<" in page and ">To<" in page and ">Tre<" not in page
|
||||||
|
assert client.get("/tag/skog/").status_code == 200
|
||||||
|
assert client.get("/tag/").status_code == 200 and "(2)" in client.get("/tag/").text
|
||||||
|
assert "<category>skog</category>" in client.get("/feed.xml").text
|
||||||
|
assert "https://blog.example.no/tag/friluftsliv/" in client.get("/sitemap.xml").text
|
||||||
|
# "tag" er reservert som slug
|
||||||
|
publish("tag", "")
|
||||||
|
assert not (cfg.content_dir / "posts/tag").exists()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue