All Posts
{totalPosts} posts · Page {currentPage} of {totalPages}
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
index b79baa8..4cce310 100644
--- a/src/pages/blog/index.astro
+++ b/src/pages/blog/index.astro
@@ -28,6 +28,8 @@ const topTags = [...tagCounts.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 20);
+const ogImage = withBase("/og/blog.png");
+
// Collect years for sidebar
const yearSet = new Set
();
for (const post of posts) {
@@ -36,7 +38,7 @@ for (const post of posts) {
const years = [...yearSet].sort((a, b) => b - a);
---
-
+
All Posts
{posts.length} posts from the Python core development team
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 88816d1..88c714e 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -17,7 +17,7 @@ const totalPosts = posts.length;
const authors = new Set(posts.map((p) => p.data.author));
---
-
+
diff --git a/src/pages/og/[...slug].png.ts b/src/pages/og/[...slug].png.ts
index 4a9ee38..b81ce81 100644
--- a/src/pages/og/[...slug].png.ts
+++ b/src/pages/og/[...slug].png.ts
@@ -3,32 +3,7 @@ import { getCollection } from "astro:content";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
import { OgImage } from "../../lib/og-image";
-
-// Load Inter font from Google Fonts at build time
-async function loadFont(
- weight: number,
-): Promise
{
- const url = `https://fonts.googleapis.com/css2?family=Inter:wght@${weight}&display=swap`;
- const css = await fetch(url).then((r) => r.text());
- const match = css.match(/src: url\((.+?)\) format/);
- if (!match?.[1]) throw new Error(`Failed to load Inter ${weight}`);
- return fetch(match[1]).then((r) => r.arrayBuffer());
-}
-
-let fontsPromise: Promise<
- { name: string; data: ArrayBuffer; weight: 400 | 600 | 700; style: "normal" }[]
-> | null = null;
-
-function getFonts() {
- if (!fontsPromise) {
- fontsPromise = Promise.all([
- loadFont(400).then((data) => ({ name: "Inter" as const, data, weight: 400 as const, style: "normal" as const })),
- loadFont(600).then((data) => ({ name: "Inter" as const, data, weight: 600 as const, style: "normal" as const })),
- loadFont(700).then((data) => ({ name: "Inter" as const, data, weight: 700 as const, style: "normal" as const })),
- ]);
- }
- return fontsPromise;
-}
+import { getFonts } from "../../lib/og-fonts";
export const prerender = true;
diff --git a/src/pages/og/authors/[author].png.ts b/src/pages/og/authors/[author].png.ts
new file mode 100644
index 0000000..fff8b15
--- /dev/null
+++ b/src/pages/og/authors/[author].png.ts
@@ -0,0 +1,53 @@
+import type { APIRoute, GetStaticPaths } from "astro";
+import { getCollection } from "astro:content";
+import satori from "satori";
+import { Resvg } from "@resvg/resvg-js";
+import { OgAuthorImage } from "../../../lib/og-image";
+import { getFonts } from "../../../lib/og-fonts";
+import { slugify } from "../../../lib/utils";
+
+export const prerender = true;
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const allPosts = await getCollection("posts");
+ const publishedPosts = allPosts.filter((p) => p.data.published);
+ const allAuthors = await getCollection("authors");
+
+ const authorCounts = new Map();
+ for (const post of publishedPosts) {
+ const slug = slugify(post.data.author);
+ authorCounts.set(slug, (authorCounts.get(slug) || 0) + 1);
+ }
+
+ return allAuthors
+ .filter((a) => authorCounts.has(a.id))
+ .map((author) => ({
+ params: { author: author.id },
+ props: {
+ name: author.data.name,
+ bio: author.data.bio,
+ postCount: authorCounts.get(author.id) || 0,
+ },
+ }));
+};
+
+export const GET: APIRoute = async ({ props }) => {
+ const { name, bio, postCount } = props as { name: string; bio?: string; postCount: number };
+
+ const fonts = await getFonts();
+
+ const svg = await satori(
+ OgAuthorImage({ name, bio, postCount }),
+ { width: 1200, height: 630, fonts },
+ );
+
+ const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } });
+ const png = resvg.render().asPng();
+
+ return new Response(new Uint8Array(png), {
+ headers: {
+ "Content-Type": "image/png",
+ "Cache-Control": "public, max-age=31536000, immutable",
+ },
+ });
+};
diff --git a/src/pages/og/authors/index.png.ts b/src/pages/og/authors/index.png.ts
new file mode 100644
index 0000000..3721f37
--- /dev/null
+++ b/src/pages/og/authors/index.png.ts
@@ -0,0 +1,35 @@
+import type { APIRoute } from "astro";
+import { getCollection } from "astro:content";
+import satori from "satori";
+import { Resvg } from "@resvg/resvg-js";
+import { OgPageImage } from "../../../lib/og-image";
+import { getFonts } from "../../../lib/og-fonts";
+
+export const prerender = true;
+
+export const GET: APIRoute = async () => {
+ const allPosts = await getCollection("posts");
+ const publishedPosts = allPosts.filter((p) => p.data.published);
+ const authors = new Set(publishedPosts.map((p) => p.data.author));
+
+ const fonts = await getFonts();
+
+ const svg = await satori(
+ OgPageImage({
+ title: "Authors",
+ subtitle: "Contributors to the Python Insider blog.",
+ detail: `${authors.size} authors`,
+ }),
+ { width: 1200, height: 630, fonts },
+ );
+
+ const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } });
+ const png = resvg.render().asPng();
+
+ return new Response(new Uint8Array(png), {
+ headers: {
+ "Content-Type": "image/png",
+ "Cache-Control": "public, max-age=31536000, immutable",
+ },
+ });
+};
diff --git a/src/pages/og/blog.png.ts b/src/pages/og/blog.png.ts
new file mode 100644
index 0000000..4fce952
--- /dev/null
+++ b/src/pages/og/blog.png.ts
@@ -0,0 +1,34 @@
+import type { APIRoute } from "astro";
+import { getCollection } from "astro:content";
+import satori from "satori";
+import { Resvg } from "@resvg/resvg-js";
+import { OgPageImage } from "../../lib/og-image";
+import { getFonts } from "../../lib/og-fonts";
+
+export const prerender = true;
+
+export const GET: APIRoute = async () => {
+ const allPosts = await getCollection("posts");
+ const postCount = allPosts.filter((p) => p.data.published).length;
+
+ const fonts = await getFonts();
+
+ const svg = await satori(
+ OgPageImage({
+ title: "Blog",
+ subtitle: "News and updates from the Python core development team.",
+ detail: `${postCount} posts`,
+ }),
+ { width: 1200, height: 630, fonts },
+ );
+
+ const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } });
+ const png = resvg.render().asPng();
+
+ return new Response(new Uint8Array(png), {
+ headers: {
+ "Content-Type": "image/png",
+ "Cache-Control": "public, max-age=31536000, immutable",
+ },
+ });
+};
diff --git a/src/pages/og/index.png.ts b/src/pages/og/index.png.ts
new file mode 100644
index 0000000..faf48ef
--- /dev/null
+++ b/src/pages/og/index.png.ts
@@ -0,0 +1,30 @@
+import type { APIRoute } from "astro";
+import satori from "satori";
+import { Resvg } from "@resvg/resvg-js";
+import { OgPageImage } from "../../lib/og-image";
+import { getFonts } from "../../lib/og-fonts";
+
+export const prerender = true;
+
+export const GET: APIRoute = async () => {
+ const fonts = await getFonts();
+
+ const svg = await satori(
+ OgPageImage({
+ title: "Python Insider",
+ subtitle: "The official blog of the Python core development team.",
+ detail: "blog.python.org",
+ }),
+ { width: 1200, height: 630, fonts },
+ );
+
+ const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } });
+ const png = resvg.render().asPng();
+
+ return new Response(new Uint8Array(png), {
+ headers: {
+ "Content-Type": "image/png",
+ "Cache-Control": "public, max-age=31536000, immutable",
+ },
+ });
+};