lakedrops / social_promo
Assists authors in promoting blog posts to social media channels via ECA.
Package info
gitlab.lakedrops.com/drupal/recipes/social-promo
Type:drupal-recipe
pkg:composer/lakedrops/social_promo
Requires
- drupal/ai: ^1.4
- drupal/ai_integration_eca: ^1.0
- drupal/ai_provider_ollama: ^1.2
- drupal/eca: ^3.1
- drupal/eca_tamper: ^2.0
- drupal/key: ^1.22
- drupal/orchestration: ^1.0
- drupal/storage: ^1.4
This package is not auto-updated.
Last update: 2026-07-23 15:26:58 UTC
README
A Drupal recipe that helps authors promote content (e.g. blog posts) across social media channels — LinkedIn, Mastodon, Bluesky, Slack — without a custom module. Per-channel promo text and tags are edited in a modal directly from the node form and node view, optionally drafted by a local AI model, and dispatched as a single JSON payload to an external webhook (e.g. an ActivePieces flow) when the content is published.
Everything is built with ECA (Events-Conditions-Actions) — no bespoke PHP.
What it does
- Adds a channel dropbutton to enabled node bundles, on both the edit form (sorted to the top) and the view page (only for users who can edit the node).
- Each dropbutton item opens that channel's native storage entity form in a Drupal Ajax modal — nice widgets, add-or-edit per channel, no page reload.
- An optional "Suggest with AI" button in each modal generates a channel-appropriate promo post from the node's title + body using a local Ollama model, filling the text field in place for the author to edit.
- On first publish of the node, a single JSON payload with all channels that have text is POSTed to a configured webhook. A per-channel guard ensures a channel is only posted once.
Data model
Each social channel is its own storage entity bundle:
| Bundle | Channel |
|---|---|
sp_linkedin | |
sp_mastodon | Mastodon |
sp_bluesky | Bluesky |
sp_slack | Slack |
Each bundle carries the shared fields field_node_ref (link to the node),
field_text (promo text), field_tags (tags/hashtags), and field_promoted
(first-publish guard flag). The channel set is operator-extensible: add
another storage bundle and the settings token derives it automatically.
Requirements
Composer dependencies (installed automatically):
drupal/eca^3.1 (+ submodules: base, access, content, form, endpoint, render)drupal/eca_tamper^2.0drupal/storage^1.4drupal/orchestration^1.0 (+orchestration_eca) — outbound webhookdrupal/key^1.22drupal/ai^1.4,drupal/ai_integration_eca^1.0,drupal/ai_provider_ollama^1.2- core
jsonapi(required by orchestration)
⚠ Release-candidate modules:
ai_integration_ecaandai_provider_ollamaare at RC stage (no stable GA at time of writing). Fine for dev/greenfield; review before relying on them in production.
Installation
composer require lakedrops/social_promo
php core/scripts/drupal recipe recipes/social_promo
# or, in an L3D/ddev/lando environment:
drush recipe /path/to/recipes/social_promo
drush cache:rebuild
Operator setup (required after applying)
The recipe cannot ship a few things because they are site-specific. Complete these steps or the feature will not work end-to-end.
1. Grant channel storage permissions to your editor roles
The per-channel modal renders the native storage form under the author's
session. Grant these permissions per channel bundle to the roles that edit
your enabled node bundles (example for LinkedIn):
add sp_linkedin storage entitiesedit any sp_linkedin storage entitiesdelete any sp_linkedin storage entitiesview published sp_linkedin storage entitiesview unpublished sp_linkedin storage entities
Repeat for sp_mastodon, sp_bluesky, sp_slack (and any channel bundles you
add). These are not granted via the recipe because role machine names are
site-specific.
The security boundary is the endpoint access model, which grants access only to users who can update the node. The view-page dropbutton is likewise hidden from anyone without node edit access.
2. Set up the receiving automation (ActivePieces)
A sample ActivePieces flow is included at
docs/activepieces-social-post-webhook.flow.json.
It expects this recipe's exact payload (see Webhook payload contract below):
a Webhook trigger → Loop on channels → a Router that posts each
item to the matching LinkedIn / Mastodon / Bluesky / Slack piece, using
node.url, image_url and each channel's text.
Import it, then adjust it — it will not work as-is:
- In ActivePieces, import the flow (Flows → Import flow / Import from file) and select the JSON file.
- Reconnect every channel piece. The exported connections/auth are
placeholders (e.g.
"auth": "Something"and staleconnections[...]references). For each of LinkedIn, Mastodon, Bluesky and Slack, open the step and select/create your own connection. - Trim to the channels you use. If you don't post to all four, delete the unused Router branches (and drop the matching storage bundle — see Data model).
- Tune per-channel formatting (text template, link handling, image usage) to
your preference. The flow appends
Link: {{node.url}}to the post text and passesimage_urlwhere supported. - Copy the flow's Webhook trigger URL — you register that URL in the next
step. Use the flow's live URL for production (see the
/testnote below). - Publish/enable the flow in ActivePieces so it listens.
3. Register the outbound webhook URL
The publish flow dispatches via drupal/orchestration to the webhook id
social_promo. Register the ActivePieces trigger URL under exactly that
id:
drush ev '\Drupal::service("orchestration.webhooks")->register("social_promo", "https://YOUR-WEBHOOK-URL");'
Critical — id must match.
orchestrationlooks the URL up by id and silently sends nothing if the id isn't registered. Register undersocial_promo, not a human label.register()overwrites the entry for that id.
ActivePieces
/testURLs: a/testwebhook URL generally only captures data while the flow's test/debug view is actively listening. For production use the live URL (the same path without the trailing/test).
Dispatch permission (known issue): the orchestration dispatch action checks a permission (
use orchestration) that the module does not define, so only admins currently dispatch. See issue #1.
4. Configure which node bundles are enabled + the header image field
The settings model social_promo_config provides the enabled node bundles, the
per-bundle header-image field map, and the webhook secret as tokens. Edit
config/eca.eca.social_promo_config.yml (or the active config) to match your
site's bundles and image fields.
5. AI provider (Ollama)
The recipe ships ai_provider_ollama.settings (host/port) and sets Ollama as the
default chat provider (qwen2_5_1_5b).
- Point
ai_provider_ollama.settingsat your Ollama server (host_name,port). - The Ollama endpoint must be reachable without authentication (the provider sends no auth headers) — put it behind an auth-injecting proxy if needed.
- Live model discovery: the model list is fetched live from Ollama and cached
in State. The site must reach the Ollama server once (visit the provider
settings or clear cache) for
qwen2_5_1_5bto resolve.
Webhook payload contract
On first publish, the recipe POSTs Content-Type: application/json:
{
"secret": "<your configured webhook secret>",
"node": {
"id": "40",
"title": "Article title",
"url": "https://example.com/blog/article-title",
"langcode": "en"
},
"image_url": "https://example.com/…/header.jpg",
"channels": [
{ "channel": "linkedin", "text": "…promo text…", "tags": "drupal, awards" },
{ "channel": "bluesky", "text": "…promo text…", "tags": "bsky" }
]
}
- Only channels that have promo text are included.
- Each channel is posted once (subsequent publishes/edits do not re-post it).
image_urlresolves the node's header image (see issue #2 for a known gap in some update-hook contexts).- Validate
secreton the receiving side.
The included ActivePieces flow (docs/activepieces-social-post-webhook.flow.json)
consumes this exact structure — see Operator setup → step 2.
Security notes
- JSON:API is enabled with
read_only: false(core default istrue) becausedrupal/orchestrationrequires it. This widens the JSON:API surface to include write endpoints site-wide. Those endpoints remain gated by normal Drupal entity/field access and permissions, but you are exposing more of the API. Review your JSON:API access controls: restrict which roles can authenticate, keep entity/field permissions tight, and considerjsonapi_extrasor a reverse proxy if broad write access is undesirable. - The webhook payload includes a shared secret — treat the webhook URL and secret as sensitive; prefer HTTPS endpoints and verify the secret server-side.
Known issues / roadmap
Tracked on the project issue queue:
- #1 — orchestration dispatch works only for admins (permission string bug).
- #2 —
image_urlempty in some publish contexts. - #3 — AI: small-model char limits, latency, tags still manual.
- #5 — packaging polish + automated tests.
License
GPL-2.0-or-later.