Skip to content

Commit 541acf5

Browse files
committed
feat(nx-dev): add metrics and related blogs section
1 parent 13b9c23 commit 541acf5

File tree

12 files changed

+289
-81
lines changed

12 files changed

+289
-81
lines changed

nx-dev/data-access-documents/src/lib/blog.api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export class BlogApi {
7575
podcastAppleUrl: frontmatter.podcastAppleUrl,
7676
podcastAmazonUrl: frontmatter.podcastAmazonUrl,
7777
published: frontmatter.published ?? true,
78+
metrics: frontmatter.metrics,
7879
};
7980
const isDevelopment = process.env.NODE_ENV === 'development';
8081
const shouldIncludePost = !frontmatter.draft || isDevelopment;

nx-dev/data-access-documents/src/lib/blog.model.ts

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export type BlogPostDataEntry = {
1818
podcastAppleUrl?: string;
1919
podcastIHeartUrl?: string;
2020
published?: boolean;
21+
ogImage?: string;
22+
ogImageType?: string;
23+
metrics?: Array<{ value: string; label: string }>;
2124
};
2225

2326
export type BlogAuthor = {

nx-dev/nx-dev/app/blog/[slug]/page.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ export default async function BlogPostDetail({
4747
}: BlogPostDetailProps) {
4848
const ctaHeaderConfig = [tryNxCloudForFree];
4949
const blog = await blogApi.getBlogPostBySlug(slug);
50+
const allPosts = await blogApi.getBlogs((p) => !!p.published);
51+
5052
return blog ? (
5153
<>
5254
{/* This empty div is necessary as app router does not automatically scroll on route changes */}
5355
<div></div>
5456
<DefaultLayout headerCTAConfig={ctaHeaderConfig}>
55-
<BlogDetails post={blog} />
57+
<BlogDetails post={blog} allPosts={allPosts} />
5658
</DefaultLayout>
5759
</>
5860
) : null;

nx-dev/ui-blog/src/lib/blog-container.tsx

+9-52
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FeaturedBlogs } from './featured-blogs';
55
import { useEffect, useMemo, useState } from 'react';
66
import { Filters } from './filters';
77
import { useSearchParams } from 'next/navigation';
8+
import { ALL_TOPICS } from './topics';
89
import {
910
ComputerDesktopIcon,
1011
BookOpenIcon,
@@ -21,57 +22,6 @@ export interface BlogContainerProps {
2122
tags: string[];
2223
}
2324

24-
let ALL_TOPICS = [
25-
{
26-
label: 'All',
27-
icon: ListBulletIcon,
28-
value: 'All',
29-
heading: 'All Blogs',
30-
},
31-
{
32-
label: 'Stories',
33-
icon: BookOpenIcon,
34-
value: 'customer story',
35-
heading: 'Customer Stories',
36-
},
37-
{
38-
label: 'Webinars',
39-
icon: ComputerDesktopIcon,
40-
value: 'webinar',
41-
heading: 'Webinars',
42-
},
43-
{
44-
label: 'Podcasts',
45-
icon: MicrophoneIcon,
46-
value: 'podcast',
47-
heading: 'Podcasts',
48-
},
49-
{
50-
label: 'Releases',
51-
icon: CubeIcon,
52-
value: 'release',
53-
heading: 'Release Blogs',
54-
},
55-
{
56-
label: 'Talks',
57-
icon: ChatBubbleOvalLeftEllipsisIcon,
58-
value: 'talk',
59-
heading: 'Talks',
60-
},
61-
{
62-
label: 'Tutorials',
63-
icon: AcademicCapIcon,
64-
value: 'tutorial',
65-
heading: 'Tutorials',
66-
},
67-
{
68-
label: 'Livestreams',
69-
icon: VideoCameraIcon,
70-
value: 'livestream',
71-
heading: 'Livestreams',
72-
},
73-
];
74-
7525
// first five blog posts contain potentially pinned plus the last published ones. They
7626
// should be sorted by date (not just all pinned first)
7727
export function sortFirstFivePosts(
@@ -142,7 +92,14 @@ export function BlogContainer({ blogPosts, tags }: BlogContainerProps) {
14292
</div>
14393
</div>
14494
<FeaturedBlogs blogs={firstFiveBlogs} />
145-
{!!remainingBlogs.length && <MoreBlogs blogs={remainingBlogs} />}
95+
{!!remainingBlogs.length && (
96+
<>
97+
<div className="mx-auto mb-8 mt-20 border-b-2 border-slate-300 pb-3 text-sm dark:border-slate-700">
98+
<h2 className="font-semibold">More blogs</h2>
99+
</div>
100+
<MoreBlogs blogs={remainingBlogs} />
101+
</>
102+
)}
146103
</div>
147104
</main>
148105
);

nx-dev/ui-blog/src/lib/blog-details.tsx

+101-22
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ import Link from 'next/link';
22
import { BlogPostDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
33
import Image from 'next/image';
44
import { BlogAuthors } from './authors';
5-
import { ChevronLeftIcon } from '@heroicons/react/24/outline';
5+
import { ChevronLeftIcon, ListBulletIcon } from '@heroicons/react/24/outline';
66
import { renderMarkdown } from '@nx/nx-dev/ui-markdoc';
77
import { EpisodePlayer } from './episode-player';
88
import { YouTube } from '@nx/nx-dev/ui-common';
9+
import { FeaturedBlogs } from './featured-blogs';
10+
import { MoreBlogs } from './more-blogs';
11+
import { ALL_TOPICS, type Topic } from './topics';
12+
import { Metrics } from '@nx/nx-dev/ui-markdoc';
913

1014
export interface BlogDetailsProps {
1115
post: BlogPostDataEntry;
16+
allPosts: BlogPostDataEntry[];
1217
}
1318

14-
export function BlogDetails({ post }: BlogDetailsProps) {
19+
export function BlogDetails({ post, allPosts }: BlogDetailsProps) {
1520
const { node } = renderMarkdown(post.content, {
1621
filePath: post.filePath ?? '',
1722
headingClass: 'scroll-mt-20',
@@ -23,30 +28,48 @@ export function BlogDetails({ post }: BlogDetailsProps) {
2328
year: 'numeric',
2429
});
2530

31+
// Find the primary topic of the current post
32+
const primaryTopic = ALL_TOPICS.find((topic: Topic) =>
33+
post.tags.includes(topic.value.toLowerCase())
34+
);
35+
36+
const relatedPosts = allPosts
37+
.filter(
38+
(p) =>
39+
p.slug !== post.slug && // Exclude current post
40+
p.tags.some((tag) => post.tags.includes(tag)) // Include posts with matching tags
41+
)
42+
.slice(0, 5);
43+
2644
return (
2745
<main id="main" role="main" className="w-full py-8">
28-
<div className="mx-auto flex max-w-3xl justify-between px-4 lg:px-0">
29-
<Link
30-
href="/blog"
31-
className="flex w-20 shrink-0 items-center gap-2 text-slate-400 hover:text-slate-800 dark:text-slate-600 dark:hover:text-slate-200"
32-
prefetch={false}
33-
>
34-
<ChevronLeftIcon className="h-3 w-3" />
35-
Blog
36-
</Link>
37-
<div className="flex max-w-sm flex-1 grow items-center justify-end gap-2">
38-
<BlogAuthors authors={post.authors} />
39-
<span className="text-sm text-slate-400 dark:text-slate-600">
40-
{formattedDate}
41-
</span>
46+
<div className="mx-auto max-w-screen-md">
47+
{/* Top navigation and author info */}
48+
<div className="mx-auto flex justify-between px-4">
49+
<Link
50+
href="/blog"
51+
className="flex w-20 shrink-0 items-center gap-2 text-slate-400 hover:text-slate-800 dark:text-slate-600 dark:hover:text-slate-200"
52+
prefetch={false}
53+
>
54+
<ChevronLeftIcon className="h-3 w-3" />
55+
Blog
56+
</Link>
57+
<div className="flex max-w-sm flex-1 grow items-center justify-end gap-2">
58+
<BlogAuthors authors={post.authors} />
59+
<span className="text-sm text-slate-400 dark:text-slate-600">
60+
{formattedDate}
61+
</span>
62+
</div>
4263
</div>
43-
</div>
44-
<div id="content-wrapper">
45-
<header className="mx-auto mb-16 mt-8 max-w-3xl px-4 lg:px-0">
64+
65+
{/* Title */}
66+
<header className="mx-auto mb-16 mt-8 px-4">
4667
<h1 className="text-center text-4xl font-semibold text-slate-900 dark:text-white">
4768
{post.title}
4869
</h1>
4970
</header>
71+
72+
{/* Media content (podcast, youtube, or image) */}
5073
{post.podcastYoutubeId && post.podcastSpotifyId ? (
5174
<div className="mx-auto mb-16 w-full max-w-screen-md">
5275
<EpisodePlayer
@@ -74,17 +97,73 @@ export function BlogDetails({ post }: BlogDetailsProps) {
7497
</div>
7598
)
7699
)}
77-
<div className="mx-auto min-w-0 max-w-3xl flex-auto px-4 pb-24 lg:px-0 lg:pb-16">
78-
<div className="relative">
100+
</div>
101+
102+
{/* Main grid layout */}
103+
<div className="mx-auto max-w-7xl px-4 lg:px-8">
104+
<div className="relative isolate grid grid-cols-1 gap-8 xl:grid-cols-[200px_minmax(0,1fr)_200px]">
105+
<div className="hidden min-h-full xl:block">
106+
{post.metrics && (
107+
<div className="sticky top-28 pr-4 pt-8">
108+
<Metrics metrics={post.metrics} variant="vertical" />
109+
</div>
110+
)}
111+
</div>
112+
113+
{/* Middle column - main content */}
114+
<div className="w-full min-w-0 md:mx-auto md:max-w-screen-md">
115+
{post.metrics && (
116+
<div className="mb-8 xl:hidden">
117+
<Metrics metrics={post.metrics} variant="horizontal" />
118+
</div>
119+
)}
79120
<div
80121
data-document="main"
81-
className="prose prose-lg prose-slate dark:prose-invert w-full max-w-none 2xl:max-w-4xl"
122+
className="prose prose-lg prose-slate dark:prose-invert w-full max-w-none"
82123
>
83124
{node}
84125
</div>
85126
</div>
127+
128+
{/* Right column - for future sticky content */}
129+
<div className="hidden xl:block">
130+
<div className="sticky top-24">
131+
{/* Right sidebar content can go here */}
132+
</div>
133+
</div>
86134
</div>
87135
</div>
136+
137+
{/* Related Posts Section */}
138+
{post.tags.length > 0 && relatedPosts.length > 0 && (
139+
<section className="mt-24 border-b border-t border-slate-200 bg-slate-50 py-24 sm:py-32 dark:border-slate-800 dark:bg-slate-900">
140+
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
141+
<div className="mx-auto max-w-2xl lg:mx-0 lg:max-w-none">
142+
<h2 className="mb-8 flex items-center gap-3 text-2xl font-semibold text-slate-900 dark:text-white">
143+
{primaryTopic ? (
144+
<>
145+
<primaryTopic.icon className="h-7 w-7" />
146+
More {primaryTopic.label}
147+
</>
148+
) : (
149+
<>
150+
<ListBulletIcon className="h-7 w-7" />
151+
More Articles
152+
</>
153+
)}
154+
</h2>
155+
{/* Show list view on small screens */}
156+
<div className="md:hidden">
157+
<MoreBlogs blogs={relatedPosts} />
158+
</div>
159+
{/* Show grid view on larger screens */}
160+
<div className="hidden md:block">
161+
<FeaturedBlogs blogs={relatedPosts} />
162+
</div>
163+
</div>
164+
</div>
165+
</section>
166+
)}
88167
</main>
89168
);
90169
}

nx-dev/ui-blog/src/lib/more-blogs.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ export interface MoreBlogsProps {
99
export function MoreBlogs({ blogs }: MoreBlogsProps) {
1010
return (
1111
<>
12-
<div className="mx-auto mb-8 mt-20 border-b-2 border-slate-300 pb-3 text-sm dark:border-slate-700">
13-
<h2 className="font-semibold">More blogs</h2>
14-
</div>
1512
<div className="mx-auto">
1613
{blogs?.map((post) => {
1714
const formattedDate = new Date(post.date).toLocaleDateString(

nx-dev/ui-blog/src/lib/topics.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { FC, SVGProps } from 'react';
2+
import {
3+
ComputerDesktopIcon,
4+
BookOpenIcon,
5+
MicrophoneIcon,
6+
CubeIcon,
7+
AcademicCapIcon,
8+
ChatBubbleOvalLeftEllipsisIcon,
9+
ListBulletIcon,
10+
VideoCameraIcon,
11+
} from '@heroicons/react/24/outline';
12+
13+
export interface Topic {
14+
label: string;
15+
icon: FC<SVGProps<SVGSVGElement>>;
16+
value: string;
17+
heading: string;
18+
}
19+
20+
export const ALL_TOPICS: Topic[] = [
21+
{
22+
label: 'All',
23+
icon: ListBulletIcon,
24+
value: 'All',
25+
heading: 'All Blogs',
26+
},
27+
{
28+
label: 'Stories',
29+
icon: BookOpenIcon,
30+
value: 'customer story',
31+
heading: 'Customer Stories',
32+
},
33+
{
34+
label: 'Webinars',
35+
icon: ComputerDesktopIcon,
36+
value: 'webinar',
37+
heading: 'Webinars',
38+
},
39+
{
40+
label: 'Podcasts',
41+
icon: MicrophoneIcon,
42+
value: 'podcast',
43+
heading: 'Podcasts',
44+
},
45+
{
46+
label: 'Releases',
47+
icon: CubeIcon,
48+
value: 'release',
49+
heading: 'Release Blogs',
50+
},
51+
{
52+
label: 'Talks',
53+
icon: ChatBubbleOvalLeftEllipsisIcon,
54+
value: 'talk',
55+
heading: 'Talks',
56+
},
57+
{
58+
label: 'Tutorials',
59+
icon: AcademicCapIcon,
60+
value: 'tutorial',
61+
heading: 'Tutorials',
62+
},
63+
{
64+
label: 'Livestreams',
65+
icon: VideoCameraIcon,
66+
value: 'livestream',
67+
heading: 'Livestreams',
68+
},
69+
];

nx-dev/ui-common/src/lib/default-layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function DefaultLayout({
1616
headerCTAConfig?: ButtonLinkProps[];
1717
} & PropsWithChildren): JSX.Element {
1818
return (
19-
<div className="w-full overflow-hidden dark:bg-slate-950">
19+
<div className="w-full dark:bg-slate-950">
2020
{!hideHeader && <Header ctaButtons={headerCTAConfig} />}
2121
<div className="relative isolate">
2222
<div

0 commit comments

Comments
 (0)