docs: blog layout update (#8472)

Signed-off-by: Angie Jones <jones.angie@gmail.com>
Co-authored-by: Angie Jones <jones.angie@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Adewale Abati
2026-04-11 01:26:49 +02:00
committed by GitHub
parent 0e743112ea
commit 3f5277538d
44 changed files with 502 additions and 18 deletions
@@ -0,0 +1,19 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Head from '@docusaurus/Head';
import {useBlogListPageStructuredData} from '@docusaurus/plugin-content-blog/client';
export default function BlogListPageStructuredData(props) {
const structuredData = useBlogListPageStructuredData(props);
return (
<Head>
<script type="application/ld+json">
{JSON.stringify(structuredData)}
</script>
</Head>
);
}
@@ -0,0 +1,185 @@
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import useBaseUrl from '@docusaurus/useBaseUrl';
import {
PageMetadata,
HtmlClassNameProvider,
ThemeClassNames,
} from '@docusaurus/theme-common';
import BlogLayout from '@theme/BlogLayout';
import BlogListPaginator from '@theme/BlogListPaginator';
import SearchMetadata from '@theme/SearchMetadata';
import type {Props} from '@theme/BlogListPage';
import BlogListPageStructuredData from '@theme/BlogListPage/StructuredData';
import styles from './styles.module.css';
function BlogListPageMetadata(props: Props): ReactNode {
const {metadata} = props;
const {
siteConfig: {title: siteTitle},
} = useDocusaurusContext();
const {blogDescription, blogTitle, permalink} = metadata;
const isBlogOnlyMode = permalink === '/';
const title = isBlogOnlyMode ? siteTitle : blogTitle;
return (
<>
<PageMetadata title={title} description={blogDescription} />
<SearchMetadata tag="blog_posts_list" />
</>
);
}
const getAuthorName = (author: any): string =>
typeof author === 'string' ? author : (author.name || author.key || author);
function AuthorDisplay({ authors }: { authors: any[] }) {
if (!authors?.length) return null;
const authorsToDisplay = authors.slice(0, 3);
const hasMore = authors.length > 3;
const hasResolvedAuthors = authorsToDisplay.some(author =>
typeof author === 'object' && (author.imageURL || author.image_url)
);
if (hasResolvedAuthors) {
return (
<div className={styles.postAuthors}>
{authorsToDisplay.map((author, index) => (
<div key={index} className={styles.authorInfo}>
{(author.imageURL || author.image_url) && (
<img
src={author.imageURL || author.image_url}
alt={getAuthorName(author)}
className={styles.authorAvatar}
/>
)}
<span className={styles.authorName}>{getAuthorName(author)}</span>
</div>
))}
{hasMore && <span className={styles.authorName}>+{authors.length - 3} more</span>}
</div>
);
}
const authorNames = authorsToDisplay.map(getAuthorName);
const displayText = authorNames.join(', ') + (hasMore ? `, +${authors.length - 3} more` : '');
return (
<div className={styles.postAuthors}>
<span className={styles.authorName}>{displayText}</span>
</div>
);
}
function FeaturedPost({ post }: { post: any }) {
const url = useBaseUrl(post.content.metadata.permalink);
const imageUrl = post.content.frontMatter.image ? useBaseUrl(post.content.frontMatter.image) : null;
const title = post.content.metadata.title;
const formattedDate = post.content.metadata.formattedDate;
const description = post.content.metadata.description || post.content.frontMatter.description;
const authors = post.content?.metadata?.authors || post.content?.frontMatter?.authors || [];
return (
<article className={styles.featuredPost}>
<div className={styles.featuredContent}>
<div className={styles.featuredDate}>{formattedDate}</div>
<h2 className={styles.featuredTitle}>
<a href={url}>{title}</a>
</h2>
<AuthorDisplay authors={authors} />
<div className={styles.featuredDescription}>{description}</div>
<a href={url} className={styles.featuredButton}>Read full article</a>
</div>
{imageUrl && (
<div className={styles.featuredImage}>
<img src={imageUrl} alt={title} />
</div>
)}
</article>
);
}
function BlogPostCard({ post }: { post: any }) {
const url = useBaseUrl(post.content.metadata.permalink);
const imageUrl = post.content.frontMatter.image ? useBaseUrl(post.content.frontMatter.image) : null;
const title = post.content.metadata.title;
const formattedDate = post.content.metadata.formattedDate;
const description = post.content.metadata.description || post.content.frontMatter.description;
const authors = post.content?.metadata?.authors || post.content?.frontMatter?.authors || [];
return (
<article className={styles.postCard}>
{imageUrl && (
<div className={styles.postImage}>
<img src={imageUrl} alt={title} />
</div>
)}
<div className={styles.postContent}>
<div className={styles.postDate}>{formattedDate}</div>
<h3 className={styles.postTitle}>
<a href={url}>{title}</a>
</h3>
<AuthorDisplay authors={authors} />
<div className={styles.postDescription}>{description}</div>
</div>
</article>
);
}
function BlogPostGrid({ posts }: { posts: any[] }) {
return (
<div className={styles.postsGrid}>
{posts.map((post, index) => (
<BlogPostCard key={index} post={post} />
))}
</div>
);
}
function BlogListPageContent(props: Props): ReactNode {
const { metadata, items } = props;
const isFirstPage = !metadata.permalink.includes('/page/');
const validItems = items.filter(item =>
item.content?.metadata?.title && item.content?.frontMatter
);
const featuredPosts = isFirstPage
? validItems.filter(item => item.content.frontMatter.featured === true)
: [];
const regularPosts = isFirstPage
? validItems.filter(item => item !== featuredPosts[0])
: validItems;
return (
<BlogLayout sidebar={undefined}>
<div className={styles.blogContainer}>
{featuredPosts.length > 0 && (
<div className={styles.featuredSection}>
<FeaturedPost post={featuredPosts[0]} />
</div>
)}
{regularPosts.length > 0 && <BlogPostGrid posts={regularPosts} />}
<div className={styles.paginationWrapper}>
<BlogListPaginator metadata={metadata} />
</div>
</div>
</BlogLayout>
);
}
export default function BlogListPage(props: Props): ReactNode {
return (
<HtmlClassNameProvider
className={clsx(
ThemeClassNames.wrapper.blogPages,
ThemeClassNames.page.blogListPage,
)}>
<BlogListPageMetadata {...props} />
<BlogListPageStructuredData {...props} />
<BlogListPageContent {...props} />
</HtmlClassNameProvider>
);
}
@@ -0,0 +1,258 @@
/* Magazine-style Blog Layout */
.blogContainer {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.featuredSection {
margin-bottom: 3rem;
}
.featuredPost {
background: var(--ifm-card-background-color);
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: row;
gap: 2rem;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
margin-bottom: 2rem;
}
.featuredPost:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.featuredContent {
flex: 1;
display: flex;
flex-direction: column;
}
.featuredImage {
flex: 0 0 350px;
order: 2;
display: flex;
}
.featuredImage img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.featuredDate {
font-size: 0.9rem;
color: var(--ifm-color-content-secondary);
margin-bottom: 0.5rem;
}
.featuredTitle {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 0.75rem;
color: var(--ifm-color-content);
line-height: 1.2;
}
.featuredTitle a {
text-decoration: none;
color: inherit;
}
.featuredTitle a:hover {
color: var(--ifm-color-primary);
}
.postAuthors {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 1rem;
}
.authorInfo {
display: flex;
align-items: center;
gap: 0.25rem;
}
.authorAvatar {
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid var(--ifm-color-emphasis-200);
}
.authorName {
font-size: 0.85rem;
color: var(--ifm-color-content-secondary);
}
.featuredDescription {
color: var(--ifm-color-content-secondary);
line-height: 1.6;
margin-bottom: 1.5rem;
flex-grow: 1;
}
.featuredButton {
align-self: flex-start;
background: var(--ifm-color-primary);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: background-color 0.3s ease;
width: fit-content;
}
.featuredButton:hover {
background: var(--ifm-color-primary-dark);
color: white;
text-decoration: none;
}
.postsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.postCard {
background: var(--ifm-card-background-color);
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
display: flex;
flex-direction: column;
height: 100%;
}
.postCard:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.postImage {
width: 100%;
height: 200px;
overflow: hidden;
}
.postImage img {
width: 100%;
height: 100%;
object-fit: cover;
}
.postContent {
padding: 1.5rem;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.postDate {
font-size: 0.85rem;
color: var(--ifm-color-content-secondary);
margin-bottom: 0.5rem;
}
.postTitle {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.75rem;
color: var(--ifm-color-content);
line-height: 1.3;
}
.postTitle a {
text-decoration: none;
color: inherit;
}
.postTitle a:hover {
color: var(--ifm-color-primary);
}
.postDescription {
color: var(--ifm-color-content-secondary);
line-height: 1.6;
margin-bottom: 1rem;
flex-grow: 1;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.paginationWrapper {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--ifm-color-emphasis-200);
}
/* Responsive Design */
@media (max-width: 996px) {
.postsGrid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.featuredPost {
flex-direction: column;
gap: 1.5rem;
}
.featuredImage {
order: 0;
flex: none;
}
.featuredImage img {
height: 250px;
}
}
@media (max-width: 768px) {
.postsGrid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.blogContainer {
padding: 0 0.5rem;
}
.featuredPost {
padding: 1.5rem;
}
.featuredTitle {
font-size: 1.5rem;
}
}
@media (max-width: 480px) {
.postContent {
padding: 1rem;
}
.featuredPost {
padding: 1rem;
}
}