Social Publishing (Admin)¶
Overview¶
Staff-only tool to upload a 30-day Excel calendar and schedule posts to Instagram, TikTok, Facebook, LinkedIn, YouTube, Pinterest, X (Twitter), Threads, Snapchat, and Reddit.
Path: /social-publishing/ (requires is_staff)
Workflow¶
- Open
/social-publishing/→ Download Excel template - Fill one row per post (
publish_at,platform,caption, …) - Upload plan and optionally mark posts as Scheduled
- Execute due posts via:
- Run due queue now in the UI
- Celery beat (every 60s when Celery is enabled)
- Script:py -3.12 manage.py publish_social_posts
Publish modes (SOCIAL_PUBLISH_MODE)¶
| Mode | Behavior |
|---|---|
dry_run (default) |
Marks posts published without calling social APIs — safe for local testing |
webhook |
POSTs JSON payload to SOCIAL_PUBLISH_WEBHOOK_URL (Zapier / Make / n8n / Buffer) |
api |
Calls configured platform APIs (Facebook page, LinkedIn, X, TikTok, Instagram when connected); other media-heavy platforms prefer webhook |
TikTok (API mode)¶
- Set in env:
TIKTOK_CLIENT_KEY,TIKTOK_CLIENT_SECRET,TIKTOK_REDIRECT_URI, optionallyTIKTOK_ENVIRONMENT=sandbox - Register redirect URI exactly:
https://www.nigall.com/integrations/tiktok/callback/ - Enable Content Posting API. Use scopes available on your app:
-user.info.basic+video.upload→ uploads to the creator’s TikTok inbox (finish posting in the TikTok app)
-video.publish(when TikTok grants it) → Direct Post to the profile - As staff, open
/social-publishing/→ Connect TikTok - Set
SOCIAL_PUBLISH_MODE=api - Upload a plan row with
platform=tiktokand a videomedia_urlormedia_path, then Publish now
Sandbox posts / unaudited apps: keep the TikTok account private. Without video.publish, NIGALL uses inbox upload only.
Instagram (API mode)¶
- Meta Developer app with Facebook Login + Instagram Graph product
- Env:
META_APP_ID,META_APP_SECRET,META_REDIRECT_URI
- Meta requires HTTPS redirect URIs (plainhttp://localhostis blocked with “isn't using a secure connection”).
- Local testing: run an HTTPS tunnel to Django, then set redirect to that HTTPS URL:- Cloudflare quick tunnel:
cloudflared tunnel --url http://127.0.0.1:8000 - or ngrok:
ngrok http 8000 - Example:
META_REDIRECT_URI=https://<tunnel-host>/integrations/instagram/callback/ - Also set
SITE_URL=https://<tunnel-host>(same host) - Critical: open Social Publishing via the HTTPS tunnel URL, not
http://127.0.0.1:8000. Starting Connect on localhost while Meta callbacks on the tunnel causes OAuth state mismatch (different session cookies). After clicking Connect, the app will bounce you to the tunnel host if needed — sign in there, then Connect again. - In Meta Developer Console → App settings → Basic:
- App Domains:
<tunnel-host>(e.g.abc.trycloudflare.com) — not127.0.0.1 - Add platform Website if missing → Site URL:
https://<tunnel-host>/ - Facebook Login → Settings:
- Client OAuth Login = Yes
- Web OAuth Login = Yes
- Valid OAuth Redirect URIs: exact same URI as
META_REDIRECT_URI(https, trailing slash matching) - Save changes and wait ~1–2 minutes
- If Meta shows “domain of this URL isn't included in the app's domains”, App Domains/Site URL are still wrong — fixing that is required; otherwise Meta may return to your callback without a
code - Restart Django after changing
.env
- Cloudflare quick tunnel:
- Instagram account must be Professional (Business/Creator) and linked to a Facebook Page
- Staff: open
https://<tunnel-host>/social-publishing/→ Connect Instagram SOCIAL_PUBLISH_MODE=api- Plan row:
platform=instagram+ public HTTPSmedia_url
- Image (jpg/png) → feed photo
- Video (mp4/mov) → Reel (media_type=REELS)
- On-image branding (images only): Instagram cannot overlay text via API. Before upload, NIGALL composes:- Logo at the top (default NIGALL mark, or
SOCIAL_BRAND_LOGO_PATH) titlecentered on the imagectabutton at the bottom (orSOCIAL_IG_DEFAULT_CTA/ link host fallback)caption+ hashtags remain the feed caption under the image- Toggle:
SOCIAL_IG_IMAGE_BRANDING=True(default). Custom logo:SOCIAL_BRAND_LOGO_PATH=static/.../logo.png
- Logo at the top (default NIGALL mark, or
- Meta must be able to fetch the media URL — localhost media will not work (use a public CDN URL or ngrok)
Optional shortcut without OAuth: set META_IG_USER_ID + META_PAGE_ACCESS_TOKEN.
X / Twitter (API mode)¶
- X Developer Portal app with OAuth 2.0 User authentication (confidential client)
- Env:
X_CLIENT_ID,X_CLIENT_SECRET,X_REDIRECT_URI
- Local:http://127.0.0.1:8000/integrations/x/callback/(or/integrations/twitter/callback/— both are wired) - Scopes (default):
tweet.read tweet.write users.read offline.access media.write - Staff:
/social-publishing/→ Connect X (OAuth 2.0 + PKCE) SOCIAL_PUBLISH_MODE=api- Plan row:
platform=xortwitter
- Caption / hashtags / optionallink_url→ text post (truncated to 280)
- Optionalmedia_urlor localmedia_path→ image / GIF / video attached via Media Upload API
Optional shortcut without OAuth UI: set X_BEARER_TOKEN to a user-context access token (app-only bearer tokens cannot create posts).
Media 403 Forbidden: reconnect after adding media.write to X_SCOPES (tokens keep the scopes from connect time). Also ensure the X Developer Portal app lists media.write, and that your API tier allows media upload. Text-only posts work without media scopes.