Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/docs-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ jobs:
!docs/realm/.templates/*
!docs/realm/customization/add-color-mode.md
!docs/realm/customization/eject-components/eject-components-tutorial/index.md
!docs/realm/customization/mcp-server/index.md
52 changes: 52 additions & 0 deletions @theme/components/Blog/RecentPosts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';
import styled from 'styled-components';

import { useThemeHooks } from '@redocly/theme/core/hooks';

import { ArticleCard } from '@redocly/marketing-pages/components/Blog/RecentPosts.js';
import { H2Title } from '@redocly/marketing-pages/components/TypographyElements/TypographyElements.js';

type RecentPost = { slug: string; title: string; description?: string };

// Same layout as marketing-pages RecentPosts, but reads the deeper 'blog-recent-posts'
// shared data and filters out the post it renders on, so a post never lists itself.
export function RecentPosts({ currentSlug }: { currentSlug?: string }) {
// @ts-ignore
const { usePageSharedData } = useThemeHooks();
const recentPosts = usePageSharedData<RecentPost[]>('blog-recent-posts') ?? [];

const posts = recentPosts.filter((post) => post.slug !== currentSlug).slice(0, 3);

if (posts.length === 0) {
return null;
}

return (
<>
<H2Title textAlign={{ base: 'center' }} mb={{ base: '34px', small: '42px' }}>
Latest from our blog
</H2Title>
<RecentPostsGrid>
{posts.map((post) => (
<ArticleCard
key={post.slug}
title={post.title}
description={post.description ?? ''}
slug={post.slug}
/>
))}
</RecentPostsGrid>
</>
);
}

const RecentPostsGrid = styled.div`
display: grid;
grid-template-columns: 1fr;
grid-gap: 5rem;
justify-items: center;

@media screen and (min-width: 900px) {
grid-template-columns: 1fr 1fr 1fr;
}
`;
32 changes: 23 additions & 9 deletions @theme/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const ABOUT_SLUG = '/about/';
const BLOG_METADATA_PATH = 'blog/metadata/blog-metadata.yaml';

const LATEST_POSTS_SHARED_DATA_ID = 'blog-latest-posts';
const RECENT_POSTS_SHARED_DATA_ID = 'blog-recent-posts';
const ALL_POSTS_SHARED_DATA_ID = 'blog-posts';

function __dirname(url) {
Expand All @@ -26,11 +27,11 @@ export default function themePlugin() {
// Register preview route for the editor iframe
const previewTemplateId = actions.createTemplate(
'preview-template',
fromCurrentDir(import.meta.url, './preview.route.tsx')
fromCurrentDir(import.meta.url, './preview.route.tsx'),
);
const blogTemplateId = actions.createTemplate(
'blog-template',
fromCurrentDir(import.meta.url, './blog.page.tsx')
'blog-template',
fromCurrentDir(import.meta.url, './blog.page.tsx'),
);
actions.addRoute({
excludeFromSidebar: true,
Expand Down Expand Up @@ -58,7 +59,7 @@ export default function themePlugin() {
templateId: blogTemplateId,
hasClientRoutes: true,
});

const metadataContentRecord = await context.cache.load(BLOG_METADATA_PATH, 'yaml');
const categories = metadataContentRecord.data.categories || [];

Expand All @@ -84,10 +85,11 @@ export default function themePlugin() {
// Existing blog data processing
const postRoutes = actions
.getAllRoutes()
.filter((route) =>
route.slug.startsWith(BLOG_SLUG) &&
route.slug !== BLOG_SLUG &&
!route.slug.startsWith('/blog/category/')
.filter(
(route) =>
route.slug.startsWith(BLOG_SLUG) &&
route.slug !== BLOG_SLUG &&
!route.slug.startsWith('/blog/category/'),
);

const categoryRoutes = actions
Expand All @@ -98,8 +100,14 @@ export default function themePlugin() {

const latestPosts = postsData.posts.slice(0, 3);

// Top 4 posts, so a post page can exclude itself and still show 3 recent posts
const recentPosts = postsData.posts
.slice(0, 4)
.map(({ slug, title, description }) => ({ slug, title, description }));

// Create shared data for blog pages
await actions.createSharedData(LATEST_POSTS_SHARED_DATA_ID, latestPosts);
await actions.createSharedData(RECENT_POSTS_SHARED_DATA_ID, recentPosts);
await actions.createSharedData(ALL_POSTS_SHARED_DATA_ID, postsData);

// Add latest posts shared data to all blog posts and update metadata
Expand All @@ -110,11 +118,17 @@ export default function themePlugin() {
LATEST_POSTS_SHARED_DATA_ID,
);

actions.addRouteSharedData(
post.slug,
RECENT_POSTS_SHARED_DATA_ID,
RECENT_POSTS_SHARED_DATA_ID,
);

const postRoute = actions.getRouteBySlug(post.slug);

postRoute.metadata = { ...postRoute.metadata, ...post };
}

// Add all posts shared data to category routes
for (const categoryRoute of categoryRoutes) {
actions.addRouteSharedData(
Expand Down
64 changes: 62 additions & 2 deletions @theme/templates/BlogPost.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,63 @@
import Page from '@redocly/marketing-pages/templates/BlogPost.js';
import React from 'react';
import styled from 'styled-components';

export default Page;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to modify Page in theme? It looks a better option to either modify it in the marketing pages or to enhance a specific page with the RecentPosts component. What do you think?

import type { Post } from '@redocly/marketing-pages/components/Blog/types.js';

import { useThemeHooks } from '@redocly/theme/core/hooks';
import { Markdown } from '@redocly/theme/components/Markdown/Markdown';
import PostInfo from '@redocly/marketing-pages/components/Blog/PostInfo.js';
import { MediaBox } from '@redocly/marketing-pages/components/PositionItems/MediaBox.js';
import { Box } from '@redocly/marketing-pages/ui/Box.js';

import { RecentPosts } from '../components/Blog/RecentPosts';

// Local version of @redocly/marketing-pages/templates/BlogPost.js — the only
// difference is the RecentPosts section, which excludes the currently open post.
export default function BlogPost(props: { children?: React.ReactNode }) {
const { usePageProps } = useThemeHooks();
const pageProps = usePageProps();

const { publishedDate, author, categories, title, image, slug } = pageProps.metadata as Post & {
slug?: string;
};

return (
<PageWrapper style={{ background: 'white' }}>
<BlogMediaBox>
<PostInfo
authorName={author?.name}
authorBIO={author?.authorBIO}
publishedDate={publishedDate}
categories={categories}
title={title}
author={author}
avatar={author?.image}
image={image}
/>

<Markdown>{props.children}</Markdown>
</BlogMediaBox>

<MediaBox>
<Box style={{ margin: '80px 0' }}>
<RecentPosts currentSlug={slug} />
</Box>
</MediaBox>
</PageWrapper>
);
}

const BlogMediaBox = styled.div`
margin-left: auto;
margin-right: auto;
max-width: calc(90vw);

@media screen and (min-width: 900px) {
max-width: 800px;
}
`;

const PageWrapper = styled.div`
position: relative;
overflow: hidden;
`;
34 changes: 21 additions & 13 deletions @theme/utils/blog-post.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
const metadata = await transformMetadata(metadataContentRecord.data, context.fs.cwd, outdir);

for (const route of postRoutes) {
const {
data: { content, frontmatter },
} = await context.cache.load(route.fsPath, 'markdown-frontmatter');
// React blog posts export `frontmatter`; markdown posts use YAML frontmatter
const isReactPage = /\.page\.tsx?$/.test(route.fsPath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using regext seems fragile to me.

const { data } = await context.cache.load(
route.fsPath,
isReactPage ? 'react-frontmatter' : 'markdown-frontmatter',
);
const frontmatter = isReactPage ? data : data?.frontmatter;

if (frontmatter?.ignore === true || (await context.isPathIgnored(route.fsPath))) {
if (
(isReactPage && !frontmatter) ||
frontmatter?.ignore === true ||
(await context.isPathIgnored(route.fsPath))
) {
continue;
}

Expand All @@ -26,15 +34,15 @@ export const buildAndSortBlogPosts = async (postRoutes, context, outdir) => {
.map((categoryId) => {
const categoryData = metadata.categories.get(categoryId);
if (!categoryData) return null;

if (categoryData.category && categoryData.subcategory) {
return categoryData;
return categoryData;
} else {
return {
return {
category: {
id: categoryData.id,
label: categoryData.label
}
id: categoryData.id,
label: categoryData.label,
},
};
}
})
Expand Down Expand Up @@ -72,12 +80,12 @@ async function transformMetadata(metadata, cwd, outdir) {
categories.set(fullId, {
category: {
id: category.id,
label: category.label
label: category.label,
},
subcategory: {
id: subcategory.id,
label: subcategory.label
}
label: subcategory.label,
},
});
}
}
Expand Down
Loading
Loading