
semantic-delivery — one piece of content, eleven distributors.
Schema.org enrichment and multichannel distribution for TYPO3 14. Six native platform adapters (LinkedIn, X/Twitter, Instagram, Facebook, Bluesky, Dev.to) plus five proxy adapters (Ayrshare, Buffer, n8n, Late.dev, Webhook). Per-page platform selection, backend module, CLI commands. MIT licence.
Multichannel distribution usually ends in a copy-paste loop.
With semantic-delivery
- Auto-generation of Schema.org JSON-LD from content models
- Channel transformer per channel (Web/AI/Voice/Social) with character-limit awareness
- Direct distribution on 6 platforms, proxy distribution on 5 more
- OAuth tokens AES-256-CBC encrypted in the backend, with refresh flow
- Dev.to cross-posting with canonical URL attribution
- Podcast episode detection with automatic job creation
- Per-page platform selection in the backend — only configured platforms visible
Until now
- Manual adjustment per channel, each with its own character limits
- JSON-LD writing in the template
- No channel detection in the frontend (Web vs AI Agent vs Voice)
- OAuth token management as a side project with plain-text storage
- Cross-posting in blog platforms without canonical attribution
- Podcast episode detection as a cron-job hack
Four core building blocks
Channel detection middleware
Detects from headers / user-agent / Accept whether the request is coming from an AI agent, voice assistant, social crawler, or browser — and serves the appropriate format.
Distributor architecture
Auto-discovery via Symfony DI. Custom distributors implement DistributorInterface and appear automatically in the page editor as soon as the related site credentials are configured.
Channel transformer
One transformer per channel: Web/AI Agent/Voice/Social Media. Trim to platform limits (e.g. 280 characters X, 300 graphemes Bluesky), tonality adjustment, asset selection.
Schema.org enrichment
JSON-LD is generated automatically from the content model: Article, BlogPosting, NewsArticle, FAQPage, Product, HowTo, Organization, LocalBusiness. Auto-detect with manual override per page.
Eleven distributors in three categories
Six platforms are served directly through their native APIs (direct distribution); one of these is a long-form blog platform with canonical attribution. Five more run via established proxy services — useful if, for example, you already use Buffer or Ayrshare.
Proxy services
- Ayrshare — API Key
- Buffer — Access Token
- n8n — Webhook URL
- Late.dev — API Key
- Generic Webhook — Custom URL
Long-form blog
- Dev.to — API Key · Markdown format
Automatically sets a canonical URL back to the original TYPO3 page — no SEO penalty for cross-posting.
Social Media (short-form)
- Bluesky — App Password · 300 graphemes
- LinkedIn — OAuth 2.0 · 3,000 characters
- X / Twitter — Bearer Token · 280 characters
- Instagram — Page Access Token · 2,200 characters
- Facebook — Page Access Token · 63,000 characters
Backend, CLI and operations
semantic-delivery does not ship with backend extensions that nobody uses — the backend module is editor-ready from the start, the CLI commands are cron-friendly, and the OAuth service stores tokens AES-256-CBC encrypted.
DDD + tests
4-layer DDD (Domain / Application / Infrastructure / Presentation), strictly enforced via deptrac. PHPStan Level 8, PER-CS3x0, PHPUnit unit + functional tests. Four database tables (channel_content, distribution_job, schema_cache, oauth_token).
OAuth token service
For platforms with OAuth 2.0 + refresh: tokens are stored AES-256-CBC encrypted in the backend (TYPO3 encryption key). API for storage, lookup, refresh, and expiry check is in the service container.
CLI commands
semantic-delivery:detect-episodes detects new podcast episodes and creates distribution jobs (with --dry-run and --auto-publish). semantic-delivery:process-jobs works the queue.
Backend module
Under Content → Semantic Delivery (admin access): per-platform preview before publish, job queue management, log with external URLs of successful distributions. Per-page "Semantic Delivery" tab with platform checkbox list.
Database and page fields
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) |
tx_semanticdelivery_channels | CheckBox | Delivery channels (Website, AI Agent, Voice, Social Media) |
tx_semanticdelivery_distribution_enabled | Check | Enable distribution for this page |
tx_semanticdelivery_distribution_platforms | CheckBox | Target platforms (dynamically populated) |
The platform selector only shows platforms with credentials configured for the current site. This stops editors from picking targets that would fail.
Supported platforms
Social media (short-form)
| Platform | Distributor | Auth | Limit |
|---|---|---|---|
| Bluesky | BlueskyDistributor | App Password | 300 graphemes |
LinkedInDistributor | OAuth 2.0 Access Token | 3,000 chars | |
| X/Twitter | TwitterXDistributor | Bearer Token | 280 chars |
InstagramDistributor | Page Access Token | 2,200 chars | |
FacebookDistributor | Page Access Token | 63,000 chars |
Blog platforms (long-form)
| Platform | Distributor | Auth | Format |
|---|---|---|---|
| Dev.to | DevToDistributor | API Key | Markdown |
Blog distributors automatically set a canonical URL back to the original TYPO3 page. Note: Medium has discontinued issuing new API tokens; the MediumDistributor has been removed.
Proxy services
| Platform | Distributor | Auth |
|---|---|---|
| Ayrshare | AyrshareDistributor | API Key |
| Buffer | BufferDistributor | Access Token |
| n8n | N8nDistributor | Webhook URL |
| Late.dev | LateDevDistributor | API Key |
| Generic Webhook | WebhookDistributor | Custom URL |
Setup
1. Enable distribution in site settings
semanticDelivery:
channels:
socialMedia:
enabled: true
distribution:
enabled: true
defaultDistributor: 'bluesky'
2. Configure platform credentials
Credentials sit per site in config/sites/<name>/config.yaml. Sensitive tokens use the %secret()% syntax (see moselwal/secret-resolver); public identifiers go directly into the YAML.
Only platforms with a non-empty primary credential appear in the backend editor. Platforms without credentials are hidden, so editors cannot select targets that would fail.
3. Run the database schema update
vendor/bin/typo3 database:updateschema
4. Per-page platform selection
In the TYPO3 backend, every page has a Semantic Delivery tab:
- Enable Distribution
- Pick the target platforms (only configured ones appear)
- Save and publish
The distribution system honours this choice — pages are only sent to the platforms you ticked.
CLI, backend module and extension
CLI commands
# Detect new podcast episodes
vendor/bin/typo3 semantic-delivery:detect-episodes --dry-run
vendor/bin/typo3 semantic-delivery:detect-episodes
vendor/bin/typo3 semantic-delivery:detect-episodes --auto-publish
# Process pending jobs
vendor/bin/typo3 semantic-delivery:process-jobs
Recommended cron
*/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 — inspect completed and failed distributions with external URLs
Adding a distributor
- Create a class implementing
DistributorInterfaceinClasses/Infrastructure/Distribution/ - Optionally add a
TransformerInterfaceinClasses/Infrastructure/Transformer/ - Register the credential mapping in
DistributionPlatformItemsProcFunc::CREDENTIAL_KEYS - Add the corresponding site setting in
settings.definitions.yamlandsettings.yaml
Once credentials are configured, the platform automatically appears in the page editor. OAuth tokens flow through the OAuthTokenService and are encrypted at rest with AES-256-CBC using the TYPO3 encryption key.
Source code & docs
TYPO3 Extension Repository
Not in the official TER — public Composer distribution is being prepared (coming soon).
Composer package
Public release as moselwal/semantic-delivery in preparation. Coming soon.
Repository
Source code and issue tracker will be opened with the public release. Coming soon.
Mirror
Public mirror and pull-request workflow follow with the release. Coming soon.
Schema.org-Resolver-Logik
Der SchemaTypeResolverService entscheidet pro Seite, ob ein Dokument der Article- oder WebPage-Familie ausgegeben wird. ArticleSchemaGenerator (Priority 50) und WebPageSchemaGenerator (Priority 45) delegieren beide ihr canHandle() an den Resolver, sodass nie beide Familien gleichzeitig erscheinen.
Auflösungs-Reihenfolge
- Manueller Override über
pages.tx_semanticdelivery_schema_type. Werte aus Article- oder WebPage-Familie werden direkt übernommen. Andere Werte (z. B.Product,FAQPage) lassen den Resolvernullliefern, damit der zuständige Generator die Seite übernimmt. - Slug-Heuristik — nur sprachstabile Index-/Section-Patterns.
- Hero-Content-Block-Hinweis — der erste passende CType gewinnt.
- Keywords-Heuristik auf Basis von
pages.keywords. - Default:
Articlebei Standardseiten (doktype1 oder 2 mit Inhalten), sonstWebPage.
Sprachspezifische Patterns wie /kontakt, /ueber-uns, /about, /contact, /team, /profil werden bewusst nicht automatisch erkannt — hier setzen Sie den Override im Backend.
Slug-Heuristik
| Slug-Pattern | Schema-Typ |
|---|---|
/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> | ItemPage |
Hero-CType-Mapping
| Hero-CType | Familie | Schema-Typ |
|---|---|---|
moselwal_bloghero | article | BlogPosting |
moselwal_pagehero | article | Article |
moselwal_herocard | article | Article |
moselwal_teaser | article | NewsArticle |
moselwal_hero | webpage | WebPage |
moselwal_olehero | webpage | AboutPage |
moselwal_nozzlehero | webpage | WebPage |
Keywords-Heuristik
| Keyword enthält | Schema-Typ |
|---|---|
tech, technical | TechArticle |
paper, study, research | ScholarlyArticle |
report | Report |
social | SocialMediaPosting |
blog | BlogPosting |
news, press | NewsArticle |
collection, index | CollectionPage |
Backend-Override-Dropdown
Das Page-Property-Dropdown ist via --div---Separatoren gruppiert; jede Option trägt einen Tooltip für das Backend.
- Auto (Erkennung über Slug, Hero-CB, Keywords)
- Article-Familie:
Article,BlogPosting,NewsArticle,TechArticle,ScholarlyArticle,Report,SocialMediaPosting - WebPage-Familie:
WebPage,AboutPage,ContactPage,CollectionPage,ItemPage,ProfilePage,QAPage - Other:
FAQPage,Product,HowTo,Organization,LocalBusiness
Werte außerhalb der Article-/WebPage-Familien geben die Seite an den jeweils zuständigen Generator (FAQ, Product, Organization …) ab. Der Resolver liefert ebenfalls null, wenn die Seite FAQ-, Service-, Product-, Podcast- oder PackagesGrid-CTypes enthält oder unterhalb von /leistungen, /impressum, /datenschutz, /karriere oder /podcasts liegt.
Plattform-Setup-Guides
Schritt-für-Schritt-Einstieg pro Plattform. Sensible Tokens werden über %secret()% aufgelöst, öffentliche Identifier (Handle, Page-ID, Account-ID) stehen direkt in der YAML.
Bluesky
Auth: App Password. Token läuft nicht ab und kann jederzeit in den App-Passwords-Einstellungen widerrufen werden.
- Auf
bsky.appeinloggen. - Settings → Privacy and Security → App Passwords öffnen.
- Add App Password, Namen vergeben (z. B. „TYPO3 Distribution“).
- Generiertes Passwort kopieren und unter
semanticDelivery.social.bluesky.appPasswordvia%secret(BLUESKY_APP_PASSWORD)%referenzieren. Handle (z. B.yourhandle.bsky.social) direkt in YAML.
Auth: OAuth 2.0 Access Token. Modi: w_member_social (privat) oder w_organization_social (Company Page). Tokens laufen nach 60 Tagen ab; Refresh manuell oder per OAuthTokenService.
- Auf
linkedin.com/developers/appsüber Create app eine App anlegen, Company Page verknüpfen, Logo hochladen. - Im Reiter Products das Produkt Share on LinkedIn aktivieren.
- Im Reiter Auth Client ID, Client Secret und Redirect URL hinterlegen.
- Authorization-Code über
www.linkedin.com/oauth/v2/authorizationmit Scopeopenid profile w_member_socialeinholen. - Code per
POSTaufwww.linkedin.com/oauth/v2/accessTokengegen Access- und Refresh-Token tauschen. - Person-ID per
GET api.linkedin.com/v2/userinfoermitteln (Feldsub). - Token,
personIdund optionalorganizationIdinsemanticDelivery.social.linkedineintragen. Wenn beide gesetzt sind, hatorganizationIdVorrang.
Detail-Doku im README.
X / Twitter
Auth: OAuth 2.0 Bearer Token. Scopes: tweet.read, tweet.write, users.read. Für tweet.write ist mindestens Basic-Access erforderlich.
- Auf
developer.x.com/en/portalein Project und darin eine App anlegen. - Unter User authentication settings OAuth 2.0 aktivieren.
- App-Permissions auf Read and write setzen.
- Unter Keys and tokens einen Bearer Token erzeugen.
- Token in
semanticDelivery.social.twitter.bearerTokenvia%secret(TWITTER_BEARER_TOKEN)%hinterlegen.
Auth: Long-Lived Page Access Token. Scopes: pages_manage_posts, pages_read_engagement.
- Auf
developers.facebook.comüber Create App eine Business-App anlegen und das Produkt Facebook Login hinzufügen. - Im Graph API Explorer die App auswählen, Generate Access Token klicken und
pages_manage_postserteilen. - Im Dropdown User or Page die Ziel-Page wählen — das angezeigte Token ist nun ein Page-Token.
- Token gegen Long-Lived-Token tauschen:
GET graph.facebook.com/v21.0/oauth/access_token?grant_type=fb_exchange_token&client_id=APP_ID&client_secret=APP_SECRET&fb_exchange_token=SHORT_LIVED_TOKEN. - Page-ID via
GET graph.facebook.com/v21.0/me/accountsauslesen. accessTokenundpageIduntersemanticDelivery.social.facebooksetzen.
Auth: Instagram Graph API über Facebook-Page-Token. Scopes: instagram_basic, instagram_content_publish. Voraussetzung: Facebook-Page mit verknüpftem Instagram-Business- oder Creator-Konto. Reine Text-Posts werden von der Graph API nicht unterstützt.
- Account-ID ermitteln:
GET graph.facebook.com/v21.0/FACEBOOK_PAGE_ID?fields=instagram_business_account&access_token=PAGE_ACCESS_TOKEN. instagram_business_account.idaus der Antwort alsaccountIdübernehmen.- Page-Token (identisch zu Facebook) als
accessTokeninsemanticDelivery.social.instagrameintragen.
Dev.to
Auth: API-Key, kein OAuth. Token läuft nicht ab. Posts werden als Markdown publiziert, kanonische URL zeigt zurück auf die TYPO3-Seite.
- Auf
dev.toeinloggen. - Settings → Extensions öffnen.
- Unter DEV Community API Keys einen neuen Key erzeugen und kopieren.
- Key unter
semanticDelivery.blogging.devto.apiKeyvia%secret(DEVTO_API_KEY)%hinterlegen.
Your platform missing?
Auto-discovery turns adding a new distributor into a question of two classes — implement DistributorInterface, optionally a transformer, add credential mapping in DistributionPlatformItemsProcFunc::CREDENTIAL_KEYS, done. If you want to do it yourself: docs in the repo. If you want us to build it: get in touch.
Oder direkt schreiben: kontakt@moselwal.de
Where we use this …
This package carries the multi-channel distribution in AI-Ready CMS and AI-Ready Commerce. In the managed variant it runs as part of AI-Ready CMS as a Service.