diff --git a/src/pages/api/buffer_posts.ts b/src/pages/api/buffer_posts.ts
new file mode 100644
index 000000000..6e1ff6d2a
--- /dev/null
+++ b/src/pages/api/buffer_posts.ts
@@ -0,0 +1,170 @@
+import { getCollection, getEntry } from "astro:content";
+import type { APIRoute } from "astro";
+
+// Get @username from Twitter URL
+function getTwitterUsername(url: string): string | undefined {
+  if (!url) return undefined;
+  const username = url.split("/").pop();
+  return (username ?? url).startsWith("@") ? username : `@${username}`;
+}
+
+// Get @username from Bluesky URL
+function getBlueskyUsername(url: string): string | undefined {
+  if (!url) return undefined;
+  const username = url.split("/").pop()?.replace(/^@/, "");
+  return username ? `@${username}` : undefined;
+}
+
+// Get Bluesky profile link from username
+function getBlueskyProfileLink(username: string): string {
+  // Remove any leading @ if present
+  const cleanUsername = username.replace(/^@/, "");
+  return `https://bsky.app/profile/${cleanUsername}`;
+}
+
+// Get @username@instance.tld from Mastodon URL
+function getMastodonUsername(url: string): string | undefined {
+  if (!url) return undefined;
+  const match = url.match(/https?:\/\/([^\/]+)\/@([^\/]+)(\/|\?|$)/);
+  return match ? `@${match[2]}@${match[1]}` : undefined;
+}
+
+function getLinkedInUsernameHandler(url: string): string | undefined {
+  if (!url) return undefined;
+  const match = url.match(/https?:\/\/([^\/]+)\/in\/([^\/]+)(\/|\?|$)/);
+  if (match) {
+    try {
+      return `https://www.linkedin.com/in/${decodeURIComponent(match[2])}`;
+    } catch {
+      return `https://www.linkedin.com/in/${match[2]}`;
+    }
+  }
+  return undefined;
+}
+
+export const GET: APIRoute = async ({ params, request }) => {
+  const limit = Infinity;
+  const speakers = await getCollection("speakers");
+  const exclude = [
+    "sebastian-ramirez",
+    "savannah-ostrowski",
+    "nerea-luis",
+    "petr-baudis",
+    "brett-cannon",
+  ];
+  const records: any[] = [];
+
+  const charLimits: Record<string, number> = {
+    instagram: 2200,
+    x: 280,
+    linkedin: 3000,
+    bsky: 300,
+    fosstodon: 500,
+  };
+
+  // Tailor message templates for each platform using appropriate handle formats
+  const message_template = {
+    instagram: ({ name, talkTitle, talkUrl }) =>
+      `Join ${name} at EuroPython for "${talkTitle}".`,
+
+    x: ({ name, handle, talkTitle, talkUrl }) =>
+      handle
+        ? `Join ${name} (${handle}) at EuroPython for "${talkTitle}". Talk: ${talkUrl}`
+        : `Join ${name} at EuroPython for "${talkTitle}". Talk: ${talkUrl}`,
+
+    linkedin: ({ name, handle, talkTitle, talkUrl }) =>
+      `Join ${name} at EuroPython for "${talkTitle}".`,
+
+    bsky: ({ name, handle, talkTitle, talkUrl }) =>
+      handle
+        ? `Join ${name} (${handle}) at EuroPython for "${talkTitle}". Talk: ${talkUrl}`
+        : `Join ${name} at EuroPython for "${talkTitle}". Talk: ${talkUrl}`,
+
+    fosstodon: ({ name, handle, talkTitle, talkUrl }) =>
+      handle
+        ? `Join ${name} (${handle}) at EuroPython for "${talkTitle}". talk: ${talkUrl}`
+        : `Join ${name} at EuroPython for "${talkTitle}". Talk: ${talkUrl}`,
+  };
+
+  const trimToLimit = (text: string, limit: number) =>
+    text.length <= limit ? text : text.slice(0, limit - 1) + "…";
+
+  for (const speaker of speakers) {
+    if (records.length >= limit) break;
+    if (exclude.includes(speaker.id)) continue;
+
+    const {
+      name,
+      twitter_url,
+      linkedin_url,
+      bluesky_url,
+      mastodon_url,
+      submissions,
+    } = speaker.data;
+
+    const sessions = await Promise.all(
+      submissions.map((session) => getEntry("sessions", session.id))
+    );
+
+    const validSessions = sessions.filter(
+      (session) => session && session.data.title
+    );
+
+    if (validSessions.length === 0) continue;
+
+    const talkTitle = validSessions[0]?.data.title || "an exciting topic";
+    const talkCode = validSessions[0]?.data.code;
+    const talkUrl = `https://ep2025.europython.eu/${talkCode}`;
+    const speakerImage = `https://ep2025.europython.eu/media/social-${speaker.id}.png`;
+    const fallbackUrl = `https://ep2025.europython.eu/speaker/${speaker.id}`;
+
+    // Extract handles for each platform
+    const handles = {
+      x: getTwitterUsername(twitter_url || ""),
+      linkedin: getLinkedInUsernameHandler(linkedin_url || ""),
+      bsky: getBlueskyUsername(bluesky_url || ""),
+      fosstodon: getMastodonUsername(mastodon_url || ""),
+    };
+
+    // Generate appropriate messages for each platform
+    const generateMessage = (platform: keyof typeof message_template) => {
+      const templateFn = message_template[platform];
+      const handle =
+        platform === "instagram"
+          ? undefined
+          : handles[platform as keyof typeof handles];
+
+      const full = templateFn({
+        name,
+        handle,
+        talkTitle,
+        talkUrl: platform === "instagram" ? fallbackUrl : talkUrl,
+      });
+
+      const limit = charLimits[platform];
+      return trimToLimit(full, limit);
+    };
+
+    const record = {
+      name,
+      image: speakerImage,
+      handles: handles,
+      channel: {
+        instagram: generateMessage("instagram"),
+        x: generateMessage("x"),
+        linkedin: generateMessage("linkedin"),
+        bsky: generateMessage("bsky"),
+        fosstodon: generateMessage("fosstodon"),
+      },
+    };
+
+    records.push(record);
+  }
+
+  return new Response(JSON.stringify(records, null, 2), {
+    status: 200,
+    headers: {
+      "Content-Type": "application/json",
+    },
+  });
+};
diff --git a/src/pages/media/social_media.csv.ts b/src/pages/media/social_media.csv.ts
new file mode 100644
index 000000000..553668b6d
--- /dev/null
+++ b/src/pages/media/social_media.csv.ts
@@ -0,0 +1,78 @@
+import { getCollection, getEntry } from "astro:content";
+export async function GET({ params, request }) {
+  const speakers = await getCollection("speakers");
+
+  const header = [
+    "Talk Title",
+    "Speaker Name",
+    "Speaker Photo URL",
+    "Primary Social ULR",
+    "X URL",
+    "LinkedIn URL",
+    "Bluesky URL",
+    "Mastodon URL",
+  ];
+
+  const exclude = [
+    "sebastian-ramirez",
+    "savannah-ostrowski",
+    "nerea-luis",
+    "petr-baudis",
+    "brett-cannon",
+  ];
+
+  const rows: string[][] = [];
+
+  for (const speaker of speakers) {
+    if (exclude.includes(speaker.id)) continue;
+
+    const {
+      name,
+      twitter_url,
+      linkedin_url,
+      bluesky_url,
+      mastodon_url,
+      submissions,
+    } = speaker.data;
+
+    const sessions = await Promise.all(
+      submissions.map((session) => getEntry("sessions", session.id))
+    );
+
+    for (const session of sessions) {
+      if (session) {
+        const speaker_page = `https://ep2025.europython.eu/speaker/${speaker.id}`;
+        rows.push([
+          session.data.title || "",
+          name,
+          `https://ep2025-buffer.ep-preview.click/media/social-${speaker.id}.png`,
+          twitter_url || linkedin_url || mastodon_url || speaker_page,
+          twitter_url ?? speaker_page,
+          linkedin_url ?? speaker_page,
+          bluesky_url ?? speaker_page,
+          mastodon_url ?? speaker_page,
+        ]);
+      }
+    }
+  }
+
+  const csvLines = [header, ...rows]
+    .map((row) =>
+      row
+        .map((field) =>
+          field.includes('"') || field.includes(",") || field.includes("\n")
+            ? `"${field.replace(/"/g, '""')}"`
+            : field
+        )
+        .join(",")
+    )
+    .join("\r\n");
+
+  return new Response(csvLines, {
+    status: 200,
+    headers: {
+      "Content-Type": "text/csv; charset=utf-8",
+      "Content-Disposition": 'attachment; filename="social_media.csv"',
+    },
+  });
+}