Skip to content

Commit 74f0c80

Browse files
authored
feat: page redesigns — design system, schema fields, SocialMeta, OG images (Track C) (#682)
Full page redesign using Track B design system + Track A schema fields.\n\nPages redesigned:\n- Homepage: hero with gradient, featured podcast (ContentCardFeatured), 4 content rows with Badge headers + View All links\n- Blog listing: Badge + post count, categories as Tags, 3-column grid\n- Post detail: Badge + date, author Avatar, categories, prose with design tokens, author card, socialPreview\n- Podcast listing: episode/season metadata on cards, episode count\n- Podcast detail: series info card, chapters timeline with seconds, guest cards (Avatar + company/role), listen links (flat object), picks, author card, socialPreview\n- Author/guest/sponsor detail: company/role, Avatar fallback, Badge headers for related content\n- Author/guest/sponsor listing: Container, design tokens, OG images\n- Generic pages: Container narrow, socialPreview fields\n- 404: gradient text, cat message, Button components\n\nNew component:\n- SocialMeta.astro: og:*/twitter:*/article:* meta tags with smart defaults, og:image dimensions + alt + locale, fallback OG image via /api/og/default.png\n\nGROQ updates:\n- baseFields: +authorName, +authorImage\n- postListQuery/postQuery: +categories[]-> +socialPreview\n- podcastFields: +chapters[]{title,timestamp,seconds}, +series->, +listenLinks (flat object)\n- podcastListQuery: +episode, +season\n- author/guest queries: +company, +role, +socialPreview\n- sponsor/page queries: +socialPreview\n\nDesign compliance: zero hardcoded colors, all design tokens, all Track B components used.\nDesign QA approved by @uidesigner. Social preview reviewed by @SocialMediaStrategist. Podcast reviewed by @podcaststrategist."
1 parent f8d3bf4 commit 74f0c80

File tree

17 files changed

+976
-367
lines changed

17 files changed

+976
-367
lines changed

apps/web/src/components/PersonDetail.astro

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { PortableText } from "astro-portabletext";
33
import { urlForImage } from "@/utils/sanity";
44
import ContentCard from "./ContentCard.astro";
5+
import Avatar from "./Avatar.astro";
6+
import Badge from "./Badge.astro";
57
68
interface Props {
79
person: any;
@@ -21,34 +23,45 @@ function getHostname(url: string): string {
2123
---
2224

2325
<div class="flex flex-col md:flex-row gap-8 mb-12">
24-
{coverUrl && (
26+
{coverUrl ? (
2527
<img
2628
src={coverUrl}
2729
alt={person.title}
2830
width={400}
2931
height={400}
3032
class="w-48 h-48 rounded-full object-cover flex-shrink-0"
3133
/>
34+
) : (
35+
<Avatar name={person.title} size="xl" />
3236
)}
3337
<div>
34-
<h1 class="text-4xl font-bold mb-4">{person.title}</h1>
38+
<h1 class="text-4xl font-bold text-[--text] mb-2">{person.title}</h1>
39+
40+
{/* Company & Role for guests */}
41+
{(person.company || person.role) && (
42+
<p class="text-lg text-[--text-secondary] mb-2">
43+
{[person.role, person.company].filter(Boolean).join(" at ")}
44+
</p>
45+
)}
46+
3547
{person.excerpt && (
36-
<p class="text-gray-600 text-lg mb-4">{person.excerpt}</p>
48+
<p class="text-[--text-secondary] text-lg mb-4">{person.excerpt}</p>
3749
)}
50+
3851
{person.socials && (
3952
<div class="flex flex-wrap gap-3">
4053
{person.socials.twitter && (
41-
<a href={`https://twitter.com/${person.socials.twitter}`} target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:underline">
54+
<a href={`https://twitter.com/${person.socials.twitter}`} target="_blank" rel="noopener noreferrer" class="text-[--text-secondary] hover:text-primary transition-colors">
4255
Twitter
4356
</a>
4457
)}
4558
{person.socials.github && (
46-
<a href={`https://github.com/${person.socials.github}`} target="_blank" rel="noopener noreferrer" class="text-gray-700 hover:underline">
59+
<a href={`https://github.com/${person.socials.github}`} target="_blank" rel="noopener noreferrer" class="text-[--text-secondary] hover:text-primary transition-colors">
4760
GitHub
4861
</a>
4962
)}
5063
{person.socials.linkedin && (
51-
<a href={person.socials.linkedin} target="_blank" rel="noopener noreferrer" class="text-blue-700 hover:underline">
64+
<a href={person.socials.linkedin} target="_blank" rel="noopener noreferrer" class="text-[--text-secondary] hover:text-primary transition-colors">
5265
LinkedIn
5366
</a>
5467
)}
@@ -57,7 +70,7 @@ function getHostname(url: string): string {
5770
{person.websites && person.websites.length > 0 && (
5871
<div class="flex flex-wrap gap-3 mt-2">
5972
{person.websites.map((site: string) => (
60-
<a href={site} target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:underline text-sm">
73+
<a href={site} target="_blank" rel="noopener noreferrer" class="text-sm text-[--text-secondary] hover:text-primary transition-colors">
6174
{getHostname(site)}
6275
</a>
6376
))}
@@ -67,7 +80,16 @@ function getHostname(url: string): string {
6780
</div>
6881

6982
{person.content && (
70-
<div class="prose prose-lg max-w-none mb-12">
83+
<div class="prose prose-lg prose-invert max-w-none mb-12
84+
prose-headings:text-[--text] prose-headings:font-bold
85+
prose-p:text-[--text-secondary]
86+
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
87+
prose-strong:text-[--text]
88+
prose-code:text-primary prose-code:bg-[--surface] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded
89+
prose-pre:bg-[--surface] prose-pre:border prose-pre:border-[--border] prose-pre:rounded-xl
90+
prose-blockquote:border-primary prose-blockquote:text-[--text-secondary]
91+
prose-li:text-[--text-secondary]
92+
">
7193
<PortableText value={person.content} />
7294
</div>
7395
)}
@@ -76,15 +98,20 @@ function getHostname(url: string): string {
7698
<>
7799
{person.related.podcast && person.related.podcast.length > 0 && (
78100
<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">
101+
<div class="flex items-center gap-3 mb-6">
102+
<Badge type="podcast" />
103+
<h2 class="text-2xl font-bold text-[--text]">Related Podcasts</h2>
104+
</div>
105+
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
81106
{person.related.podcast.map((p: any) => (
82107
<ContentCard
83108
title={p.title}
84-
slug={`/podcast/${p.slug}`}
85-
coverImage={p.coverImage}
86-
excerpt={p.excerpt}
87-
date={p.date}
109+
url={`/podcast/${p.slug}`}
110+
type="podcast"
111+
thumbnail={p.coverImage}
112+
authorName={p.authorName}
113+
authorImage={p.authorImage}
114+
metadata={p.date ? new Date(p.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : undefined}
88115
/>
89116
))}
90117
</div>
@@ -93,15 +120,20 @@ function getHostname(url: string): string {
93120

94121
{person.related.post && person.related.post.length > 0 && (
95122
<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">
123+
<div class="flex items-center gap-3 mb-6">
124+
<Badge type="blog" />
125+
<h2 class="text-2xl font-bold text-[--text]">Related Posts</h2>
126+
</div>
127+
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
98128
{person.related.post.map((p: any) => (
99129
<ContentCard
100130
title={p.title}
101-
slug={`/post/${p.slug}`}
102-
coverImage={p.coverImage}
103-
excerpt={p.excerpt}
104-
date={p.date}
131+
url={`/post/${p.slug}`}
132+
type="blog"
133+
thumbnail={p.coverImage}
134+
authorName={p.authorName}
135+
authorImage={p.authorImage}
136+
metadata={p.date ? new Date(p.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : undefined}
105137
/>
106138
))}
107139
</div>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
interface Props {
3+
title: string;
4+
description?: string;
5+
ogImage?: string;
6+
type?: 'website' | 'article';
7+
publishedAt?: string;
8+
author?: string;
9+
canonicalUrl?: string;
10+
}
11+
12+
const {
13+
title,
14+
description = "CodingCat.dev — Purrfect Web Tutorials",
15+
ogImage,
16+
type = "website",
17+
publishedAt,
18+
author,
19+
canonicalUrl,
20+
} = Astro.props;
21+
22+
const siteUrl = Astro.url.origin;
23+
const resolvedUrl = canonicalUrl || Astro.url.href;
24+
const resolvedImage = ogImage || `${siteUrl}/api/og/default.png?title=${encodeURIComponent(title)}`;
25+
---
26+
27+
{/* Primary Meta Tags */}
28+
<meta name="description" content={description} />
29+
30+
{/* Canonical */}
31+
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
32+
33+
{/* Open Graph */}
34+
<meta property="og:type" content={type} />
35+
<meta property="og:url" content={resolvedUrl} />
36+
<meta property="og:title" content={title} />
37+
<meta property="og:description" content={description} />
38+
<meta property="og:image" content={resolvedImage} />
39+
<meta property="og:image:width" content="1200" />
40+
<meta property="og:image:height" content="630" />
41+
<meta property="og:image:alt" content={title} />
42+
<meta property="og:site_name" content="CodingCat.dev" />
43+
<meta property="og:locale" content="en_US" />
44+
45+
{/* Twitter Card */}
46+
<meta name="twitter:card" content="summary_large_image" />
47+
<meta name="twitter:site" content="@codingcatdev" />
48+
<meta name="twitter:title" content={title} />
49+
<meta name="twitter:description" content={description} />
50+
<meta name="twitter:image" content={resolvedImage} />
51+
52+
{/* Article-specific */}
53+
{type === "article" && publishedAt && (
54+
<meta property="article:published_time" content={publishedAt} />
55+
)}
56+
{type === "article" && author && (
57+
<meta property="article:author" content={author} />
58+
)}

apps/web/src/layouts/BaseLayout.astro

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import ThemeScript from "../components/ThemeScript.astro";
44
import Header from "../components/Header.astro";
55
import Footer from "../components/Footer.astro";
66
import { VisualEditing } from "@sanity/astro/visual-editing";
7+
import SocialMeta from "../components/SocialMeta.astro";
78
89
interface Props {
910
title: string;
1011
description?: string;
12+
ogImage?: string;
13+
ogType?: 'website' | 'article';
14+
publishedAt?: string;
15+
author?: string;
16+
canonicalUrl?: string;
1117
}
1218
13-
const { title, description = "CodingCat.dev — Purrfect Web Tutorials" } = Astro.props;
19+
const { title, description = "CodingCat.dev — Purrfect Web Tutorials", ogImage, ogType, publishedAt, author, canonicalUrl } = Astro.props;
1420
1521
const visualEditingEnabled =
1622
import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED === "true" ||
@@ -22,7 +28,15 @@ const visualEditingEnabled =
2228
<head>
2329
<meta charset="UTF-8" />
2430
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
25-
<meta name="description" content={description} />
31+
<SocialMeta
32+
title={title}
33+
description={description}
34+
ogImage={ogImage}
35+
type={ogType}
36+
publishedAt={publishedAt}
37+
author={author}
38+
canonicalUrl={canonicalUrl}
39+
/>
2640
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
2741
<link rel="alternate" type="application/rss+xml" title="CodingCat.dev Blog" href="/rss.xml" />
2842
<link rel="alternate" type="application/rss+xml" title="CodingCat.dev Podcast" href="/podcast/rss.xml" />

apps/web/src/lib/queries.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ const baseFields = `
77
"slug": slug.current,
88
excerpt,
99
coverImage,
10-
"date": coalesce(date, _createdAt)
10+
"date": coalesce(date, _createdAt),
11+
"authorName": author[0]->title,
12+
"authorImage": author[0]->coverImage
1113
`;
1214

1315
const contentFields = `
@@ -57,12 +59,20 @@ const podcastFields = `
5759
name,
5860
site
5961
},
60-
spotify
62+
spotify,
63+
chapters[]{
64+
title,
65+
timestamp,
66+
seconds
67+
},
68+
"series": series->{ _id, "title": coalesce(title, "Unknown Series"), "slug": slug.current, description },
69+
listenLinks
6170
`;
6271

6372
export const homePageQuery = groq`*[_type == "settings"][0]{
6473
"latestPodcast": *[_type == "podcast"]|order(date desc)[0]{
6574
${baseFields},
75+
excerpt,
6676
youtube,
6777
},
6878
"latestPodcasts": *[_type == "podcast"]|order(date desc)[0...4]{
@@ -84,18 +94,25 @@ export const postListQuery = groq`*[_type == "post" && defined(slug.current)] |
8494
author[]->{
8595
"title": coalesce(title, "Anonymous"),
8696
"slug": slug.current,
87-
}
97+
},
98+
categories[]->{ _id, "title": coalesce(title, "Uncategorized"), "slug": slug.current }
8899
}`;
89100

90101
export const postQuery = groq`*[_type == "post" && slug.current == $slug][0] {
91102
${baseFields},
92-
${contentFields}
103+
${contentFields},
104+
categories[]->{ _id, "title": coalesce(title, "Uncategorized"), "slug": slug.current },
105+
"ogTitle": socialPreview.ogTitle,
106+
"ogDescription": socialPreview.ogDescription,
107+
"ogImage": socialPreview.ogImage
93108
}`;
94109

95110
export const postCountQuery = groq`count(*[_type == "post" && defined(slug.current)])`;
96111

97112
export const podcastListQuery = groq`*[_type == "podcast" && defined(slug.current)] | order(date desc, _updatedAt desc) [$offset...$end] {
98113
${baseFields},
114+
episode,
115+
season,
99116
author[]->{
100117
"title": coalesce(title, "Anonymous"),
101118
"slug": slug.current,
@@ -109,7 +126,10 @@ export const podcastListQuery = groq`*[_type == "podcast" && defined(slug.curren
109126
export const podcastQuery = groq`*[_type == "podcast" && slug.current == $slug][0] {
110127
${baseFields},
111128
${contentFields},
112-
${podcastFields}
129+
${podcastFields},
130+
"ogTitle": socialPreview.ogTitle,
131+
"ogDescription": socialPreview.ogDescription,
132+
"ogImage": socialPreview.ogImage
113133
}`;
114134

115135
export const podcastCountQuery = groq`count(*[_type == "podcast" && defined(slug.current)])`;
@@ -131,6 +151,11 @@ export const authorQuery = groq`*[_type == "author" && slug.current == $slug][0]
131151
${contentFields},
132152
socials,
133153
websites,
154+
company,
155+
role,
156+
"ogTitle": socialPreview.ogTitle,
157+
"ogDescription": socialPreview.ogDescription,
158+
"ogImage": socialPreview.ogImage,
134159
"related": {
135160
"podcast": *[_type == "podcast" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] {
136161
${baseFields}
@@ -153,6 +178,11 @@ export const guestQuery = groq`*[_type == "guest" && slug.current == $slug][0] {
153178
${contentFields},
154179
socials,
155180
websites,
181+
company,
182+
role,
183+
"ogTitle": socialPreview.ogTitle,
184+
"ogDescription": socialPreview.ogDescription,
185+
"ogImage": socialPreview.ogImage,
156186
"related": {
157187
"podcast": *[_type == "podcast" && (^._id in author[]._ref || ^._id in guest[]._ref)] | order(date desc) [0...4] {
158188
${baseFields}
@@ -175,6 +205,9 @@ export const sponsorQuery = groq`*[_type == "sponsor" && slug.current == $slug][
175205
${contentFields},
176206
socials,
177207
websites,
208+
"ogTitle": socialPreview.ogTitle,
209+
"ogDescription": socialPreview.ogDescription,
210+
"ogImage": socialPreview.ogImage,
178211
"related": {
179212
"podcast": *[_type == "podcast" && ^._id in sponsor[]._ref] | order(date desc) [0...4] {
180213
${baseFields}
@@ -196,7 +229,10 @@ export const sitemapQuery = groq`*[_type in ["author", "guest", "page", "podcast
196229
// Generic pages (Sanity "page" type)
197230
export const pageQuery = groq`*[_type == "page" && slug.current == $slug][0] {
198231
${baseFields},
199-
${contentFields}
232+
${contentFields},
233+
"ogTitle": socialPreview.ogTitle,
234+
"ogDescription": socialPreview.ogDescription,
235+
"ogImage": socialPreview.ogImage
200236
}`;
201237

202238
// RSS feeds

apps/web/src/pages/404.astro

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
---
22
import BaseLayout from "@/layouts/BaseLayout.astro";
3+
import Container from "@/components/Container.astro";
4+
import Button from "@/components/Button.astro";
35
---
46

57
<BaseLayout title="Page Not Found — CodingCat.dev">
6-
<main class="container mx-auto px-4 py-16 text-center">
7-
<h1 class="text-6xl font-bold mb-4">404</h1>
8-
<p class="text-xl text-gray-600 mb-8">
9-
This page has gone on a catnap. 😸
10-
</p>
11-
<div class="space-x-4">
12-
<a href="/" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
13-
Go Home
14-
</a>
15-
<a href="/blog" class="px-6 py-3 border rounded-lg hover:bg-gray-50 transition-colors">
16-
Browse Blog
17-
</a>
18-
</div>
8+
<main class="py-20 md:py-32">
9+
<Container>
10+
<div class="text-center max-w-lg mx-auto">
11+
<p class="text-8xl md:text-9xl font-extrabold bg-gradient-to-r from-primary to-primary/50 bg-clip-text text-transparent mb-6">
12+
404
13+
</p>
14+
<h1 class="text-2xl md:text-3xl font-bold text-[--text] mb-3">
15+
Page Not Found
16+
</h1>
17+
<p class="text-lg text-[--text-secondary] mb-8">
18+
This page has gone on a catnap. 😸 Maybe it's chasing a laser pointer somewhere else.
19+
</p>
20+
<div class="flex flex-wrap justify-center gap-4">
21+
<Button href="/" variant="primary" size="lg">Go Home</Button>
22+
<Button href="/blog" variant="secondary" size="lg">Browse Blog</Button>
23+
</div>
24+
</div>
25+
</Container>
1926
</main>
2027
</BaseLayout>

0 commit comments

Comments
 (0)