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">
+              &laquo;
+              {' '}
+              <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>
+              {' '}
+              &raquo;
+            </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==