Skip to content

Commit f5fdebd

Browse files
committed
feat: add blog category pages
1 parent 9f169bc commit f5fdebd

File tree

6 files changed

+380
-55
lines changed

6 files changed

+380
-55
lines changed

@theme/blog.page.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import * as React from 'react';
2+
3+
import { Route, Routes as DomRoutes, useParams } from 'react-router-dom';
4+
import { PageLayout } from '@redocly/theme/layouts/PageLayout';
5+
import { useThemeHooks } from '@redocly/theme/core/hooks';
6+
7+
import { SecondaryPostCard } from '@redocly/marketing-pages/components/Blog/SecondaryPostCard.js';
8+
9+
import { Button } from '@redocly/marketing-pages/components/Button/CustomButton.js';
10+
import { CallToAction } from '@redocly/marketing-pages/components/CallToAction/CallToAction.js';
11+
import { FirstThreePosts } from '@redocly/marketing-pages/components/Blog/FirstThreePosts.js';
12+
import { FeaturedClassics } from '@redocly/marketing-pages/components/Blog/FeaturedClassics.js';
13+
import { LatestPosts } from '@redocly/marketing-pages/components/Blog/LatestPosts.js';
14+
import { ContactUs } from '@redocly/marketing-pages/components/Blog/ContactUs.js';
15+
import { Breadcrumbs } from '@redocly/theme/components/Breadcrumbs/Breadcrumbs';
16+
import { BreadcrumbItem } from '@redocly/theme/core/types';
17+
import { H2 } from '@redocly/theme/components/Typography/H2';
18+
import styled from 'styled-components';
19+
20+
export default function BlogRoutes() {
21+
return (
22+
<PageLayout>
23+
<DomRoutes>
24+
<Route path="/" element={<BlogMain />} />
25+
<Route path="/category/:category" element={<CategoryPage />} />
26+
<Route path="/category/:category/:subcategory/" element={<CategoryPage />} />
27+
</DomRoutes>
28+
</PageLayout>
29+
);
30+
}
31+
32+
// Blog main page component
33+
function BlogMain() {
34+
return (
35+
<PageWrapper>
36+
<FirstThreePosts />
37+
38+
<FeaturedClassics />
39+
40+
<LatestPosts />
41+
42+
<ContactUs />
43+
44+
<CallToAction title="Launch API docs you'll be proud of">
45+
<Button
46+
to="https://auth.cloud.redocly.com/registration"
47+
size="large"
48+
className="landing-button"
49+
>
50+
Start 30-day free trial
51+
</Button>
52+
</CallToAction>
53+
</PageWrapper>
54+
);
55+
}
56+
57+
const PageWrapper = styled.div`
58+
position: relative;
59+
overflow: hidden;
60+
`;
61+
62+
// Category page component
63+
function CategoryPage() {
64+
const { category, subcategory } = useParams();
65+
// @ts-ignore
66+
const { usePageSharedData } = useThemeHooks();
67+
const { posts, metadata } = usePageSharedData<any>('blog-posts');
68+
69+
const postList = React.useMemo(() => {
70+
return posts.filter((post) => {
71+
if (!post.categories || post.categories.length === 0) {
72+
return false;
73+
}
74+
return post.categories.some((postCategory) => {
75+
if (
76+
subcategory &&
77+
postCategory.subcategory &&
78+
postCategory.subcategory.id === subcategory &&
79+
postCategory.category.id === category
80+
) {
81+
return true;
82+
} else if (postCategory.category.id === category && !subcategory) {
83+
return true;
84+
}
85+
return false;
86+
});
87+
});
88+
}, [posts, category, subcategory]);
89+
90+
const categoryLabel = React.useMemo(() => {
91+
if (!metadata?.categories) return category;
92+
const categoryData = metadata.categories.find(cat => cat.id === category);
93+
return categoryData?.label || category;
94+
}, [metadata, category]);
95+
96+
const subcategoryLabel = React.useMemo(() => {
97+
if (!subcategory || !metadata?.categories) return subcategory;
98+
const categoryData = metadata.categories.find(cat => cat.id === category);
99+
const subcategoryData = categoryData?.subcategories?.find(sub => sub.id === subcategory);
100+
return subcategoryData?.label || subcategory;
101+
}, [metadata, category, subcategory]);
102+
103+
const breadcrumbItems: BreadcrumbItem[] = subcategory
104+
? [
105+
{ label: 'Blog', link: '/blog' },
106+
{ label: categoryLabel || '', link: `/blog/category/${category}` },
107+
{ label: subcategoryLabel || '', link: `/blog/category/${category}/${subcategory}` },
108+
]
109+
: [
110+
{ label: 'Blog', link: '/blog' },
111+
{ label: categoryLabel || '', link: `/blog/category/${category}` },
112+
];
113+
114+
return (
115+
<CategoryWrapper>
116+
<Breadcrumbs additionalBreadcrumbs={breadcrumbItems} />
117+
<StyledH2>{subcategory ? subcategoryLabel : categoryLabel}</StyledH2>
118+
<CardsWrapper>
119+
{postList.map((post) => (
120+
<SecondaryPostCard
121+
key={post.slug}
122+
title={post.title}
123+
to={`${post.slug}`}
124+
img={post.image}
125+
description={post.description}
126+
date={post.date}
127+
author={post.author}
128+
/>
129+
))}
130+
</CardsWrapper>
131+
</CategoryWrapper>
132+
);
133+
}
134+
135+
const StyledH2 = styled(H2)`
136+
font-family: 'Red Hat Display';
137+
font-weight: 700;
138+
font-size: 54px;
139+
line-height: 64px;
140+
141+
margin: 0;
142+
143+
`;
144+
145+
const CardsWrapper = styled.div`
146+
display: grid;
147+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
148+
gap: 20px;
149+
`;
150+
151+
const CategoryWrapper = styled.div`
152+
display: flex;
153+
flex-direction: column;
154+
gap: 32px;
155+
margin: 96px auto 160px;
156+
max-width: 1032px;
157+
`;

@theme/plugin.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export default function themePlugin() {
2727
'preview-template',
2828
fromCurrentDir(import.meta.url, './preview.route.tsx')
2929
);
30+
const blogTemplateId = actions.createTemplate(
31+
'blog-template',
32+
fromCurrentDir(import.meta.url, './blog.page.tsx')
33+
);
3034
actions.addRoute({
3135
excludeFromSidebar: true,
3236
slug: '/preview',
@@ -47,6 +51,12 @@ export default function themePlugin() {
4751
};
4852
},
4953
});
54+
actions.addRoute({
55+
slug: '/blog/',
56+
fsPath: '/blog/',
57+
templateId: blogTemplateId,
58+
hasClientRoutes: true,
59+
});
5060
},
5161
async afterRoutesCreated(actions, context) {
5262
// Existing blog data processing

@theme/utils/blog-post.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,21 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
2323
slug: route.slug,
2424
author: metadata.authors.get(frontmatter.author),
2525
categories: (frontmatter.categories || [])
26-
.map((categoryId) => metadata.categories.get(categoryId))
26+
.map((categoryId) => {
27+
const categoryData = metadata.categories.get(categoryId);
28+
if (!categoryData) return null;
29+
30+
if (categoryData.category && categoryData.subcategory) {
31+
return categoryData;
32+
} else {
33+
return {
34+
category: {
35+
id: categoryData.id,
36+
label: categoryData.label
37+
}
38+
};
39+
}
40+
})
2741
.filter(Boolean),
2842
image:
2943
frontmatter.image &&
@@ -46,8 +60,27 @@ async function transformMetadata(metadata, cwd, outdir) {
4660
});
4761
}
4862

63+
// Mapping category and subcategory
4964
for (const category of metadata.categories) {
65+
// Store main category as-is (for posts with just main categories)
5066
categories.set(category.id, category);
67+
68+
// Store subcategories with both category and subcategory objects
69+
if (category.subcategories) {
70+
for (const subcategory of category.subcategories) {
71+
const fullId = `${category.id}:${subcategory.id}`;
72+
categories.set(fullId, {
73+
category: {
74+
id: category.id,
75+
label: category.label
76+
},
77+
subcategory: {
78+
id: subcategory.id,
79+
label: subcategory.label
80+
}
81+
});
82+
}
83+
}
5184
}
5285

5386
return { authors, categories };

blog/index.page.tsx

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,8 @@
11
import React from 'react';
2-
import styled from 'styled-components';
3-
4-
import { Button } from '@redocly/marketing-pages/components/Button/CustomButton.js';
5-
import { CallToAction } from '@redocly/marketing-pages/components/CallToAction/CallToAction.js';
6-
import { FirstThreePosts } from '@redocly/marketing-pages/components/Blog/FirstThreePosts.js';
7-
import { FeaturedClassics } from '@redocly/marketing-pages/components/Blog/FeaturedClassics.js';
8-
import { LatestPosts } from '@redocly/marketing-pages/components/Blog/LatestPosts.js';
9-
import { ContactUs } from '@redocly/marketing-pages/components/Blog/ContactUs.js';
2+
import BlogRoutes from '../@theme/blog.page';
103

114
export default function Blog() {
125
return (
13-
<PageWrapper>
14-
<FirstThreePosts />
15-
16-
<FeaturedClassics />
17-
18-
<LatestPosts />
19-
20-
<ContactUs />
21-
22-
<CallToAction title="Launch API docs you’ll be proud of">
23-
<Button to="https://auth.cloud.redocly.com/registration" size="large" className="landing-button">
24-
Start 30-day free trial
25-
</Button>
26-
</CallToAction>
27-
</PageWrapper>
6+
<BlogRoutes />
287
);
298
}
30-
31-
const PageWrapper = styled.div`
32-
position: relative;
33-
overflow: hidden;
34-
`;

0 commit comments

Comments
 (0)