Skip to content
Open
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
Binary file added .github/pull-request-assets/podcast-player.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions .github/workflows/generate-podcast.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Generate Blog Podcasts

on:
push:
branches:
- main
paths:
- 'apps/web/src/content/blog/en/**/*.md'
schedule:
- cron: '17 3 * * *'
workflow_dispatch:
inputs:
slug:
description: Generate one eligible English blog post by slug.
required: false
type: string
limit:
default: '2'
description: Maximum number of episodes to create in this run.
required: false
type: string

permissions:
contents: write

concurrency:
group: blog-podcast-generation
cancel-in-progress: false

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
generate:
if: vars.PODCAST_AUTOMATION_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
AUTOCONTENT_API_KEY: ${{ secrets.AUTOCONTENT_API_KEY }}
AUTOCONTENT_PODCAST_SHOW_ID: ${{ secrets.AUTOCONTENT_PODCAST_SHOW_ID }}
AUTOCONTENT_PODCAST_VOICE_1: ${{ secrets.AUTOCONTENT_PODCAST_VOICE_1 }}
AUTOCONTENT_PODCAST_VOICE_2: ${{ secrets.AUTOCONTENT_PODCAST_VOICE_2 }}
NODE_OPTIONS: '--max-old-space-size=16384'
PODCAST_SITE_URL: ${{ vars.PODCAST_SITE_URL || 'https://capgo.app' }}
UV_THREADPOOL_SIZE: '32'
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: 24
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.14
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test podcast generator
run: bun test scripts/blogs/generate-podcast.test.ts
- name: Select and generate episodes
id: generate
shell: bash
run: |
limit="${{ inputs.limit }}"
if [[ -z "$limit" ]]; then
if [[ "${{ github.event_name }}" == "schedule" ]]; then
limit=2
else
limit=1
fi
fi
args=(--limit "$limit")

if [[ -n "${{ inputs.slug }}" ]]; then
args+=(--slug "${{ inputs.slug }}")
elif [[ "${{ github.event_name }}" == "push" ]]; then
base="${{ github.event.before }}"
head="${{ github.sha }}"
if [[ -z "$base" || "$base" == "0000000000000000000000000000000000000000" ]]; then
base="$(git rev-list --max-parents=0 "$head")"
fi
git diff --name-only --diff-filter=AM "$base" "$head" -- apps/web/src/content/blog/en > "$RUNNER_TEMP/changed-blog-files.txt"
args+=(--changed-files "$RUNNER_TEMP/changed-blog-files.txt")
else
args+=(--backfill)
fi

bun run blogs:generate_podcast -- "${args[@]}"
- name: Verify website
if: steps.generate.outputs.manifest_changed == 'true'
run: bun run ci:verify:web
- name: Commit generated episode records
if: steps.generate.outputs.manifest_changed == 'true'
shell: bash
run: |
set -euo pipefail
if git diff --quiet -- apps/web/src/data/podcastEpisodes.json; then
echo 'No podcast manifest changes to commit.'
exit 0
fi

git config user.email 'action@github.com'
git config user.name 'GitHub Action'
git add apps/web/src/data/podcastEpisodes.json
git commit -m 'chore: record generated podcast episodes'
git pull --rebase origin "${{ github.event.repository.default_branch }}"
git push origin "HEAD:${{ github.event.repository.default_branch }}"
15 changes: 13 additions & 2 deletions .github/workflows/import-outrank.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ jobs:
if: steps.import.outputs.files != ''
run: xargs bunx prettier --write < .outrank-imported-files

- name: Generate podcasts for imported articles
if: steps.import.outputs.files != '' && vars.PODCAST_AUTOMATION_ENABLED == 'true'
continue-on-error: true
env:
AUTOCONTENT_API_KEY: ${{ secrets.AUTOCONTENT_API_KEY }}
AUTOCONTENT_PODCAST_SHOW_ID: ${{ secrets.AUTOCONTENT_PODCAST_SHOW_ID }}
AUTOCONTENT_PODCAST_VOICE_1: ${{ secrets.AUTOCONTENT_PODCAST_VOICE_1 }}
AUTOCONTENT_PODCAST_VOICE_2: ${{ secrets.AUTOCONTENT_PODCAST_VOICE_2 }}
PODCAST_SITE_URL: ${{ vars.PODCAST_SITE_URL || 'https://capgo.app' }}
run: bun run blogs:generate_podcast -- --changed-files .outrank-imported-files --limit 1

- name: Verify website
if: steps.import.outputs.files != ''
run: bun run ci:verify:web
Expand All @@ -89,15 +100,15 @@ jobs:
run: |
rm -f .outrank-imported-files

if [[ -z "$(git status --porcelain apps/web/src/content/blog/en)" ]]; then
if [[ -z "$(git status --porcelain -- apps/web/src/content/blog/en apps/web/src/data/podcastEpisodes.json)" ]]; then
echo "No blog changes to publish."
exit 0
fi

default_branch="${{ github.event.repository.default_branch }}"
git config user.email "action@github.com"
git config user.name "GitHub Action"
git add apps/web/src/content/blog/en
git add apps/web/src/content/blog/en apps/web/src/data/podcastEpisodes.json
git commit -m "chore: import Outrank articles"
git pull --rebase origin "$default_branch"
git push origin "HEAD:$default_branch"
7 changes: 7 additions & 0 deletions apps/web/public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
/sitemap.xml
Content-Type: text/xml


/podcast.xml
Content-Type: application/rss+xml; charset=utf-8
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD
Cache-Control: public, max-age=3600
Link: </podcast.xml>; rel="self"; type="application/rss+xml"
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Expand Down
Binary file added apps/web/public/capgo-podcast.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

Sitemap: https://capgo.app/sitemap-index.xml
Sitemap: https://capgo.app/podcast.xml
33 changes: 33 additions & 0 deletions apps/web/src/components/PodcastEpisode.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import { formatPodcastDurationLabel, getPodcastAudioMimeType, getPodcastEpisodeSummary, type PlayablePodcastEpisode } from '@/lib/podcast'

interface Props {
episode: PlayablePodcastEpisode
feedUrl: string
}

const { episode, feedUrl } = Astro.props
const duration = formatPodcastDurationLabel(episode.durationSeconds)
const summary = getPodcastEpisodeSummary(episode)
const publishedAt = new Date(episode.generatedAt)
const publishedDate = Number.isNaN(publishedAt.getTime()) ? undefined : new Intl.DateTimeFormat('en', { dateStyle: 'medium' }).format(publishedAt)
---

<section id="podcast" aria-labelledby="podcast-episode-title" class="my-8 rounded-xl border border-white/10 bg-white/[0.04] p-5 sm:p-6">
<p class="text-xs font-semibold tracking-[0.18em] text-blue-300 uppercase">Capgo podcast, English episode</p>
<h2 id="podcast-episode-title" class="mt-2 text-xl font-semibold text-white sm:text-2xl">Listen: {episode.title}</h2>
<p id="podcast-episode-description" class="mt-3 text-sm leading-6 text-gray-300">{summary}</p>

<audio class="mt-5 w-full" controls preload="metadata" aria-describedby="podcast-episode-description">
<source src={episode.providerAudioUrl} type={getPodcastAudioMimeType(episode)} />
Your browser does not support audio playback. <a href={episode.providerAudioUrl}>Open the MP3</a>.
</audio>

<div class="mt-4 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
{(publishedDate || duration) && <span class="text-gray-400">{[publishedDate, duration].filter(Boolean).join(' · ')}</span>}
<a class="font-medium text-white underline decoration-white/40 underline-offset-4 hover:decoration-white" href={feedUrl}>RSS feed</a>
<a class="font-medium text-white underline decoration-white/40 underline-offset-4 hover:decoration-white" href={episode.providerAudioUrl} target="_blank" rel="noreferrer"
>Open MP3</a
>
</div>
</section>
1 change: 1 addition & 0 deletions apps/web/src/components/SEO.astro
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const titleFix = titleString || (imageUrl.split('/').pop() || '.').split('.')[0]
<meta name="keywords" content={keywords} />

<link rel="canonical" href={canonicalString} />
<link rel="alternate" type="application/rss+xml" title="Capgo Podcast" href={new URL('/podcast.xml', baseUrl).toString()} />
<link rel="apple-touch-icon" href="/capgo.webp?v=2" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/data/podcastEpisodes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 1,
"episodes": []
}
Loading
Loading