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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,31 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 30

# Builds a freshly scaffolded consumer project against the local package.
# This exercises the installed-from-npm code path (JSX transform for
# node_modules sources, CJS interop, bundled assets, dependency pins) that
# the workspace build cannot see.
scaffold:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: pnpm/action-setup@v4
- name: Scaffold a project against the local package
run: |
node packages/starpod/cli/index.mjs new /tmp/scaffold-smoke --yes --name "Scaffold Smoke" --rss "https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml"
node -e "const fs=require('fs');const f='/tmp/scaffold-smoke/package.json';const p=JSON.parse(fs.readFileSync(f));p.dependencies.starpod='file:'+process.env.GITHUB_WORKSPACE+'/packages/starpod';fs.writeFileSync(f,JSON.stringify(p,null,2))"
- name: Build the scaffolded project
working-directory: /tmp/scaffold-smoke
run: |
pnpm install
pnpm exec astro build
- name: Verify agent endpoints in the output
run: |
test -f /tmp/scaffold-smoke/.vercel/output/static/llms.txt
test -f /tmp/scaffold-smoke/.vercel/output/static/openapi.json
node -e "const c=require('/tmp/scaffold-smoke/.vercel/output/config.json');const n=c.routes.filter(r=>JSON.stringify(r).includes('text/markdown')).length;if(!n)throw new Error('no negotiation routes in config.json');console.log('negotiation routes:',n)"
61 changes: 61 additions & 0 deletions .github/workflows/plan-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Plan Release
on:
workflow_dispatch:
push:
branches:
- main
- master
pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
types:
- labeled
- unlabeled

concurrency:
group: plan-release # only the latest one of these should ever be running
cancel-in-progress: true

jobs:
should-run-release-plan-prepare:
name: Should we run release-plan prepare?
runs-on: ubuntu-latest
outputs:
should-prepare: ${{ steps.should-prepare.outputs.should-prepare }}
steps:
- uses: release-plan/actions/should-prepare-release@v1
with:
ref: 'main'
id: should-prepare

create-prepare-release-pr:
name: Create Prepare Release PR
runs-on: ubuntu-latest
timeout-minutes: 5
needs: should-run-release-plan-prepare
permissions:
contents: write
issues: read
pull-requests: write
if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true'
steps:
- uses: release-plan/actions/prepare@v1
name: Run release-plan prepare
with:
ref: 'main'
env:
GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
id: explanation

- uses: peter-evans/create-pull-request@v8
name: Create Prepare Release PR
with:
commit-message: "Prepare Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'"
labels: "internal"
sign-commits: true
branch: release-preview
title: Prepare Release ${{ steps.explanation.outputs.new-version }}
body: |
This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍

-----------------------------------------

${{ steps.explanation.outputs.text }}
40 changes: 40 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# For every push to the primary branch with .release-plan.json modified,
# runs release-plan.

name: Publish Stable

on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- '.release-plan.json'

concurrency:
group: publish-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
publish:
name: "NPM Publish"
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write

steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Publish to NPM
run: NPM_CONFIG_PROVENANCE=true pnpm release-plan publish
env:
GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
92 changes: 69 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,48 @@
Starpod is the easiest way to create a podcast website in 5 minutes or less and
it is 100% free and open source.

It ships as an [Astro integration](./packages/starpod): install the `starpod`
package, point it at your RSS feed, and it generates your whole site — episode
pages, a persistent audio player, search, transcripts with clickable
timestamps, and a suite of agent-friendly endpoints. Updating your site is
`pnpm update`, like any other dependency.

## Getting started

Scaffold a new site:

```bash
npx starpod new my-podcast
```

Or add Starpod to an existing Astro project — see the
[package README](./packages/starpod/README.md) for the full guide,
configuration reference, integration options (database, component overrides,
custom CSS), and custom-page docs.

## This repository

This is a pnpm workspace:

- [`packages/starpod`](./packages/starpod) — the `starpod` package: the Astro
integration, the site engine (components, pages, player, search, LLM
endpoints), and the `npx starpod new` scaffolder
- The repository root is [whiskey.fm](https://whiskey.fm) (Whiskey Web and
Whatnot), the reference site that consumes the package via `workspace:*`

```bash
pnpm install
pnpm dev # run whiskey.fm locally on :4321
pnpm test # unit (Vitest) + e2e (Playwright)
pnpm build # astro check + production build
```

### Configuration

You will need to configure your RSS feed and a few other pieces of info for your
podcast in starpod.config.mjs. We provide a util function `defineStarpodConfig`
that provides TypeScript types and enforces the correct formats for config
values.
You will need to configure your RSS feed and a few other pieces of info for
your podcast in `starpod.config.ts`. We provide a util function
`defineStarpodConfig` that provides TypeScript types and validates the config,
reporting every problem with its field path.

An example config can be found [here](./starpod.config.ts).

Expand All @@ -34,12 +70,14 @@ ideally be fairly short, and should usually be 2-4 sentences.

```ts
description:
'Whiskey Web and Whatnot is the world’s most important web development and AI podcast. Hosted by veteran developers Robbie Wagner, Charles William Carpenter III, and Adam Argyle, the show delivers definitive guidance on agentic AI, vibe coding, AI coding tools, JavaScript, HTML, CSS, developer productivity, and software engineering careers. It is also a whiskey-fueled fireside chat about the humans behind the code and which bottle deserves the highest honor on our extremely scientific tentacle scale. Many people are saying it’s the most accurate podcast ever made.',
'Whiskey Web and Whatnot is the world’s most important web development and AI podcast. Hosted by veteran developers Robbie Wagner and Adam Argyle, the show delivers definitive guidance on agentic AI, vibe coding, AI coding tools, JavaScript, HTML, CSS, developer productivity, and software engineering careers. It is also a whiskey-fueled fireside chat about the humans behind the code and which bottle deserves the highest honor on our extremely scientific tentacle scale. Many people are saying it’s the most accurate podcast ever made.',
```

##### hosts

A list of your show's hosts and their info.
A list of your show's hosts and their info. `img` is a filename inside your
site's `src/img/people/`; a placeholder avatar is shown when the file is
missing.

**Example:**

Expand All @@ -48,18 +86,11 @@ hosts: [
{
name: 'RobbieTheWagner',
bio: 'Huge Ember and Tailwind fanboy. I used to work at Netflix btw.',
img: '/src/img/people/robbiethewagner.jpg',
img: 'robbiethewagner.jpg',
github: 'https://github.com/RobbieTheWagner',
twitter: 'https://twitter.com/RobbieTheWagner',
twitter: 'https://x.com/RobbieWagner',
website: 'https://robbiethewagner.dev'
},
{
name: 'Charles William Carpenter III',
bio: 'Third of his name, user of gifs, hater of ESM.',
img: '/src/img/people/chuckcarpenter.jpg',
github: 'https://github.com/chuckcarpenter',
twitter: 'https://twitter.com/CharlesWthe3rd'
},
{
name: 'Adam Argyle',
bio: 'Devigner unicorn, CSS dork, punky but nice.',
Expand Down Expand Up @@ -97,14 +128,28 @@ The url to the RSS feed where your podcast is hosted.
rssFeed: 'https://rss.flightcast.com/w7bqgc792i30fd43a32uawx0.xml';
```

##### links

Extra navigation links shown after About and Contact — a merch store, or
custom pages your site adds (like a [sponsor page](./src/pages/sponsor.astro)).

**Example:**

```ts
links: [
{ label: 'Store', href: 'https://whiskey.fund/' },
{ label: 'Become a sponsor', href: '/sponsor' }
],
```

#### Setting up the contact form

The contact form hits an APIRoute at `/api/contact`. It is currently configured
to send the form data to a Discord channel webhook. It reads the url from
`import.meta.env.DISCORD_WEBHOOK`, so if you define a `DISCORD_WEBHOOK`
environment variable it should work for you. Of course, feel free to customize
the code [here](./src/pages/api/contact.ts) to send the data elsewhere as you
see fit.
the code [here](./packages/starpod/src/pages/api/contact.ts) to send the data
elsewhere as you see fit.

#### standard.site (ATProto Federation)

Expand Down Expand Up @@ -198,8 +243,11 @@ the [`@bryanguffey/astro-standard-site` README](https://github.com/musicjunkieg/

#### Configuring guests

We use Turso and Astro DB to setup guests per episode. If you would also like to
do this, you will need a Turso account.
Per-episode guests and sponsors are optional, powered by Turso and Drizzle.
Enable with `starpod(starpodConfig, { database: true })` in
`astro.config.mjs` — see the
[package README](./packages/starpod/README.md#database-guests--sponsors) for
setup. When disabled, episode pages simply render without those sections.

### LLM Discovery Features

Expand Down Expand Up @@ -272,10 +320,8 @@ Agents can also request any page that has a markdown twin with an
URL, per [acceptmarkdown.com](https://acceptmarkdown.com). Both variants are
served with `Vary: Accept` so CDNs cache them separately.

This is implemented by `scripts/vercel-md-negotiation.mjs`, which runs as part
of `pnpm build` and injects Accept-based rewrite routes into the Vercel build
output. If you customize the `build` script in `package.json`, keep the
`node scripts/vercel-md-negotiation.mjs` step after `astro build`. (Deploying
The integration injects the Accept-based rewrite routes into the Vercel build
output automatically during `astro build` — no extra build step. (Deploying
somewhere other than Vercel? The `.html.md` URLs still work everywhere; only
the Accept-header negotiation is Vercel-specific.)

Expand Down
27 changes: 27 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Release Process

Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged.

## Preparation

Since the majority of the actual release process is automated, the remaining tasks before releasing are:

- correctly labeling **all** pull requests that have been merged since the last release
- updating pull request titles so they make sense to our users

Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall
guiding principle here is that changelogs are for humans, not machines.

When reviewing merged PR's the labels to be used are:

- breaking - Used when the PR is considered a breaking change.
- enhancement - Used when the PR adds a new feature or enhancement.
- bug - Used when the PR fixes a bug included in a previous release.
- documentation - Used when the PR adds or updates documentation.
- internal - Internal changes or things that don't fit in any other category.

**Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal`

## Release

Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/shipshapecode/starpod/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR
65 changes: 4 additions & 61 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { defineConfig, fontProviders } from 'astro/config';
import preact from '@astrojs/preact';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
import vercel from '@astrojs/vercel';
import { defineConfig } from 'astro/config';
import starpod from 'starpod';

import rehypeTranscriptTimestamps from './src/lib/rehype-transcript-timestamps.mjs';
import starpodConfig from './starpod.config';

// https://astro.build/config
export default defineConfig({
Expand All @@ -28,60 +26,8 @@ export default defineConfig({
enabled: true
}
}),
build: {
inlineStylesheets: 'always'
},
markdown: {
// Makes bracketed timestamps in markdown transcripts clickable for seeking.
rehypePlugins: [rehypeTranscriptTimestamps]
},
experimental: {
clientPrerender: true
},
fonts: [
{
provider: fontProviders.google(),
name: 'Inter',
cssVariable: '--astro-font-inter',
formats: ['woff2'],
styles: ['normal'],
subsets: ['latin'],
weights: ['300 900'],
options: {
experimental: {
variableAxis: {
opsz: ['14..32']
}
}
}
}
],
image: {
remotePatterns: [
{
protocol: 'https'
},
{
protocol: 'http'
}
]
},
prefetch: {
prefetchAll: true,
defaultStrategy: 'viewport'
},
site: 'https://whiskey.fm',
trailingSlash: 'never',
integrations: [
preact(),
sitemap({
filter: (page) => {
const pathname = new URL(page).pathname;
// Exclude episode number pages and only include slug pages.
return !/^\/\d+\/?$/.test(pathname);
}
})
],
integrations: [starpod(starpodConfig, { database: true })],
// These were specific redirects we needed for our podcast, if you do not have any routes to redirect, you can safely remove this.
redirects: {
'/hot-takes-tan-stack-and-open-source-with-tanner-linsley':
Expand All @@ -90,8 +36,5 @@ export default defineConfig({
'creating-codepen-tackling-tailwind-and-keeping-it-simple-with-chris-coyier',
'/coding-languages-ai-and-the-evolution-of-game-development-with-phillip-winston':
'/coding-languages-ai-and-the-evolution-of-game-development-with-philip-winston'
},
vite: {
plugins: [tailwindcss()]
}
});
Loading
Loading