Skip to content
Merged
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
11 changes: 11 additions & 0 deletions content/authors/jacob-coffee.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "Jacob Coffee",
"bio": "Python Software Foundation Staff. Litestar Maintainer.",
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bio field contains a typo: "Infradwre" should be "Infrastructure".

Copilot uses AI. Check for mistakes.
"github": "JacobCoffee",
"avatar": "https://avatars.githubusercontent.com/u/45884264",
"twitter": "_scriptr",
"bluesky": "scriptr.dev",
"mastodon": "https://fosstodon.org/@Monorepo",
"website": "https://scriptr.dev",
"featured": false
}
82 changes: 82 additions & 0 deletions content/posts/the-python-insider-blog-has-moved/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: The Python Insider Blog Has Moved!
publishDate: '2026-03-03'
updatedDate: '2026-03-03'
author: Jacob Coffee
description: 'Python Insider now lives at blog.python.org, backed by a Git repository. All 307 posts from the Blogger era have been migrated, and old URLs redirect automatically.'
tags:
- python
- community
published: true
legacyUrl: /2026/03/the-python-insider-blog-has-moved.html
---

import ShowcasePostCards from '../../../src/components/showcase/ShowcasePostCards.astro';
import ShowcaseTagCloud from '../../../src/components/showcase/ShowcaseTagCloud.astro';
import ShowcaseAuthors from '../../../src/components/showcase/ShowcaseAuthors.astro';
import ShowcaseSearch from '../../../src/components/showcase/ShowcaseSearch.astro';

Python Insider now lives at [blog.python.org](https://blog.python.org), backed by a Git repository. All 307 posts from the Blogger era have been migrated over, and old URLs redirect to the new ones automatically. Your RSS readers should pick up the new feed without any action on your part, but if something looks off, the new feed URL is [blog.python.org/rss.xml](https://blog.python.org/rss.xml).

## Why we moved

Blogger worked fine for a long time, but contributing to the blog meant having a Google account and using Blogger's editor. That's a higher bar than it needs to be. The new setup is just Markdown files in a Git repo. If you can open a pull request, you can write a post.

Posts live in `content/posts/{slug}/index.md` with YAML frontmatter for the title, date, authors, and tags. Images go right next to the post in the same directory. No special tooling required beyond a text editor.

## Contributing

Want to write about a Python release, core sprint, governance update, or anything else that belongs on the official Python blog? Here's the short version:

1. Fork [python/python-insider-blog](https://github.com/python/python-insider-blog)
2. Create a new directory under `content/posts/` with your post slug
3. Add an `index.md` with your content (and optionally upload your images)
4. Open a PR

The repo README has more detail on frontmatter fields and local development if you want to preview your post before submitting.

## What's new on the site

Beyond the content itself, the new site has a few features the old Blogger setup never had. Here's a live look:

### Browse & filter posts

All posts are browsable with pagination, a year filter, and a tag sidebar. Click any tag or year to narrow things down.

<ShowcasePostCards />

### Every author has a page

See who's been writing, how much they've contributed, and browse their posts individually.

<ShowcaseAuthors />

### Tags at a glance

Every tag across the archive, ranked by how often it appears. Great for finding all the release announcements or security updates in one place.

<ShowcaseTagCloud />

### Search everything

Hit <kbd>Ctrl+K</kbd> (or <kbd>Cmd+K</kbd> on Mac) from any page to open the command palette. It searches across all 307+ posts by title, author, tags, and description. There are also keyboard chord shortcuts for quick navigation.

<ShowcaseSearch />

### And more

- **RSS feed** at [blog.python.org/rss.xml](https://blog.python.org/rss.xml), compatible with the old Blogger feed URL so existing subscribers don't need to change anything.
- **Dark mode** that follows your system preference (try the toggle in the header).
- **Open Graph images** generated automatically for every post, so links shared on social media get proper preview cards.

## What's under the hood

The site is built with [Astro](https://astro.build) and deployed as fully static HTML. There's a [Keystatic](https://keystatic.com) CMS available in dev mode if you prefer a visual editor over raw Markdown, but it's entirely optional. Tailwind handles the styling. The whole thing builds and deploys through GitHub Actions.

## Links

- New site: [blog.python.org](https://blog.python.org)
- Repository: [github.com/python/python-insider-blog](https://github.com/python/python-insider-blog)
- RSS feed: [blog.python.org/rss.xml](https://blog.python.org/rss.xml)

If you spot broken links, missing images, or formatting issues from the migration, [file an issue](https://github.com/python/python-insider-blog/issues) on the repo. PRs are welcome too.
80 changes: 80 additions & 0 deletions src/components/showcase/ShowcaseAuthors.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
import { getCollection } from "astro:content";
import { slugify, withBase } from "../../lib/utils";

const allPosts = await getCollection("posts");
const publishedPosts = allPosts.filter((p) => p.data.published);
const allAuthors = await getCollection("authors");

const authorCounts = new Map<string, number>();
for (const post of publishedPosts) {
const slug = slugify(post.data.author);
authorCounts.set(slug, (authorCounts.get(slug) || 0) + 1);
}

const authorsWithCounts = allAuthors
.map((a) => ({
slug: a.id,
name: a.data.name,
github: a.data.github,
count: authorCounts.get(a.id) || 0,
}))
.filter((a) => a.count > 0)
.sort((a, b) => b.count - a.count);

const topAuthors = authorsWithCounts.slice(0, 6);
const maxCount = topAuthors[0]?.count ?? 1;
---

<div class="not-prose my-8 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800">
<div class="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-5 py-3 dark:border-zinc-800 dark:bg-zinc-900/50">
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="9" cy="7" r="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="text-sm font-semibold text-zinc-700 dark:text-zinc-300" style="font-family: var(--font-display);">Authors</span>
<span class="text-xs text-zinc-400 dark:text-zinc-500">{authorsWithCounts.length} contributors</span>
</div>
<a href={withBase("/authors")} class="text-xs font-medium text-[#306998] hover:underline dark:text-[#ffd43b]">
View all &rarr;
</a>
</div>
<div class="space-y-0.5 p-3">
{topAuthors.map((author) => {
const pct = Math.round((author.count / maxCount) * 100);
return (
<a
href={withBase(`/authors/${author.slug}`)}
class="group flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800/60"
>
{author.github ? (
<img
src={`https://github.com/${author.github}.png`}
alt=""
class="h-6 w-6 flex-shrink-0 rounded-full"
loading="lazy"
/>
) : (
<div class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-zinc-200 text-xs font-bold text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400">
{author.name.charAt(0)}
</div>
)}
<span class="w-36 flex-shrink-0 truncate text-sm font-semibold text-zinc-700 dark:text-zinc-300" style="font-family: var(--font-display);">
{author.name}
</span>
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
<div
class="h-full rounded-full bg-[#306998] transition-all duration-500 group-hover:opacity-80 dark:bg-[#ffd43b]"
style={`width: ${Math.max(pct, 4)}%;`}
/>
</div>
<span class="w-8 flex-shrink-0 text-right text-xs tabular-nums font-medium text-zinc-400 dark:text-zinc-500">
{author.count}
</span>
</a>
);
})}
</div>
</div>
100 changes: 100 additions & 0 deletions src/components/showcase/ShowcasePostCards.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
import { getCollection } from "astro:content";
import { formatDate, postUrl, slugify, withBase } from "../../lib/utils";
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slugify is imported from ../../lib/utils but is never used anywhere in this file. The only usage in the template refers to postUrl, formatDate, and withBase. Removing the unused import keeps the file clean.

Suggested change
import { formatDate, postUrl, slugify, withBase } from "../../lib/utils";
import { formatDate, postUrl, withBase } from "../../lib/utils";

Copilot uses AI. Check for mistakes.

const allPosts = await getCollection("posts");
const recentPosts = allPosts
.filter((p) => p.data.published)
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime())
.slice(0, 3);

// Collect years from all posts for the mini sidebar
const yearSet = new Set<number>();
for (const post of allPosts.filter((p) => p.data.published)) {
yearSet.add(post.data.publishDate.getFullYear());
}
const years = [...yearSet].sort((a, b) => b - a).slice(0, 8);

// Top tags for the mini sidebar
const tagCounts = new Map<string, number>();
for (const post of allPosts.filter((p) => p.data.published)) {
for (const tag of post.data.tags) {
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
}
}
const topTags = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
---

<div class="not-prose my-8 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800">
<div class="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-5 py-3 dark:border-zinc-800 dark:bg-zinc-900/50">
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z" stroke-linecap="round" stroke-linejoin="round" />
<path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="text-sm font-semibold text-zinc-700 dark:text-zinc-300" style="font-family: var(--font-display);">Blog</span>
<span class="text-xs text-zinc-400 dark:text-zinc-500">{allPosts.filter((p) => p.data.published).length} posts with filters & pagination</span>
Comment on lines +6 to +36
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filter allPosts.filter((p) => p.data.published) is applied four times — on lines 7, 13, 20, and 36. With 300+ posts in the collection, this means iterating the full array four times unnecessarily. Consider extracting this into a publishedPosts variable (as is done in the sibling components ShowcaseTagCloud.astro and ShowcaseAuthors.astro) and reusing it throughout.

Copilot uses AI. Check for mistakes.
</div>
<a href={withBase("/blog")} class="text-xs font-medium text-[#306998] hover:underline dark:text-[#ffd43b]">
Browse all &rarr;
</a>
</div>

<div class="flex">
<!-- Posts column -->
<div class="min-w-0 flex-1 divide-y divide-zinc-100 dark:divide-zinc-800/60">
{recentPosts.map((post) => (
<a href={withBase(postUrl(post.id, post.data.publishDate.toISOString()))} class="group block px-5 py-3.5 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/30">
<h4 class="text-sm font-semibold leading-snug text-zinc-800 transition-colors group-hover:text-[#306998] dark:text-zinc-200 dark:group-hover:text-[#ffd43b]" style="font-family: var(--font-display);">
{post.data.title}
</h4>
<div class="mt-1 flex items-center gap-2 text-xs text-zinc-400 dark:text-zinc-500">
<span class="font-medium text-zinc-500 dark:text-zinc-400">{post.data.author}</span>
<span class="text-zinc-300 dark:text-zinc-700">&middot;</span>
<time datetime={post.data.publishDate.toISOString()}>{formatDate(post.data.publishDate.toISOString())}</time>
</div>
{post.data.tags.length > 0 && (
<div class="mt-2 flex flex-wrap gap-1">
{post.data.tags.slice(0, 3).map((tag) => (
<span class="rounded-md bg-zinc-100 px-1.5 py-0.5 text-[10px] font-medium text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
{tag}
</span>
))}
</div>
)}
</a>
))}
</div>

<!-- Mini sidebar -->
<div class="hidden w-44 flex-shrink-0 border-l border-zinc-100 p-4 dark:border-zinc-800/60 sm:block">
<div class="mb-4">
<h5 class="mb-2 text-[10px] font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-600">Years</h5>
<div class="flex flex-wrap gap-1">
{years.map((y) => (
<a
href={withBase(`/blog/year/${y}`)}
class="rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-medium text-zinc-500 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700"
>
{y}
</a>
))}
</div>
</div>
<div>
<h5 class="mb-2 text-[10px] font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-600">Tags</h5>
<div class="flex flex-wrap gap-1">
{topTags.map(([tag, count]) => (
<a
href={withBase(`/tags/${tag}`)}
class="tag-pill inline-flex items-center gap-0.5 rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400"
>
{tag}
<span class="text-[9px] text-zinc-400 dark:text-zinc-600">{count}</span>
</a>
))}
</div>
</div>
</div>
</div>
</div>
Loading