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

  1. Open /social-publishing/Download Excel template
  2. Fill one row per post (publish_at, platform, caption, …)
  3. Upload plan and optionally mark posts as Scheduled
  4. 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)

  1. Set in env: TIKTOK_CLIENT_KEY, TIKTOK_CLIENT_SECRET, TIKTOK_REDIRECT_URI, optionally TIKTOK_ENVIRONMENT=sandbox
  2. Register redirect URI exactly: https://www.nigall.com/integrations/tiktok/callback/
  3. 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
  4. As staff, open /social-publishing/Connect TikTok
  5. Set SOCIAL_PUBLISH_MODE=api
  6. Upload a plan row with platform=tiktok and a video media_url or media_path, then Publish now

Sandbox posts / unaudited apps: keep the TikTok account private. Without video.publish, NIGALL uses inbox upload only.

Instagram (API mode)

  1. Meta Developer app with Facebook Login + Instagram Graph product
  2. Env: META_APP_ID, META_APP_SECRET, META_REDIRECT_URI
    - Meta requires HTTPS redirect URIs (plain http://localhost is 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) — not 127.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
  3. Instagram account must be Professional (Business/Creator) and linked to a Facebook Page
  4. Staff: open https://<tunnel-host>/social-publishing/Connect Instagram
  5. SOCIAL_PUBLISH_MODE=api
  6. Plan row: platform=instagram + public HTTPS media_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)
    • title centered on the image
    • cta button at the bottom (or SOCIAL_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
  7. 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)

  1. X Developer Portal app with OAuth 2.0 User authentication (confidential client)
  2. 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)
  3. Scopes (default): tweet.read tweet.write users.read offline.access media.write
  4. Staff: /social-publishing/Connect X (OAuth 2.0 + PKCE)
  5. SOCIAL_PUBLISH_MODE=api
  6. Plan row: platform=x or twitter
    - Caption / hashtags / optional link_url → text post (truncated to 280)
    - Optional media_url or local media_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.