WP Manifestindependent plugin directory
manifest / social / lazaro

Lazaro – Publicador Social

Lazaro – WordPress plugin that auto-publishes posts to X, Facebook Pages and Instagram

by Next Tech · github.com/crocdc/lazaro · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/crocdc/lazaro/archive/refs/heads/main.zip

Lazaro – Social Auto-Publisher

Standalone WordPress plugin that publishes posts to a site's social networks (X / Twitter, Facebook Page, Instagram). It auto-publishes when a post is published, re-posts when the title or featured image changes, and offers a manual per-post button to (re)publish to selected networks.

It is site-agnostic: every network is credential-driven from the admin screen, so the same plugin serves any number of sites.

The plugin UI is Spanish-only. Source strings are written in Spanish and there is no translation catalogue: the sites it serves are Argentinian, and a single language keeps the screens and the shipped guide in sync. The credential field names (API Key, Access Token, Page ID, Page Access Token, Instagram Business Account ID) stay untranslated on purpose — they are the literal names X and Meta use in their consoles, which is what makes the guide's field-by-field mapping work.

Content published per post: title + link + featured image.

  • X / Facebook: text (title) + the note's link; the featured image shows up through the note's Open Graph card on the link preview.
  • Instagram: image-first — it uploads the featured image with the title + link as the caption. A post without a featured image is skipped on Instagram.

Install

The repository root is the plugin, so it drops straight into a WordPress install:

git clone <repo> wp-content/plugins/lazaro-social   # or web/app/plugins on Bedrock
wp plugin activate lazaro-social

On a Bedrock site managed by Composer, add it as a VCS repository instead (composer.json here already declares type: wordpress-plugin, so composer/installers puts it under web/app/plugins/):

{
  "repositories": [
    { "type": "vcs", "url": "<repo url>" }
  ],
  "require": {
    "nexttech/lazaro-social": "^1.0"
  }
}

Then configure the credentials at Settings → Lazaro Redes.

That screen also offers a downloadable PDF guide (in Spanish) walking through how to obtain each network's credentials, screenshot by screenshot — including the two traps that silently break publishing: an X token minted before write permissions were set, and an Instagram account not linked to its Facebook Page. Its source lives in docs/credentials-guide/.

Migrating from accesonorte-social

This plugin started life as accesonorte-social, embedded in the Acceso Norte site repo. On a site that ran that version, deactivate the old plugin first, then activate this one: on its first load it imports the legacy settings, queue and per-post state (an_social_*lazaro_social_*).

The import copies, never moves — the old rows stay untouched, so falling back to the old plugin loses nothing. Running both at once would double-post.

Delivery is asynchronous (bulletproof)

Publishing never blocks saving the post. Jobs go into a queue processed in the background by WP-Cron, with:

  • Atomic locking (add_option UNIQUE-key mutex) + job leasing so concurrent cron runs / saves never double-post or corrupt the queue.
  • Retries with backoff (1m → 5m → 15m → 1h → 3h, up to 5 attempts) on transient errors only (HTTP 429 / 5xx / network timeouts). Permanent errors (bad credentials, missing permission, invalid content) are not retried.
  • Cron kick: a non-blocking loopback to wp-cron.php so jobs run within seconds instead of waiting for the next visitor.
  • Idempotency: at most one pending job per (post, network); networks already up to date are skipped, so re-saving an unchanged post posts nothing.

The editor metabox polls a status endpoint, so outcomes (published / out of date / queued / failed) appear live without leaving the screen.

Updates: "re-post on change"

When a published post's title or featured image changes, the plugin makes each network reflect the new content. Platform capabilities differ:

Network On change
Facebook Deletes the previous Page post and publishes a fresh one.
X Deletes the previous tweet (DELETE /2/tweets/:id) and re-posts.
Instagram The API cannot edit or delete media, so it posts a new item (the old one stays — this is the only option Instagram offers).

A changed post is detected via a content fingerprint (title + image + link) stored per network. The metabox shows an "out of date" badge when a live post no longer matches the current content.

Deletion: removing a note removes its social posts

When a note is trashed (what "delete" does in the editor) or permanently deleted, the items it published are removed from each network that can delete via API:

Network On delete
Facebook Deletes the Page post (DELETE /{post-id}).
X Deletes the tweet (DELETE /2/tweets/:id).
Instagram The API cannot delete media, so its post stays (delete it by hand).

The deletion uses the same async queue as publishing: it is non-blocking, retries on transient errors, and snapshots the remote id into the job so it still runs after the post's meta is gone. Trashing also cancels any pending publish for that note, and clears its "published" pointer so restoring from the Trash re-publishes a fresh post (Instagram keeps its original post and pointer, since it could not be deleted).

Triggers

  • Auto (transition_post_status to publish): on first publish and on every later save of a published post. Gated by the global Auto-publish setting and a per-post checkbox. The Publisher decides per network: publish / re-post / nothing.
  • Manual: the Redes Sociales metabox button enqueues the selected networks and forces a (re)post even if unchanged.
  • Bulk / back-publish old notes: the Posts list has a "Publish to social networks" bulk action. Select any existing notes and it enqueues them — handy for notes that predate the plugin or were published while a network was off. It is not forced: each note is published only where it is not already live, so re-running never duplicates; it just fills the gaps.

Credentials

Everything is credential-driven. A network stays disabled until it is enabled and every credential is filled in at Settings → Lazaro Redes.

X (Twitter)

OAuth 1.0a user context, App with Read and Write: API Key, API Key Secret, Access Token, Access Token Secret.

Facebook

A Page: Page ID + a long-lived Page Access Token with pages_manage_posts (delete also needs the same token).

Instagram

A Business/Creator account linked to the Page + a Meta App with instagram_content_publish (Meta App Review for production): Instagram Business Account ID + Access Token.

Layout

lazaro-social.php          plugin header, constants, autoloader, bootstrap
includes/
  class-plugin.php         container: network registry + hook wiring
  class-settings.php       settings store + Settings → Lazaro Redes screen
  class-publisher.php      decides publish / re-post / skip per network
  class-queue.php          async delivery queue (locking, leasing, backoff)
  class-state.php          per-post publishing state in post meta
  class-post-meta.php      editor metabox + its AJAX endpoints
  class-deleter.php        removes social posts when a note is trashed
  class-bulk.php           "Publish to social networks" bulk action
  class-payload.php        title + link + image payload, caption filter
  class-migrator.php       one-shot import of legacy accesonorte-social data
  networks/                one class per network, behind a shared interface
assets/                    admin CSS + JS (metabox and settings screen)

Extending

Add a network by implementing Lazaro_Social_Network (extend Lazaro_Social_Network_Base) and registering it in Lazaro_Social_Plugin::register_networks(); the settings screen renders its fields from settings_fields().

Customise the caption sent to each network with the lazaro_social_caption filter:

add_filter( 'lazaro_social_caption', function ( $caption, $network, $payload ) {
    return 'x' === $network ? $caption . ' #Noticias' : $caption;
}, 10, 3 );

Notes / limitations

  • X media is not uploaded directly; it relies on the link's Open Graph card.
  • Instagram needs a publicly reachable featured-image URL (fetched by Meta's servers) — fine for a live site, not for localhost.
  • Instagram cannot be edited/deleted via API, so re-posts there create a new item rather than replacing the old one, and trashing a note leaves its Instagram post in place (delete it by hand).
  • Deletion fires on trash (and on permanent delete), not on merely unpublishing a post (publish → draft): an unpublished note keeps its social posts. Trash it to remove them.
  • Only the post post type is handled.