Compare commits

..

2 commits

Author SHA1 Message Date
682755c6a0
feat(wordpress): only assign tags that are established
Some checks failed
🚀 Deploy to Hetzner / deploy (push) Has been cancelled
Backend Tests / backend-tests (push) Has been cancelled
_resolve_wp_tag_ids created a WordPress tag for every keyword the rewriter
invented, up to 12 per post. That is where the 3.055 tags for 955 posts
came from - 1.683 of them used exactly once, 473 attached to no post at
all. The categories are stable now, but the tags would simply grow back.

Three changes, all on the write path:

A proposed tag has to appear for wordpress_new_tag_min_proposals (3)
different articles before it is created. Proposals are counted in the new
tag_proposals table, keyed on (name, article), so re-publishing an article
does not inflate its own count. Tags that already exist in WordPress are
assigned as before - the gate only guards creation.

Only the first wordpress_max_tags_per_post (5) tags reach WordPress. The
full list still feeds the category rules, which were validated against it.

The lookup fallback of reusing the first search hit is gone. It filed
"Camping" under the unrelated existing tag "Campingplatz" whenever the
exact tag was missing, which quietly produced wrong tags rather than none.

If the proposal bookkeeping fails, nothing is creatable that run: existing
tags still get assigned and the taxonomy stays put, rather than falling
back to creating everything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:34:18 +02:00
4282759b2c
feat(categorize): close rule gaps found on scheduled posts
The first pass only covered published posts, so 137 scheduled ones were
still uncategorised. Seven of them matched no rule at all and exposed
real gaps: the regions Pfalz and Erzgebirge, and the gear nouns in
"Duschzelte", "Outdoor-Messer" and "Outdoor-Stuhl", which the existing
compound keywords could not reach because the noun sits at the end.

Adding those flipped two discount posts ("Lidl Zelt im Angebot", "Aldi
Relax-Stuhl") into the gear category, because the new product keywords
outweighed the weaker "lidl"/"aldi" signals. Both retailers only ever
appear in discount posts here, so they now carry enough weight to
trigger the offer override themselves.

Region keywords are weight 2, not 3: at 3 they outranked an explicit
"Campingplatz" in the title and pulled campsite articles into
"Reiseziele". Bare "pfalz" is weight 1 because it double-counts inside
"rheinland-pfalz".

Scheduled posts now match a rule 136 of 137 times. Ten published posts
change category as a result and were updated in WordPress to keep the
archive and the rules in sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:21:46 +02:00

View file

@ -40,8 +40,11 @@ _RULES: tuple[tuple[str, tuple[tuple[str, int], ...]], ...] = (
("rabatt*", 3), ("schnaeppchen*", 3), ("prime day", 3), ("black friday", 3), ("rabatt*", 3), ("schnaeppchen*", 3), ("prime day", 3), ("black friday", 3),
("gutschein*", 3), ("sparangebot*", 3), ("top-angebot*", 3), ("sommerangebot*", 3), ("gutschein*", 3), ("sparangebot*", 3), ("top-angebot*", 3), ("sommerangebot*", 3),
("knaller", 3), ("ausverkauf", 3), ("tiefpreis*", 3), ("rekordpreis*", 3), ("knaller", 3), ("ausverkauf", 3), ("tiefpreis*", 3), ("rekordpreis*", 3),
("preissturz", 3), ("bestpreis*", 3), ("deal", 2), ("deals", 2), ("lidl", 2), ("preissturz", 3), ("bestpreis*", 3), ("deal", 2), ("deals", 2),
("aldi", 2), ("reduziert", 2), ("sale", 1), ("prozent auf", 3), ("guenstiger*", 1), # Lidl and Aldi articles are always discount posts on this blog, so they
# carry enough weight to trigger the override below on their own.
("lidl", 3), ("aldi", 3), ("im angebot", 3),
("reduziert", 2), ("sale", 1), ("prozent auf", 3), ("guenstiger*", 1),
)), )),
("in-eigener-sache", ( ("in-eigener-sache", (
("vanityontour", 3), ("vanitycast", 3), ("expense logbook", 3), ("vanityontour", 3), ("vanitycast", 3), ("expense logbook", 3),
@ -96,6 +99,8 @@ _RULES: tuple[tuple[str, tuple[tuple[str, int], ...]], ...] = (
("kassettentoilette*", 3), ("toilette*", 2), ("gaswarner", 3), ("rauchmelder", 3), ("kassettentoilette*", 3), ("toilette*", 2), ("gaswarner", 3), ("rauchmelder", 3),
("feuerloescher", 3), ("co melder", 3), ("router", 3), ("mobilfunk", 3), ("wlan", 3), ("feuerloescher", 3), ("co melder", 3), ("router", 3), ("mobilfunk", 3), ("wlan", 3),
("lte", 3), ("5g", 3), ("internet", 2), ("buchtipp*", 2), ("lte", 3), ("5g", 3), ("internet", 2), ("buchtipp*", 2),
("duschzelt*", 3), ("kuppelzelt*", 3), ("pop-up-zelt*", 3), ("messer", 3),
("stuhl", 3), ("faltstuhl*", 3), ("klappstuhl*", 3),
)), )),
("campingplaetze", ( ("campingplaetze", (
("campingplatz*", 3), ("campingplaetze", 3), ("5-sterne*", 3), ("fuenf sterne", 3), ("campingplatz*", 3), ("campingplaetze", 3), ("5-sterne*", 3), ("fuenf sterne", 3),
@ -145,7 +150,11 @@ _RULES: tuple[tuple[str, tuple[tuple[str, int], ...]], ...] = (
("rundreise*", 3), ("roadtrip*", 3), ("reisebericht*", 3), ("ausflugsziel*", 3), ("rundreise*", 3), ("roadtrip*", 3), ("reisebericht*", 3), ("ausflugsziel*", 3),
("kurztrip*", 3), ("europa", 2), ("kueste", 2), ("region", 1), ("uckermark", 3), ("kurztrip*", 3), ("europa", 2), ("kueste", 2), ("region", 1), ("uckermark", 3),
("lausitz", 3), ("schwaebische alb", 3), ("ausflug*", 2), ("geheimtipp*", 2), ("lausitz", 3), ("schwaebische alb", 3), ("ausflug*", 2), ("geheimtipp*", 2),
("uebersee", 2), ("kanada", 3), ("uebersee", 2), ("kanada", 3), ("pfalz", 1), ("erzgebirge", 2),
("spreewald", 2), ("vogtland", 2), ("rhoen", 2), ("odenwald", 2),
("chiemsee", 2), ("ammersee", 2), ("muensterland", 2), ("ostfriesland", 2),
("emsland", 2), ("altmuehltal", 2), ("teutoburger wald", 2), ("taunus", 2),
("westerwald", 2), ("bergisches land", 2), ("nordwesten", 2),
)), )),
("camping-tipps", ( ("camping-tipps", (
("tipp", 3), ("tipps", 3), ("tricks", 3), ("ratgeber", 3), ("anleitung", 3), ("tipp", 3), ("tipps", 3), ("tricks", 3), ("ratgeber", 3), ("anleitung", 3),