koh / semantic-delivery
Multi-channel content delivery and distribution for TYPO3 — Markdown/AI-Agent/Voice channel negotiation, Distribution adapters for LinkedIn, X, Bluesky, Facebook, Instagram, Dev.to, Buffer, Ayrshare, n8n. Consumes Schema.org via koh/structured-content.
Package info
github.com/ohartwig/semantic-delivery
Type:typo3-cms-extension
pkg:composer/koh/semantic-delivery
Requires
- php: ^8.3
- koh/structured-content: ^2.14
- typo3/cms-backend: ^14.0
- typo3/cms-core: ^14.0
- typo3/cms-frontend: ^14.0
Requires (Dev)
- koh/dev-typo3: ^1.0
Suggests
- koh/content-intelligence: Required for AI-powered social media content transformation
Provides
None
Conflicts
None
Replaces
None
README
Schema.org enrichment and multi-channel content transformation for TYPO3. Delivers structured content across Website, AI Agent, Voice, and Social Media channels with automated distribution to external platforms.
Features
- Schema.org JSON-LD — Automatic structured data enrichment for TYPO3 content
- Multi-Channel Transformation — Optimized output for Website, AI Agent, Voice, and Social Media
- Platform Distributors — Distributor classes for LinkedIn, X/Twitter, Instagram, Facebook, Bluesky, Medium and Dev.to (native APIs) and for Ayrshare, Buffer, webhooks and n8n; they post only when the caller passes the credentials (see Distribution)
- Podcast Episode Detection — Automatic new-episode monitoring with multi-platform job creation
- Channel Detection — Middleware-based automatic detection of requesting channel
- OAuth Token Management — Encrypted per-platform token storage with refresh support
- Schema Caching — Performance-optimized schema generation with TYPO3 caching
- DDD 4-Layer Architecture — Strict layer separation enforced by deptrac
Installation
composer require koh/semantic-delivery
Requirements
- PHP 8.3+
- TYPO3 14.0
Architecture
Classes/
├── Domain/ # Channel models, schema value objects, contracts
├── Application/ # Transformation services, distribution orchestration
├── Infrastructure/ # Distribution adapters, schema generators, TCA helpers
└── Presentation/ # Controllers, event listeners
Database Tables
| Table | Purpose |
|---|---|
tx_semanticdelivery_channel_content |
Channel-specific content variants |
tx_semanticdelivery_distribution_job |
Distribution job queue |
tx_semanticdelivery_schema_cache |
Cached Schema.org data |
tx_semanticdelivery_oauth_token |
Encrypted platform OAuth tokens |
Page Fields
| Field | Type | Description |
|---|---|---|
tx_semanticdelivery_schema_type |
Select | Schema.org type override (auto-detect by default, grouped by Article / WebPage / Other family — see Schema.org Generation) |
tx_semanticdelivery_channels |
CheckBox | Delivery channels (Website, AI Agent, Voice, Social Media) |
Schema.org Generation
Each frontend response is enriched with one or more <script type="application/ld+json"> blocks. Generators are auto-discovered (semantic_delivery.schema_generator tag) and ordered by priority — every generator that returns true from canHandle() contributes one document.
Generator Priority Order
| Priority | Generator | Output |
|---|---|---|
| 70 | FaqSchemaGenerator |
FAQPage (CType *faq* or Q&A patterns) |
| 60 | ProductSchemaGenerator / PackagesGridProductSchemaGenerator / ServiceSchemaGenerator |
Product, Service, Offer |
| 55 | BreadcrumbListSchemaGenerator, OrganizationSchemaGenerator, … |
BreadcrumbList, Organization |
| 50 | ArticleSchemaGenerator |
Article family — see resolver |
| 45 | WebPageSchemaGenerator |
WebPage family — see resolver |
| 30 | ContentBlockSchemaMapper |
Site-settings-driven CType → schema mapping |
| 10 | AutoDetectSchemaGenerator |
Fuzzy fallback (only when resolver did not match) |
Article / WebPage Family Routing
Application/Service/SchemaTypeResolverService is the single source of truth for picking a schema type from the Article- or WebPage-family. Both ArticleSchemaGenerator (priority 50) and WebPageSchemaGenerator (priority 45) delegate their canHandle() to the resolver, so a page emits either an Article-family or a WebPage-family document — never both.
Resolution order (highest priority first):
-
Manual override —
pages.tx_semanticdelivery_schema_type. Article-/WebPage-family values are honoured directly. Other values (e.g.Product,FAQPage) cause the resolver to returnnullso the dedicated generator owns the page. -
Slug heuristic — language-stable index/section patterns only:
Slug pattern Schema Type /blog(root)CollectionPage/blog/<sub>BlogPosting/news,/press,/presse(root)CollectionPage/news/<sub>,/press/<sub>,/presse/<sub>NewsArticle/portfolio,/referenz*(root)CollectionPage/portfolio/<sub>,/referenz*/<sub>ItemPageLanguage-specific patterns (
/kontakt,/ueber-uns,/about,/contact,/team,/profil) are not auto-detected — for these pages the editor sets the override in the backend (the dropdown shows a description per type). -
Hero content block hint — first matching CType wins:
Hero CType Family Schema Type moselwal_blogheroarticle BlogPostingmoselwal_pageheroarticle Articlemoselwal_herocardarticle Articlemoselwal_teaserarticle NewsArticlemoselwal_herowebpage WebPagemoselwal_oleherowebpage AboutPagemoselwal_nozzleherowebpage WebPage -
Keywords heuristic (
pages.keywordssubstrings):Keyword contains Schema Type tech,technicalTechArticlepaper,study,researchScholarlyArticlereportReportsocialSocialMediaPostingblogBlogPostingnews,pressNewsArticlecollection,indexCollectionPage -
Default —
Articlefor standard pages (doktype1 or 2 with content), otherwiseWebPage.
The resolver returns null (and the dedicated generator takes over) when the page contains FAQ / Service / Product / Podcast / PackagesGrid CTypes, or sits below /leistungen, /impressum, /datenschutz, /karriere, /podcasts.
Backend Override Dropdown
The page property dropdown is grouped via --div-- separators and every option carries a descriptive tooltip rendered by the TYPO3 backend:
Auto (detect via Slug / Hero-CB / Keywords)
── Article family ──
Article, BlogPosting, NewsArticle, TechArticle,
ScholarlyArticle, Report, SocialMediaPosting
── WebPage family ──
WebPage, AboutPage, ContactPage, CollectionPage,
ItemPage, ProfilePage, QAPage
── Other ──
FAQPage, Product, HowTo, Organization, LocalBusiness
The descriptions guide editors on when to pick each type — important for language-specific pages (/kontakt, /ueber-uns, /about …) that the resolver does not auto-detect. Values outside the Article/WebPage families let the dedicated generator (FAQ, Product, Organization, …) own the page.
Distribution
Supported Platforms
Social Media (short-form)
| Platform | Distributor | Auth Method | Character Limit |
|---|---|---|---|
| Bluesky | BlueskyDistributor |
App Password | 300 graphemes |
LinkedInDistributor |
OAuth 2.0 Access Token | 3,000 chars | |
| X/Twitter | TwitterXDistributor |
Bearer Token (OAuth 2.0) | 280 chars |
InstagramDistributor |
Page Access Token | 2,200 chars | |
FacebookDistributor |
Page Access Token | 63,000 chars |
Blog Platforms (long-form)
| Platform | Distributor | Auth Method | Content Format |
|---|---|---|---|
| Dev.to | DevToDistributor |
API Key | Markdown |
Blog distributors include canonical URL attribution back to the original TYPO3 page.
Note: Medium has discontinued issuing new API integration tokens. The MediumDistributor has been removed.
Proxy Services
| Platform | Distributor | Auth Method |
|---|---|---|
| Ayrshare | AyrshareDistributor |
API Key |
| Buffer | BufferDistributor |
Access Token |
| n8n | N8nDistributor |
Webhook URL |
| Late.dev | LateDevDistributor |
API Key |
| Generic Webhook | WebhookDistributor |
Custom URL |
Credentials are not read from site settings
The distributors take their credentials from the $options array of DistributionService::distribute() and refuse without them. Nothing in this package maps site settings into that array, and semantic-delivery:process-jobs passes none, so a queued job ends failed with a "… is required" message until an integration supplies the credentials itself.
Up to 3.2 the site set defined social and blogging settings subtrees under semanticDelivery for platform credentials, and a per-page platform selector read it. Nothing ever passed those values to a distributor, so an installation held long-lived tokens for its public channels in its site configuration for a feature that could not use them (AUD-026-F01). The settings tree, the two unread settings semanticDelivery.distribution.enabled / .defaultDistributor and the page fields tx_semanticdelivery_distribution_enabled / _platforms were removed. Do not put platform credentials into site settings. If an installation still has them in config/sites/*/config.yaml, delete them there, delete the secrets behind them, and revoke the tokens at the platforms.
CLI Commands
Detect new podcast episodes
# Dry run vendor/bin/typo3 semantic-delivery:detect-episodes --dry-run # Create distribution jobs vendor/bin/typo3 semantic-delivery:detect-episodes # Auto-publish without review vendor/bin/typo3 semantic-delivery:detect-episodes --auto-publish
Cache key v3.0.4+: the episode detector reads from the
content_blocks_collection_rssfeedcache with'rssfeed_' . hash('sha256', $feedUrl). This must match the write key inmoselwal/content-blocks-collection'sPodcastFeedService::cacheIdentifierFor()(previously md5 — the mismatch meant the detector found no episodes). Its ownpodcast_hash_*/podcast_guids_*keys still use md5, because the detector maintains both sides itself.
Process pending jobs
vendor/bin/typo3 semantic-delivery:process-jobs
Recommended cron schedule
*/15 * * * * cd /app && vendor/bin/typo3 semantic-delivery:detect-episodes */5 * * * * cd /app && vendor/bin/typo3 semantic-delivery:process-jobs
Backend Module
Available under Content → Semantic Delivery (admin access):
- Preview — View transformed content per platform before publishing
- Queue — Manage pending distribution jobs
- Log — Review completed and failed distributions with external URLs
Adding a New Distributor
New distributors are auto-discovered via dependency injection:
- Create a class implementing
DistributorInterfaceinClasses/Infrastructure/Distribution/ - Optionally create a
TransformerInterfaceinClasses/Infrastructure/Transformer/
final class MastodonDistributor implements DistributorInterface { public function getIdentifier(): string { return 'mastodon'; } public function distribute(ChannelContent $content, string $platform, array $options = []): DistributionResult { // Call Mastodon API return DistributionResult::success($statusId, $statusUrl); } public function getAvailablePlatforms(): array { return ['mastodon']; } public function testConnection(): bool { return true; } }
OAuth Token Service
For platforms requiring OAuth 2.0 with token refresh:
$oauthService->storeToken( platform: 'linkedin', siteIdentifier: 'example', accessToken: $accessToken, refreshToken: $refreshToken, expiresAt: time() + 3600, scope: 'w_organization_social', accountId: '12345678', accountName: 'Example Org', ); $token = $oauthService->getToken('linkedin', 'example'); if ($oauthService->isTokenExpired('linkedin', 'example')) { // Refresh token flow }
Tokens are encrypted at rest using AES-256-CBC with the TYPO3 encryption key.
Configuration Reference
Site Settings
| Setting | Type | Default | Description |
|---|---|---|---|
semanticDelivery.enabled |
bool | true |
Master switch |
semanticDelivery.schema.enabled |
bool | true |
JSON-LD generation |
semanticDelivery.schema.autoDetect |
bool | true |
Auto-detect schema types |
semanticDelivery.channels.socialMedia.enabled |
bool | true |
Social media channel |
Development
composer install composer test # Unit tests composer phpstan # Static analysis (level 8) composer deptrac # DDD layer enforcement vendor/bin/php-cs-fixer fix # Code style (PER-CS3x0)
Dependencies
| Package | Type | Purpose |
|---|---|---|
koh/content-intelligence |
Optional | Content quality analysis |
moselwal/secret-resolver |
Suggested | %secret()% syntax for credentials |
koh/dev |
Dev | Shared QA tooling |
License
MIT — see LICENSE for details.