Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions docs/keynoter-scheduling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Keynoter Social Media Scheduling

This document describes the process for scheduling keynote speaker announcement
posts to Buffer. In short, you should:

- prepare the images in canva and deploy them to have a live url to the image
- prepare copy for each platform and put it into a json file
- run the script to schedule posts for the keynoters, one at a time, to all the
supported social media platform

The script posts to all connected channels in one run per keynoter: `instagram`,
`linkedin`, `fosstodon`, `bsky`, `x`, `tiktok`.

---

## Prerequisites

- Python environment with `requests` and `python-dotenv` installed
- A `.env.local` file in the **repo root** with your Buffer API key:

```
BUFFER_API_KEY=your_buffer_api_key_here
```

### Getting the Buffer API key

1. Log in to [buffer.com](https://buffer.com) with the EuroPython account
2. Go to **Account Settings → Apps & Integrations**
3. Copy the personal access token under **Access Token**
4. Paste it as `BUFFER_API_KEY` in `.env.local`

---

## Step 1 — Prepare and deploy the images

Buffer requires a **publicly accessible image URL** — local file paths, Google
Drive links, and Canva share links do not work. The image must be hosted on the
live site before the script can use it.

### Prepare the images

- Export images as PNG from your design tool (Canva)
- Name them `firstname-lastname.png` (lowercase, hyphenated)
- Place them in `website/public/media/keynoters/`

### Deploy

1. Commit the images and open a PR
2. Wait for the PR to be merged and deployed to production
3. Verify each image is accessible, e.g.:
`https://ep2026.europython.eu/media/keynoters/leah-wasser.png`

> ⚠️ Do not run the scheduling script until the images are live. Buffer fetches
> the URL at scheduling time and will fail with a "Not Found" error if the file
> hasn't been deployed yet.

## Step 2 — Prepare the post copy (`keynoters.json`)

All post text lives in `scripts/keynoters.json`

```json
{
"Speaker Name": {
"image": "https://ep<year>.europython.eu/media/keynoters/firstname-lastname.png",
"instagram": "Post text for Instagram...",
"linkedin": "Post text for LinkedIn...",
"fosstodon": "Post text for Fosstodon/Mastodon...",
"bsky": "Post text for Bluesky...",
"x": "Post text for X/Twitter...",
"tiktok": "Post text for TikTok..."
}
}
```

A few things to keep in mind when writing copy:

- Social handles (e.g. `@leahawasser.bsky.social`) go **inline in the post
text**, not as separate fields. Each platform's post should use the handle
format native to that platform.
- X and Bluesky have character limits — keep those posts short.
- Instagram and TikTok don't render clickable URLs, so use the short form
(`europython.eu/tickets/`) rather than the full URL.
- LinkedIn and Fosstodon support full clickable URLs.

Add one entry per keynoter. The key must match exactly what you'll set in the
script in Step 3.

---

## Step 3 — Schedule posts via Buffer

The scheduling script is `scripts/buffer-keynoters.py`. Run it once per
keynoter.

### Configure the script

Open `scripts/buffer-keynoters.py` and edit the two lines at the top:

```python
KEYNOTER = "Leah Wasser" # must match the key in keynoters.json exactly
SCHEDULED_AT = datetime(2026, 6, 16, 10, 0,
tzinfo=ZoneInfo("Europe/London"))
```

Set `SCHEDULED_AT` to the date and time you want the post to go live. The
timezone is `Europe/London` — adjust the year, month, day, hour, and minute as
needed.

### Preview before posting

```bash
python scripts/buffer-keynoters.py --dry-run
```

This prints the first 200 characters of the post for each platform without
sending anything to Buffer. Use it to sanity-check the copy and confirm the
right keynoter is selected.

### Run

```bash
python scripts/buffer-keynoters.py
```

The script will:

1. Connect to Buffer and fetch your channel IDs
2. Fetch the image from the live URL in `keynoters.json`
3. Schedule the post to every platform that has text defined
4. Skip any platform not connected in your Buffer account
5. Print a confirmation line with the Buffer post ID for each channel

Repeat for each keynoter, updating `KEYNOTER` and `SCHEDULED_AT` each time.

---

## Troubleshooting

| Error | Cause | Fix |
| ----------------------------------- | ---------------------------- | ------------------------------------------------ |
| `BUFFER_API_KEY not set` | Missing `.env.local` | Create `website/.env.local` with the key |
| `Image upload failed (404)` | Image not deployed yet | Merge the images PR and wait for deployment |
| `not connected in Buffer — skipped` | Channel not linked in Buffer | Log in to Buffer and connect the missing channel |
| `not found in keynoters.json` | Typo in `KEYNOTER` | Check the exact key spelling in `keynoters.json` |
41 changes: 21 additions & 20 deletions docs/social-media-scheduling.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,24 @@ speaker → speaker → sponsor → speaker → partner → (repeat)
Regenerate it by hitting the API endpoint while the dev server is running:

```bash
curl -s http://localhost:4321/api/media/combined_socials_queue > src/pages/api/media/combined_socials_queue.json
curl -s http://localhost:4321/api/media/combined_socials_queue > scripts/combined_socials_queue.json
```

This overwrites `src/pages/api/media/combined_socials_queue.json` with all items
sorted and interleaved.
This overwrites `scripts/combined_socials_queue.json` with all items sorted and
interleaved.

> **Note:** There are two queue files:
>
> - `combined_socials_queue.json` — the freshly generated queue, used as the
> source of truth and read by `buffer-scheduling.py`
> - `combined_socials_queue_2026.json` — the manually curated queue that
> - `scripts/combined_socials_queue.json` — the freshly generated queue, used as
> the source of truth and read by `buffer-scheduling.py`
> - `scripts/combined_socials_queue_2026.json` — the manually curated queue that
> preserves already-scheduled posts at the top; new entries are appended after
> the last scheduled position

When new speakers or sponsors are added, regenerate
`combined_socials_queue.json` and then merge the new entries into
`combined_socials_queue_2026.json` manually — keeping already-posted entries
intact and appending only new/unposted ones.
`scripts/combined_socials_queue.json` and then merge the new entries into
`scripts/combined_socials_queue_2026.json` manually — keeping already-posted
entries intact and appending only new/unposted ones.

### Session types and labels

Expand All @@ -150,7 +150,8 @@ Sponsors are split into two buckets:
Financial Aid

If a new sponsor tier is added, update `commercialTiers` in
`src/pages/api/media/combined_socials_queue.ts`.
`src/pages/api/media/combined_socials_queue.ts` (this file stays in the API
layer).

### Manual queue adjustments

Expand All @@ -160,7 +161,7 @@ reorder entries. For example, to swap two sponsors:
```python
python3 -c "
import json
path = 'src/pages/api/media/combined_socials_queue.json'
path = 'scripts/combined_socials_queue.json'
with open(path) as f:
q = json.load(f)
q[2], q[17] = q[17], q[2] # swap positions 3 and 18 (0-indexed)
Expand All @@ -169,9 +170,9 @@ with open(path, 'w') as f:
"
```

`src/pages/api/media/combined_socials_queue.json` is committed to the repo. Any
manual reordering should be committed so the intentional order is preserved and
not lost when the queue is regenerated.
`scripts/combined_socials_queue.json` is committed to the repo. Any manual
reordering should be committed so the intentional order is preserved and not
lost when the queue is regenerated.

---

Expand Down Expand Up @@ -233,7 +234,7 @@ Buffer operates as a FIFO queue against pre-configured time slots:
> remove them one by one. Before each run, open Buffer and verify the number and
> cadence of slots for each channel matches your intended rollout pace.

The scheduling script is at `src/pages/api/media/buffer-scheduling.py`.
The scheduling script is at `scripts/buffer-scheduling.py`.

### Configuration

Expand All @@ -251,13 +252,13 @@ the results, increment the range for subsequent runs.
### Running the script

```bash
uv run python src/pages/api/media/buffer-scheduling.py
uv run python scripts/buffer-scheduling.py
```

Or with your venv activated:

```bash
python src/pages/api/media/buffer-scheduling.py
python scripts/buffer-scheduling.py
```

### What it does
Expand Down Expand Up @@ -292,9 +293,9 @@ python src/pages/api/media/buffer-scheduling.py
After each successful run, note the last `QUEUE_END` value. The next run should
set `QUEUE_START = previous QUEUE_END + 1`.

The script reads from `combined_socials_queue.json` by default. When switching
to the curated `combined_socials_queue_2026.json`, update `queue_path` in the
script accordingly.
The script reads from `scripts/combined_socials_queue.json` by default. When
switching to the curated `scripts/combined_socials_queue_2026.json`, update
`queue_path` in the script accordingly.

Already scheduled as of June 2026:

Expand Down
Binary file added public/media/keynoters/imogen-wright.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/media/keynoters/leah-wasser.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/media/keynoters/marlene-mhangami.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/media/keynoters/william-woodruff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests
python-dotenv
Loading
Loading