Skip to content

Commit 6b17ea6

Browse files
authored
Post: blog has moved (#27)
1 parent 37778f1 commit 6b17ea6

File tree

7 files changed

+429
-1
lines changed

7 files changed

+429
-1
lines changed

content/authors/jacob-coffee.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "Jacob Coffee",
3+
"bio": "Python Software Foundation Staff. Litestar Maintainer.",
4+
"github": "JacobCoffee",
5+
"avatar": "https://avatars.githubusercontent.com/u/45884264",
6+
"twitter": "_scriptr",
7+
"bluesky": "scriptr.dev",
8+
"mastodon": "https://fosstodon.org/@Monorepo",
9+
"website": "https://scriptr.dev",
10+
"featured": false
11+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
title: The Python Insider Blog Has Moved!
3+
publishDate: '2026-03-03'
4+
updatedDate: '2026-03-03'
5+
author: Jacob Coffee
6+
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.'
7+
tags:
8+
- python
9+
- community
10+
published: true
11+
legacyUrl: /2026/03/the-python-insider-blog-has-moved.html
12+
---
13+
14+
import ShowcasePostCards from '../../../src/components/showcase/ShowcasePostCards.astro';
15+
import ShowcaseTagCloud from '../../../src/components/showcase/ShowcaseTagCloud.astro';
16+
import ShowcaseAuthors from '../../../src/components/showcase/ShowcaseAuthors.astro';
17+
import ShowcaseSearch from '../../../src/components/showcase/ShowcaseSearch.astro';
18+
19+
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).
20+
21+
## Why we moved
22+
23+
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.
24+
25+
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.
26+
27+
## Contributing
28+
29+
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:
30+
31+
1. Fork [python/python-insider-blog](https://github.com/python/python-insider-blog)
32+
2. Create a new directory under `content/posts/` with your post slug
33+
3. Add an `index.md` with your content (and optionally upload your images)
34+
4. Open a PR
35+
36+
The repo README has more detail on frontmatter fields and local development if you want to preview your post before submitting.
37+
38+
## What's new on the site
39+
40+
Beyond the content itself, the new site has a few features the old Blogger setup never had. Here's a live look:
41+
42+
### Browse & filter posts
43+
44+
All posts are browsable with pagination, a year filter, and a tag sidebar. Click any tag or year to narrow things down.
45+
46+
<ShowcasePostCards />
47+
48+
### Every author has a page
49+
50+
See who's been writing, how much they've contributed, and browse their posts individually.
51+
52+
<ShowcaseAuthors />
53+
54+
### Tags at a glance
55+
56+
Every tag across the archive, ranked by how often it appears. Great for finding all the release announcements or security updates in one place.
57+
58+
<ShowcaseTagCloud />
59+
60+
### Search everything
61+
62+
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.
63+
64+
<ShowcaseSearch />
65+
66+
### And more
67+
68+
- **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.
69+
- **Dark mode** that follows your system preference (try the toggle in the header).
70+
- **Open Graph images** generated automatically for every post, so links shared on social media get proper preview cards.
71+
72+
## What's under the hood
73+
74+
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.
75+
76+
## Links
77+
78+
- New site: [blog.python.org](https://blog.python.org)
79+
- Repository: [github.com/python/python-insider-blog](https://github.com/python/python-insider-blog)
80+
- RSS feed: [blog.python.org/rss.xml](https://blog.python.org/rss.xml)
81+
82+
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.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
import { getCollection } from "astro:content";
3+
import { slugify, withBase } from "../../lib/utils";
4+
5+
const allPosts = await getCollection("posts");
6+
const publishedPosts = allPosts.filter((p) => p.data.published);
7+
const allAuthors = await getCollection("authors");
8+
9+
const authorCounts = new Map<string, number>();
10+
for (const post of publishedPosts) {
11+
const slug = slugify(post.data.author);
12+
authorCounts.set(slug, (authorCounts.get(slug) || 0) + 1);
13+
}
14+
15+
const authorsWithCounts = allAuthors
16+
.map((a) => ({
17+
slug: a.id,
18+
name: a.data.name,
19+
github: a.data.github,
20+
count: authorCounts.get(a.id) || 0,
21+
}))
22+
.filter((a) => a.count > 0)
23+
.sort((a, b) => b.count - a.count);
24+
25+
const topAuthors = authorsWithCounts.slice(0, 6);
26+
const maxCount = topAuthors[0]?.count ?? 1;
27+
---
28+
29+
<div class="not-prose my-8 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800">
30+
<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">
31+
<div class="flex items-center gap-2">
32+
<svg class="h-4 w-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
33+
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke-linecap="round" stroke-linejoin="round" />
34+
<circle cx="9" cy="7" r="4" stroke-linecap="round" stroke-linejoin="round" />
35+
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke-linecap="round" stroke-linejoin="round" />
36+
</svg>
37+
<span class="text-sm font-semibold text-zinc-700 dark:text-zinc-300" style="font-family: var(--font-display);">Authors</span>
38+
<span class="text-xs text-zinc-400 dark:text-zinc-500">{authorsWithCounts.length} contributors</span>
39+
</div>
40+
<a href={withBase("/authors")} class="text-xs font-medium text-[#306998] hover:underline dark:text-[#ffd43b]">
41+
View all &rarr;
42+
</a>
43+
</div>
44+
<div class="space-y-0.5 p-3">
45+
{topAuthors.map((author) => {
46+
const pct = Math.round((author.count / maxCount) * 100);
47+
return (
48+
<a
49+
href={withBase(`/authors/${author.slug}`)}
50+
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"
51+
>
52+
{author.github ? (
53+
<img
54+
src={`https://github.com/${author.github}.png`}
55+
alt=""
56+
class="h-6 w-6 flex-shrink-0 rounded-full"
57+
loading="lazy"
58+
/>
59+
) : (
60+
<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">
61+
{author.name.charAt(0)}
62+
</div>
63+
)}
64+
<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);">
65+
{author.name}
66+
</span>
67+
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
68+
<div
69+
class="h-full rounded-full bg-[#306998] transition-all duration-500 group-hover:opacity-80 dark:bg-[#ffd43b]"
70+
style={`width: ${Math.max(pct, 4)}%;`}
71+
/>
72+
</div>
73+
<span class="w-8 flex-shrink-0 text-right text-xs tabular-nums font-medium text-zinc-400 dark:text-zinc-500">
74+
{author.count}
75+
</span>
76+
</a>
77+
);
78+
})}
79+
</div>
80+
</div>
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
import { getCollection } from "astro:content";
3+
import { formatDate, postUrl, slugify, withBase } from "../../lib/utils";
4+
5+
const allPosts = await getCollection("posts");
6+
const recentPosts = allPosts
7+
.filter((p) => p.data.published)
8+
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime())
9+
.slice(0, 3);
10+
11+
// Collect years from all posts for the mini sidebar
12+
const yearSet = new Set<number>();
13+
for (const post of allPosts.filter((p) => p.data.published)) {
14+
yearSet.add(post.data.publishDate.getFullYear());
15+
}
16+
const years = [...yearSet].sort((a, b) => b - a).slice(0, 8);
17+
18+
// Top tags for the mini sidebar
19+
const tagCounts = new Map<string, number>();
20+
for (const post of allPosts.filter((p) => p.data.published)) {
21+
for (const tag of post.data.tags) {
22+
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
23+
}
24+
}
25+
const topTags = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
26+
---
27+
28+
<div class="not-prose my-8 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800">
29+
<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">
30+
<div class="flex items-center gap-2">
31+
<svg class="h-4 w-4 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
32+
<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z" stroke-linecap="round" stroke-linejoin="round" />
33+
<path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z" stroke-linecap="round" stroke-linejoin="round" />
34+
</svg>
35+
<span class="text-sm font-semibold text-zinc-700 dark:text-zinc-300" style="font-family: var(--font-display);">Blog</span>
36+
<span class="text-xs text-zinc-400 dark:text-zinc-500">{allPosts.filter((p) => p.data.published).length} posts with filters & pagination</span>
37+
</div>
38+
<a href={withBase("/blog")} class="text-xs font-medium text-[#306998] hover:underline dark:text-[#ffd43b]">
39+
Browse all &rarr;
40+
</a>
41+
</div>
42+
43+
<div class="flex">
44+
<!-- Posts column -->
45+
<div class="min-w-0 flex-1 divide-y divide-zinc-100 dark:divide-zinc-800/60">
46+
{recentPosts.map((post) => (
47+
<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">
48+
<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);">
49+
{post.data.title}
50+
</h4>
51+
<div class="mt-1 flex items-center gap-2 text-xs text-zinc-400 dark:text-zinc-500">
52+
<span class="font-medium text-zinc-500 dark:text-zinc-400">{post.data.author}</span>
53+
<span class="text-zinc-300 dark:text-zinc-700">&middot;</span>
54+
<time datetime={post.data.publishDate.toISOString()}>{formatDate(post.data.publishDate.toISOString())}</time>
55+
</div>
56+
{post.data.tags.length > 0 && (
57+
<div class="mt-2 flex flex-wrap gap-1">
58+
{post.data.tags.slice(0, 3).map((tag) => (
59+
<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">
60+
{tag}
61+
</span>
62+
))}
63+
</div>
64+
)}
65+
</a>
66+
))}
67+
</div>
68+
69+
<!-- Mini sidebar -->
70+
<div class="hidden w-44 flex-shrink-0 border-l border-zinc-100 p-4 dark:border-zinc-800/60 sm:block">
71+
<div class="mb-4">
72+
<h5 class="mb-2 text-[10px] font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-600">Years</h5>
73+
<div class="flex flex-wrap gap-1">
74+
{years.map((y) => (
75+
<a
76+
href={withBase(`/blog/year/${y}`)}
77+
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"
78+
>
79+
{y}
80+
</a>
81+
))}
82+
</div>
83+
</div>
84+
<div>
85+
<h5 class="mb-2 text-[10px] font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-600">Tags</h5>
86+
<div class="flex flex-wrap gap-1">
87+
{topTags.map(([tag, count]) => (
88+
<a
89+
href={withBase(`/tags/${tag}`)}
90+
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"
91+
>
92+
{tag}
93+
<span class="text-[9px] text-zinc-400 dark:text-zinc-600">{count}</span>
94+
</a>
95+
))}
96+
</div>
97+
</div>
98+
</div>
99+
</div>
100+
</div>

0 commit comments

Comments
 (0)