Skip to content

Commit 4df698b

Browse files
authored
fix(site): serve responsive Retina srcset images and enable author portal (#774)
1 parent 26ebf2e commit 4df698b

8 files changed

Lines changed: 115 additions & 41 deletions

File tree

‎apps/site/src/components/Avatar.astro‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ const {
2020
height = 48,
2121
} = Astro.props;
2222
23-
const src = Astro.locals.sanity
24-
.urlForImage(coverImage ?? undefined)
25-
?.width(width)
26-
.height(height)
23+
const builder = Astro.locals.sanity.urlForImage(coverImage ?? undefined);
24+
const src = builder
25+
?.width(width * 2)
26+
.height(height * 2)
27+
.quality(82)
2728
.url();
2829
2930
// stegaClean: href is an attribute; zero-width characters break the URL.

‎apps/site/src/components/ContentCard.astro‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,17 @@ const people = [
2828
<CoverImage
2929
image={item.coverImage}
3030
fallbackAlt={item.title || "CodingCat.dev author photo"}
31-
width={256}
32-
height={256}
31+
width={640}
32+
height={640}
33+
sizes="(max-width: 768px) 100vw, 560px"
3334
class="w-full h-auto aspect-square rounded-md object-cover"
3435
priority={priority}
3536
/>
3637
) : (
3738
<CoverImage
3839
image={item.coverImage}
3940
fallbackAlt={item.title || "CodingCat.dev tutorial cover image"}
41+
sizes="(max-width: 768px) 100vw, 560px"
4042
priority={priority}
4143
/>
4244
)}

‎apps/site/src/components/CoverImage.astro‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface Props {
88
width?: number;
99
height?: number;
1010
quality?: number;
11+
sizes?: string;
1112
/** Above-the-fold images opt out of lazy loading. */
1213
priority?: boolean;
1314
class?: string;
@@ -18,18 +19,37 @@ const {
1819
fallbackAlt = "CodingCat.dev tutorial cover image",
1920
width = 1920,
2021
height = 1080,
21-
quality = 80,
22+
quality = 82,
23+
sizes = "(max-width: 768px) 100vw, (max-width: 1280px) 50vw, 960px",
2224
priority = false,
2325
class: className = "w-full h-auto aspect-video rounded-md",
2426
} = Astro.props;
2527
26-
const src = Astro.locals.sanity
27-
.urlForImage(image ?? undefined)
28+
const builder = Astro.locals.sanity.urlForImage(image ?? undefined);
29+
const aspect = height / width;
30+
31+
const src = builder
2832
?.width(width)
2933
.height(height)
3034
.quality(quality)
3135
.url();
3236
37+
const responsiveWidths = [320, 480, 640, 960, 1280, 1600, 1920].filter(
38+
(w) => w <= width,
39+
);
40+
if (!responsiveWidths.includes(width)) {
41+
responsiveWidths.push(width);
42+
}
43+
const srcset = builder
44+
? responsiveWidths
45+
.map((w) => {
46+
const h = Math.round(w * aspect);
47+
const u = builder.width(w).height(h).quality(quality).url();
48+
return `${u} ${w}w`;
49+
})
50+
.join(", ")
51+
: undefined;
52+
3353
// stegaClean: alt lands in an attribute, where stega's zero-width characters
3454
// would be read out verbatim by screen readers.
3555
const rawAlt = stegaClean((image as { alt?: string } | null)?.alt);
@@ -42,13 +62,14 @@ const alt = rawAlt && rawAlt.trim().length > 0 ? rawAlt : fallbackAlt;
4262
<img
4363
class={className}
4464
src={src}
65+
srcset={srcset}
4566
width={width}
4667
height={height}
4768
alt={alt}
4869
loading={priority ? "eager" : "lazy"}
4970
fetchpriority={priority ? "high" : "auto"}
5071
decoding={priority ? "sync" : "async"}
51-
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
72+
sizes={sizes}
5273
/>
5374
) : (
5475
<div class="bg-muted rounded-md" style="padding-top: 50%" />

‎apps/site/src/components/CoverMedia.astro‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ const youtubeUrl = stegaClean(youtube);
5454
</video>
5555
</div>
5656
) : youtubeUrl ? (
57-
<PtYouTube node={{ _type: "youtube", youtube: youtubeUrl }} index={0} isInline={false} />
57+
<PtYouTube
58+
node={{ _type: "youtube", youtube: youtubeUrl }}
59+
coverImage={image}
60+
priority
61+
index={0}
62+
isInline={false}
63+
/>
5864
) : (
59-
<CoverImage image={image} priority class={className} />
65+
<CoverImage image={image} priority sizes="(max-width: 1280px) 100vw, 1280px" class={className} />
6066
)
6167
}

‎apps/site/src/components/HomeSection.astro‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ const { title, description, items, muted = false } = Astro.props;
4343
<CoverImage
4444
image={item.coverImage}
4545
fallbackAlt={item.title || "CodingCat.dev tutorial cover image"}
46-
width={320}
47-
height={180}
46+
width={960}
47+
height={540}
48+
sizes="(max-width: 1024px) 100vw, 512px"
4849
class="aspect-video w-full object-cover object-center"
4950
/>
5051
)}

‎apps/site/src/components/portable-text/PtYouTube.astro‎

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,74 @@
11
---
22
import { stegaClean } from "@sanity/client/stega";
3+
import type { SanityImageSource } from "@sanity/image-url";
34
import type { Props as PtProps } from "astro-portabletext/types";
45
import { youtubeId } from "@/lib/youtube";
56
67
type YouTubeNode = { _type: "youtube"; youtube?: string };
78
8-
type Props = PtProps<YouTubeNode>;
9+
type Props = PtProps<YouTubeNode> & {
10+
coverImage?: (SanityImageSource & { alt?: string }) | null;
11+
priority?: boolean;
12+
};
913
10-
const { node } = Astro.props;
14+
const { node, coverImage, priority = false } = Astro.props;
1115
const id = youtubeId(stegaClean(node.youtube));
16+
17+
const builder = Astro.locals.sanity.urlForImage(coverImage ?? undefined);
18+
const sanitySrc = builder?.width(1920).height(1080).quality(82).url();
19+
const sanitySrcset = builder
20+
? [480, 640, 960, 1280, 1600, 1920]
21+
.map((w) => {
22+
const h = Math.round((w * 9) / 16);
23+
return `${builder.width(w).height(h).quality(82).url()} ${w}w`;
24+
})
25+
.join(", ")
26+
: undefined;
27+
const rawAlt = stegaClean((coverImage as { alt?: string } | null)?.alt);
28+
const alt = rawAlt && rawAlt.trim().length > 0 ? rawAlt : "";
1229
---
1330

1431
{
1532
id && (
1633
<div class="not-prose my-6">
1734
<div class="yt-embed relative aspect-video isolate overflow-hidden rounded-md" data-yt={id}>
18-
<picture>
19-
<source
20-
type="image/webp"
21-
srcset={[
22-
`https://i.ytimg.com/vi_webp/${id}/mqdefault.webp 320w`,
23-
`https://i.ytimg.com/vi_webp/${id}/hqdefault.webp 480w`,
24-
`https://i.ytimg.com/vi_webp/${id}/sddefault.webp 640w`,
25-
].join(", ")}
26-
/>
35+
{sanitySrc ? (
2736
<img
2837
class="size-full object-cover"
29-
src={`https://i.ytimg.com/vi/${id}/sddefault.jpg`}
30-
alt=""
31-
width={640}
32-
height={480}
33-
loading="lazy"
34-
decoding="async"
38+
src={sanitySrc}
39+
srcset={sanitySrcset}
40+
sizes="(max-width: 1280px) 100vw, 1280px"
41+
alt={alt}
42+
width={1920}
43+
height={1080}
44+
loading={priority ? "eager" : "lazy"}
45+
fetchpriority={priority ? "high" : "auto"}
46+
decoding={priority ? "sync" : "async"}
3547
/>
36-
</picture>
48+
) : (
49+
<picture>
50+
<source
51+
type="image/webp"
52+
sizes="(max-width: 1280px) 100vw, 1280px"
53+
srcset={[
54+
`https://i.ytimg.com/vi_webp/${id}/mqdefault.webp 320w`,
55+
`https://i.ytimg.com/vi_webp/${id}/hqdefault.webp 480w`,
56+
`https://i.ytimg.com/vi_webp/${id}/sddefault.webp 640w`,
57+
`https://i.ytimg.com/vi_webp/${id}/maxresdefault.webp 1280w`,
58+
].join(", ")}
59+
/>
60+
<img
61+
class="size-full object-cover"
62+
src={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`}
63+
alt=""
64+
width={1280}
65+
height={720}
66+
loading={priority ? "eager" : "lazy"}
67+
fetchpriority={priority ? "high" : "auto"}
68+
decoding={priority ? "sync" : "async"}
69+
/>
70+
</picture>
71+
)}
3772
<button
3873
type="button"
3974
class="absolute inset-0 grid size-full place-items-center border-0 bg-transparent"

‎apps/site/src/pages/api/guest-portal/update.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,13 @@ export const POST: APIRoute = async ({ request }) => {
5959

6060
const existingGuest = await client.fetch<{
6161
_id: string;
62+
_type: string;
6263
title?: string;
6364
socials?: Record<string, string>;
6465
websites?: Array<any>;
6566
} | null>(
66-
`*[_type == "guest" && _id == $id][0]{ _id, title, socials, websites }`,
67-
{ id: guestId },
67+
`*[_type in ["guest", "author"] && (_id == $id || slug.current == $slug)][0]{ _id, _type, title, socials, websites }`,
68+
{ id: guestId, slug: verification.payload?.slug || "" },
6869
);
6970

7071
if (!existingGuest) {

‎apps/site/src/pages/guest-portal/[token].astro‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface EpisodeRef {
2020
2121
interface GuestDoc {
2222
_id: string;
23+
_type: "guest" | "author";
2324
title: string;
2425
slug: string;
2526
avatarUrl?: string;
@@ -42,16 +43,17 @@ let guestDoc: GuestDoc | null = null;
4243
4344
if (isValid && verification.payload) {
4445
guestDoc = await client.fetch<GuestDoc | null>(
45-
`*[_type == "guest" && (_id == $id || slug.current == $slug)][0]{
46+
`*[_type in ["guest", "author"] && (_id == $id || slug.current == $slug)][0]{
4647
_id,
48+
_type,
4749
title,
4850
"slug": slug.current,
4951
"avatarUrl": coverImage.asset->url,
5052
excerpt,
5153
content,
5254
socials,
5355
websites,
54-
"episodes": *[_type == "podcast" && references(^._id) && !(_id in path("drafts.**"))] | order(season desc, episode desc) {
56+
"episodes": *[_type == "podcast" && references(^._id) && !(_id in path("drafts.**"))] | order(season desc, episode desc) [0...10] {
5557
_id,
5658
title,
5759
"slug": slug.current,
@@ -64,6 +66,8 @@ if (isValid && verification.payload) {
6466
);
6567
}
6668
69+
const isAuthor = guestDoc?._type === "author";
70+
const profilePathPrefix = isAuthor ? "author" : "guest";
6771
const socials = guestDoc?.socials || {};
6872
const primaryWebsite =
6973
guestDoc?.websites?.[0]?.link?.href || guestDoc?.websites?.[0]?.site || "";
@@ -168,8 +172,11 @@ const socialFields = [
168172
<div class="mx-auto max-w-4xl px-4 py-10 sm:px-6 lg:px-8">
169173
<Breadcrumbs
170174
links={[
171-
{ title: "Guests", href: "/guests" },
172-
{ title: guestDoc?.title || "Guest Portal" },
175+
{
176+
title: isAuthor ? "Authors" : "Guests",
177+
href: isAuthor ? "/authors/page/1" : "/guests/page/1",
178+
},
179+
{ title: guestDoc?.title || "Profile Portal" },
173180
]}
174181
/>
175182

@@ -212,14 +219,14 @@ const socialFields = [
212219
{guestDoc.title}
213220
</h1>
214221
<p class="text-sm text-muted-foreground">
215-
Public Speaker Profile:{" "}
222+
Public {isAuthor ? "Author" : "Speaker"} Profile:{" "}
216223
<a
217-
href={`/guest/${guestDoc.slug}`}
224+
href={`/${profilePathPrefix}/${guestDoc.slug}`}
218225
target="_blank"
219226
rel="noreferrer"
220227
class="font-medium text-primary underline"
221228
>
222-
codingcat.dev/guest/{guestDoc.slug}
229+
codingcat.dev/{profilePathPrefix}/{guestDoc.slug}
223230
</a>
224231
</p>
225232
</div>

0 commit comments

Comments
 (0)