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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user