Skip to content

Commit 04e1e83

Browse files
BekahHWdanieltott
andauthored
feat: migrate podcast data from Craft CMS to vc-data (#1510)
* feat: migrate podcast data from Craft CMS to vc-data Refactor podcast data handling to use vc-data as the source of truth. Update queries and types to accommodate new data structure. * Prettified Code! * fix: use GitHub Contents API with token for private vc-data repo raw.githubusercontent.com returns 404 for private repos without auth. Switch to the GitHub Contents API with Accept: application/vnd.github.v3.raw and Authorization: token GITHUB_TOKEN (already available in Netlify env vars). This unblocks the Netlify deploy-preview build. * Prettified Code! * feat: bundle episodes.json locally for build-time access * Prettified Code! * fix: use static import for episodes.json, no network fetch needed * Prettified Code! * fix: cast rawEpisodes through unknown to satisfy TypeScript * Reverse episode order --------- Co-authored-by: BekahHW <BekahHW@users.noreply.github.com> Co-authored-by: Dan Ott <dan@dtott.com>
1 parent 4be1587 commit 04e1e83

2 files changed

Lines changed: 2038 additions & 170 deletions

File tree

src/data/podcast.ts

Lines changed: 116 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,27 @@
1-
import { GraphQLClient, gql } from 'graphql-request';
21
import { unstable_cache } from 'next/cache';
2+
import rawEpisodes from './podcast/episodes.json';
33

44
export const buzzsproutPodcastId = '1558601' as const;
55

6-
const episodeQuery = gql`
7-
query getEpisode($slug: String!) {
8-
entry(section: "podcast", slug: [$slug]) {
9-
title
10-
... on podcast_default_Entry {
11-
id
12-
episodeSponsors {
13-
title
14-
... on podcastSponsors_default_Entry {
15-
sponsorUrl: urlValue
16-
sponsorImage: podcastEpisodeCard {
17-
path
18-
width
19-
height
20-
}
21-
sponsorDescription: podcastShowNotes {
22-
renderHtml
23-
}
24-
}
25-
}
26-
metaDescription
27-
podcastEpisode
28-
podcastBuzzsproutId
29-
podcastPublishDate
30-
podcastSeason
31-
podcastShortDescription {
32-
renderHtml
33-
}
34-
podcastShowNotes {
35-
renderHtml
36-
}
37-
podcastGuests {
38-
... on podcastGuests_guest_BlockType {
39-
id
40-
guestName
41-
guestBio {
42-
renderHtml
43-
}
44-
headshot {
45-
path
46-
}
47-
}
48-
}
49-
podcastEpisodeCard {
50-
path
51-
}
52-
}
53-
}
54-
}
55-
`;
56-
57-
const episodesQuery = gql`
58-
query getEpisodes($limit: Int!) {
59-
entries(section: "podcast", limit: $limit) {
60-
title
61-
slug
62-
... on podcast_default_Entry {
63-
id
64-
metaDescription
65-
podcastEpisode
66-
podcastSeason
67-
podcastPublishDate
68-
podcastBuzzsproutId
69-
episodeSponsors {
70-
title
71-
... on podcastSponsors_default_Entry {
72-
sponsorUrl: urlValue
73-
sponsorImage: podcastEpisodeCard {
74-
path
75-
width
76-
height
77-
}
78-
sponsorDescription: podcastShowNotes {
79-
renderHtml
80-
}
81-
}
82-
}
83-
}
84-
}
85-
}
86-
`;
87-
88-
export async function getEpisodeQueryParams(request: Request) {
89-
const url = new URL(request.url);
90-
91-
const sp = new URLSearchParams();
92-
const craftPreview = url.searchParams.get('x-craft-preview');
93-
if (craftPreview) {
94-
sp.set('x-craft-preview', craftPreview);
95-
}
6+
// episodes.json is sourced from vc-data and bundled here at build time.
7+
// To update: copy the latest episodes.json from Virtual-Coffee/vc-data into
8+
// src/data/podcast/episodes.json and open a PR.
9+
// https://github.com/Virtual-Coffee/vc-data/blob/main/podcast/episodes.json
9610

97-
const token = url.searchParams.get('token');
98-
if (token) {
99-
sp.set('token', token);
100-
}
11+
const IMGIX_PREFIX = 'https://virtualcoffeeio-cms.imgix.net/podcast/';
10112

102-
return sp.toString();
13+
/** Strip the full imgix URL down to just the filename, which is what createCmsImage expects. */
14+
function extractPath(fullUrl: string | null | undefined): string {
15+
if (!fullUrl) return '';
16+
return fullUrl.startsWith(IMGIX_PREFIX)
17+
? fullUrl.slice(IMGIX_PREFIX.length)
18+
: fullUrl;
10319
}
10420

21+
// ---------------------------------------------------------------------------
22+
// Types that match the shape the rest of the app expects (unchanged from before)
23+
// ---------------------------------------------------------------------------
24+
10525
export interface PodcastEpisode {
10626
title: string;
10727
slug: string;
@@ -133,7 +53,6 @@ export interface PodcastEpisode {
13353
}>;
13454
}
13555

136-
// type PodcastEpisodes = Partial<PodcastEpisode>[];
13756
type PodcastEpisodes = Pick<
13857
PodcastEpisode,
13958
| 'title'
@@ -147,97 +66,115 @@ type PodcastEpisodes = Pick<
14766
| 'url'
14867
| 'episodeSponsors'
14968
>[];
150-
type PodcastEpisodeResponse = {
151-
entries: PodcastEpisode[];
152-
};
15369

154-
export const getEpisodes = unstable_cache(
155-
async ({ limit = 5 }: { limit?: number } = {}): Promise<PodcastEpisodes> => {
156-
if (!(process.env.CMS_URL && process.env.CMS_TOKEN)) {
157-
const fakeData = await import('./mocks/podcast.server');
158-
return fakeData.getEpisodes({ limit }).map((entry) => ({
159-
...entry,
160-
url: `/podcast/${entry.slug}`,
161-
}));
162-
}
70+
// ---------------------------------------------------------------------------
71+
// Shape of data in episodes.json (sourced from vc-data)
72+
// ---------------------------------------------------------------------------
16373

164-
const graphQLClient = new GraphQLClient(`${process.env.CMS_URL}/api`, {
165-
headers: {
166-
Authorization: `bearer ${process.env.CMS_TOKEN}`,
167-
},
168-
});
74+
interface VcDataEpisode {
75+
title: string;
76+
slug: string;
77+
id: string;
78+
metaDescription: string;
79+
season: number;
80+
episode: number;
81+
buzzsproutId: string;
82+
publishDate: string;
83+
shortDescription: string;
84+
showNotes: string;
85+
episodeCard: string;
86+
guests: Array<{
87+
guestName: string;
88+
guestBio: string;
89+
headshot: string | null;
90+
}>;
91+
sponsors: Array<{
92+
title: string;
93+
url: string;
94+
logo: string | null;
95+
logoWidth: number;
96+
logoHeight: number;
97+
description: string;
98+
}>;
99+
}
169100

170-
try {
171-
const episodesResponse =
172-
await graphQLClient.request<PodcastEpisodeResponse>(episodesQuery, {
173-
limit,
174-
});
175-
return episodesResponse.entries.map((entry) => ({
176-
...entry,
177-
url: `/podcast/${entry.slug}`,
178-
}));
179-
} catch (e) {
180-
console.error(e);
181-
return [];
182-
}
101+
// ---------------------------------------------------------------------------
102+
// Mapping: vc-data shape → legacy Craft shape the app expects
103+
// ---------------------------------------------------------------------------
104+
105+
function mapEpisode(e: VcDataEpisode): PodcastEpisode {
106+
return {
107+
title: e.title,
108+
slug: e.slug,
109+
id: e.id,
110+
metaDescription: e.metaDescription,
111+
podcastEpisode: e.episode,
112+
podcastSeason: e.season,
113+
podcastPublishDate: e.publishDate,
114+
podcastBuzzsproutId: e.buzzsproutId,
115+
podcastShortDescription: { renderHtml: e.shortDescription },
116+
podcastShowNotes: { renderHtml: e.showNotes },
117+
podcastEpisodeCard: e.episodeCard
118+
? [{ path: extractPath(e.episodeCard) }]
119+
: [],
120+
podcastGuests: (e.guests || []).map((g, i) => ({
121+
id: i,
122+
guestName: g.guestName,
123+
guestBio: { renderHtml: g.guestBio },
124+
headshot: g.headshot ? [{ path: extractPath(g.headshot) }] : [],
125+
})),
126+
episodeSponsors: (e.sponsors || []).map((s) => ({
127+
title: s.title,
128+
sponsorUrl: s.url,
129+
sponsorImage: s.logo
130+
? [
131+
{
132+
path: extractPath(s.logo),
133+
width: s.logoWidth,
134+
height: s.logoHeight,
135+
},
136+
]
137+
: [],
138+
sponsorDescription: { renderHtml: s.description },
139+
})),
140+
url: `/podcast/${e.slug}`,
141+
};
142+
}
143+
144+
// Map all episodes once at module load (bundled JSON, no async needed)
145+
const allMappedEpisodes: PodcastEpisode[] = (
146+
rawEpisodes as unknown as VcDataEpisode[]
147+
).map(mapEpisode);
148+
149+
// ---------------------------------------------------------------------------
150+
// Public API (same signatures as before)
151+
// ---------------------------------------------------------------------------
152+
153+
export const getEpisodes = unstable_cache(
154+
async ({ limit = 5 }: { limit?: number } = {}): Promise<PodcastEpisodes> => {
155+
return allMappedEpisodes.slice(0, limit);
183156
},
184157
[],
185-
{ revalidate: 86400, tags: ['podcast'] },
158+
{ revalidate: false, tags: ['podcast'] },
186159
);
187160

188161
export const getEpisode = unstable_cache(
189162
async ({
190163
slug,
191-
queryParams = '',
192164
}: {
193165
slug: PodcastEpisode['slug'];
194166
queryParams?: string;
195167
}): Promise<PodcastEpisode | null> => {
196-
if (!(process.env.CMS_URL && process.env.CMS_TOKEN)) {
197-
const fakeData = await import('./mocks/podcast.server');
198-
const episode = fakeData.getEpisode({ slug });
199-
return {
200-
...episode,
201-
url: `/podcast/${episode.slug}`,
202-
};
203-
}
204-
205-
const graphQLClient = new GraphQLClient(
206-
`${process.env.CMS_URL}/api?${queryParams || ''}`,
207-
{
208-
headers: {
209-
Authorization: `bearer ${process.env.CMS_TOKEN}`,
210-
},
211-
},
212-
);
213-
214-
try {
215-
console.log('requesting');
216-
const episodesResponse = await graphQLClient.request<{
217-
entry: PodcastEpisode;
218-
}>(episodeQuery, {
219-
slug,
220-
});
221-
console.log('finished:');
222-
223-
// return response.slice(0, 10);
224-
if (!episodesResponse.entry) {
225-
throw new Error('No episode found');
226-
}
227-
228-
return {
229-
...episodesResponse.entry,
230-
url: `/podcast/${episodesResponse.entry.slug}`,
231-
};
232-
} catch (e) {
233-
console.error(e);
234-
return null;
235-
}
168+
return allMappedEpisodes.find((e) => e.slug === slug) ?? null;
236169
},
237170
[],
238-
{ revalidate: 86400, tags: ['podcast'] },
171+
{ revalidate: false, tags: ['podcast'] },
239172
);
240173

174+
// ---------------------------------------------------------------------------
175+
// Transcript — unchanged, reads from feeds.virtualcoffee.io
176+
// ---------------------------------------------------------------------------
177+
241178
type TranscriptSegment = {
242179
speaker: string;
243180
startTime: number;
@@ -300,3 +237,12 @@ export const getTranscript = unstable_cache(
300237
[],
301238
{ revalidate: 86400, tags: ['podcast'] },
302239
);
240+
241+
// ---------------------------------------------------------------------------
242+
// Kept for backwards compatibility — no longer needed with vc-data
243+
// but removing it would be a breaking change if anything imports it.
244+
// ---------------------------------------------------------------------------
245+
246+
export async function getEpisodeQueryParams(_request: Request) {
247+
return '';
248+
}

0 commit comments

Comments
 (0)