dirnbauer/webconsulting-skills · Archived

typo3-news-tags

Bulk-generates thematic tags for georgringer/news (EXT:news) and assigns them to existing news records via keyword matching.

First seen May 26, 2026

Installation

$ npx skills add dirnbauer/webconsulting-skills --skill typo3-news-tags

Summary

  • Bulk-generates thematic tags for georgringer/news (EXT:news) and assigns them to existing news records via keyword matching.
  • Use when tagging large news corpora, defining a generic tag catalogue, building a Symfony console command for tag assignment, working with tx_news_domain_model_tag / tx_news_domain_model_news_tag_mm, or installing b13/tag as a generic site-wide tagging capability alongside news tags.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from dirnbauer/webconsulting-skills · top by installs.

npx skills add dirnbauer/webconsulting-skills

Browse all from dirnbauer/webconsulting-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 33
License LICENSE
Default branch main
Open issues 0
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0
LicenseMIT / CC-BY-SA-4.0
CompatibilityTYPO3 14.x (EXT:news ^14)
More metadata
version
1.0.0
origin
webconsulting

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 16,970 B
  • docs SUMMARY.md 432 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 11 installs

SKILL.md

TYPO3 News Tags — Bulk Generation & Assignment

Source: https://github.com/dirnbauer/webconsulting-skills

Compatibility: TYPO3 v14.x with georgringer/news ^14.
All code examples target TYPO3 v14 APIs only. Do not use this skill for v12 / v13 sites.

Scope. Designing, generating and assigning thematic news tags (txnewsdomainmodeltag)
in georgringer/news at scale (hundreds to tens of thousands of news), and optionally installing
the orthogonal generic tag system from b13/tag (sys_tag).

Not in scope. Frontend rendering of tags in Fluid templates, tag translations, or tag merging.

When to use this skill

Trigger this skill when the user asks to:

  • "Tag all news / tag the latest N news"
  • "Define a tag catalogue / generic tags / thematic tags"
  • "Assign categories or tags in bulk to existing news"
  • "Install a generic tagging extension" (b13/tag)
  • "Build a Symfony command that tags news" / "automate tagging in TYPO3"
  • "Why are large UIDs missing from my QueryBuilder result" (DBAL gotcha, §8)

Mental model

EXT:news ships two relations for news classification:

Relation Table Purpose Routing
Categories syscategorysyscategoryrecordmm Primary hierarchical taxonomy Optional
Tags txnewsdomainmodeltagtxnewsdomainmodelnewstagmm Flat, slug-routable thematic crosscuts NewsTag aspect in site config

Categories answer "what bucket is this in?" — typically a few per news, hierarchical, often locale-aware. Tags answer "what themes does this touch?" — typically 5–10 per news, flat, URL-friendly via /{tag-slug}/.

If the site already has narrow business categories ("Betrügerische Shops", "Phishing", …), tags should be orthogonal and thematic (e.g. "Künstliche Intelligenz", "Banking", "Senioren") — not redundant copies of categories.

b13/tag is a different system: systag + systag_mm with a keywords int column on the target table. It is generic across all record types but not integrated with EXT:news. Use it for tagging other tables; keep EXT:news's native tags for news.

Default workflow

1. Verify the corpus (always)

ddev mysql -e "SELECT COUNT(*) FROM tx_news_domain_model_news WHERE pid=<PID> AND deleted=0 AND hidden=0;"
ddev mysql -e "SELECT COUNT(*) FROM tx_news_domain_model_tag WHERE deleted=0;"
ddev mysql -e "SELECT COUNT(*) FROM tx_news_domain_model_news_tag_mm;"

Identify where the readable text actually lives. With EXT:news's built-in contentElementRelation extension configuration or mask based content, txnewsdomainmodelnews.bodytext is often empty — the real content sits in ttcontent rows linked via txnewsrelatednews. Verify:

SELECT AVG(LENGTH(bodytext)) FROM tx_news_domain_model_news WHERE pid=<PID> AND deleted=0;
SELECT CType, COUNT(*) FROM tt_content
  WHERE tx_news_related_news > 0 AND deleted=0 AND hidden=0
  GROUP BY CType ORDER BY 2 DESC;

If average bodytext is ~0, you must harvest tt_content to get meaningful scoring.

2. Derive tag candidates from corpus frequency

Don't guess the catalogue — let the corpus pick it. Read 50–100 titles + teasers first to get a feel:

SELECT uid, title, LEFT(teaser, 200) FROM tx_news_domain_model_news
  WHERE pid=<PID> AND deleted=0 AND hidden=0
  ORDER BY datetime DESC LIMIT 100;

Then run a frequency analysis on the full target corpus (latest N news + their linked tt_content). Brainstorm ~100 candidate concepts with 1–3 keywords each, count how many news mention each, and pick the top ~65 (or whatever count you need). This is much more defensible than a guessed catalogue and surfaces non-obvious recurring themes (e.g. Unternehmen and Polizei ranked top-5 in the example corpus — neither was on the original guess list).

Single-concept rule. Each tag should be ONE concept — never a X & Y combination. Compose multiple tags per news instead. So:

Avoid Prefer
Banking & Konto Banking + Konto (two tags)
Künstliche Intelligenz & Deepfake Künstliche Intelligenz + Deepfake
Paket & Lieferung Paket + Lieferung (+ DHL if relevant)
Reise & Urlaub Reise + Hotel + Flug

Hyphenated compounds (Fake-Shop, Online-Shopping, Login-Daten) and standard German two-word concepts (Künstliche Intelligenz) are fine — they are one concept.

Each tag needs a name, a slug (lowercase, ASCII-only, hyphenated), and a curated list of keywords. See references/NewsThematicTags.example.php for a worked German example covering 65 single-concept fraud-prevention themes derived from a real ~1500-news corpus.

Keyword design rules:

  • Lowercase. Both singular and plural where common (fake-shop, fake-shops).
  • Both hyphenated and spaced spellings (fake-shop, fake shop, fakeshop).
  • Compound nouns over generic single words (kostenpflichtiges abo over kosten).
  • Beware short prefixes: auto matches autor, automatisch, autorin under the

left-only word boundary. Use compound forms instead (autokauf, autoverkauf, kfz). Same trap with apple (applied), bank (bankrott), post (posten).

  • 1–3 char ambiguous tokens are OK with strict both-sides boundaries (tan, ki, sms)

— the example command auto-applies stricter boundaries for short keywords.

  • 5–15 keywords per tag is a good target for single-concept tags; broaden if a tag is

legitimately under-firing in the dry-run.

  • Multi-word keywords are matched verbatim after whitespace collapse — be specific.

3. Decide on tag storage

For EXT:news, tags live on a storage PID. Use the same PID as the news (most common) — the route enhancer in config/sites/<id>/config.yaml uses the tag's slug, not its PID. Verify your site has the tag route wired:

routeEnhancers:
  News:
    type: Extbase
    extension: News
    plugin: Pi1
    routes:
      - routePath: '/{tag-name}'
        _controller: 'News::list'
        _arguments:
          tag-name: overwriteDemand/tags
    aspects:
      tag-name:
        type: NewsTag

If not present, add it before publishing tag URLs. The NewsTag aspect type ships with EXT:news and is the documented best practice; a plain PersistedAliasMapper with tableName: txnewsdomainmodeltag and routeFieldName: slug is a legacy alternative.

4. Build a Symfony Console command

Create one command per extension/package in Classes/Command/AssignNewsTagsCommand.php and register it in Configuration/Services.yaml with the console.command tag. See references/AssignNewsTagsCommand.example.php for a complete, idempotent implementation that:

  • loads the tag catalogue from a PHP config file
  • upserts tags via DataHandler (slug auto-generates, refindex updated)
  • harvests content from ttcontent linked via txnewsrelatednews
  • normalizes HTML → lowercase → collapsed whitespace
  • scores each tag by distinct keyword matches (not total occurrences — avoids spam from

one repeated word dominating)

  • selects the top N tags per news (5–10 typical, threshold ≥ 1)
  • bulk-inserts MM rows via raw SQL multi-row INSERT (batch of 500)
  • updates txnewsdomainmodelnews.tags counter for backend list display
  • supports --dry-run, --reset, --limit, --storage-pid, --force, --debug-uid

5. Iterate with --dry-run

ddev exec vendor/bin/typo3 cache:flush
ddev exec vendor/bin/typo3 <vendor>:news:assign-tags --dry-run --limit=300

The dry-run prints the tags-per-news distribution and per-tag popularity. Healthy targets for fraud/news corpora:

  • median 5+, p25 ≥ 3, p75 ≤ 8
  • unmatched (0 tags) < 1% of corpus
  • per-tag count: most-popular tag covers ≤ 60% of news; least-popular ≥ 1%

If many news fall to 1–2 tags, broaden keywords on common tags (E-Mail-Betrug, Banking, Werbung — these typically anchor most scam stories). If a tag has 0 matches, either keywords are wrong or the tag is not actually represented in the corpus — adjust or replace.

6. Run for real

ddev exec vendor/bin/typo3 <vendor>:news:assign-tags --reset --force
ddev exec vendor/bin/typo3 cache:flush

--reset truncates txnewsdomainmodelnewstagmm and deletes tags on the storage PID before recreating — safest for re-runs while iterating on keywords. Drop --reset for additive runs once the catalogue is stable.

Expected throughput: ~150 news/sec on a typical DDEV setup (1500 news ≈ 10 seconds).

7. Verify

-- 1. All tags present
SELECT COUNT(*) FROM tx_news_domain_model_tag WHERE pid=<PID> AND deleted=0;

-- 2. MM rows = sum(tags-per-news)
SELECT COUNT(*) FROM tx_news_domain_model_news_tag_mm;

-- 3. Distribution
SELECT tags_per_news, COUNT(*) AS news FROM (
  SELECT uid_local, COUNT(*) AS tags_per_news
  FROM tx_news_domain_model_news_tag_mm GROUP BY uid_local
) t GROUP BY tags_per_news ORDER BY tags_per_news;

-- 4. Counter consistency (must return 0 rows)
SELECT n.uid, n.tags, COUNT(mm.uid_foreign) AS actual
FROM tx_news_domain_model_news n
LEFT JOIN tx_news_domain_model_news_tag_mm mm ON mm.uid_local = n.uid
WHERE n.pid=<PID> AND n.deleted=0
GROUP BY n.uid, n.tags HAVING n.tags <> actual LIMIT 20;

-- 5. Per-tag popularity
SELECT t.title, COUNT(mm.uid_local) AS n
FROM tx_news_domain_model_tag t
LEFT JOIN tx_news_domain_model_news_tag_mm mm ON mm.uid_foreign = t.uid
WHERE t.pid=<PID> AND t.deleted=0
GROUP BY t.uid, t.title ORDER BY n DESC;

-- 6. Backend spot-check
SELECT n.uid, LEFT(n.title, 60), GROUP_CONCAT(t.title ORDER BY mm.sorting)
FROM tx_news_domain_model_news n
JOIN tx_news_domain_model_news_tag_mm mm ON mm.uid_local = n.uid
JOIN tx_news_domain_model_tag t ON t.uid = mm.uid_foreign
WHERE n.pid=<PID> AND n.deleted=0
GROUP BY n.uid ORDER BY n.datetime DESC LIMIT 10;

Backend visual check: open News module → pick a recent news → the "Relations" tab shows the assigned tags (categories sit in the separate "Categories" tab). Frontend route check: visit https://<site>/<tag-slug>/.

Adding b13/tag (optional generic capability)

b13/tag provides a generic sys_tag table that can tag any record via an int keywords column. It does not integrate with EXT:news tags and should not replace them.

ddev composer require b13/tag
ddev exec vendor/bin/typo3 extension:setup
ddev mysql -e "SHOW TABLES LIKE 'sys_tag%';"   # sys_tag + sys_tag_mm

To use it on a custom table:

  1. Add keywords int(11) unsigned DEFAULT '0' NOT NULL to the table's SQL.
  2. Configure TCA via B13\Tag\TcaHelper.
  3. Register the field via ExtensionManagementUtility::addToAllTCAtypes().

See the b13/tag README for current TCA wiring.

8. DBAL createNamedParameter(PARAM_INT) gotcha

In some DBAL stacks, $qb->createNamedParameter($uid, Connection::PARAM_INT) silently drops rows when the integer is large (observed for UIDs in the tens of millions). The query returns 0 rows even though the row exists. Reproduce in your environment before adopting the workaround; on a clean TYPO3 v14 install with current Doctrine DBAL this may no longer fire.

Symptom. --debug-uid=29386376 returns "not found", but SELECT … WHERE uid = 29386376 in MySQL returns the row. The full fetchNews() query returns rows but the loop processes different news than expected — the result set is silently truncated.

Workaround. Use raw SQL with ? placeholders for queries that touch large integer columns (news UIDs, tt_content UIDs, content element relations):

// AVOID for large ints:
$qb->expr()->eq('uid', $qb->createNamedParameter($uid, Connection::PARAM_INT));

// PREFER:
$conn->executeQuery(
    'SELECT … FROM tx_news_domain_model_news WHERE uid = ?',
    [$uid]
)->fetchAssociative();

// For IN clauses with int arrays, inline the cast values:
$ids = implode(',', array_map(static fn($v) => (int)$v, $chunk));
$conn->executeQuery("SELECT … WHERE tx_news_related_news IN ($ids) AND …");

Small ints (PID, hidden, deleted) work fine with createNamedParameter — only large UIDs exhibit the issue. The reference command applies this workaround throughout.

9. Performance notes

  • Tag upsert via DataHandler for the ~65 tag rows (slug eval, refindex) — negligible cost.
  • MM inserts via direct multi-row SQL (batch 500). DataHandler MM writes are O(n²) per

record and unnecessary for MM tables that have no TCA semantics.

  • Sort by datetime DESC with LIMIT N to get the latest N news — index it if cold.
  • Chunk ttcontent fetches in groups of 200 news per IN-query to keep maxallowed_packet

and prepared-statement parameter caps safe.

  • Disable any caches that observe txnewsdomainmodelnews during the run, then cache:flush

afterwards. The reference command does not call DataHandler per news (only for tag upsert), so caches are minimally affected during the bulk pass.

10. Keyword matching — Unicode-safe word boundaries

PHP's \b is ASCII-only and breaks on ä/ö/ü/ß. Use \p{L}\p{N} lookarounds and the /u modifier:

private function buildKeywordPattern(string $keyword): string
{
    $quoted = preg_quote($keyword, '/');
    // Short tokens (<=3 chars): require both word boundaries to avoid false positives.
    // Longer tokens: require left boundary only — covers German plural/genitive and compound
    // suffixes (e.g. "phishing" matches "phishing-welle", "phishings", "phishingsoftware").
    if (mb_strlen($keyword, 'UTF-8') <= 3) {
        return '/(?<![\p{L}\p{N}])' . $quoted . '(?![\p{L}\p{N}])/u';
    }
    return '/(?<![\p{L}\p{N}])' . $quoted . '/u';
}

Score by count of distinct matched keywords — not total occurrences. A single keyword repeated 50 times should not outweigh four different keywords matching once each.

Files in this skill

  • references/AssignNewsTagsCommand.example.php — complete, idempotent Symfony console

command (~370 LOC) with all options, scoring logic, DataHandler upsert, bulk MM insert, DBAL workaround, --debug-uid per-news inspector, summary report.

  • references/NewsThematicTags.example.php — 65 single-concept German fraud-prevention

tags (no X & Y combinations), derived from a ~1500-news corpus frequency analysis, with ~5–15 curated keywords each.

  • references/Services.example.yaml — minimal command registration snippet.

Common pitfalls

Symptom Likely cause Fix
Median tags-per-news is 1–2 Keywords too narrow; or bodytext empty without harvesting tt_content Broaden top tags' keywords; verify fetchContentByNews is wired
Newest news (large UID) have 0 tags despite obvious keywords DBAL createNamedParameter(PARAM_INT) truncation Switch the affected query to raw SQL, see §8
Tags exist but slugs are NULL Created via direct INSERT instead of DataHandler Use DataHandler with process_datamap so TCA slug eval fires
txnewsdomainmodelnews.tags counter is wrong Counter not updated after MM writes Run an UPDATE news SET tags = (SELECT COUNT(*) FROM mm WHERE mm.uid_local = news.uid) once, then ensure command writes it
Frontend /<tag-slug>/ 404s Route enhancer missing in site config Add the News route with the NewsTag aspect (§3) and flush caches
Identical tag created twice on re-run Lookup-before-insert missing SELECT uid FROM tag WHERE slug = ? before each DataHandler NEW_x; reuse UID if found

Acceptance checklist

Before reporting "done":

  • Storage PID confirmed and tags created there
  • --dry-run distribution reviewed (median ≥ 4, unmatched < 2%)
  • Counter consistency query returns 0 rows
  • Backend News module shows tags on a recent record
  • https://<site>/<tag-slug>/ resolves to the filtered news list
  • Command is re-runnable (--reset works; second run is idempotent)
  • If b13/tag was installed, systag + systag_mm exist and the extension is active