Skip to content

Commit 2c74f46

Browse files
committed
refactor: add blur image component
1 parent 3d03464 commit 2c74f46

File tree

13 files changed

+202
-62
lines changed

13 files changed

+202
-62
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -----------------------------------------------------------------------------
2-
# App
2+
# App - Don't add "/" in the end of the url (same in production)
33
# -----------------------------------------------------------------------------
44
NEXT_PUBLIC_APP_URL=http://localhost:3000
55

app/(docs)/docs/[[...slug]]/page.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { allDocs } from "contentlayer/generated";
21
import { notFound } from "next/navigation";
2+
import { allDocs } from "contentlayer/generated";
33

4+
import { getTableOfContents } from "@/lib/toc";
45
import { Mdx } from "@/components/content/mdx-components";
56
import { DocsPageHeader } from "@/components/docs/page-header";
67
import { DocsPager } from "@/components/docs/pager";
78
import { DashboardTableOfContents } from "@/components/shared/toc";
8-
import { getTableOfContents } from "@/lib/toc";
99

1010
import "@/styles/mdx.css";
1111

1212
import { Metadata } from "next";
1313

14-
import { constructMetadata } from "@/lib/utils";
14+
import { constructMetadata, getBlurDataURL } from "@/lib/utils";
1515

1616
interface DocPageProps {
1717
params: {
@@ -60,12 +60,19 @@ export default async function DocPage({ params }: DocPageProps) {
6060

6161
const toc = await getTableOfContents(doc.body.raw);
6262

63+
const images = await Promise.all(
64+
doc.images.map(async (src: string) => ({
65+
src,
66+
blurDataURL: await getBlurDataURL(src),
67+
})),
68+
);
69+
6370
return (
6471
<main className="relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_300px]">
6572
<div className="mx-auto w-full min-w-0">
6673
<DocsPageHeader heading={doc.title} text={doc.description} />
6774
<div className="pb-4 pt-11">
68-
<Mdx code={doc.body.code} />
75+
<Mdx code={doc.body.code} images={images} />
6976
</div>
7077
<hr className="my-4 md:my-6" />
7178
<DocsPager doc={doc} />

app/(marketing)/(blog-post)/blog/[slug]/page.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1-
import { allPosts } from "contentlayer/generated";
21
import { notFound } from "next/navigation";
2+
import { allPosts } from "contentlayer/generated";
33

44
import { Mdx } from "@/components/content/mdx-components";
55

66
import "@/styles/mdx.css";
77

88
import { Metadata } from "next";
9-
import Image from "next/image";
109
import Link from "next/link";
1110

11+
import { BLOG_CATEGORIES } from "@/config/blog";
12+
import { getTableOfContents } from "@/lib/toc";
13+
import {
14+
cn,
15+
constructMetadata,
16+
formatDate,
17+
getBlurDataURL,
18+
placeholderBlurhash,
19+
} from "@/lib/utils";
20+
import { buttonVariants } from "@/components/ui/button";
1221
import Author from "@/components/content/author";
22+
import BlurImage from "@/components/shared/blur-image";
1323
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
1424
import { DashboardTableOfContents } from "@/components/shared/toc";
15-
import { buttonVariants } from "@/components/ui/button";
16-
import { BLOG_CATEGORIES } from "@/config/blog";
17-
import { getTableOfContents } from "@/lib/toc";
18-
import { cn, constructMetadata, formatDate } from "@/lib/utils";
1925

2026
export async function generateStaticParams() {
2127
return allPosts.map((post) => ({
@@ -68,6 +74,16 @@ export default async function PostPage({
6874

6975
const toc = await getTableOfContents(post.body.raw);
7076

77+
const [thumbnailBlurhash, images] = await Promise.all([
78+
getBlurDataURL(post.image),
79+
await Promise.all(
80+
post.images.map(async (src: string) => ({
81+
src,
82+
blurDataURL: await getBlurDataURL(src),
83+
})),
84+
),
85+
]);
86+
7187
return (
7288
<>
7389
<MaxWidthWrapper className="pt-6 md:pt-10">
@@ -111,17 +127,20 @@ export default async function PostPage({
111127
<div className="absolute top-52 w-full border-t" />
112128

113129
<MaxWidthWrapper className="grid grid-cols-4 gap-10 pt-8 max-md:px-0">
114-
<div className="relative col-span-4 mb-10 flex flex-col space-y-8 bg-background sm:border md:rounded-xl lg:col-span-3">
115-
<Image
130+
<div className="relative col-span-4 mb-10 flex flex-col space-y-8 border-y bg-background md:rounded-xl md:border lg:col-span-3">
131+
<BlurImage
132+
alt={post.title}
133+
blurDataURL={thumbnailBlurhash ?? placeholderBlurhash}
116134
className="aspect-[1200/630] border-b object-cover md:rounded-t-xl"
117-
src={post.image}
118135
width={1200}
119136
height={630}
120-
alt={post.title}
121137
priority
138+
placeholder="blur"
139+
src={post.image}
140+
sizes="(max-width: 768px) 770px, 1000px"
122141
/>
123142
<div className="px-[.8rem] pb-10 md:px-8">
124-
<Mdx code={post.body.code} />
143+
<Mdx code={post.body.code} images={images} />
125144
</div>
126145
</div>
127146

app/(marketing)/[slug]/page.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { allPages } from "contentlayer/generated";
21
import { notFound } from "next/navigation";
2+
import { allPages } from "contentlayer/generated";
33

44
import { Mdx } from "@/components/content/mdx-components";
55

66
import "@/styles/mdx.css";
77

88
import { Metadata } from "next";
99

10-
import { constructMetadata } from "@/lib/utils";
10+
import { constructMetadata, getBlurDataURL } from "@/lib/utils";
1111

1212
export async function generateStaticParams() {
1313
return allPages.map((page) => ({
@@ -46,6 +46,13 @@ export default async function PagePage({
4646
notFound();
4747
}
4848

49+
const images = await Promise.all(
50+
page.images.map(async (src: string) => ({
51+
src,
52+
blurDataURL: await getBlurDataURL(src),
53+
})),
54+
);
55+
4956
return (
5057
<article className="container max-w-3xl py-6 lg:py-12">
5158
<div className="space-y-4">
@@ -57,7 +64,7 @@ export default async function PagePage({
5764
)}
5865
</div>
5966
<hr className="my-4" />
60-
<Mdx code={page.body.code} />
67+
<Mdx code={page.body.code} images={images} />
6168
</article>
6269
);
6370
}

app/(marketing)/blog/category/[slug]/page.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { allPosts } from "contentlayer/generated";
21
import { Metadata } from "next";
32
import { notFound } from "next/navigation";
3+
import { allPosts } from "contentlayer/generated";
44

5-
import { BlogCard } from "@/components/content/blog-card";
65
import { BLOG_CATEGORIES } from "@/config/blog";
7-
import { constructMetadata } from "@/lib/utils";
6+
import { constructMetadata, getBlurDataURL } from "@/lib/utils";
7+
import { BlogCard } from "@/components/content/blog-card";
88

99
export async function generateStaticParams() {
1010
return BLOG_CATEGORIES.map((category) => ({
@@ -39,17 +39,21 @@ export default async function BlogCategory({
3939
slug: string;
4040
};
4141
}) {
42-
const data = BLOG_CATEGORIES.find(
43-
(category) => category.slug === params.slug,
44-
);
42+
const category = BLOG_CATEGORIES.find((ctg) => ctg.slug === params.slug);
4543

46-
if (!data) {
44+
if (!category) {
4745
notFound();
4846
}
4947

50-
const articles = allPosts
51-
.filter((post) => post.categories.includes(data.slug))
52-
.sort((a, b) => b.date.localeCompare(a.date));
48+
const articles = await Promise.all(
49+
allPosts
50+
.filter((post) => post.categories.includes(category.slug))
51+
.sort((a, b) => b.date.localeCompare(a.date))
52+
.map(async (post) => ({
53+
...post,
54+
blurDataURL: await getBlurDataURL(post.image),
55+
})),
56+
);
5357

5458
return (
5559
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">

app/(marketing)/blog/page.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import { allPosts } from "contentlayer/generated";
2-
import { compareDesc } from "date-fns";
32

3+
import { constructMetadata, getBlurDataURL } from "@/lib/utils";
44
import { BlogPosts } from "@/components/content/blog-posts";
5-
import { constructMetadata } from "@/lib/utils";
65

76
export const metadata = constructMetadata({
87
title: "Blog – SaaS Starter",
98
description: "Latest news and updates from Next SaaS Starter.",
109
});
1110

1211
export default async function BlogPage() {
13-
const posts = allPosts
14-
.filter((post) => post.published)
15-
.sort((a, b) => {
16-
return compareDesc(new Date(a.date), new Date(b.date));
17-
});
12+
const posts = await Promise.all(
13+
allPosts
14+
.filter((post) => post.published)
15+
.sort((a, b) => b.date.localeCompare(a.date))
16+
.map(async (post) => ({
17+
...post,
18+
blurDataURL: await getBlurDataURL(post.image),
19+
})),
20+
);
1821

1922
return <BlogPosts posts={posts} />;
2023
}

components/content/author.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import Image from "next/image";
21
import Link from "next/link";
32

43
import { BLOG_AUTHORS } from "@/config/blog";
4+
import { getBlurDataURL } from "@/lib/utils";
5+
import BlurImage from "@/components/shared/blur-image";
56

67
export default async function Author({
78
username,
@@ -13,11 +14,14 @@ export default async function Author({
1314
const authors = BLOG_AUTHORS;
1415

1516
return imageOnly ? (
16-
<Image
17+
<BlurImage
1718
src={authors[username].image}
1819
alt={authors[username].name}
1920
width={32}
2021
height={32}
22+
priority
23+
placeholder="blur"
24+
blurDataURL={await getBlurDataURL(authors[username].image!)}
2125
className="size-8 rounded-full transition-all group-hover:brightness-90"
2226
/>
2327
) : (
@@ -27,18 +31,23 @@ export default async function Author({
2731
target="_blank"
2832
rel="noopener noreferrer"
2933
>
30-
<Image
34+
<BlurImage
3135
src={authors[username].image}
3236
alt={authors[username].name}
3337
width={40}
3438
height={40}
39+
priority
40+
placeholder="blur"
41+
blurDataURL={await getBlurDataURL(authors[username].image!)}
3542
className="size-8 rounded-full transition-all group-hover:brightness-90 md:size-10"
3643
/>
3744
<div className="flex flex-col -space-y-0.5">
3845
<p className="font-semibold text-foreground max-md:text-sm">
3946
{authors[username].name}
4047
</p>
41-
<p className="text-sm text-muted-foreground">@{authors[username].twitter}</p>
48+
<p className="text-sm text-muted-foreground">
49+
@{authors[username].twitter}
50+
</p>
4251
</div>
4352
</Link>
4453
);

components/content/blog-card.tsx

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Post } from "contentlayer/generated";
2-
import Image from "next/image";
31
import Link from "next/link";
2+
import { Post } from "contentlayer/generated";
43

5-
import { cn, formatDate } from "@/lib/utils";
4+
import { cn, formatDate, placeholderBlurhash } from "@/lib/utils";
5+
import BlurImage from "@/components/shared/blur-image";
66

77
import Author from "./author";
88

@@ -11,7 +11,9 @@ export function BlogCard({
1111
priority,
1212
horizontale = false,
1313
}: {
14-
data: Post;
14+
data: Post & {
15+
blurDataURL: string;
16+
};
1517
priority?: boolean;
1618
horizontale?: boolean;
1719
}) {
@@ -25,17 +27,22 @@ export function BlogCard({
2527
)}
2628
>
2729
{data.image && (
28-
<Image
29-
alt={data.title}
30-
src={data.image}
31-
width={804}
32-
height={452}
33-
className={cn(
34-
"w-full rounded-xl border object-cover object-center",
35-
horizontale ? "lg:h-72" : null,
36-
)}
37-
priority={priority}
38-
/>
30+
<div className="w-full overflow-hidden rounded-xl border">
31+
<BlurImage
32+
alt={data.title}
33+
blurDataURL={data.blurDataURL ?? placeholderBlurhash}
34+
className={cn(
35+
"size-full object-cover object-center",
36+
horizontale ? "lg:h-72" : null,
37+
)}
38+
width={800}
39+
height={400}
40+
priority={priority}
41+
placeholder="blur"
42+
src={data.image}
43+
sizes="(max-width: 768px) 750px, 600px"
44+
/>
45+
</div>
3946
)}
4047
<div
4148
className={cn(
@@ -54,11 +61,9 @@ export function BlogCard({
5461
)}
5562
</div>
5663
<div className="mt-4 flex items-center space-x-3">
57-
{/* <Author username={data.authors[0]} imageOnly /> */}
58-
5964
<div className="flex items-center -space-x-2">
6065
{data.authors.map((author) => (
61-
<Author username={author} key={data._id+author} imageOnly />
66+
<Author username={author} key={data._id + author} imageOnly />
6267
))}
6368
</div>
6469

components/content/blog-posts.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { Post } from "@/.contentlayer/generated";
22

33
import { BlogCard } from "./blog-card";
44

5-
export function BlogPosts({ posts }: { posts: Post[] }) {
5+
export function BlogPosts({
6+
posts,
7+
}: {
8+
posts: (Post & {
9+
blurDataURL: string;
10+
})[];
11+
}) {
612
return (
713
<main className="space-y-8">
814
<BlogCard data={posts[0]} horizontale priority />

0 commit comments

Comments
 (0)