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