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",