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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ jobs:
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
env:
ASTRO_DB_REMOTE_URL: ${{ secrets.ASTRO_DB_REMOTE_URL }}
ASTRO_DB_APP_TOKEN: ${{ secrets.ASTRO_DB_APP_TOKEN }}
- uses: actions/upload-artifact@v4
if: always()
with:
Expand Down
61 changes: 45 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository.

## What is Starpod?

Starpod is an open-source Astro-based podcast website generator. It creates a full podcast site from an RSS feed and a `starpod.config.ts` configuration file. The reference deployment is [whiskey.fm](https://whiskey.fm) (Whiskey Web and Whatnot podcast).
Starpod is an open-source Astro-based podcast website generator. It creates a
full podcast site from an RSS feed and a `starpod.config.ts` configuration file.
The reference deployment is [whiskey.fm](https://whiskey.fm) (Whiskey Web and
Whatnot podcast).

## Commands

- **Dev server:** `pnpm dev` (runs on localhost:4321)
- **Build:** `pnpm build` (runs `astro check` then `astro build --remote`)
- **Build:** `pnpm build` (runs `astro check` then `astro build`)
- **Lint:** `pnpm lint` (ESLint with caching)
- **Lint fix:** `pnpm lint:fix`
- **All tests:** `pnpm test` (runs unit + e2e concurrently)
- **Unit tests only:** `pnpm test:unit` (Vitest)
- **Single unit test:** `pnpm exec vitest run tests/unit/Player.test.tsx`
- **E2E tests only:** `pnpm test:e2e` (Playwright, auto-starts dev server)
- **Seed remote DB:** `pnpm db:seed`
- **Push schema to DB:** `pnpm db:push`
- **Drizzle Studio:** `pnpm db:studio`

## Architecture

Expand All @@ -25,37 +31,60 @@ Starpod is an open-source Astro-based podcast website generator. It creates a fu
- **Astro 5** with static output, deployed to Vercel
- **Preact** for interactive components (player, search, contact form)
- **Tailwind CSS v4** via Vite plugin
- **Astro DB** (Turso/libSQL) for episode guests and sponsors
- **Drizzle ORM** with Turso/libSQL for episode guests and sponsors
- **Valibot** for config validation

### Key Configuration

- `starpod.config.ts` — podcast metadata (hosts, platforms, RSS feed URL, description). Uses `defineStarpodConfig()` from `src/utils/config.ts` for type safety and validation.
- `astro.config.mjs` — Astro config with Vercel adapter, Preact, sitemap, and DB integrations.
- `starpod.config.ts` — podcast metadata (hosts, platforms, RSS feed URL,
description). Uses `defineStarpodConfig()` from `src/utils/config.ts` for type
safety and validation.
- `astro.config.mjs` — Astro config with Vercel adapter, Preact, and sitemap
integrations.
- `drizzle.config.ts` — Drizzle Kit config for schema push, migrations, and
studio.

### Data Flow

Episodes are fetched from the RSS feed at build time via `src/lib/rss.ts`. Guest/sponsor data lives in `db/data/` as TypeScript files and is seeded to Turso via `db/seed.ts`. The DB schema is in `db/config.ts` with tables: Episode, Person, HostOrGuest, Sponsor, SponsorForEpisode.
Episodes are fetched from the RSS feed at build time via `src/lib/rss.ts`.
Guest/sponsor data lives in `db/data/` as TypeScript files and is seeded to
Turso via `db/seed.ts`. The DB schema is in `db/schema.ts` (Drizzle ORM) with
tables: Episode, Person, HostOrGuest, Sponsor, SponsorForEpisode. The DB
connection is configured in `db/index.ts`.

### Source Structure

- `src/pages/` — Astro pages and API routes. Dynamic episode pages use `[episode].astro`. LLM-friendly `.html.md.ts` endpoints generate markdown versions.
- `src/components/` — Mix of `.astro` (static) and `.tsx` (Preact interactive) components. The audio player (`src/components/player/`) and search dialog are Preact.
- `src/pages/` — Astro pages and API routes. Dynamic episode pages use
`[episode].astro`. LLM-friendly `.html.md.ts` endpoints generate markdown
versions.
- `src/components/` — Mix of `.astro` (static) and `.tsx` (Preact interactive)
components. The audio player (`src/components/player/`) and search dialog are
Preact.
- `src/components/state.ts` — Preact signals for shared player state.
- `src/lib/` — Core utilities: RSS fetching, image optimization, LLM content generation.
- `src/content/transcripts/` — Markdown transcript files named by episode number.
- `src/lib/` — Core utilities: RSS fetching, image optimization, LLM content
generation.
- `src/content/transcripts/` — Markdown transcript files named by episode
number.
- `src/layouts/Layout.astro` — Single shared layout.
- `db/` — Database schema (`schema.ts`), connection (`index.ts`), seed script
(`seed.ts`), and static data files (`data/`).

### Testing

- **Unit tests** (`tests/unit/`): Vitest + jsdom + @testing-library/preact. Setup file at `tests/unit/test-setup.ts`.
- **E2E tests** (`tests/e2e/`): Playwright testing against chromium, firefox, and webkit.
- **Unit tests** (`tests/unit/`): Vitest + jsdom + @testing-library/preact.
Setup file at `tests/unit/test-setup.ts`.
- **E2E tests** (`tests/e2e/`): Playwright testing against chromium, firefox,
and webkit.

### TypeScript

Strict mode with `baseUrl: "."` allowing bare `src/...` imports. JSX is configured for Preact (`jsxImportSource: "preact"`).
Strict mode with `baseUrl: "."` allowing bare `src/...` imports. JSX is
configured for Preact (`jsxImportSource: "preact"`).

## Environment Variables

- `DISCORD_WEBHOOK` — Used by the contact form API route (`src/pages/api/contact.ts`) to post to Discord.
- Astro DB connection requires `ASTRO_STUDIO_APP_TOKEN` for remote operations (build, seed).
- `DISCORD_WEBHOOK` — Used by the contact form API route
(`src/pages/api/contact.ts`) to post to Discord.
- `ASTRO_DB_REMOTE_URL` — Turso/libSQL database URL (e.g.,
`libsql://your-db.turso.io`).
- `ASTRO_DB_APP_TOKEN` — Authentication token for Turso database.
2 changes: 0 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineConfig, fontProviders } from 'astro/config';
import db from '@astrojs/db';
import preact from '@astrojs/preact';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
Expand Down Expand Up @@ -62,7 +61,6 @@ export default defineConfig({
site: 'https://whiskey.fm',
trailingSlash: 'never',
integrations: [
db(),
preact(),
sitemap({
filter: (page) => {
Expand Down
49 changes: 0 additions & 49 deletions db/config.ts

This file was deleted.

19 changes: 19 additions & 0 deletions db/data/people-per-episode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@ import people from './people';
type PersonId = (typeof people)[number]['id'];

export default {
// 237
'the-transactional-trap-how-97-of-developers-are-using-ai-wrong-w-leon-noel-danny-thompson':
[
{ id: 'robbiethewagner' },
{ id: 'argyleink' },
{ id: 'leonnoel' },
{ id: 'dthompsondev' }
],
// 236
'the-manager-has-become-the-managed-presented-by-warp': [
{ id: 'robbiethewagner' },
{ id: 'argyleink' },
{ id: 'wattenberger' }
],
// 235
'hot-pockets-pro-max-presented-by-warp': [
{ id: 'robbiethewagner' },
{ id: 'argyleink' }
],
// 234
'pay-no-attention-to-the-llm-behind-the-terminal-w-zach-lloyd': [
{ id: 'robbiethewagner' },
Expand Down
11 changes: 11 additions & 0 deletions db/data/people.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export const people = [
name: 'Diego Gonzalez',
img: 'diegogonzalez.jpg'
},
{
id: 'dthompsondev',
name: 'Danny Thompson',
img: 'dthompsondev.jpg'
},
{
id: 'engineering_bae',
name: 'Taylor Poindexter',
Expand Down Expand Up @@ -151,6 +156,11 @@ export const people = [
name: 'Kelly Vaughn',
img: 'kvlly.jpg'
},
{
id: 'leonnoel',
name: 'Leon Noel',
img: 'leonnoel.jpg'
},
{
id: 'madisonkanna',
name: 'Madison Kanna',
Expand Down Expand Up @@ -296,6 +306,7 @@ export const people = [
img: 'typecraft_dev.jpg'
},
{ id: 'wagslane', name: 'Lane Wagner', img: 'wagslane.jpg' },
{ id: 'wattenberger', name: 'Amelia Wattenberger', img: 'wattenberger.jpg' },
{ id: 'wesbos', name: 'Wes Bos', img: 'wesbos.jpg' },
{ id: 'willjohnsonio', name: 'Will Johnson', img: 'willjohnsonio.jpg' },
{ id: 'zachlloyd', name: 'Zach Lloyd', img: 'zachlloyd.jpg' },
Expand Down
7 changes: 6 additions & 1 deletion db/data/sponsors-per-episode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export default {
// 237
'the-transactional-trap-how-97-of-developers-are-using-ai-wrong-w-leon-noel-danny-thompson':
[{ id: 'warp' }],
// 236
'the-manager-has-become-the-managed-presented-by-warp': [{ id: 'warp' }],
// 235
// '': [{ id: 'warp' }],
'hot-pockets-pro-max-presented-by-warp': [{ id: 'warp' }],
// 234
'pay-no-attention-to-the-llm-behind-the-terminal-w-zach-lloyd': [
{ id: 'cascadiajs' }
Expand Down
15 changes: 15 additions & 0 deletions db/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { drizzle } from 'drizzle-orm/libsql';

import * as schema from './schema';

// Uses ASTRO_DB_REMOTE_URL and ASTRO_DB_APP_TOKEN from environment.
// In Astro files, these are available via import.meta.env.
// In standalone scripts (seed), they are loaded via process.env.
export function createDb(url: string, authToken: string) {
return drizzle({
connection: { url, authToken },
schema
});
}

export type Database = ReturnType<typeof createDb>;
62 changes: 62 additions & 0 deletions db/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
integer,
sqliteTable,
text,
uniqueIndex
} from 'drizzle-orm/sqlite-core';

export const Episode = sqliteTable('Episode', {
episodeSlug: text().primaryKey()
});

export const Person = sqliteTable('Person', {
id: text().primaryKey(),
img: text(),
name: text().notNull()
});

export const HostOrGuest = sqliteTable(
'HostOrGuest',
{
_id: integer('_id').primaryKey(),
episodeSlug: text()
.notNull()
.references(() => Episode.episodeSlug),
isHost: integer({ mode: 'boolean' }).notNull(),
personId: text()
.notNull()
.references(() => Person.id)
},
(table) => [
uniqueIndex('HostOrGuest_episodeSlug_personId_idx').on(
table.episodeSlug,
table.personId
)
]
);

export const Sponsor = sqliteTable('Sponsor', {
id: text().primaryKey(),
img: text(),
name: text().notNull(),
url: text().notNull()
});

export const SponsorForEpisode = sqliteTable(
'SponsorForEpisode',
{
_id: integer('_id').primaryKey(),
episodeSlug: text()
.notNull()
.references(() => Episode.episodeSlug),
sponsorId: text()
.notNull()
.references(() => Sponsor.id)
},
(table) => [
uniqueIndex('SponsorForEpisode_episodeSlug_sponsorId_idx').on(
table.episodeSlug,
table.sponsorId
)
]
);
26 changes: 20 additions & 6 deletions db/seed.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import 'dotenv/config';

import { sql } from 'drizzle-orm';

import { createDb } from './index';
import {
db,
Episode,
HostOrGuest,
Person,
Sponsor,
SponsorForEpisode,
sql
} from 'astro:db';
SponsorForEpisode
} from './schema';

import { getAllEpisodes } from '../src/lib/rss';
import people from './data/people';
import peoplePerEpisode from './data/people-per-episode';
import sponsors from './data/sponsors';
import sponsorsPerEpisode from './data/sponsors-per-episode';

// https://astro.build/db/seed
export default async function seed() {
const db = createDb(
process.env.ASTRO_DB_REMOTE_URL!,
process.env.ASTRO_DB_APP_TOKEN!
);

async function seed() {
await db
.insert(Person)
.values(people as any)
Expand Down Expand Up @@ -82,4 +89,11 @@ export default async function seed() {
.insert(SponsorForEpisode)
.values(sponsorsForEpisodesToInsert)
.onConflictDoNothing();

console.log('Seed complete!');
}

seed().catch((err) => {
console.error('Seed failed:', err);
process.exit(1);
});
11 changes: 11 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
schema: './db/schema.ts',
out: './drizzle',
dialect: 'turso',
dbCredentials: {
url: process.env.ASTRO_DB_REMOTE_URL!,
authToken: process.env.ASTRO_DB_APP_TOKEN!
}
});
Comment on lines +1 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if drizzle-kit documentation mentions automatic .env loading
# and verify the current setup works

# Check if there's a .env or .env.local file in the repo
fd -t f '\.env' --max-depth 1

# Check if any other config files load dotenv
rg -n "dotenv" --type ts --type js -g '!node_modules'

Repository: shipshapecode/starpod

Length of output: 216


🏁 Script executed:

# Check package.json for drizzle scripts and configuration
cat -n package.json | grep -A 20 '"scripts"'

Repository: shipshapecode/starpod

Length of output: 877


🏁 Script executed:

# Look for any references to how drizzle-kit is invoked
rg -n "drizzle" package.json -A 2 -B 2

Repository: shipshapecode/starpod

Length of output: 641


🏁 Script executed:

# Check if there's a .env file referenced elsewhere or if env vars are set differently
find . -name ".env*" -o -name "*.env*" 2>/dev/null | head -20

Repository: shipshapecode/starpod

Length of output: 47


🏁 Script executed:

# Check the actual drizzle.config.ts more carefully for any dotenv loading
cat -n drizzle.config.ts

Repository: shipshapecode/starpod

Length of output: 401


🏁 Script executed:

# Search for CI/CD configurations that might set environment variables
fd -t f "\.github|\.gitlab|\.circleci|github|gitlab|circleci" --max-depth 2 2>/dev/null | head -20

Repository: shipshapecode/starpod

Length of output: 47


🏁 Script executed:

# Check .gitignore to see if .env files are ignored
cat -n .gitignore 2>/dev/null | grep -i "env"

Repository: shipshapecode/starpod

Length of output: 130


🏁 Script executed:

# Look for any Vercel or deployment configuration
fd -t f "vercel.json|netlify.toml|astro.config|environment" --max-depth 2

Repository: shipshapecode/starpod

Length of output: 81


🌐 Web query:

drizzle-kit .env automatic loading environment variables

💡 Result:

Drizzle-kit does not automatically load environment variables from .env files by default. Users must manually load them in the drizzle.config.ts (or .js) file using a library like dotenv. To enable .env loading: 1. Install dotenv: npm i -D dotenv 2. In drizzle.config.ts: import * as dotenv from "dotenv"; dotenv.config({ path: ".env.local" }); // or appropriate .env file import type { Config } from "drizzle-kit"; export default { schema: "./src/db/schema.ts", out: "./drizzle", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL!, }, } satisfies Config; For Next.js projects, use @next/env instead: import { loadEnvConfig } from "@next/env"; loadEnvConfig("./"); Alternative approaches: - Use Node.js --env-file flag (Node v20.6+): node --env-file=.env.local ./node_modules/drizzle-kit/bin.cjs generate - Prefix commands: dotenv -e .env.local -- drizzle-kit generate A GitHub discussion (#3405) from Nov 2024 announced planned built-in support for automatic .env loading (including .env*, .env.local based on NODE_ENV, --env-file, --no-env flags), using dotenvx. Some users report it "working out of the box" in recent versions (e.g., 0.30.5+), and logs show "Env file: .env.development.local", but official docs do not confirm full implementation as of 2026-03-27. Check latest changelog or test npx drizzle-kit push to verify if automatic loading is active in your version. Official docs recommend manual dotenv import for dbCredentials.

Citations:


Add dotenv loading to drizzle.config.ts to ensure environment variables are available when running drizzle-kit commands.

Drizzle Kit doesn't automatically load .env files. The configuration references process.env.ASTRO_DB_REMOTE_URL and process.env.ASTRO_DB_APP_TOKEN without loading them, which means these values will be undefined unless set externally (e.g., in CI/CD or shell environment). This is inconsistent with db/seed.ts, which already imports dotenv/config.

Solution: Add dotenv import
+import 'dotenv/config';
 import { defineConfig } from 'drizzle-kit';
 
 export default defineConfig({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle.config.ts` around lines 1 - 11, The config file exports defineConfig
and reads process.env.ASTRO_DB_REMOTE_URL and process.env.ASTRO_DB_APP_TOKEN but
never loads .env, so drizzle-kit runs may see undefined values; fix by importing
dotenv (e.g., require('dotenv/config') or import 'dotenv/config') at the top of
drizzle.config.ts before the default export so dbCredentials (the
ASTRO_DB_REMOTE_URL and ASTRO_DB_APP_TOKEN references) are populated when
defineConfig executes.

Loading
Loading