Skip to main content

Documentation

No results found.
Features

Social Media Posting

Social Media Posting turns the site into the hub of your social presence: compose a post once and publish it to your connected Facebook Page, Instagram professional account, LinkedIn profile or company page, X (Twitter), and Google Business...

Social Media Posting turns the site into the hub of your social presence: compose a post once and publish it to your connected Facebook Page, Instagram professional account, LinkedIn profile or company page, X (Twitter), and Google Business Profile accounts — immediately or on a schedule. Flip on auto-posting and every new blog post (or any content type you choose) announces itself the moment it publishes, including scheduled items going live later — and new shop products and property listings announce themselves too. Engagement counts flow back into the history, so you see what landed without opening five apps. It pairs naturally with the Social Feed Embed addon: Posting pushes your content out, Feed pulls the results back onto your pages.

The problem

Publishing a blog post is half the job — someone still has to open five social apps and paste the link into each one, every time. Third-party schedulers (Buffer, Hootsuite, Publer) solve it for another monthly fee, another login, and another place your content calendar lives. And none of them know when a scheduled CMS post actually goes live, so the social announcement either leads the content or trails it by hours.

The fix

The CMS already knows the moment content publishes — it's the system flipping the switch. This addon hooks that exact moment: when an item of a watched type becomes published (a dashboard save or the scheduler flipping a scheduled item live), a social post is generated from a template ({title}, {excerpt}, {url} tokens), attached to the item's featured image and public URL, and delivered to every active account. Manual posts work the same way from a composer — write once (or let the AI draft it), tweak per network if you like, pick accounts, post now or schedule.

Networks & connection model

Accounts are connected with pasted tokens (same model as the Social Feed addon) — no OAuth app of your own to host, no redirect flows. Credentials are stored encrypted in social_posting_accounts.credentials.

Network Credential Expiry Post shape
Facebook Page Long-lived Page access token (pages_manage_posts) Never expires Text + native link, one photo via /photos, or a multi-photo album (unpublished uploads + one feed post with attached_media)
Instagram "Instagram API with Instagram Login" long-lived token 60 days, auto-renews (refreshed by the cron ~every 2 weeks) Image required — container flow (/me/media → /me/media_publish); 2–10 images publish as a carousel; link appended to the caption
LinkedIn Access token (w_member_social, or w_organization_social + Organization ID to post as a company page) 60 days, no renewal API — expiry shown on the account row and emailed ahead of time, re-paste to reconnect Commentary + native image upload through the Images API (2+ images become a multiImage post, alt text carried over); link-only posts render the article card from the page's Open Graph tags
X (Twitter) The four OAuth 1.0a keys (API key/secret + access token/secret), Read & Write Never expire Text with the link appended (X renders the card); up to 4 images uploaded natively from the media library via the v2 media upload endpoint
Google Business Profile OAuth client ID + secret + refresh token (business.manage scope) from your own Google Cloud project, plus an optional location ID Refresh tokens never expire — a short-lived access token is minted per post "What's New" local post: summary text, the link as a Learn more button, and the first image as the post photo

Notes:

  • Instagram, Facebook photo posts, and Google Business Profile photos need a public site. Those providers fetch the image from your site's URL, so publishing images only works from an installation reachable on the public internet — not from a local dev site. X and LinkedIn images are uploaded as bytes from local storage, so they work anywhere.
  • Google Business Profile setup: create a Google Cloud project with the Business Profile APIs enabled, add an OAuth client, and mint a refresh token with the https://www.googleapis.com/auth/business.manage scope (e.g. via the OAuth 2.0 Playground using your own client). If the Google account manages several locations, pin one with the optional location ID; otherwise the first location is used.
  • Accounts can be paused (kept connected, skipped by new posts) and removed. Removing an account deletes its delivery records here; published posts on the platform are untouched.
  • Per-target errors are recorded on the delivery row and mirrored to the account's last_error, so a dead token is visible on the settings page before the next post fails.

Dashboard

Two pages under Social Posting in the sidebar (manager+, feature:social_posting middleware):

  • Posts (/dashboard/social-posting) — the queue (scheduled/sending posts, cancellable) and history (delivered posts with per-network result badges linking to the live post, like/comment/share counts pulled back from the networks, failed targets with the provider's error and a retry button that re-sends only the failed targets). The New post composer:
    • Message with a character counter, and — when an AI text provider is configured in Settings → API Keys — a Write with AI button that drafts the post from your notes and the link.
    • Optional link, and up to 10 images from the media library (required when an Instagram account is selected — validated before saving). Multiple images become an Instagram carousel / Facebook album; X takes the first 4, Google Business Profile the first one.
    • Customize per network — an optional per-network message override for each selected network (short take for X, longer one for LinkedIn…), each with its own Adapt with AI button that rewrites the shared message in that platform's voice. Networks without an override use the shared message.
    • Live previews — one card per selected network showing the text exactly as that publisher will send it (override-aware, link appended where it rides in the text), a character count against the network's limit (X counts every URL as 23 t.co characters), which images that network will use, and warnings for over-limit text or a missing Instagram image.
    • Account checkboxes, and Post now / Schedule with a date-time picker.
  • Settings (/dashboard/social-posting/settings) — connected accounts (status, token expiry, pause/resume, remove; add-account modal with per-network credential help), the auto-posting panel (on/off switch, content-type checkboxes, other-source checkboxes, post template), and the notifications & link tracking panel (see below).

A dashboard widget card (registered in DashboardWidgetRegistry) shows total delivered posts, the scheduled count, the last-posted time, and — once engagement metrics arrive — the reaction count of the best performing post of the last 30 days.

Sharing from the content pages

  • The content item editor's save dropdown gains a Share on social media action for published items (visible when the feature is on). It deep-links to the composer with the item's template-rendered message, public URL, and featured image prefilled — the escape hatch for content outside the auto-post window (backdated posts, older items, auto-posting off).
  • The content list shows a small share icon next to items that have been posted to social, so you can see coverage at a glance.

Auto-posting

Settings (stored in Setting under social_posting.*):

  • auto_enabled — master switch, off by default.
  • auto_types — everything that announces itself. Content types go in as bare slugs (blog); the other sources use a colon key (shop:product, real-estate:listing) that Str::slug can never produce, so one list holds both without collisions.
  • auto_template — defaults to {title}\n\n{excerpt}. Tokens: {title}, {excerpt}, {url}. The link is attached natively per network, so {url} is only for putting it inside the text too — publishers never double-append a link already present in the content.

Qualifying rules (all enforced in AutoPoster):

  • The source is watched, the feature + switch are on, and at least one active account exists.
  • Backdated records are skipped — only things that appeared within the last 24 hours announce, so bulk imports and edits to old records never blast the accounts. (Anything older can still be shared manually via Share on social media.)
  • One post per record, ever — an existing social post for it blocks a second one (unpublish → republish is safe). The dedupe key is the post's polymorphic subject, so every source dedupes the same way.
  • Instagram targets are only created when the record has an image; if Instagram is the only account and there's no image, no post is created at all.

What can announce

Source Becomes a post when Excerpt Image
Content types Status transitions to published, item is listed (not an owned child) searchExcerpt(), 300 chars Featured image
Shop products Status published with a fresh published_at; seeded demo products never announce The product excerpt Featured image
Property listings Active + not hidden, new to the site (created_at) and new to the market (on_market_date) Price · beds · baths · sq ft First 3 feed photos

The two freshness tests on a listing mean different things and both are needed: without the market test, connecting a feed would announce every property already on it.

How each source is noticed

Content items ride a ContentItem::saved listener registered in the module's ServiceProvider — the content:publish-due scheduler flips items with a real model update(), so one listener covers dashboard saves and scheduled publishes, and the post goes out immediately.

Products and listings are found by a sweep on the 5-minute publish-due cron (AutoPoster::sweep() over AutoPostSources), so they announce within about five minutes rather than instantly. That's not a shortcut: the MLS feed writes listings with Listing::query()->upsert(), which fires no Eloquent events at all, so a model hook would never see a single feed listing. At most 3 records per source per sweep are queued and the overflow is dropped — a feed connected after the source was switched on would otherwise empty itself onto the timeline, and nobody wants the backlog either.

Listing photos

A listing's pictures come from the MLS feed or the local photo cache, never the media library, so they ride on the post as image_urls (public URLs) instead of media-item ids. SocialPost::imageUrls() prefers media items and falls back to these, which means Facebook, Instagram and Google Business post listings with photos. LinkedIn and X upload image bytes from a media item, so a listing posts there as text plus its link — LinkedIn still renders the article card from the listing page's OG image.

Toggle Tag outgoing links for analytics (UTM) in Settings → Notifications & link tracking (off by default; social_posting.utm_enabled). When on, every posted link is tagged per network — utm_source={network}, utm_medium=social, utm_campaign=social-post-{id} — so the Analytics feature attributes visits to the exact network and post they came from. Links that already carry utm_ parameters are left alone, and a {url} the author placed inside the text stays untagged (only the natively attached/appended link is tagged).

Notifications

Set a notification email in Settings → Notifications & link tracking (social_posting.notify_email; empty = off). Sent through the shared Marketing mail transport (SocialPostingNotifier):

  • Failed unattended posts — when a cron/deferred delivery ends failed or partial, one email lists the failing accounts and their provider errors. Interactive "Post now"/retry results are already on screen, so they don't email. One email per post (failure_notified_at claim), reset never — a retry that still fails won't re-alert.
  • Expiring tokens — active accounts whose token expires within 7 days (or already expired) trigger one email per token cycle with a reconnect link. Re-pasting a token moves the expiry and arms the next cycle. This is the safety net for LinkedIn's non-renewable 60-day tokens; Instagram normally auto-renews long before this fires.

Engagement metrics

EngagementMetricsSync runs from the publish-due cron and pulls likes / comments / shares back onto each delivery record:

  • Facebook (reactions/comments/shares summary), Instagram (like_count, comments_count), X (public_metrics — replies count as comments, retweets + quotes as shares), LinkedIn (socialActions — likes and comments). Google Business Profile has no public engagement metrics API and is skipped.
  • Each sent target re-syncs at most every 6 hours for its first 30 days, a few per cron pass — quiet sites cost nothing, busy ones never hammer the APIs. Provider errors are swallowed; the previous counts stand until the next window.
  • The history list shows the summed counts per post; the dashboard widget surfaces the best performing post of the last 30 days.

Delivery pipeline

  • Every post fans out to one target row per account (social_post_targets); each succeeds or fails independently and the post rolls up to published / partial / failed. Sent targets are never re-sent — retry resets only failed ones.
  • Per-network text resolution lives on the model: contentFor($provider) (override or shared message), outboundLink($provider) (UTM-tagged when enabled), captionFor($provider) (message + appended link for networks where the link rides in the text). Publishers only ever call these three.
  • "Post now" delivers synchronously in the request (the composer reports the outcome). Auto posts are created as due-now and delivered by a defer()ed send after the response, with the cron as the safety net — matching the platform's no-queue-worker convention.
  • social-posting:publish-due runs via LazyCron every 5 minutes (feature:social-posting source): refreshes Instagram tokens nearing expiry, delivers due posts (notifying on unattended failures), sends token-expiry warnings, then syncs a batch of engagement metrics. The send loop is lock-guarded (Cache::lock) so the deferred send and the cron never double-post, and posts stuck in publishing for 15+ minutes (a send that died mid-flight) are recovered.

Internals

app/Features/SocialPosting/
├── SocialPostingServiceProvider.php   ← routes, Livewire ns, widget, LazyCron, ContentItem::saved hook
├── Models/{SocialPostingAccount,SocialPost,SocialPostTarget}.php
├── Support/
│   ├── SocialPostSender.php           ← fan-out delivery, rollup, retry, sendDue lock, IG token refresh
│   ├── AutoPoster.php                 ← publish-hook qualifier, module-source sweep, one queueFor() + template renderer
│   ├── AutoPostSources.php            ← the non-content sources: which are available, what's pending, how a record becomes a post
│   ├── EngagementMetricsSync.php      ← pulls likes/comments/shares back per target
│   ├── SocialPostingNotifier.php      ← failed-post + token-expiry owner emails
│   └── Publishers/{PostPublisher,FacebookPublisher,InstagramPublisher,LinkedInPublisher,XPublisher,GoogleBusinessPublisher}.php
├── Console/PublishDueSocialPostsCommand.php   ← social-posting:publish-due
├── Database/{Migrations,Factories}/
├── routes/cms.php                     ← web+auth+verified+feature:social_posting+role:manager
└── resources/views/dashboard/{⚡index,⚡settings}.blade.php

Tests: SocialPostingSendTest (per-network request shapes incl. the OAuth 1.0a signature header, rollup, retry, due/stuck recovery, command), SocialPostingV2PublishTest (X/LinkedIn native image uploads, multi-image albums/carousels/multiImage, Google Business Profile connect + publish, UTM tagging, per-network overrides), SocialPostingEngagementTest (per-network metric pulls, sync throttling, failed-post + token-expiry notifications, command integration), SocialPostingAutoPostTest (publish transitions incl. the content:publish-due flip, backdating, dedupe, Instagram image rules), SocialPostingDashboardTest (gating, composer incl. multi-image + overrides + share prefill, connect flows, settings), SocialPostingModuleSourcesTest (product + listing sweeps, both freshness tests, the per-sweep cap, external photo URLs, the settings checkboxes).

Comparison

WebProCMS Social Posting Buffer / Hootsuite Manual posting
Auto-post on publish ✅ the CMS is the trigger — scheduled posts included ⚠️ RSS polling, minutes-to-hours late ❌
Extra cost / login ✅ included with membership ❌ separate subscription ✅ free, ❌ your time
Featured image + link handling ✅ automatic from the content item; native uploads on X/LinkedIn ⚠️ re-attach per post ⚠️ per app
Per-network variants + AI drafting ✅ overrides, platform-tuned AI adapt, live previews ✅ on paid tiers ❌ retype five times
Google Business Profile ✅ first-class network ⚠️ limited/paid tiers ✅
Engagement pulled back ✅ likes/comments/shares on the post history ✅ ❌ scattered
Credentials stored ✅ encrypted, on your own server ❌ third party holds your tokens —
Post history ✅ next to your content, with per-network links ✅ ❌ scattered