lakedrops/social_promo

Assists authors in promoting blog posts to social media channels via ECA.

Maintainers

Package info

gitlab.lakedrops.com/drupal/recipes/social-promo

Type:drupal-recipe

pkg:composer/lakedrops/social_promo

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

dev-main 2026-07-23 15:20 UTC

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:

BundleChannel
sp_linkedinLinkedIn
sp_mastodonMastodon
sp_blueskyBluesky
sp_slackSlack

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.0
  • drupal/storage ^1.4
  • drupal/orchestration ^1.0 (+ orchestration_eca) — outbound webhook
  • drupal/key ^1.22
  • drupal/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_eca and ai_provider_ollama are 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 entities
  • edit any sp_linkedin storage entities
  • delete any sp_linkedin storage entities
  • view published sp_linkedin storage entities
  • view 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:

  1. In ActivePieces, import the flow (Flows → Import flow / Import from file) and select the JSON file.
  2. Reconnect every channel piece. The exported connections/auth are placeholders (e.g. "auth": "Something" and stale connections[...] references). For each of LinkedIn, Mastodon, Bluesky and Slack, open the step and select/create your own connection.
  3. 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).
  4. Tune per-channel formatting (text template, link handling, image usage) to your preference. The flow appends Link: {{node.url}} to the post text and passes image_url where supported.
  5. 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 /test note below).
  6. 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. orchestration looks the URL up by id and silently sends nothing if the id isn't registered. Register under social_promo, not a human label. register() overwrites the entry for that id.

ActivePieces /test URLs: a /test webhook 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.settings at 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_5b to 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_url resolves the node's header image (see issue #2 for a known gap in some update-hook contexts).
  • Validate secret on 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 is true) because drupal/orchestration requires 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 consider jsonapi_extras or 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).
  • #2image_url empty 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.