|
1
|
# Content Calendar |
|
2
|
|
|
3
|
Auto-schedule generated content across platforms using best-practice timing rules and content mix optimization. |
|
4
|
|
|
5
|
## Table of Contents |
|
6
|
|
|
7
|
- [Platform Timing Rules](#platform-timing-rules) |
|
8
|
- [Content Mix Strategy](#content-mix-strategy) |
|
9
|
|
|
10
|
- [Calendar Generation Logic](#calendar-generation-logic) |
|
11
|
- [Output Format](#output-format) |
|
12
|
|
|
13
|
- [Multi-Episode Scheduling](#multi-episode-scheduling) |
|
14
|
|
|
15
|
## Platform Timing Rules |
|
16
|
|
|
17
|
Optimal posting times based on platform-specific engagement data. Times are in the user's local timezone (ask if not provided, default to US Eastern). |
|
18
|
|
|
19
|
| Platform | Best Days | Best Times | Frequency | Notes | |
|
20
|
|
|
21
|
|----------|----------|-----------|-----------|-------| |
|
22
|
|
|
23
|
| Twitter/X | Mon-Fri | 9-11am, 1-3pm | 1-2 posts/day | Threads perform best Tue-Thu morning | |
|
24
|
|
|
25
|
| LinkedIn | Tue-Thu | 8-10am | 2-3 posts/week | Avoid weekends — engagement drops 60% | |
|
26
|
|
|
27
|
| Instagram Feed | Mon, Wed, Fri | 11am-1pm | 3-5 posts/week | Quote cards + carousels outperform text | |
|
28
|
|
|
29
|
| Instagram Reels | Tue, Thu, Sat | 9am, 12pm, 7pm | 3-5/week | First 3 seconds are critical | |
|
30
|
|
|
31
|
| TikTok | Tue, Thu, Sat | 7-9pm | 3-5/week | Evening posts outperform morning | |
|
32
|
|
|
33
|
| YouTube Shorts | Sat, Sun | 9am-12pm | 1-2/week | Weekend discovery is strongest | |
|
34
|
|
|
35
|
| Newsletter | Tue or Thu | 7-9am | 1/week | Tuesday has highest open rates | |
|
36
|
|
|
37
|
| Blog | Any weekday | Morning publish | 1/episode | SEO value accumulates over time | |
|
38
|
|
|
39
|
## Content Mix Strategy |
|
40
|
|
|
41
|
Don't dump all content on release day. Spread it across the week following an episode release to maximize reach and avoid audience fatigue. |
|
42
|
|
|
43
|
### Release Week Schedule (Single Episode) |
|
44
|
|
|
45
|
| Day | Platform | Content Piece | Why | |
|
46
|
|
|
47
|
|-----|----------|--------------|-----| |
|
48
|
|
|
49
|
| Day 0 (Release) | Blog, Newsletter | Blog post + newsletter | Drive listens on release day | |
|
50
|
|
|
51
|
| Day 1 | Twitter/X | Thread (top atoms) | Morning engagement peak | |
|
52
|
|
|
53
|
| Day 1 | Instagram | Quote card (highest-scored quote) | Visual hook | |
|
54
|
|
|
55
|
| Day 2 | LinkedIn | Long-form post | Professional audience, mid-week peak | |
|
56
|
|
|
57
|
| Day 2 | TikTok | Video clip \#1 (most controversial) | Evening engagement | |
|
58
|
|
|
59
|
| Day 3 | Twitter/X | Single tweet (data point atom) | Maintain presence | |
|
60
|
|
|
61
|
| Day 3 | Instagram Reels | Video clip \#1 (cross-post) | Reels algorithm favors consistency | |
|
62
|
|
|
63
|
| Day 4 | Instagram | Quote card \#2 | Keep feed active | |
|
64
|
|
|
65
|
| Day 4 | YouTube Shorts | Video clip \#2 | Weekend discovery window | |
|
66
|
|
|
67
|
| Day 5 | Twitter/X | Quote tweet thread with commentary | Re-engage thread audience | |
|
68
|
|
|
69
|
| Day 5 | TikTok | Video clip \#2 | Different angle from clip \#1 | |
|
70
|
|
|
71
|
| Day 6 | Instagram Stories | Behind-the-scenes or teaser for next ep | Build anticipation | |
|
72
|
|
|
73
|
### Content Priority by Score |
|
74
|
|
|
75
|
Map viral scores to scheduling priority: |
|
76
|
|
|
77
|
- **80-100 (Priority)**: Schedule in prime-time slots (peak engagement hours). These are your hero posts — threads, lead video clips, newsletter leads. |
|
78
|
- **60-79 (Strong)**: Fill standard slots. Good supporting content — quote cards, secondary clips, individual tweets. |
|
79
|
|
|
80
|
- **40-59 (Fill)**: Use for low-competition slots or story content. Keep the feed active without risking weak performance in prime slots. |
|
81
|
- **Below 40**: Don't schedule. Archive for potential future use. |
|
82
|
|
|
83
|
## Calendar Generation Logic |
|
84
|
|
|
85
|
### Algorithm |
|
86
|
|
|
87
|
1. Sort all content pieces by viral score (descending) |
|
88
|
2. Assign hero content (score 80+) to prime-time slots first |
|
89
|
|
|
90
|
3. Fill remaining slots with strong content (60-79), respecting platform frequency limits |
|
91
|
4. Add fill content (40-59) to any remaining open slots |
|
92
|
|
|
93
|
5. Check for conflicts (no two posts on the same platform within 4 hours) |
|
94
|
6. Verify content variety (no two quote cards back-to-back on the same platform) |
|
95
|
|
|
96
|
### Slot Assignment |
|
97
|
|
|
98
|
```python |
|
99
|
|
|
100
|
from datetime import datetime, timedelta |
|
101
|
|
|
102
|
PLATFORM_SLOTS = { |
|
103
|
|
|
104
|
"twitter": [ |
|
105
|
|
|
106
|
{"day_offset": 1, "time": "09:00", "type": "thread"}, |
|
107
|
|
|
108
|
{"day_offset": 3, "time": "10:00", "type": "single_tweet"}, |
|
109
|
|
|
110
|
{"day_offset": 5, "time": "09:00", "type": "quote_tweet"}, |
|
111
|
|
|
112
|
], |
|
113
|
|
|
114
|
"linkedin": [ |
|
115
|
|
|
116
|
{"day_offset": 2, "time": "08:30", "type": "article"}, |
|
117
|
|
|
118
|
], |
|
119
|
|
|
120
|
"instagram_feed": [ |
|
121
|
|
|
122
|
{"day_offset": 1, "time": "12:00", "type": "quote_card"}, |
|
123
|
|
|
124
|
{"day_offset": 4, "time": "11:00", "type": "quote_card"}, |
|
125
|
|
|
126
|
], |
|
127
|
|
|
128
|
"instagram_reels": [ |
|
129
|
|
|
130
|
{"day_offset": 3, "time": "12:00", "type": "video_clip"}, |
|
131
|
|
|
132
|
], |
|
133
|
|
|
134
|
"tiktok": [ |
|
135
|
|
|
136
|
{"day_offset": 2, "time": "19:00", "type": "video_clip"}, |
|
137
|
|
|
138
|
{"day_offset": 5, "time": "19:00", "type": "video_clip"}, |
|
139
|
|
|
140
|
], |
|
141
|
|
|
142
|
"youtube_shorts": [ |
|
143
|
|
|
144
|
{"day_offset": 4, "time": "10:00", "type": "video_clip"}, |
|
145
|
|
|
146
|
], |
|
147
|
|
|
148
|
"newsletter": [ |
|
149
|
|
|
150
|
{"day_offset": 0, "time": "08:00", "type": "newsletter"}, |
|
151
|
|
|
152
|
], |
|
153
|
|
|
154
|
"blog": [ |
|
155
|
|
|
156
|
{"day_offset": 0, "time": "09:00", "type": "blog_post"}, |
|
157
|
|
|
158
|
], |
|
159
|
|
|
160
|
} |
|
161
|
|
|
162
|
def generate_calendar( |
|
163
|
|
|
164
|
content_pieces: list[dict], |
|
165
|
|
|
166
|
release_date: datetime, |
|
167
|
|
|
168
|
timezone: str = "US/Eastern", |
|
169
|
|
|
170
|
) -> list[dict]: |
|
171
|
|
|
172
|
"""Generate a content calendar from scored content pieces.""" |
|
173
|
|
|
174
|
sorted_pieces = sorted( |
|
175
|
|
|
176
|
content_pieces, key=lambda p: p.get("score", 0), reverse=True |
|
177
|
|
|
178
|
) |
|
179
|
|
|
180
|
calendar = [] |
|
181
|
|
|
182
|
used_slots = set() |
|
183
|
|
|
184
|
for piece in sorted_pieces: |
|
185
|
|
|
186
|
platform = piece["platform"] |
|
187
|
|
|
188
|
content_type = piece["type"] |
|
189
|
|
|
190
|
slots = PLATFORM_SLOTS.get(platform, []) |
|
191
|
|
|
192
|
for slot in slots: |
|
193
|
|
|
194
|
if slot["type"] != content_type: |
|
195
|
|
|
196
|
continue |
|
197
|
|
|
198
|
slot_key = f"{platform}-{slot['day_offset']}-{slot['time']}" |
|
199
|
|
|
200
|
if slot_key in used_slots: |
|
201
|
|
|
202
|
continue |
|
203
|
|
|
204
|
publish_date = release_date + timedelta(days=slot["day_offset"]) |
|
205
|
|
|
206
|
hour, minute = map(int, slot["time"].split(":")) |
|
207
|
|
|
208
|
publish_dt = publish_date.replace(hour=hour, minute=minute) |
|
209
|
|
|
210
|
calendar.append({ |
|
211
|
|
|
212
|
"platform": platform, |
|
213
|
|
|
214
|
"type": content_type, |
|
215
|
|
|
216
|
"publish_at": publish_dt.isoformat(), |
|
217
|
|
|
218
|
"content": piece["content"], |
|
219
|
|
|
220
|
"score": piece.get("score", 0), |
|
221
|
|
|
222
|
"atom_id": piece.get("atom_id"), |
|
223
|
|
|
224
|
}) |
|
225
|
|
|
226
|
used_slots.add(slot_key) |
|
227
|
|
|
228
|
break |
|
229
|
|
|
230
|
calendar.sort(key=lambda c: c["publish_at"]) |
|
231
|
|
|
232
|
return calendar |
|
233
|
|
|
234
|
``` |
|
235
|
|
|
236
|
## Output Format |
|
237
|
|
|
238
|
The calendar is saved as JSON for easy integration with scheduling tools: |
|
239
|
|
|
240
|
```json |
|
241
|
|
|
242
|
{ |
|
243
|
|
|
244
|
"episode": { |
|
245
|
|
|
246
|
"title": "Episode Title", |
|
247
|
|
|
248
|
"release_date": "2024-01-15", |
|
249
|
|
|
250
|
"url": "https://..." |
|
251
|
|
|
252
|
}, |
|
253
|
|
|
254
|
"timezone": "US/Eastern", |
|
255
|
|
|
256
|
"calendar": [ |
|
257
|
|
|
258
|
{ |
|
259
|
|
|
260
|
"platform": "blog", |
|
261
|
|
|
262
|
"type": "blog_post", |
|
263
|
|
|
264
|
"publish_at": "2024-01-15T09:00:00", |
|
265
|
|
|
266
|
"content_file": "blog-post.md", |
|
267
|
|
|
268
|
"score": 82 |
|
269
|
|
|
270
|
}, |
|
271
|
|
|
272
|
{ |
|
273
|
|
|
274
|
"platform": "twitter", |
|
275
|
|
|
276
|
"type": "thread", |
|
277
|
|
|
278
|
"publish_at": "2024-01-16T09:00:00", |
|
279
|
|
|
280
|
"content_file": "social/twitter-thread.md", |
|
281
|
|
|
282
|
"score": 91 |
|
283
|
|
|
284
|
} |
|
285
|
|
|
286
|
], |
|
287
|
|
|
288
|
"stats": { |
|
289
|
|
|
290
|
"total_pieces": 15, |
|
291
|
|
|
292
|
"scheduled": 12, |
|
293
|
|
|
294
|
"cut_below_threshold": 3, |
|
295
|
|
|
296
|
"avg_score": 72, |
|
297
|
|
|
298
|
"highest_score": 91, |
|
299
|
|
|
300
|
"platforms_covered": 7, |
|
301
|
|
|
302
|
"calendar_span_days": 6 |
|
303
|
|
|
304
|
} |
|
305
|
|
|
306
|
} |
|
307
|
|
|
308
|
``` |
|
309
|
|
|
310
|
## Multi-Episode Scheduling |
|
311
|
|
|
312
|
When processing multiple episodes (batch mode), interleave content from different episodes to maintain variety: |
|
313
|
|
|
314
|
- Never schedule content from two different episodes on the same platform on the same day |
|
315
|
- Space episode releases at least 3 days apart in the calendar |
|
316
|
|
|
317
|
- Prioritize newer episodes for prime-time slots |
|
318
|
- Use older episode content as gap filler between new releases |