Drei parallele matt-schwarze Kanalrohre auf gebürstetem Stahl-Rail, jedes Rohr mit dezentem Farb-Marker am Ende, weiches Tageslicht.
Extension · moselwal/semantic-delivery · MIT

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.

Die Realität

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.

Availability: coming soon — public release in preparation

Public availability as a Composer package is being prepared. If you already want to use the component in your TYPO3 platform, contact us via the form — we currently deliver as part of platform engagements.

02 — Plattformen

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
03 — Backend & Operations

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

TablePurpose
tx_semanticdelivery_channel_contentChannel-specific content variants
tx_semanticdelivery_distribution_jobDistribution job queue
tx_semanticdelivery_schema_cacheCached Schema.org data
tx_semanticdelivery_oauth_tokenEncrypted platform OAuth tokens

Page fields

FieldTypeDescription
tx_semanticdelivery_schema_typeSelectSchema.org type override (auto-detect by default)
tx_semanticdelivery_channelsCheckBoxDelivery channels (Website, AI Agent, Voice, Social Media)
tx_semanticdelivery_distribution_enabledCheckEnable distribution for this page
tx_semanticdelivery_distribution_platformsCheckBoxTarget 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)

PlatformDistributorAuthLimit
BlueskyBlueskyDistributorApp Password300 graphemes
LinkedInLinkedInDistributorOAuth 2.0 Access Token3,000 chars
X/TwitterTwitterXDistributorBearer Token280 chars
InstagramInstagramDistributorPage Access Token2,200 chars
FacebookFacebookDistributorPage Access Token63,000 chars

Blog platforms (long-form)

PlatformDistributorAuthFormat
Dev.toDevToDistributorAPI KeyMarkdown

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

PlatformDistributorAuth
AyrshareAyrshareDistributorAPI Key
BufferBufferDistributorAccess Token
n8nN8nDistributorWebhook URL
Late.devLateDevDistributorAPI Key
Generic WebhookWebhookDistributorCustom 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:

  1. Enable Distribution
  2. Pick the target platforms (only configured ones appear)
  3. 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):

Adding a distributor

  1. Create a class implementing DistributorInterface in Classes/Infrastructure/Distribution/
  2. Optionally add a TransformerInterface in Classes/Infrastructure/Transformer/
  3. Register the credential mapping in DistributionPlatformItemsProcFunc::CREDENTIAL_KEYS
  4. Add the corresponding site setting in settings.definitions.yaml and settings.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

  1. 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 Resolver null liefern, damit der zuständige Generator die Seite übernimmt.
  2. Slug-Heuristik — nur sprachstabile Index-/Section-Patterns.
  3. Hero-Content-Block-Hinweis — der erste passende CType gewinnt.
  4. Keywords-Heuristik auf Basis von pages.keywords.
  5. Default: Article bei Standardseiten (doktype 1 oder 2 mit Inhalten), sonst WebPage.

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-PatternSchema-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-CTypeFamilieSchema-Typ
moselwal_blogheroarticleBlogPosting
moselwal_pageheroarticleArticle
moselwal_herocardarticleArticle
moselwal_teaserarticleNewsArticle
moselwal_herowebpageWebPage
moselwal_oleherowebpageAboutPage
moselwal_nozzleherowebpageWebPage

Keywords-Heuristik

Keyword enthältSchema-Typ
tech, technicalTechArticle
paper, study, researchScholarlyArticle
reportReport
socialSocialMediaPosting
blogBlogPosting
news, pressNewsArticle
collection, indexCollectionPage

Backend-Override-Dropdown

Das Page-Property-Dropdown ist via --div---Separatoren gruppiert; jede Option trägt einen Tooltip für das Backend.

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.

  1. Auf bsky.app einloggen.
  2. Settings → Privacy and Security → App Passwords öffnen.
  3. Add App Password, Namen vergeben (z. B. „TYPO3 Distribution“).
  4. Generiertes Passwort kopieren und unter semanticDelivery.social.bluesky.appPassword via %secret(BLUESKY_APP_PASSWORD)% referenzieren. Handle (z. B. yourhandle.bsky.social) direkt in YAML.

LinkedIn

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.

  1. Auf linkedin.com/developers/apps über Create app eine App anlegen, Company Page verknüpfen, Logo hochladen.
  2. Im Reiter Products das Produkt Share on LinkedIn aktivieren.
  3. Im Reiter Auth Client ID, Client Secret und Redirect URL hinterlegen.
  4. Authorization-Code über www.linkedin.com/oauth/v2/authorization mit Scope openid profile w_member_social einholen.
  5. Code per POST auf www.linkedin.com/oauth/v2/accessToken gegen Access- und Refresh-Token tauschen.
  6. Person-ID per GET api.linkedin.com/v2/userinfo ermitteln (Feld sub).
  7. Token, personId und optional organizationId in semanticDelivery.social.linkedin eintragen. Wenn beide gesetzt sind, hat organizationId Vorrang.

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.

  1. Auf developer.x.com/en/portal ein Project und darin eine App anlegen.
  2. Unter User authentication settings OAuth 2.0 aktivieren.
  3. App-Permissions auf Read and write setzen.
  4. Unter Keys and tokens einen Bearer Token erzeugen.
  5. Token in semanticDelivery.social.twitter.bearerToken via %secret(TWITTER_BEARER_TOKEN)% hinterlegen.

Facebook

Auth: Long-Lived Page Access Token. Scopes: pages_manage_posts, pages_read_engagement.

  1. Auf developers.facebook.com über Create App eine Business-App anlegen und das Produkt Facebook Login hinzufügen.
  2. Im Graph API Explorer die App auswählen, Generate Access Token klicken und pages_manage_posts erteilen.
  3. Im Dropdown User or Page die Ziel-Page wählen — das angezeigte Token ist nun ein Page-Token.
  4. 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.
  5. Page-ID via GET graph.facebook.com/v21.0/me/accounts auslesen.
  6. accessToken und pageId unter semanticDelivery.social.facebook setzen.

Instagram

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.

  1. Account-ID ermitteln: GET graph.facebook.com/v21.0/FACEBOOK_PAGE_ID?fields=instagram_business_account&access_token=PAGE_ACCESS_TOKEN.
  2. instagram_business_account.id aus der Antwort als accountId übernehmen.
  3. Page-Token (identisch zu Facebook) als accessToken in semanticDelivery.social.instagram eintragen.

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.

  1. Auf dev.to einloggen.
  2. Settings → Extensions öffnen.
  3. Unter DEV Community API Keys einen neuen Key erzeugen und kopieren.
  4. Key unter semanticDelivery.blogging.devto.apiKey via %secret(DEVTO_API_KEY)% hinterlegen.
Nächster Schritt

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.

Custom-Distributor besprechen

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.