You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by bz...@apache.org on 2022/07/25 08:54:35 UTC
[apisix-website] branch master updated: refactor: blog pages (#1236)
This is an automated email from the ASF dual-hosted git repository.
bzp2010 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-website.git
The following commit(s) were added to refs/heads/master by this push:
new a1635086a42 refactor: blog pages (#1236)
a1635086a42 is described below
commit a1635086a42fc9cc159a990d82c75303322942e5
Author: Young <is...@outlook.com>
AuthorDate: Mon Jul 25 16:54:31 2022 +0800
refactor: blog pages (#1236)
---
blog/en/docusaurus.config.js | 7 +-
blog/package.json | 2 +
blog/src/theme/BlogLayout/index.tsx | 79 ++++++++
blog/src/theme/BlogLayout/style.module.scss | 16 ++
blog/src/theme/BlogListPage/index.tsx | 156 ++++++++++++++++
blog/src/theme/BlogListPage/style.module.scss | 202 +++++++++++++++++++++
blog/src/theme/BlogListPaginator/index.tsx | 64 +++++++
blog/src/theme/BlogListPaginator/style.module.scss | 3 +
blog/src/theme/BlogPostAuthor/index.tsx | 49 +++++
blog/src/theme/BlogPostAuthor/styles.module.css | 12 ++
blog/src/theme/BlogPostAuthors/index.tsx | 36 ++++
blog/src/theme/BlogPostAuthors/styles.module.css | 11 ++
blog/src/theme/BlogPostItem/index.tsx | 152 ++++++++++++++++
blog/src/theme/BlogPostItem/styles.module.css | 18 ++
blog/src/theme/BlogPostPage/index.tsx | 4 +-
blog/src/theme/BlogTagsPostsPage/index.tsx | 182 +++++++++++++++++++
blog/src/theme/BlogTagsPostsPage/style.module.scss | 130 +++++++++++++
blog/static/img/default-blog-header.jpg | Bin 0 -> 57281 bytes
blog/zh/docusaurus.config.js | 4 +-
website/static/img/default-blog-header.jpg | Bin 0 -> 57281 bytes
yarn.lock | 33 +++-
21 files changed, 1150 insertions(+), 10 deletions(-)
diff --git a/blog/en/docusaurus.config.js b/blog/en/docusaurus.config.js
index 63af19daa7a..de662259875 100644
--- a/blog/en/docusaurus.config.js
+++ b/blog/en/docusaurus.config.js
@@ -31,8 +31,8 @@ module.exports = {
{
docs: false,
blog: {
- blogSidebarTitle: 'All posts',
- blogSidebarCount: 'ALL',
+ blogSidebarCount: 0,
+ postsPerPage: 9,
},
theme: {
customCss: require.resolve('./src/css/customTheme.css'),
@@ -82,8 +82,7 @@ module.exports = {
metadatas: [
{
name: 'description',
- content:
- 'Open Source and Cloud-Native API gateway, based on the Nginx library and etcd.',
+ content: 'Open Source and Cloud-Native API gateway, based on the Nginx library and etcd.',
},
{
name: 'robots',
diff --git a/blog/package.json b/blog/package.json
index cb64277f8d5..25adff0505d 100644
--- a/blog/package.json
+++ b/blog/package.json
@@ -34,6 +34,7 @@
"@iconify/icons-akar-icons": "^1.2.8",
"@iconify/react": "^3.2.2",
"@use-gesture/react": "^10.2.16",
+ "avvvatars-react": "^0.4.2",
"change-case": "^4.1.2",
"clsx": "^1.1.1",
"docusaurus-plugin-sass": "^0.2.2",
@@ -45,6 +46,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-lazy-load-image-component": "^1.5.4",
+ "react-share": "^4.4.0",
"react-spring": "^9.4.5",
"react-transition-group": "^4.4.1",
"react-use": "^17.4.0",
diff --git a/blog/src/theme/BlogLayout/index.tsx b/blog/src/theme/BlogLayout/index.tsx
new file mode 100644
index 00000000000..b9b335a4440
--- /dev/null
+++ b/blog/src/theme/BlogLayout/index.tsx
@@ -0,0 +1,79 @@
+/* eslint-disable react/prop-types */
+
+/**
+ * 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 clsx from 'clsx';
+import Layout from '@theme/Layout';
+import BlogSidebar from '@theme/BlogSidebar';
+import TOC from '@theme/TOC';
+import {
+ LinkedinIcon,
+ LinkedinShareButton,
+ RedditIcon,
+ RedditShareButton,
+ TwitterIcon,
+ TwitterShareButton,
+} from 'react-share';
+
+import type { Props } from '@theme/BlogLayout';
+import style from './style.module.scss';
+
+const Share = ({ metadata }) => {
+ const { title, description, permalink } = metadata;
+ const url = `https://apisix.apache.org${permalink}`;
+ return (
+ <section className={style.shareSection}>
+ <h4>Share</h4>
+ <div>
+ <LinkedinShareButton title={title} summary={description} url={url}>
+ <LinkedinIcon size={32} round />
+ </LinkedinShareButton>
+ <TwitterShareButton url={url} title={title} via="ApacheAPISIX">
+ <TwitterIcon size={32} round />
+ </TwitterShareButton>
+ <RedditShareButton title={title} url={url}>
+ <RedditIcon size={32} round />
+ </RedditShareButton>
+ </div>
+ </section>
+ );
+};
+
+const BlogLayout = (props: Props): JSX.Element => {
+ const {
+ sidebar, toc, children, metadata, ...layoutProps
+ } = props;
+ const hasSidebar = sidebar && sidebar.items.length > 0;
+
+ return (
+ <Layout {...layoutProps}>
+ <div className="container margin-vert--lg">
+ <div className="row">
+ {hasSidebar && (
+ <aside className="col col--3">
+ <BlogSidebar sidebar={sidebar!} />
+ </aside>
+ )}
+ <div className={clsx({ col: true, 'col--10': toc })}>{children}</div>
+ {toc && (
+ <div className={clsx('col col--2', style.section)}>
+ {metadata && <Share metadata={metadata} />}
+ <section className={style.tocSection}>
+ <h4>Table of Contents</h4>
+ <TOC toc={toc} style={{ position: 'static' }} />
+ </section>
+ </div>
+ )}
+ </div>
+ </div>
+ </Layout>
+ );
+};
+
+export default BlogLayout;
diff --git a/blog/src/theme/BlogLayout/style.module.scss b/blog/src/theme/BlogLayout/style.module.scss
new file mode 100644
index 00000000000..71ca2f3c582
--- /dev/null
+++ b/blog/src/theme/BlogLayout/style.module.scss
@@ -0,0 +1,16 @@
+.shareSection {
+ button {
+ margin-right: 0.5rem;
+ }
+}
+
+.tocSection {
+ margin-top: 1rem;
+}
+
+.section {
+ position: sticky;
+ top: calc(var(--ifm-navbar-height) + 1rem);
+ height: fit-content;
+ margin-left: 1rem;
+}
diff --git a/blog/src/theme/BlogListPage/index.tsx b/blog/src/theme/BlogListPage/index.tsx
new file mode 100644
index 00000000000..728af4c97b0
--- /dev/null
+++ b/blog/src/theme/BlogListPage/index.tsx
@@ -0,0 +1,156 @@
+/**
+ * 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 type { FC } from 'react';
+import React from 'react';
+
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import BlogLayout from '@theme/BlogLayout';
+import BlogListPaginator from '@theme/BlogListPaginator';
+import type { Props } from '@theme/BlogListPage';
+import Link from '@docusaurus/Link';
+import type { Props as OldBlogPostItemProps } from '@theme/BlogPostItem';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { MDXProvider } from '@mdx-js/react';
+import type { ScrollPosition } from 'react-lazy-load-image-component';
+import { trackWindowScroll, LazyLoadImage } from 'react-lazy-load-image-component';
+import Avvvatars from 'avvvatars-react';
+import clsx from 'clsx';
+import style from './style.module.scss';
+
+const components = {
+ blockquote: ({ children }) => children,
+ p: ({ children }) => <p>{children.length > 200 ? `${children.slice(0, 200)}...` : children}</p>,
+ a: ({ children }) => children,
+};
+
+const defaultImg = '/img/default-blog-header.jpg';
+
+type BlogPostItemProps = OldBlogPostItemProps & {
+ scrollPosition: ScrollPosition;
+};
+
+type BlogListPageProps = Props & {
+ scrollPosition: ScrollPosition;
+};
+
+const BlogPostItem: FC<BlogPostItemProps> = (props) => {
+ const {
+ children, frontMatter, assets, metadata, scrollPosition,
+ } = props;
+ const {
+ date, formattedDate, permalink, tags, title, authors,
+ } = metadata;
+
+ const image = assets.image ?? frontMatter.image ?? defaultImg;
+
+ return (
+ <article itemProp="blogPost" itemScope itemType="http://schema.org/BlogPosting">
+ <Link itemProp="url" to={permalink} aria-label={`Read more about ${title}`}>
+ <LazyLoadImage
+ height={232}
+ width={605}
+ src={image}
+ alt={title}
+ effect="opacity"
+ visibleByDefault={image === defaultImg}
+ scrollPosition={scrollPosition}
+ />
+ </Link>
+ <div className={style.content}>
+ <header>
+ {tags.length > 0 && (
+ <div className={style.tags}>
+ {tags.slice(0, 3).map((tag) => (
+ <a key={tag.permalink} href={tag.permalink}>
+ {tag.label}
+ </a>
+ ))}
+ </div>
+ )}
+ <Link itemProp="url" to={permalink} aria-label={`Read more about ${title}`}>
+ <h2>{title}</h2>
+ {children && <MDXProvider components={components}>{children}</MDXProvider>}
+ </Link>
+ </header>
+ <footer className={style.footer}>
+ {authors.length > 0 && (
+ <>
+ <div className={style.authors}>
+ {authors.reverse().map((author) => (author.imageURL ? (
+ <LazyLoadImage
+ className={style.author}
+ key={author.name}
+ src={author.imageURL}
+ width={32}
+ height={32}
+ scrollPosition={scrollPosition}
+ />
+ ) : (
+ <div className={style.author} key={author.name}>
+ <Avvvatars value={author.name as string} />
+ </div>
+ )))}
+ </div>
+ <div className={style.divider}>•</div>
+ </>
+ )}
+ <time dateTime={date} itemProp="datePublished">
+ {formattedDate}
+ </time>
+ </footer>
+ </div>
+ </article>
+ );
+};
+
+const BlogListPage: FC<BlogListPageProps> = (props) => {
+ const {
+ metadata, items, sidebar, scrollPosition,
+ } = props;
+ const {
+ siteConfig: { title: siteTitle },
+ } = useDocusaurusContext();
+ const { blogDescription, blogTitle, permalink } = metadata;
+ const isBlogOnlyMode = permalink === '/';
+ const title = isBlogOnlyMode ? siteTitle : blogTitle;
+
+ return (
+ <BlogLayout
+ title={title}
+ description={blogDescription}
+ wrapperClassName={clsx({
+ [style.normalPage]: true,
+ [style.firstPage]: !metadata.previousPage,
+ })}
+ searchMetadatas={{
+ // assign unique search tag to exclude this page from search results!
+ tag: 'blog_posts_list',
+ }}
+ sidebar={sidebar}
+ toc={false}
+ >
+ <main itemScope itemType="http://schema.org/Blog">
+ {items.map(({ content: BlogPostContent }) => (
+ <BlogPostItem
+ key={BlogPostContent.metadata.permalink}
+ frontMatter={BlogPostContent.frontMatter}
+ assets={BlogPostContent.assets}
+ metadata={BlogPostContent.metadata}
+ truncated={BlogPostContent.metadata.truncated}
+ scrollPosition={scrollPosition}
+ >
+ <BlogPostContent />
+ </BlogPostItem>
+ ))}
+ </main>
+ <BlogListPaginator metadata={metadata} />
+ </BlogLayout>
+ );
+};
+
+export default trackWindowScroll(BlogListPage);
diff --git a/blog/src/theme/BlogListPage/style.module.scss b/blog/src/theme/BlogListPage/style.module.scss
new file mode 100644
index 00000000000..9f6934e117d
--- /dev/null
+++ b/blog/src/theme/BlogListPage/style.module.scss
@@ -0,0 +1,202 @@
+/* stylelint-disable no-descending-specificity */
+.normalPage {
+ margin: 5rem 2rem;
+ box-sizing: content-box;
+
+ main {
+ width: fit-content;
+ display: grid;
+ grid-template-columns: repeat(3, 430px);
+ margin: 0 auto;
+ align-items: start;
+ font-family: apple-system, system-ui, sans-serif;
+
+ article {
+ margin: 0 1.25rem 2.5rem;
+ border-radius: 1rem;
+ overflow: hidden;
+ border: 2px solid transparent;
+ transition: all 0.3s ease-in-out;
+ // filter: drop-shadow(0 1px 1px rgb(0 0 0 / 10%));
+
+ & > a {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ img {
+ object-fit: cover;
+ width: 100%;
+ max-width: unset;
+ // filter: drop-shadow(0 1px 0 rgb(0 0 0 / 4%));
+ transform-origin: center center;
+ transition: all 0.3s ease-in-out;
+ }
+ }
+
+ & .content {
+ padding: 0 1rem 1rem;
+
+ header {
+ & .tags {
+ display: flex;
+ justify-content: flex-start;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0.8rem 0;
+
+ & > a {
+ color: var(--ifm-link-color);
+ margin-right: 0.5rem;
+ text-transform: uppercase;
+ font-size: 0.66rem;
+
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+ }
+
+ h2 {
+ font-size: 1.375rem;
+ line-height: 1.2em;
+ color: #222;
+ margin-top: 0.8rem;
+ margin-bottom: 0.5em;
+ transition: all 0.3s ease-in-out;
+
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+
+ p {
+ color: #1d1d1f;
+ max-height: #{1.7 * 3}em;
+ overflow-y: hidden;
+ text-overflow: ellipsis;
+ font-weight: 300;
+ margin-bottom: 2rem;
+ }
+ }
+
+ & .footer {
+ display: flex;
+ align-items: center;
+ }
+
+ & .authors {
+ display: flex;
+ flex-direction: row-reverse;
+
+ & .author {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ margin-left: -1rem;
+
+ &:last-child {
+ margin-left: 0;
+ }
+ }
+ }
+
+ & .divider {
+ margin: 0 0.5rem;
+ }
+ }
+
+ &:hover {
+ border-color: var(--ifm-color-primary);
+ transform: translateY(-20px);
+
+ & > a {
+ img {
+ transform: scale3d(1.05, 1.05, 1);
+ }
+ }
+
+ h2 {
+ opacity: 0.6;
+ }
+
+ & .tags:hover + a > h2 {
+ opacity: 1;
+ }
+ }
+ }
+ }
+}
+
+.firstPage {
+ main {
+ grid-template-columns: repeat(2, 645px);
+
+ article {
+ & > a {
+ img {
+ width: 100%;
+ }
+ }
+
+ .content {
+ padding: 0 1.875rem 1.875rem;
+ }
+
+ &:first-of-type {
+ grid-column: 1 / 3;
+ display: flex;
+ filter: none;
+ margin-top: 0;
+ margin-bottom: 5rem;
+
+ & > a {
+ border-radius: 1rem;
+ width: 601px;
+ height: auto;
+ flex-shrink: 0;
+
+ img {
+ height: 100%;
+ width: 100%;
+ }
+ }
+
+ .content {
+ padding: 1rem 2rem;
+
+ header {
+ &::before {
+ content: "LATEST POST";
+ font-size: 0.7rem;
+ font-weight: 800;
+ }
+
+ & .tags > a {
+ font-size: 0.8rem;
+ }
+
+ h2 {
+ font-size: 2.55rem;
+ }
+
+ p {
+ font-size: 1.2rem;
+ }
+ }
+ }
+
+ &:hover {
+ border-color: transparent;
+ transform: none;
+
+ & > a {
+ img {
+ transform: scale3d(1.2, 1.2, 1) rotate3d(0, 0, 1, -2deg);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/blog/src/theme/BlogListPaginator/index.tsx b/blog/src/theme/BlogListPaginator/index.tsx
new file mode 100644
index 00000000000..9660ae085e4
--- /dev/null
+++ b/blog/src/theme/BlogListPaginator/index.tsx
@@ -0,0 +1,64 @@
+/**
+ * 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 Link from '@docusaurus/Link';
+import Translate, { translate } from '@docusaurus/Translate';
+import type { Props } from '@theme/BlogListPaginator';
+import clsx from 'clsx';
+import style from './style.module.scss';
+
+const BlogListPaginator = (props: Props): JSX.Element => {
+ const { metadata } = props;
+ const { previousPage, nextPage } = metadata;
+
+ return (
+ <nav
+ className={clsx('pagination-nav', style.nav)}
+ aria-label={translate({
+ id: 'theme.blog.paginator.navAriaLabel',
+ message: 'Blog list page navigation',
+ description: 'The ARIA label for the blog pagination',
+ })}
+ >
+ <div className="pagination-nav__item">
+ {previousPage && (
+ <Link className="pagination-nav__link" to={previousPage}>
+ <div className="pagination-nav__label">
+ «
+ {' '}
+ <Translate
+ id="theme.blog.paginator.newerEntries"
+ description="The label used to navigate to the newer blog posts page (previous page)"
+ >
+ Newer Entries
+ </Translate>
+ </div>
+ </Link>
+ )}
+ </div>
+ <div className="pagination-nav__item pagination-nav__item--next">
+ {nextPage && (
+ <Link className="pagination-nav__link" to={nextPage}>
+ <div className="pagination-nav__label">
+ <Translate
+ id="theme.blog.paginator.olderEntries"
+ description="The label used to navigate to the older blog posts page (next page)"
+ >
+ Older Entries
+ </Translate>
+ {' '}
+ »
+ </div>
+ </Link>
+ )}
+ </div>
+ </nav>
+ );
+};
+
+export default BlogListPaginator;
diff --git a/blog/src/theme/BlogListPaginator/style.module.scss b/blog/src/theme/BlogListPaginator/style.module.scss
new file mode 100644
index 00000000000..a9594cb25d8
--- /dev/null
+++ b/blog/src/theme/BlogListPaginator/style.module.scss
@@ -0,0 +1,3 @@
+.nav {
+ margin: 4rem 15% 0;
+}
diff --git a/blog/src/theme/BlogPostAuthor/index.tsx b/blog/src/theme/BlogPostAuthor/index.tsx
new file mode 100644
index 00000000000..79f285f59f9
--- /dev/null
+++ b/blog/src/theme/BlogPostAuthor/index.tsx
@@ -0,0 +1,49 @@
+/**
+ * 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 Link from '@docusaurus/Link';
+import type {Props} from '@theme/BlogPostAuthor';
+
+import styles from './styles.module.css';
+
+function BlogPostAuthor({author}: Props): JSX.Element {
+ const {name, title, url, imageURL} = author;
+ return (
+ <div className="avatar margin-bottom--sm">
+ {imageURL && (
+ <Link className="avatar__photo-link avatar__photo" href={url}>
+ <img className={styles.image} src={imageURL} alt={name} />
+ </Link>
+ )}
+
+ {
+ // Note: only legacy author frontmatter allow empty name (not frontMatter.authors)
+ name && (
+ <div
+ className="avatar__intro"
+ itemProp="author"
+ itemScope
+ itemType="https://schema.org/Person">
+ <div className="avatar__name">
+ <Link href={url} itemProp="url">
+ <span itemProp="name">{name}</span>
+ </Link>
+ </div>
+ {title && (
+ <small className="avatar__subtitle" itemProp="description">
+ {title}
+ </small>
+ )}
+ </div>
+ )
+ }
+ </div>
+ );
+}
+
+export default BlogPostAuthor;
diff --git a/blog/src/theme/BlogPostAuthor/styles.module.css b/blog/src/theme/BlogPostAuthor/styles.module.css
new file mode 100644
index 00000000000..aae60add2db
--- /dev/null
+++ b/blog/src/theme/BlogPostAuthor/styles.module.css
@@ -0,0 +1,12 @@
+/**
+ * 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.
+ */
+
+.image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
diff --git a/blog/src/theme/BlogPostAuthors/index.tsx b/blog/src/theme/BlogPostAuthors/index.tsx
new file mode 100644
index 00000000000..ee488ba1ef8
--- /dev/null
+++ b/blog/src/theme/BlogPostAuthors/index.tsx
@@ -0,0 +1,36 @@
+/**
+ * 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 clsx from 'clsx';
+import type {Props} from '@theme/BlogPostAuthors';
+import BlogPostAuthor from '@theme/BlogPostAuthor';
+
+import styles from './styles.module.css';
+
+// Component responsible for the authors layout
+export default function BlogPostAuthors({authors, assets}: Props): JSX.Element {
+ const authorsCount = authors.length;
+ if (authorsCount === 0) {
+ return <></>;
+ }
+ return (
+ <div className="row margin-top--md margin-bottom--sm">
+ {authors.map((author, idx) => (
+ <div className={clsx('col col--6', styles.authorCol)} key={idx}>
+ <BlogPostAuthor
+ author={{
+ ...author,
+ // Handle author images using relative paths
+ imageURL: assets.authorsImageUrls[idx] ?? author.imageURL,
+ }}
+ />
+ </div>
+ ))}
+ </div>
+ );
+}
diff --git a/blog/src/theme/BlogPostAuthors/styles.module.css b/blog/src/theme/BlogPostAuthors/styles.module.css
new file mode 100644
index 00000000000..45c1f6fc5f0
--- /dev/null
+++ b/blog/src/theme/BlogPostAuthors/styles.module.css
@@ -0,0 +1,11 @@
+/**
+ * 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.
+ */
+
+.authorCol {
+ max-width: inherit !important;
+ flex-grow: 1 !important;
+}
diff --git a/blog/src/theme/BlogPostItem/index.tsx b/blog/src/theme/BlogPostItem/index.tsx
new file mode 100644
index 00000000000..f6b5cfaa84c
--- /dev/null
+++ b/blog/src/theme/BlogPostItem/index.tsx
@@ -0,0 +1,152 @@
+/**
+ * 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 clsx from 'clsx';
+import {MDXProvider} from '@mdx-js/react';
+import MDXComponents from '@theme/MDXComponents';
+import Translate, {translate} from '@docusaurus/Translate';
+import Link from '@docusaurus/Link';
+import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
+import {usePluralForm} from '@docusaurus/theme-common';
+import EditThisPage from '@theme/EditThisPage';
+import type {Props} from '@theme/BlogPostItem';
+
+import styles from './styles.module.css';
+import TagsListInline from '@theme/TagsListInline';
+import BlogPostAuthors from '@theme/BlogPostAuthors';
+
+// Very simple pluralization: probably good enough for now
+function useReadingTimePlural() {
+ const {selectMessage} = usePluralForm();
+ return (readingTimeFloat: number) => {
+ const readingTime = Math.ceil(readingTimeFloat);
+ return selectMessage(
+ readingTime,
+ translate(
+ {
+ id: 'theme.blog.post.readingTime.plurals',
+ description:
+ 'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
+ message: 'One min read|{readingTime} min read',
+ },
+ {readingTime},
+ ),
+ );
+ };
+}
+
+function BlogPostItem(props: Props): JSX.Element {
+ const readingTimePlural = useReadingTimePlural();
+ const {withBaseUrl} = useBaseUrlUtils();
+ const {
+ children,
+ frontMatter,
+ assets,
+ metadata,
+ truncated,
+ isBlogPostPage = false,
+ } = props;
+ const {
+ date,
+ formattedDate,
+ permalink,
+ tags,
+ readingTime,
+ title,
+ editUrl,
+ authors,
+ } = metadata;
+
+ const image = assets.image ?? frontMatter.image;
+
+ const renderPostHeader = () => {
+ const TitleHeading = isBlogPostPage ? 'h1' : 'h2';
+
+ return (
+ <header>
+ <TitleHeading className={styles.blogPostTitle} itemProp="headline">
+ {isBlogPostPage ? (
+ title
+ ) : (
+ <Link itemProp="url" to={permalink}>
+ {title}
+ </Link>
+ )}
+ </TitleHeading>
+ <div className={clsx(styles.blogPostData, 'margin-vert--md')}>
+ <time dateTime={date} itemProp="datePublished">
+ {formattedDate}
+ </time>
+
+ {typeof readingTime !== 'undefined' && (
+ <>
+ {' · '}
+ {readingTimePlural(readingTime)}
+ </>
+ )}
+ </div>
+ <BlogPostAuthors authors={authors} assets={assets} />
+ </header>
+ );
+ };
+
+ return (
+ <article
+ className={!isBlogPostPage ? 'margin-bottom--xl' : undefined}
+ itemProp="blogPost"
+ itemScope
+ itemType="http://schema.org/BlogPosting">
+ {renderPostHeader()}
+
+ {image && (
+ <meta itemProp="image" content={withBaseUrl(image, {absolute: true})} />
+ )}
+
+ <div className="markdown" itemProp="articleBody">
+ <MDXProvider components={MDXComponents}>{children}</MDXProvider>
+ </div>
+
+ {(tags.length > 0 || truncated) && (
+ <footer
+ className={clsx('row docusaurus-mt-lg', {
+ [styles.blogPostDetailsFull]: isBlogPostPage,
+ })}>
+ {tags.length > 0 && (
+ <div className={clsx('col', {'col--9': !isBlogPostPage})}>
+ <TagsListInline tags={tags} />
+ </div>
+ )}
+
+ {isBlogPostPage && editUrl && (
+ <div className="col margin-top--sm">
+ <EditThisPage editUrl={editUrl} />
+ </div>
+ )}
+
+ {!isBlogPostPage && truncated && (
+ <div className="col col--3 text--right">
+ <Link
+ to={metadata.permalink}
+ aria-label={`Read more about ${title}`}>
+ <b>
+ <Translate
+ id="theme.blog.post.readMore"
+ description="The label used in blog post item excerpts to link to full blog posts">
+ Read More
+ </Translate>
+ </b>
+ </Link>
+ </div>
+ )}
+ </footer>
+ )}
+ </article>
+ );
+}
+
+export default BlogPostItem;
diff --git a/blog/src/theme/BlogPostItem/styles.module.css b/blog/src/theme/BlogPostItem/styles.module.css
new file mode 100644
index 00000000000..612fb71f584
--- /dev/null
+++ b/blog/src/theme/BlogPostItem/styles.module.css
@@ -0,0 +1,18 @@
+/**
+ * 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.
+ */
+
+.blogPostTitle {
+ font-size: 3rem;
+}
+
+.blogPostData {
+ font-size: 0.9rem;
+}
+
+.blogPostDetailsFull {
+ flex-direction: column;
+}
diff --git a/blog/src/theme/BlogPostPage/index.tsx b/blog/src/theme/BlogPostPage/index.tsx
index 7392f6f0f4e..38a397c4e43 100644
--- a/blog/src/theme/BlogPostPage/index.tsx
+++ b/blog/src/theme/BlogPostPage/index.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import Seo from '@theme/Seo';
-import BlogLayout from '@theme-original/BlogLayout';
+import BlogLayout from '@theme/BlogLayout';
import BlogPostItem from '@theme-original/BlogPostItem';
import BlogPostPaginator from '@theme-original/BlogPostPaginator';
import type { Props } from '@theme-original/BlogPostPage';
@@ -55,6 +55,8 @@ const BlogPostPage = (props: Props): JSX.Element => {
? BlogPostContents.toc
: undefined
}
+ frontMatter={frontMatter}
+ metadata={metadata}
>
<Seo
// TODO refactor needed: it's a bit annoying but Seo MUST be inside BlogLayout
diff --git a/blog/src/theme/BlogTagsPostsPage/index.tsx b/blog/src/theme/BlogTagsPostsPage/index.tsx
new file mode 100644
index 00000000000..570ccf56bd5
--- /dev/null
+++ b/blog/src/theme/BlogTagsPostsPage/index.tsx
@@ -0,0 +1,182 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/**
+ * 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 type { FC } from 'react';
+import React from 'react';
+
+import BlogLayout from '@theme/BlogLayout';
+import BlogListPaginator from '@theme/BlogListPaginator';
+import type { Props } from '@theme/BlogTagsPostsPage';
+import Link from '@docusaurus/Link';
+import type { Props as BlogPostItemProps } from '@theme/BlogPostItem';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { MDXProvider } from '@mdx-js/react';
+import { LazyLoadImage } from 'react-lazy-load-image-component';
+import Avvvatars from 'avvvatars-react';
+import clsx from 'clsx';
+import Translate, { translate } from '@docusaurus/Translate';
+import { usePluralForm } from '@docusaurus/theme-common';
+import style from './style.module.scss';
+
+const components = {
+ blockquote: ({ children }) => children,
+ p: ({ children }) => <p>{children.length > 200 ? `${children.slice(0, 200)}...` : children}</p>,
+ a: ({ children }) => children,
+};
+
+function useBlogPostsPlural() {
+ const { selectMessage } = usePluralForm();
+ return (count: number) => selectMessage(
+ count,
+ translate(
+ {
+ id: 'theme.blog.post.plurals',
+ description:
+ 'Pluralized label for "{count} posts". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
+ message: 'One post|{count} posts',
+ },
+ { count },
+ ),
+ );
+}
+
+const BlogPostItem: FC<BlogPostItemProps> = (props) => {
+ const {
+ children,
+ frontMatter,
+ assets,
+ metadata,
+ } = props;
+ const {
+ date,
+ formattedDate,
+ permalink,
+ tags,
+ title,
+ authors,
+ } = metadata;
+
+ const image = assets.image ?? frontMatter.image ?? '/img/default-blog-header.jpg';
+
+ return (
+ <article
+ itemProp="blogPost"
+ itemScope
+ itemType="http://schema.org/BlogPosting"
+ >
+ <Link itemProp="url" to={permalink} aria-label={`Read more about ${title}`}>
+ <LazyLoadImage src={image} alt={title} height={203} width={384} />
+ </Link>
+ <div className={style.content}>
+ <header>
+ {tags.length > 0 && (
+ <div className={style.tags}>
+ {tags.slice(0, 3).map((tag) => (
+ <a key={tag.permalink} href={tag.permalink}>
+ {tag.label}
+ </a>
+ ))}
+ </div>
+ )}
+ <Link itemProp="url" to={permalink} aria-label={`Read more about ${title}`}>
+ <h2>{title}</h2>
+ {children && <MDXProvider components={components}>{children}</MDXProvider>}
+ </Link>
+ </header>
+ <footer className={style.footer}>
+ {authors.length > 0
+ && (
+ <>
+ <div className={style.authors}>
+ {authors.reverse().map((author) => (
+ author.imageURL
+ ? (
+ <LazyLoadImage
+ className={style.author}
+ key={author.name}
+ src={author.imageURL}
+ />
+ )
+ : (
+ <div className={style.author}>
+ <Avvvatars
+ key={author.name}
+ value={author.name as string}
+ />
+ </div>
+ )
+ ))}
+ </div>
+ <div className={style.divider}>•</div>
+ </>
+ )}
+ <time dateTime={date} itemProp="datePublished">{formattedDate}</time>
+ </footer>
+ </div>
+ </article>
+ );
+};
+
+const BlogTagsPostsPage = (props: Props): JSX.Element => {
+ const { metadata, items, sidebar } = props;
+ const { allTagsPath, name: tagName, count } = metadata;
+ const blogPostsPlural = useBlogPostsPlural();
+ const title = translate(
+ {
+ id: 'theme.blog.tagTitle',
+ description: 'The title of the page for a blog tag',
+ message: '{nPosts} tagged with "{tagName}"',
+ },
+ { nPosts: blogPostsPlural(count), tagName },
+ );
+
+ return (
+ <BlogLayout
+ title={title}
+ wrapperClassName={clsx({
+ [style.normalPage]: true, [style.firstPage]: !metadata.previousPage,
+ })}
+ searchMetadatas={{
+ // assign unique search tag to exclude this page from search results!
+ tag: 'blog_tags_posts',
+ }}
+ sidebar={sidebar}
+ >
+ <header className={style.header}>
+ <h1>{title}</h1>
+
+ <Link href={allTagsPath}>
+ <Translate
+ id="theme.tags.tagsPageLink"
+ description="The label of the link targeting the tag list page"
+ >
+ View All Tags
+ </Translate>
+ </Link>
+ </header>
+ <main
+ itemScope
+ >
+ {items.map(({ content: BlogPostContent }) => (
+ <BlogPostItem
+ key={BlogPostContent.metadata.permalink}
+ frontMatter={BlogPostContent.frontMatter}
+ assets={BlogPostContent.assets}
+ metadata={BlogPostContent.metadata}
+ truncated={BlogPostContent.metadata.truncated}
+ >
+ <BlogPostContent />
+ </BlogPostItem>
+ ))}
+ </main>
+ <BlogListPaginator metadata={metadata} />
+ </BlogLayout>
+ );
+};
+
+export default BlogTagsPostsPage;
diff --git a/blog/src/theme/BlogTagsPostsPage/style.module.scss b/blog/src/theme/BlogTagsPostsPage/style.module.scss
new file mode 100644
index 00000000000..07114a969b6
--- /dev/null
+++ b/blog/src/theme/BlogTagsPostsPage/style.module.scss
@@ -0,0 +1,130 @@
+/* stylelint-disable no-descending-specificity */
+.header {
+ text-align: center;
+ margin-bottom: 4rem;
+}
+
+.normalPage {
+ margin: 5rem 2rem;
+ box-sizing: content-box;
+
+ main {
+ width: fit-content;
+ display: grid;
+ grid-template-columns: repeat(3, 430px);
+ margin: 0 auto;
+ align-items: start;
+ font-family: apple-system, system-ui, sans-serif;
+
+ article {
+ margin: 0 1.25rem 2.5rem;
+ border-radius: 1rem;
+ overflow: hidden;
+ background-color: #fffffd;
+ filter: drop-shadow(0 1px 1px rgb(0 0 0 / 10%));
+
+ & > a {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ img {
+ object-fit: cover;
+ width: 100%;
+ max-width: unset;
+ filter: drop-shadow(0 1px 0 rgb(0 0 0 / 4%));
+ transform-origin: center center;
+ transition: all 0.3s ease-in-out;
+ }
+ }
+
+ & .content {
+ padding: 0 1rem 1rem;
+
+ header {
+ & .tags {
+ display: flex;
+ justify-content: flex-start;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0.8rem 0;
+
+ & > a {
+ color: var(--ifm-link-color);
+ margin-right: 0.5rem;
+ text-transform: uppercase;
+ font-size: 0.66rem;
+
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+ }
+
+ h2 {
+ font-size: 1.375rem;
+ line-height: 1.2em;
+ color: #222;
+ margin-top: 0.8rem;
+ margin-bottom: 0.5em;
+ transition: all 0.3s ease-in-out;
+
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+
+ p {
+ color: #999;
+ max-height: #{1.7 * 3}em;
+ overflow-y: hidden;
+ text-overflow: ellipsis;
+ font-weight: 300;
+ margin-bottom: 2rem;
+ }
+ }
+
+ & .footer {
+ display: flex;
+ align-items: center;
+ }
+
+ & .authors {
+ display: flex;
+ flex-direction: row-reverse;
+
+ & .author {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ margin-left: -1rem;
+
+ &:last-child {
+ margin-left: 0;
+ }
+ }
+ }
+
+ & .divider {
+ margin: 0 0.5rem;
+ }
+ }
+
+ &:hover {
+ & > a {
+ img {
+ transform: scale3d(1.05, 1.05, 1);
+ }
+ }
+
+ h2 {
+ opacity: 0.6;
+ }
+
+ & .tags:hover + a > h2 {
+ opacity: 1;
+ }
+ }
+ }
+ }
+}
diff --git a/blog/static/img/default-blog-header.jpg b/blog/static/img/default-blog-header.jpg
new file mode 100644
index 00000000000..7d1532ab22e
Binary files /dev/null and b/blog/static/img/default-blog-header.jpg differ
diff --git a/blog/zh/docusaurus.config.js b/blog/zh/docusaurus.config.js
index 0a51f3a359d..4a2e2d3ff9e 100644
--- a/blog/zh/docusaurus.config.js
+++ b/blog/zh/docusaurus.config.js
@@ -31,8 +31,8 @@ module.exports = {
{
docs: false,
blog: {
- blogSidebarTitle: 'All posts',
- blogSidebarCount: 'ALL',
+ blogSidebarCount: 0,
+ postsPerPage: 9,
},
theme: {
customCss: require.resolve('./src/css/customTheme.css'),
diff --git a/website/static/img/default-blog-header.jpg b/website/static/img/default-blog-header.jpg
new file mode 100644
index 00000000000..7d1532ab22e
Binary files /dev/null and b/website/static/img/default-blog-header.jpg differ
diff --git a/yarn.lock b/yarn.lock
index c7c530ea6e8..eea934bd3f9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2959,6 +2959,13 @@ autoprefixer@^10.2.5, autoprefixer@^10.3.7:
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
+avvvatars-react@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/avvvatars-react/-/avvvatars-react-0.4.2.tgz#9569c16ae9a4925f898539ff98c6f734b6a4fe1b"
+ integrity sha512-D/bnSM+P6pQi71dFeFVqQsqmv+ct5XG7uO2lvJoiksALpXLd6kPgpRR1SUQxFZJkJNrkmg0NPgbhIgJgOsDYRQ==
+ dependencies:
+ goober "^2.1.8"
+
axe-core@^4.4.2:
version "4.4.3"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
@@ -3467,7 +3474,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
-classnames@^2.2.1, classnames@^2.2.6:
+classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
@@ -4081,7 +4088,7 @@ date-fns@^1.27.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
-debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
+debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -5519,6 +5526,11 @@ globjoin@^0.1.4:
resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==
+goober@^2.1.8:
+ version "2.1.10"
+ resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.10.tgz#058def43ba1e3b06f973dbb372a4978aa42f1049"
+ integrity sha512-7PpuQMH10jaTWm33sQgBQvz45pHR8N4l3Cu3WMGEWmHShAcTuuP7I+5/DwKo39fwti5A80WAjvqgz6SSlgWmGA==
+
got@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@@ -6670,6 +6682,13 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
+jsonp@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/jsonp/-/jsonp-0.2.1.tgz#a65b4fa0f10bda719a05441ea7b94c55f3e15bae"
+ integrity sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==
+ dependencies:
+ debug "^2.1.3"
+
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.2.tgz#afe5efe4332cd3515c065072bd4d6b0aa22152bd"
@@ -8856,6 +8875,14 @@ react-router@5.3.3, react-router@^5.2.0:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
+react-share@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/react-share/-/react-share-4.4.0.tgz#cabbf2111d7a907a888ab4d89d08410329efd5ee"
+ integrity sha512-POe8Ge/JT9Ew9iyW7CiYsCCWCb8uMJWqFl9S7W0fJ/oH5gBJNzukH0bL5vSr17KKG5h15d3GfKaoviI22BKeYA==
+ dependencies:
+ classnames "^2.2.5"
+ jsonp "^0.2.1"
+
react-side-effect@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a"
@@ -10283,7 +10310,7 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
-styled-components@^5.3.3:
+styled-components@^5, styled-components@^5.3.3:
version "5.3.5"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.5.tgz#a750a398d01f1ca73af16a241dec3da6deae5ec4"
integrity sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg==