diff --git a/@theme/blog.page.tsx b/@theme/blog.page.tsx
new file mode 100644
index 00000000..974ddc75
--- /dev/null
+++ b/@theme/blog.page.tsx
@@ -0,0 +1,157 @@
+import * as React from 'react';
+
+import { Route, Routes as DomRoutes, useParams } from 'react-router-dom';
+import { PageLayout } from '@redocly/theme/layouts/PageLayout';
+import { useThemeHooks } from '@redocly/theme/core/hooks';
+
+import { SecondaryPostCard } from '@redocly/marketing-pages/components/Blog/SecondaryPostCard.js';
+
+import { Button } from '@redocly/marketing-pages/components/Button/CustomButton.js';
+import { CallToAction } from '@redocly/marketing-pages/components/CallToAction/CallToAction.js';
+import { FirstThreePosts } from '@redocly/marketing-pages/components/Blog/FirstThreePosts.js';
+import { FeaturedClassics } from '@redocly/marketing-pages/components/Blog/FeaturedClassics.js';
+import { LatestPosts } from '@redocly/marketing-pages/components/Blog/LatestPosts.js';
+import { ContactUs } from '@redocly/marketing-pages/components/Blog/ContactUs.js';
+import { Breadcrumbs } from '@redocly/theme/components/Breadcrumbs/Breadcrumbs';
+import { BreadcrumbItem } from '@redocly/theme/core/types';
+import { H2 } from '@redocly/theme/components/Typography/H2';
+import styled from 'styled-components';
+
+export default function BlogRoutes() {
+ return (
+
+
+ } />
+ } />
+ } />
+
+
+ );
+}
+
+// Blog main page component
+function BlogMain() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const PageWrapper = styled.div`
+ position: relative;
+ overflow: hidden;
+`;
+
+// Category page component
+function CategoryPage() {
+ const { category, subcategory } = useParams();
+ // @ts-ignore
+ const { usePageSharedData } = useThemeHooks();
+ const { posts, metadata } = usePageSharedData('blog-posts');
+
+ const postList = React.useMemo(() => {
+ return posts.filter((post) => {
+ if (!post.categories || post.categories.length === 0) {
+ return false;
+ }
+ return post.categories.some((postCategory) => {
+ if (
+ subcategory &&
+ postCategory.subcategory &&
+ postCategory.subcategory.id === subcategory &&
+ postCategory.category.id === category
+ ) {
+ return true;
+ } else if (postCategory.category.id === category && !subcategory) {
+ return true;
+ }
+ return false;
+ });
+ });
+ }, [posts, category, subcategory]);
+
+ const categoryLabel = React.useMemo(() => {
+ if (!metadata?.categories) return category;
+ const categoryData = metadata.categories.find(cat => cat.id === category);
+ return categoryData?.label || category;
+ }, [metadata, category]);
+
+ const subcategoryLabel = React.useMemo(() => {
+ if (!subcategory || !metadata?.categories) return subcategory;
+ const categoryData = metadata.categories.find(cat => cat.id === category);
+ const subcategoryData = categoryData?.subcategories?.find(sub => sub.id === subcategory);
+ return subcategoryData?.label || subcategory;
+ }, [metadata, category, subcategory]);
+
+ const breadcrumbItems: BreadcrumbItem[] = subcategory
+ ? [
+ { label: 'Blog', link: '/blog' },
+ { label: categoryLabel || '', link: `/blog/category/${category}` },
+ { label: subcategoryLabel || '', link: `/blog/category/${category}/${subcategory}` },
+ ]
+ : [
+ { label: 'Blog', link: '/blog' },
+ { label: categoryLabel || '', link: `/blog/category/${category}` },
+ ];
+
+ return (
+
+
+ {subcategory ? subcategoryLabel : categoryLabel}
+
+ {postList.map((post) => (
+
+ ))}
+
+
+ );
+}
+
+const StyledH2 = styled(H2)`
+ font-family: 'Red Hat Display';
+ font-weight: 700;
+ font-size: 54px;
+ line-height: 64px;
+
+ margin: 0;
+
+`;
+
+const CardsWrapper = styled.div`
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+ gap: 20px;
+`;
+
+const CategoryWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ margin: 96px auto 160px;
+ max-width: 1032px;
+`;
\ No newline at end of file
diff --git a/@theme/plugin.js b/@theme/plugin.js
index 1b59696c..38723d23 100644
--- a/@theme/plugin.js
+++ b/@theme/plugin.js
@@ -27,6 +27,10 @@ export default function themePlugin() {
'preview-template',
fromCurrentDir(import.meta.url, './preview.route.tsx')
);
+ const blogTemplateId = actions.createTemplate(
+ 'blog-template',
+ fromCurrentDir(import.meta.url, './blog.page.tsx')
+ );
actions.addRoute({
excludeFromSidebar: true,
slug: '/preview',
@@ -47,6 +51,12 @@ export default function themePlugin() {
};
},
});
+ actions.addRoute({
+ slug: '/blog/',
+ fsPath: '/blog/',
+ templateId: blogTemplateId,
+ hasClientRoutes: true,
+ });
},
async afterRoutesCreated(actions, context) {
// Existing blog data processing
diff --git a/@theme/utils/blog-post.js b/@theme/utils/blog-post.js
index c2611062..187913a8 100644
--- a/@theme/utils/blog-post.js
+++ b/@theme/utils/blog-post.js
@@ -23,7 +23,21 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
slug: route.slug,
author: metadata.authors.get(frontmatter.author),
categories: (frontmatter.categories || [])
- .map((categoryId) => metadata.categories.get(categoryId))
+ .map((categoryId) => {
+ const categoryData = metadata.categories.get(categoryId);
+ if (!categoryData) return null;
+
+ if (categoryData.category && categoryData.subcategory) {
+ return categoryData;
+ } else {
+ return {
+ category: {
+ id: categoryData.id,
+ label: categoryData.label
+ }
+ };
+ }
+ })
.filter(Boolean),
image:
frontmatter.image &&
@@ -46,8 +60,27 @@ async function transformMetadata(metadata, cwd, outdir) {
});
}
+ // Mapping category and subcategory
for (const category of metadata.categories) {
+ // Store main category as-is (for posts with just main categories)
categories.set(category.id, category);
+
+ // Store subcategories with both category and subcategory objects
+ if (category.subcategories) {
+ for (const subcategory of category.subcategories) {
+ const fullId = `${category.id}:${subcategory.id}`;
+ categories.set(fullId, {
+ category: {
+ id: category.id,
+ label: category.label
+ },
+ subcategory: {
+ id: subcategory.id,
+ label: subcategory.label
+ }
+ });
+ }
+ }
}
return { authors, categories };
diff --git a/blog/index.page.tsx b/blog/index.page.tsx
index ee55d456..6412ef63 100644
--- a/blog/index.page.tsx
+++ b/blog/index.page.tsx
@@ -1,34 +1,8 @@
import React from 'react';
-import styled from 'styled-components';
-
-import { Button } from '@redocly/marketing-pages/components/Button/CustomButton.js';
-import { CallToAction } from '@redocly/marketing-pages/components/CallToAction/CallToAction.js';
-import { FirstThreePosts } from '@redocly/marketing-pages/components/Blog/FirstThreePosts.js';
-import { FeaturedClassics } from '@redocly/marketing-pages/components/Blog/FeaturedClassics.js';
-import { LatestPosts } from '@redocly/marketing-pages/components/Blog/LatestPosts.js';
-import { ContactUs } from '@redocly/marketing-pages/components/Blog/ContactUs.js';
+import BlogRoutes from '../@theme/blog.page';
export default function Blog() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
);
}
-
-const PageWrapper = styled.div`
- position: relative;
- overflow: hidden;
-`;
diff --git a/blog/metadata/blog-metadata.yaml b/blog/metadata/blog-metadata.yaml
index 74b6f31e..9c32f94c 100644
--- a/blog/metadata/blog-metadata.yaml
+++ b/blog/metadata/blog-metadata.yaml
@@ -40,33 +40,184 @@ authors:
authorBIO: SVP of Marketing, Redocly
image: matt-williams.png
categories:
- - id: api
- label: API
- - id: company-update
- label: Company update
- - id: openapi
- label: OpenAPI
- id: api-catalog
label: API catalog
- - id: api-design
- label: API design
- - id: api-first
- label: API first
- - id: api-marketing
- label: API marketing
- - id: dev-portal
+ subcategories:
+ - id: discovery
+ label: Discovery
+ - id: entity-automation
+ label: Entity automation
+ - id: scorecards
+ label: Scorecards
+ - id: service-ownership
+ label: Service ownership
+ - id: dependency-maps
+ label: Dependency maps
+
+ - id: api-documentation
+ label: API documentation
+ subcategories:
+ - id: api-seo
+ label: API SEO
+ - id: reference-docs
+ label: Reference docs
+ - id: tutorials-guides
+ label: Tutorials & guides
+ - id: versioning-comms
+ label: Versioning communication
+ - id: documentation-strategy
+ label: Documentation strategy
+
+ - id: api-governance
+ label: API governance
+ subcategories:
+ - id: linting-rulesets
+ label: Linting & rulesets
+ - id: breaking-changes
+ label: Breaking changes
+ - id: review-workflows
+ label: Review workflows
+ - id: lifecycle-gates
+ label: Lifecycle gates
+ - id: compliance-quality
+ label: Compliance & quality
+
+ - id: api-lifecycle
+ label: API lifecycle
+ subcategories:
+ - id: design
+ label: Design
+ - id: mocking
+ label: Mocking
+ - id: release-management
+ label: Release management
+ - id: deprecation-sunsetting
+ label: Deprecation & sunsetting
+
+ - id: api-monitoring
+ label: API monitoring
+ subcategories:
+ - id: synthetic-monitoring
+ label: Synthetic monitoring
+ - id: workflow-monitoring
+ label: Workflow monitoring
+ - id: performance-metrics
+ label: Performance metrics
+
+ - id: api-specifications
+ label: API specifications
+ subcategories:
+ - id: openapi
+ label: OpenAPI
+ - id: arazzo
+ label: Arazzo
+ - id: asyncapi
+ label: AsyncAPI
+ - id: json-schema
+ label: JSON Schema
+ - id: graphql-sdl
+ label: GraphQL SDL
+ - id: contract-patterns
+ label: Contract patterns
+ - id: modeling-best-practices
+ label: Modeling best practices
+
+ - id: api-testing
+ label: API testing
+ subcategories:
+ - id: contract-testing
+ label: Contract testing
+ - id: integration-testing
+ label: Integration testing
+ - id: mock-servers
+ label: Mock servers
+ - id: workflow-testing
+ label: Workflow testing
+
+ - id: api-versioning
+ label: API versioning
+ subcategories:
+ - id: semantic-versioning
+ label: Semantic versioning
+ - id: design-time-versioning
+ label: Design-time versioning
+ - id: runtime-compatibility
+ label: Runtime compatibility
+ - id: migrations
+ label: Migrations
+
+ - id: developer-portal
label: Developer portal
+ subcategories:
+ - id: information-architecture
+ label: Information architecture
+ - id: navigation-patterns
+ label: Navigation patterns
+ - id: search
+ label: Search
+ - id: branding-theming
+ label: Branding & theming
+ - id: multi-product-experiences
+ label: Multi-product experiences
+
- id: docs-as-code
label: Docs-as-code
- - id: learning
- label: Learning
- - id: tech-writers
- label: Technical writers
- - id: developer-experience
- label: Developer experience
- - id: teamwork
- label: Teamwork
- - id: season-of-docs
- label: Google Season of Docs
- - id: open-source
- label: Open source
+ subcategories:
+ - id: content-reuse
+ label: Content reuse
+ - id: variables-includes
+ label: Variables & includes
+ - id: git-workflows
+ label: Git workflows
+ - id: preview-environments
+ label: Preview environments
+
+ - id: internal-developer-portal
+ label: Internal developer portal
+ subcategories:
+ - id: service-catalog
+ label: Service catalog
+ - id: golden-paths
+ label: Golden paths
+ - id: scorecards-idp
+ label: Scorecards
+ - id: governance-signals
+ label: Governance signals
+ - id: dependency-maps-idp
+ label: Dependency maps
+
+ - id: redocly
+ label: Redocly
+ subcategories:
+ - id: redoc
+ label: Redoc
+ - id: redocly-cli
+ label: Redocly CLI
+ - id: recheck
+ label: Recheck
+ - id: realm
+ label: Realm
+ - id: reef
+ label: Reef
+ - id: reunite
+ label: Reunite
+ - id: pricing-usage
+ label: Pricing & usage
+ - id: product-updates
+ label: Product updates
+
+ - id: technical-documentation
+ label: Technical documentation
+ subcategories:
+ - id: conceptual-documentation
+ label: Conceptual documentation
+ - id: tutorials-onboarding
+ label: Tutorials & onboarding
+ - id: information-architecture-td
+ label: Information architecture
+ - id: writing-style
+ label: Writing style
+ - id: docs-ux
+ label: Documentation UX
+ - id: ai-assisted-docs
+ label: AI-assisted documentation
diff --git a/package.json b/package.json
index 297fd1fc..217e358c 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"author": "team@redocly.com",
"license": "UNLICENSED",
"dependencies": {
- "@redocly/marketing-pages": "0.1.37",
+ "@redocly/marketing-pages": "0.1.38",
"@redocly/realm": "0.128.0-next.0",
"buffer": "^6.0.3",
"highlight-words-core": "^1.2.3",