@nextsparkjs/plugin-social-media-publisher 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +76 -0
- package/README.md +423 -0
- package/api/social/connect/callback/route.ts +669 -0
- package/api/social/connect/route.ts +327 -0
- package/api/social/disconnect/route.ts +187 -0
- package/api/social/publish/route.ts +402 -0
- package/docs/01-getting-started/01-introduction.md +471 -0
- package/docs/01-getting-started/02-installation.md +471 -0
- package/docs/01-getting-started/03-configuration.md +515 -0
- package/docs/02-core-features/01-oauth-integration.md +501 -0
- package/docs/02-core-features/02-publishing.md +527 -0
- package/docs/02-core-features/03-token-management.md +661 -0
- package/docs/02-core-features/04-audit-logging.md +646 -0
- package/docs/03-advanced-usage/01-provider-apis.md +764 -0
- package/docs/03-advanced-usage/02-custom-integrations.md +695 -0
- package/docs/03-advanced-usage/03-per-client-architecture.md +575 -0
- package/docs/04-use-cases/01-agency-management.md +661 -0
- package/docs/04-use-cases/02-content-publishing.md +668 -0
- package/docs/04-use-cases/03-analytics-reporting.md +748 -0
- package/entities/audit-logs/audit-logs.config.ts +150 -0
- package/lib/oauth-helper.ts +167 -0
- package/lib/providers/facebook.ts +672 -0
- package/lib/providers/index.ts +21 -0
- package/lib/providers/instagram.ts +791 -0
- package/lib/validation.ts +155 -0
- package/migrations/001_social_media_tables.sql +167 -0
- package/package.json +15 -0
- package/plugin.config.ts +81 -0
- package/tsconfig.json +47 -0
- package/types/social.types.ts +171 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# Publishing
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Social Media Publisher plugin provides a unified endpoint for publishing content to Instagram Business and Facebook Pages. Publishing includes automatic token validation, token refresh if needed, and comprehensive audit logging.
|
|
6
|
+
|
|
7
|
+
**Endpoint:** `POST /api/v1/plugin/social-media-publisher/social/publish`
|
|
8
|
+
|
|
9
|
+
**Supported Platforms:**
|
|
10
|
+
- Instagram Business (photos and videos)
|
|
11
|
+
- Facebook Pages (text, photos, and links)
|
|
12
|
+
|
|
13
|
+
## Publish Endpoint
|
|
14
|
+
|
|
15
|
+
### Request Schema
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
{
|
|
19
|
+
accountId: string // UUID of social platform account
|
|
20
|
+
platform: string // 'instagram_business' | 'facebook_page'
|
|
21
|
+
imageUrl?: string // Public HTTPS URL to image
|
|
22
|
+
videoUrl?: string // Public HTTPS URL to video
|
|
23
|
+
caption?: string // Post caption/message
|
|
24
|
+
link?: string // Link URL (Facebook only)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Response Schema
|
|
29
|
+
|
|
30
|
+
**Success:**
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"success": true,
|
|
34
|
+
"platform": "instagram_business",
|
|
35
|
+
"postId": "17899618652010220",
|
|
36
|
+
"postUrl": "https://www.instagram.com/p/ABC123",
|
|
37
|
+
"message": "Successfully published to instagram_business"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Error:**
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"error": "Publishing failed",
|
|
45
|
+
"platform": "instagram_business",
|
|
46
|
+
"details": "Image URL must be publicly accessible",
|
|
47
|
+
"errorDetails": {
|
|
48
|
+
"code": 100,
|
|
49
|
+
"message": "Invalid parameter"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Instagram Business Publishing
|
|
55
|
+
|
|
56
|
+
### Photo Posts
|
|
57
|
+
|
|
58
|
+
Instagram uses a **2-step container process**:
|
|
59
|
+
|
|
60
|
+
1. **Create Media Container** - Upload image URL and caption
|
|
61
|
+
2. **Publish Container** - Make post live on profile
|
|
62
|
+
|
|
63
|
+
**Request:**
|
|
64
|
+
```typescript
|
|
65
|
+
{
|
|
66
|
+
accountId: "550e8400-e29b-41d4-a716-446655440000",
|
|
67
|
+
platform: "instagram_business",
|
|
68
|
+
imageUrl: "https://example.com/image.jpg",
|
|
69
|
+
caption: "Check out our new product! 🚀 #launch"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Process Flow:**
|
|
74
|
+
```
|
|
75
|
+
1. Validate request
|
|
76
|
+
2. Get account from database (via RLS)
|
|
77
|
+
3. Check token expiration
|
|
78
|
+
4. Auto-refresh if < 10 minutes until expiry
|
|
79
|
+
5. Decrypt access token
|
|
80
|
+
6. Create media container (Instagram API)
|
|
81
|
+
7. Wait 2 seconds (Instagram processing)
|
|
82
|
+
8. Publish container (Instagram API)
|
|
83
|
+
9. Create audit log
|
|
84
|
+
10. Return post URL
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Code Example:**
|
|
88
|
+
```typescript
|
|
89
|
+
// Using fetch API
|
|
90
|
+
const response = await fetch('/api/v1/plugin/social-media-publisher/social/publish', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
accountId: 'account-uuid-here',
|
|
97
|
+
platform: 'instagram_business',
|
|
98
|
+
imageUrl: 'https://cdn.example.com/photo.jpg',
|
|
99
|
+
caption: 'My awesome post! #instagram'
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const result = await response.json()
|
|
104
|
+
|
|
105
|
+
if (result.success) {
|
|
106
|
+
console.log('Posted to Instagram:', result.postUrl)
|
|
107
|
+
} else {
|
|
108
|
+
console.error('Failed:', result.error)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Image Requirements:**
|
|
113
|
+
- **Format:** JPG or PNG
|
|
114
|
+
- **Size:** Max 8MB
|
|
115
|
+
- **Dimensions:** 320px to 1080px (width)
|
|
116
|
+
- **Aspect Ratio:** 4:5 to 1.91:1
|
|
117
|
+
- **Accessibility:** Must be publicly accessible via HTTPS
|
|
118
|
+
- **No Redirects:** Direct image URL only
|
|
119
|
+
|
|
120
|
+
**Caption Requirements:**
|
|
121
|
+
- **Length:** Max 2,200 characters
|
|
122
|
+
- **Hashtags:** Up to 30 per post
|
|
123
|
+
- **Mentions:** Up to 20 per post (@username)
|
|
124
|
+
- **Emojis:** Supported ✅
|
|
125
|
+
- **Line Breaks:** Supported (`\n`)
|
|
126
|
+
|
|
127
|
+
### Video Posts
|
|
128
|
+
|
|
129
|
+
Similar to photos but with **longer processing time**:
|
|
130
|
+
|
|
131
|
+
**Request:**
|
|
132
|
+
```typescript
|
|
133
|
+
{
|
|
134
|
+
accountId: "account-uuid",
|
|
135
|
+
platform: "instagram_business",
|
|
136
|
+
videoUrl: "https://example.com/video.mp4",
|
|
137
|
+
caption: "Check out this video! 🎬"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Process Flow:**
|
|
142
|
+
```
|
|
143
|
+
1-5. Same as photo posts
|
|
144
|
+
6. Create video container
|
|
145
|
+
7. Poll for video processing (up to 30 seconds)
|
|
146
|
+
8. Publish when status = 'FINISHED'
|
|
147
|
+
9. Create audit log
|
|
148
|
+
10. Return post URL
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Video Requirements:**
|
|
152
|
+
- **Format:** MP4 or MOV
|
|
153
|
+
- **Size:** Max 100MB
|
|
154
|
+
- **Duration:** 3 seconds to 60 seconds
|
|
155
|
+
- **Dimensions:** Min 600px (any dimension)
|
|
156
|
+
- **Aspect Ratio:** 4:5 to 1.91:1
|
|
157
|
+
- **Frame Rate:** Max 30fps
|
|
158
|
+
- **Accessibility:** Must be publicly accessible via HTTPS
|
|
159
|
+
|
|
160
|
+
**Processing Time:**
|
|
161
|
+
- Small videos (< 10MB): 5-10 seconds
|
|
162
|
+
- Medium videos (10-50MB): 15-30 seconds
|
|
163
|
+
- Large videos (50-100MB): 30-60 seconds
|
|
164
|
+
|
|
165
|
+
## Facebook Pages Publishing
|
|
166
|
+
|
|
167
|
+
### Text Posts
|
|
168
|
+
|
|
169
|
+
**Request:**
|
|
170
|
+
```typescript
|
|
171
|
+
{
|
|
172
|
+
accountId: "account-uuid",
|
|
173
|
+
platform: "facebook_page",
|
|
174
|
+
caption: "Just a text post on Facebook!"
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Process:**
|
|
179
|
+
```
|
|
180
|
+
POST https://graph.facebook.com/v18.0/{PAGE_ID}/feed
|
|
181
|
+
{
|
|
182
|
+
message: "Post text here",
|
|
183
|
+
access_token: "{PAGE_TOKEN}"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Photo Posts
|
|
188
|
+
|
|
189
|
+
**Request:**
|
|
190
|
+
```typescript
|
|
191
|
+
{
|
|
192
|
+
accountId: "account-uuid",
|
|
193
|
+
platform: "facebook_page",
|
|
194
|
+
imageUrl: "https://example.com/image.jpg",
|
|
195
|
+
caption: "Check out this photo!"
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Process:**
|
|
200
|
+
```
|
|
201
|
+
POST https://graph.facebook.com/v18.0/{PAGE_ID}/photos
|
|
202
|
+
{
|
|
203
|
+
url: "https://example.com/image.jpg",
|
|
204
|
+
message: "Photo caption",
|
|
205
|
+
access_token: "{PAGE_TOKEN}"
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Image Requirements:**
|
|
210
|
+
- **Format:** JPG, PNG, GIF, BMP
|
|
211
|
+
- **Size:** Max 4MB (recommended), up to 15MB
|
|
212
|
+
- **Dimensions:** No strict limits, but 1200x630px recommended
|
|
213
|
+
- **Accessibility:** Must be publicly accessible
|
|
214
|
+
|
|
215
|
+
### Link Posts
|
|
216
|
+
|
|
217
|
+
**Request:**
|
|
218
|
+
```typescript
|
|
219
|
+
{
|
|
220
|
+
accountId: "account-uuid",
|
|
221
|
+
platform: "facebook_page",
|
|
222
|
+
link: "https://example.com/article",
|
|
223
|
+
caption: "Read our latest article!"
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Process:**
|
|
228
|
+
```
|
|
229
|
+
POST https://graph.facebook.com/v18.0/{PAGE_ID}/feed
|
|
230
|
+
{
|
|
231
|
+
message: "Check this out!",
|
|
232
|
+
link: "https://example.com/article",
|
|
233
|
+
access_token: "{PAGE_TOKEN}"
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Link Requirements:**
|
|
238
|
+
- **Protocol:** Must be HTTPS (HTTP not allowed)
|
|
239
|
+
- **Preview:** Facebook auto-generates preview (Open Graph)
|
|
240
|
+
- **Meta Tags:** Use Open Graph tags for better preview
|
|
241
|
+
|
|
242
|
+
## Validation
|
|
243
|
+
|
|
244
|
+
### Image URL Validation
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
function validateImageUrl(url: string): { valid: boolean; error?: string } {
|
|
248
|
+
// Must be HTTPS
|
|
249
|
+
if (!url.startsWith('https://')) {
|
|
250
|
+
return { valid: false, error: 'Image URL must use HTTPS' }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Must end with image extension
|
|
254
|
+
const validExtensions = ['.jpg', '.jpeg', '.png', '.gif']
|
|
255
|
+
const hasValidExtension = validExtensions.some(ext =>
|
|
256
|
+
url.toLowerCase().includes(ext)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if (!hasValidExtension) {
|
|
260
|
+
return { valid: false, error: 'URL must point to an image file' }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Must be publicly accessible (checked at runtime by platform)
|
|
264
|
+
return { valid: true }
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Caption Validation
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
function validateCaption(
|
|
272
|
+
caption: string,
|
|
273
|
+
platform: 'instagram_business' | 'facebook_page'
|
|
274
|
+
): { valid: boolean; error?: string } {
|
|
275
|
+
// Instagram: Max 2,200 characters
|
|
276
|
+
if (platform === 'instagram_business' && caption.length > 2200) {
|
|
277
|
+
return {
|
|
278
|
+
valid: false,
|
|
279
|
+
error: 'Instagram captions must be under 2,200 characters'
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Facebook: Max 63,206 characters
|
|
284
|
+
if (platform === 'facebook_page' && caption.length > 63206) {
|
|
285
|
+
return {
|
|
286
|
+
valid: false,
|
|
287
|
+
error: 'Facebook posts must be under 63,206 characters'
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { valid: true }
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Automatic Token Refresh
|
|
296
|
+
|
|
297
|
+
Before every publish operation, the plugin checks if the token expires soon and automatically refreshes it if needed.
|
|
298
|
+
|
|
299
|
+
**Refresh Logic:**
|
|
300
|
+
```typescript
|
|
301
|
+
const now = new Date()
|
|
302
|
+
const expiresAt = new Date(account.tokenExpiresAt)
|
|
303
|
+
const minutesUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1000 * 60)
|
|
304
|
+
|
|
305
|
+
// Refresh if < 10 minutes until expiration
|
|
306
|
+
if (minutesUntilExpiry < 10) {
|
|
307
|
+
const refreshResult = await refreshAccountToken(accountId, platform, decryptedToken)
|
|
308
|
+
|
|
309
|
+
if (refreshResult.success) {
|
|
310
|
+
// Use refreshed token for this request
|
|
311
|
+
decryptedToken = refreshResult.newAccessToken
|
|
312
|
+
} else {
|
|
313
|
+
// Fail publish operation
|
|
314
|
+
return error('Token expired and refresh failed')
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Benefits:**
|
|
320
|
+
- ✅ No publish failures due to expired tokens
|
|
321
|
+
- ✅ Transparent to users
|
|
322
|
+
- ✅ Automatic for all accounts
|
|
323
|
+
- ✅ Logged in audit trail
|
|
324
|
+
|
|
325
|
+
## Usage Examples
|
|
326
|
+
|
|
327
|
+
### React Component
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
'use client'
|
|
331
|
+
|
|
332
|
+
import { useState } from 'react'
|
|
333
|
+
|
|
334
|
+
export function PublishForm({ accountId, platform }: {
|
|
335
|
+
accountId: string
|
|
336
|
+
platform: 'instagram_business' | 'facebook_page'
|
|
337
|
+
}) {
|
|
338
|
+
const [imageUrl, setImageUrl] = useState('')
|
|
339
|
+
const [caption, setCaption] = useState('')
|
|
340
|
+
const [loading, setLoading] = useState(false)
|
|
341
|
+
const [result, setResult] = useState<any>(null)
|
|
342
|
+
|
|
343
|
+
const handlePublish = async () => {
|
|
344
|
+
setLoading(true)
|
|
345
|
+
setResult(null)
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const response = await fetch('/api/v1/plugin/social-media-publisher/social/publish', {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: { 'Content-Type': 'application/json' },
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
accountId,
|
|
353
|
+
platform,
|
|
354
|
+
imageUrl,
|
|
355
|
+
caption
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const data = await response.json()
|
|
360
|
+
setResult(data)
|
|
361
|
+
|
|
362
|
+
if (data.success) {
|
|
363
|
+
alert(`Published successfully! View at: ${data.postUrl}`)
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
setResult({ error: 'Network error' })
|
|
367
|
+
} finally {
|
|
368
|
+
setLoading(false)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<div>
|
|
374
|
+
<input
|
|
375
|
+
type="url"
|
|
376
|
+
placeholder="Image URL (https://...)"
|
|
377
|
+
value={imageUrl}
|
|
378
|
+
onChange={(e) => setImageUrl(e.target.value)}
|
|
379
|
+
/>
|
|
380
|
+
|
|
381
|
+
<textarea
|
|
382
|
+
placeholder="Caption..."
|
|
383
|
+
value={caption}
|
|
384
|
+
onChange={(e) => setCaption(e.target.value)}
|
|
385
|
+
maxLength={platform === 'instagram_business' ? 2200 : 63206}
|
|
386
|
+
/>
|
|
387
|
+
|
|
388
|
+
<button
|
|
389
|
+
onClick={handlePublish}
|
|
390
|
+
disabled={loading || !imageUrl}
|
|
391
|
+
>
|
|
392
|
+
{loading ? 'Publishing...' : 'Publish'}
|
|
393
|
+
</button>
|
|
394
|
+
|
|
395
|
+
{result && (
|
|
396
|
+
<div>
|
|
397
|
+
{result.success ? (
|
|
398
|
+
<a href={result.postUrl} target="_blank">
|
|
399
|
+
View Post →
|
|
400
|
+
</a>
|
|
401
|
+
) : (
|
|
402
|
+
<p>Error: {result.error}</p>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Batch Publishing
|
|
412
|
+
|
|
413
|
+
Publish to multiple accounts:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
async function publishToMultipleAccounts(
|
|
417
|
+
accounts: Array<{ id: string; platform: string }>,
|
|
418
|
+
imageUrl: string,
|
|
419
|
+
caption: string
|
|
420
|
+
) {
|
|
421
|
+
const results = await Promise.allSettled(
|
|
422
|
+
accounts.map(account =>
|
|
423
|
+
fetch('/api/v1/plugin/social-media-publisher/social/publish', {
|
|
424
|
+
method: 'POST',
|
|
425
|
+
headers: { 'Content-Type': 'application/json' },
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
accountId: account.id,
|
|
428
|
+
platform: account.platform,
|
|
429
|
+
imageUrl,
|
|
430
|
+
caption
|
|
431
|
+
})
|
|
432
|
+
}).then(r => r.json())
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
const successful = results.filter(r => r.status === 'fulfilled')
|
|
437
|
+
const failed = results.filter(r => r.status === 'rejected')
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
total: accounts.length,
|
|
441
|
+
successful: successful.length,
|
|
442
|
+
failed: failed.length,
|
|
443
|
+
results
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Error Handling
|
|
449
|
+
|
|
450
|
+
### Common Errors
|
|
451
|
+
|
|
452
|
+
**1. Token Expired**
|
|
453
|
+
```json
|
|
454
|
+
{
|
|
455
|
+
"error": "Token expired and refresh failed",
|
|
456
|
+
"details": "Meta API error: Invalid OAuth 2.0 Access Token",
|
|
457
|
+
"suggestion": "Please reconnect your social media account"
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
**Solution:** User must reconnect account via OAuth
|
|
461
|
+
|
|
462
|
+
**2. Image Not Accessible**
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"error": "Publishing failed",
|
|
466
|
+
"details": "Image URL must be publicly accessible",
|
|
467
|
+
"errorDetails": {
|
|
468
|
+
"code": 100,
|
|
469
|
+
"message": "Invalid parameter"
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
**Solution:** Ensure image URL is publicly accessible via HTTPS
|
|
474
|
+
|
|
475
|
+
**3. Rate Limit**
|
|
476
|
+
```json
|
|
477
|
+
{
|
|
478
|
+
"error": "Publishing failed",
|
|
479
|
+
"details": "Application request limit reached",
|
|
480
|
+
"errorDetails": {
|
|
481
|
+
"code": 4,
|
|
482
|
+
"message": "Application request limit reached"
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
**Solution:** Wait and retry, or upgrade Facebook App tier
|
|
487
|
+
|
|
488
|
+
**4. Invalid Permissions**
|
|
489
|
+
```json
|
|
490
|
+
{
|
|
491
|
+
"error": "Publishing failed",
|
|
492
|
+
"details": "(#200) Requires extended permission: instagram_content_publish"
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
**Solution:** User must reconnect account and grant required permissions
|
|
496
|
+
|
|
497
|
+
## Best Practices
|
|
498
|
+
|
|
499
|
+
### Image Hosting
|
|
500
|
+
|
|
501
|
+
✅ **Use CDN** for image hosting (CloudFlare, AWS CloudFront)
|
|
502
|
+
✅ **HTTPS Only** - HTTP URLs rejected
|
|
503
|
+
✅ **Direct URLs** - No redirects
|
|
504
|
+
✅ **Optimize Size** - Compress images before upload
|
|
505
|
+
✅ **Test Accessibility** - Verify URL is publicly accessible
|
|
506
|
+
|
|
507
|
+
### Caption Writing
|
|
508
|
+
|
|
509
|
+
✅ **Keep Under Limits** - 2,200 for Instagram, 63,206 for Facebook
|
|
510
|
+
✅ **Use Hashtags** - Max 30 per Instagram post
|
|
511
|
+
✅ **Test Emojis** - Ensure they render correctly
|
|
512
|
+
✅ **Line Breaks** - Use `\n` for readability
|
|
513
|
+
✅ **Call-to-Action** - Include clear CTA
|
|
514
|
+
|
|
515
|
+
### Error Handling
|
|
516
|
+
|
|
517
|
+
✅ **Show User-Friendly Messages** - Don't expose technical errors
|
|
518
|
+
✅ **Retry Logic** - Implement exponential backoff for rate limits
|
|
519
|
+
✅ **Token Refresh** - Handle automatic refresh transparently
|
|
520
|
+
✅ **Audit Logging** - Log all attempts for debugging
|
|
521
|
+
✅ **Validation** - Validate before sending to API
|
|
522
|
+
|
|
523
|
+
## Next Steps
|
|
524
|
+
|
|
525
|
+
- **[Token Management](./03-token-management.md)** - Understand token lifecycle
|
|
526
|
+
- **[Audit Logging](./04-audit-logging.md)** - Track all publishing events
|
|
527
|
+
- **[Provider APIs](../03-advanced-usage/01-provider-apis.md)** - Use API wrappers directly
|