Skip to content

Commit 47ed2f3

Browse files
authored
feat: Batch 2 page migration — author, guest, sponsor pages (#672)
Migrates 6 people/sponsor pages from Next.js to Astro 6:\n\n- /authors + /author/[slug] — author listing + detail with related content\n- /guests + /guest/[slug] — guest listing + detail with related content\n- /sponsors + /sponsor/[slug] — sponsor listing + detail with related content\n\nShared PersonDetail.astro component: cover image, social links, websites (safe URL parsing), Portable Text, related podcasts/posts grids.\n\n9 new GROQ queries. Same patterns as Batch 1: page validation, $offset...$end pagination, description meta, 404 redirects. Added Authors link to header nav."
1 parent 8b42a0a commit 47ed2f3

File tree

9 files changed

+385
-0
lines changed

9 files changed

+385
-0
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
import { PortableText } from "astro-portabletext";
3+
import { urlForImage } from "@/utils/sanity";
4+
import ContentCard from "./ContentCard.astro";
5+
6+
interface Props {
7+
person: any;
8+
type: "author" | "guest" | "sponsor";
9+
}
10+
11+
const { person, type } = Astro.props;
12+
13+
const coverUrl = person.coverImage
14+
? urlForImage(person.coverImage).width(400).height(400).format("webp").url()
15+
: null;
16+
17+
function getHostname(url: string): string {
18+
try { return new URL(url).hostname; }
19+
catch { return url; }
20+
}
21+
---
22+
23+
<div class="flex flex-col md:flex-row gap-8 mb-12">
24+
{coverUrl && (
25+
<img
26+
src={coverUrl}
27+
alt={person.title}
28+
width={400}
29+
height={400}
30+
class="w-48 h-48 rounded-full object-cover flex-shrink-0"
31+
/>
32+
)}
33+
<div>
34+
<h1 class="text-4xl font-bold mb-4">{person.title}</h1>
35+
{person.excerpt && (
36+
<p class="text-gray-600 text-lg mb-4">{person.excerpt}</p>
37+
)}
38+
{person.socials && (
39+
<div class="flex flex-wrap gap-3">
40+
{person.socials.twitter && (
41+
<a href={`https://twitter.com/${person.socials.twitter}`} target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:underline">
42+
Twitter
43+
</a>
44+
)}
45+
{person.socials.github && (
46+
<a href={`https://github.com/${person.socials.github}`} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:underline">
47+
GitHub
48+
</a>
49+
)}
50+
{person.socials.linkedin && (
51+
<a href={person.socials.linkedin} target="_blank" rel="noopener noreferrer" class="text-blue-700 hover:underline">
52+
LinkedIn
53+
</a>
54+
)}
55+
</div>
56+
)}
57+
{person.websites && person.websites.length > 0 && (
58+
<div class="flex flex-wrap gap-3 mt-2">
59+
{person.websites.map((site: string) => (
60+
<a href={site} target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:underline text-sm">
61+
{getHostname(site)}
62+
</a>
63+
))}
64+
</div>
65+
)}
66+
</div>
67+
</div>
68+
69+
{person.content && (
70+
<div class="prose prose-lg max-w-none mb-12">
71+
<PortableText value={person.content} />
72+
</div>
73+
)}
74+
75+
{person.related && (
76+
<>
77+
{person.related.podcast && person.related.podcast.length > 0 && (
78+
<section class="mb-12">
79+
<h2 class="text-2xl font-bold mb-6">Related Podcasts</h2>
80+
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
81+
{person.related.podcast.map((p: any) => (
82+
<ContentCard
83+
title={p.title}
84+
slug={`/podcast/${p.slug}`}
85+
coverImage={p.coverImage}
86+
excerpt={p.excerpt}
87+
date={p.date}
88+
/>
89+
))}
90+
</div>
91+
</section>
92+
)}
93+
94+
{person.related.post && person.related.post.length > 0 && (
95+
<section class="mb-12">
96+
<h2 class="text-2xl font-bold mb-6">Related Posts</h2>
97+
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
98+
{person.related.post.map((p: any) => (
99+
<ContentCard
100+
title={p.title}
101+
slug={`/post/${p.slug}`}
102+
coverImage={p.coverImage}
103+
excerpt={p.excerpt}
104+
date={p.date}
105+
/>
106+
))}
107+
</div>
108+
</section>
109+
)}
110+
</>
111+
)}

apps/web/src/layouts/BaseLayout.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const { title, description = "CodingCat.dev — Purrfect Web Tutorials" } = Astr
2525
<div class="flex items-center gap-6">
2626
<a href="/blog" class="hover:text-blue-600 transition-colors">Blog</a>
2727
<a href="/podcasts" class="hover:text-blue-600 transition-colors">Podcasts</a>
28+
<a href="/authors" class="hover:text-blue-600 transition-colors">Authors</a>
2829
<a href="/dashboard" class="hover:text-blue-600 transition-colors">Dashboard</a>
2930
</div>
3031
</nav>

apps/web/src/lib/queries.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,69 @@ export const settingsQuery = groq`*[_type == "settings"][0]{
118118
...,
119119
ogImage
120120
}`;
121+
122+
// Author queries
123+
export const authorListQuery = groq`*[_type == "author" && defined(slug.current)] | order(title) [$offset...$end] {
124+
${baseFields}
125+
}`;
126+
127+
export const authorCountQuery = groq`count(*[_type == "author" && defined(slug.current)])`;
128+
129+
export const authorQuery = groq`*[_type == "author" && slug.current == $slug][0] {
130+
${baseFields},
131+
${contentFields},
132+
socials,
133+
websites,
134+
"related": {
135+
"podcast": *[_type == "podcast" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] {
136+
${baseFields}
137+
},
138+
"post": *[_type == "post" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] {
139+
${baseFields}
140+
},
141+
}
142+
}`;
143+
144+
// Guest queries
145+
export const guestListQuery = groq`*[_type == "guest" && defined(slug.current)] | order(title) [$offset...$end] {
146+
${baseFields}
147+
}`;
148+
149+
export const guestCountQuery = groq`count(*[_type == "guest" && defined(slug.current)])`;
150+
151+
export const guestQuery = groq`*[_type == "guest" && slug.current == $slug][0] {
152+
${baseFields},
153+
${contentFields},
154+
socials,
155+
websites,
156+
"related": {
157+
"podcast": *[_type == "podcast" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] {
158+
${baseFields}
159+
},
160+
"post": *[_type == "post" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] {
161+
${baseFields}
162+
},
163+
}
164+
}`;
165+
166+
// Sponsor queries
167+
export const sponsorListQuery = groq`*[_type == "sponsor" && defined(slug.current)] | order(date desc) [$offset...$end] {
168+
${baseFields}
169+
}`;
170+
171+
export const sponsorCountQuery = groq`count(*[_type == "sponsor" && defined(slug.current)])`;
172+
173+
export const sponsorQuery = groq`*[_type == "sponsor" && slug.current == $slug][0] {
174+
${baseFields},
175+
${contentFields},
176+
socials,
177+
websites,
178+
"related": {
179+
"podcast": *[_type == "podcast" && ^._id in sponsor[]._ref] | order(date desc) [0...4] {
180+
${baseFields}
181+
},
182+
"post": *[_type == "post" && ^._id in sponsor[]._ref] | order(date desc) [0...4] {
183+
${baseFields}
184+
},
185+
}
186+
}`;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
import BaseLayout from "@/layouts/BaseLayout.astro";
3+
import PersonDetail from "@/components/PersonDetail.astro";
4+
import { sanityFetch } from "@/utils/sanity";
5+
import { authorQuery } from "@/lib/queries";
6+
7+
export const prerender = false;
8+
9+
const { slug } = Astro.params;
10+
const author = await sanityFetch<any>(authorQuery, { slug });
11+
12+
if (!author) {
13+
return Astro.redirect("/404");
14+
}
15+
---
16+
17+
<BaseLayout title={`${author.title} — CodingCat.dev`} description={author.excerpt}>
18+
<main class="container mx-auto px-4 py-8 max-w-4xl">
19+
<PersonDetail person={author} type="author" />
20+
</main>
21+
</BaseLayout>

apps/web/src/pages/authors.astro

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
import BaseLayout from "@/layouts/BaseLayout.astro";
3+
import ContentCard from "@/components/ContentCard.astro";
4+
import Pagination from "@/components/Pagination.astro";
5+
import { sanityFetch } from "@/utils/sanity";
6+
import { authorListQuery, authorCountQuery } from "@/lib/queries";
7+
8+
export const prerender = false;
9+
10+
const rawPage = Number(Astro.url.searchParams.get("page") || "1");
11+
const page = Number.isNaN(rawPage) || rawPage < 1 ? 1 : Math.floor(rawPage);
12+
const perPage = 12;
13+
const offset = (page - 1) * perPage;
14+
15+
const [items, totalCount] = await Promise.all([
16+
sanityFetch<any[]>(authorListQuery, { offset, end: offset + perPage }),
17+
sanityFetch<number>(authorCountQuery),
18+
]);
19+
20+
const totalPages = Math.ceil(totalCount / perPage);
21+
22+
if (page > totalPages && totalPages > 0) {
23+
return Astro.redirect(`/authors?page=${totalPages}`);
24+
}
25+
---
26+
27+
<BaseLayout title="Authors — CodingCat.dev">
28+
<main class="container mx-auto px-4 py-8">
29+
<h1 class="text-4xl font-bold mb-2">Authors</h1>
30+
<p class="text-gray-600 mb-8">The people behind CodingCat.dev content.</p>
31+
32+
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
33+
{items?.map((item) => (
34+
<ContentCard
35+
title={item.title}
36+
slug={`/author/${item.slug}`}
37+
coverImage={item.coverImage}
38+
excerpt={item.excerpt}
39+
date={item.date}
40+
/>
41+
))}
42+
</div>
43+
44+
{totalPages > 1 && (
45+
<Pagination currentPage={page} totalPages={totalPages} basePath="/authors" />
46+
)}
47+
</main>
48+
</BaseLayout>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
import BaseLayout from "@/layouts/BaseLayout.astro";
3+
import PersonDetail from "@/components/PersonDetail.astro";
4+
import { sanityFetch } from "@/utils/sanity";
5+
import { guestQuery } from "@/lib/queries";
6+
7+
export const prerender = false;
8+
9+
const { slug } = Astro.params;
10+
const guest = await sanityFetch<any>(guestQuery, { slug });
11+
12+
if (!guest) {
13+
return Astro.redirect("/404");
14+
}
15+
---
16+
17+
<BaseLayout title={`${guest.title} — CodingCat.dev`} description={guest.excerpt}>
18+
<main class="container mx-auto px-4 py-8 max-w-4xl">
19+
<PersonDetail person={guest} type="guest" />
20+
</main>
21+
</BaseLayout>

apps/web/src/pages/guests.astro

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
import BaseLayout from "@/layouts/BaseLayout.astro";
3+
import ContentCard from "@/components/ContentCard.astro";
4+
import Pagination from "@/components/Pagination.astro";
5+
import { sanityFetch } from "@/utils/sanity";
6+
import { guestListQuery, guestCountQuery } from "@/lib/queries";
7+
8+
export const prerender = false;
9+
10+
const rawPage = Number(Astro.url.searchParams.get("page") || "1");
11+
const page = Number.isNaN(rawPage) || rawPage < 1 ? 1 : Math.floor(rawPage);
12+
const perPage = 12;
13+
const offset = (page - 1) * perPage;
14+
15+
const [items, totalCount] = await Promise.all([
16+
sanityFetch<any[]>(guestListQuery, { offset, end: offset + perPage }),
17+
sanityFetch<number>(guestCountQuery),
18+
]);
19+
20+
const totalPages = Math.ceil(totalCount / perPage);
21+
22+
if (page > totalPages && totalPages > 0) {
23+
return Astro.redirect(`/guests?page=${totalPages}`);
24+
}
25+
---
26+
27+
<BaseLayout title="Guests — CodingCat.dev">
28+
<main class="container mx-auto px-4 py-8">
29+
<h1 class="text-4xl font-bold mb-2">Guests</h1>
30+
<p class="text-gray-600 mb-8">The people behind CodingCat.dev content.</p>
31+
32+
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
33+
{items?.map((item) => (
34+
<ContentCard
35+
title={item.title}
36+
slug={`/guest/${item.slug}`}
37+
coverImage={item.coverImage}
38+
excerpt={item.excerpt}
39+
date={item.date}
40+
/>
41+
))}
42+
</div>
43+
44+
{totalPages > 1 && (
45+
<Pagination currentPage={page} totalPages={totalPages} basePath="/guests" />
46+
)}
47+
</main>
48+
</BaseLayout>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
import BaseLayout from "@/layouts/BaseLayout.astro";
3+
import PersonDetail from "@/components/PersonDetail.astro";
4+
import { sanityFetch } from "@/utils/sanity";
5+
import { sponsorQuery } from "@/lib/queries";
6+
7+
export const prerender = false;
8+
9+
const { slug } = Astro.params;
10+
const sponsor = await sanityFetch<any>(sponsorQuery, { slug });
11+
12+
if (!sponsor) {
13+
return Astro.redirect("/404");
14+
}
15+
---
16+
17+
<BaseLayout title={`${sponsor.title} — CodingCat.dev`} description={sponsor.excerpt}>
18+
<main class="container mx-auto px-4 py-8 max-w-4xl">
19+
<PersonDetail person={sponsor} type="sponsor" />
20+
</main>
21+
</BaseLayout>

0 commit comments

Comments
 (0)