diff --git a/blog/good-dx/index.md b/blog/good-dx/index.md
index d5a036cc..a9578d21 100644
--- a/blog/good-dx/index.md
+++ b/blog/good-dx/index.md
@@ -1,7 +1,7 @@
---
title: 'How to make good DX(Developer Experience): Empathize'
description: Why DX matters and how to create great DX toolkit for developers.
-tags: [DX, zensatck]
+tags: [DX, zenstack, programming]
authors: jiasheng
date: 2023-03-24
image: ./cover.png
diff --git a/blog/microservice/index.md b/blog/microservice/index.md
index ac252f2d..45bb90a8 100644
--- a/blog/microservice/index.md
+++ b/blog/microservice/index.md
@@ -1,7 +1,7 @@
---
title: Where Did Microservices Go
description: Microservices have undergone a shift due to a combination of lessons learned and the emergence of new technologies.
-tags: [zenstack, Microservices]
+tags: [zenstack, microservices, fullstack, nextjs]
authors: jiasheng
date: 2023-04-22
image: ./cover.jpg
diff --git a/blog/ocp/index.md b/blog/ocp/index.md
index d16b8a1d..c1be131f 100644
--- a/blog/ocp/index.md
+++ b/blog/ocp/index.md
@@ -1,6 +1,6 @@
---
description: Use a real example to illustrate how to achieve good design by applying polymorphism from the database to the UI.
-tags: [prisma, orm, database, oop, design, typescript]
+tags: [orm, database, oop, design, typescript, polymorphism]
authors: jiasheng
image: ./cover.jpg
date: 2024-03-28
diff --git a/blog/polymorphism/index.md b/blog/polymorphism/index.md
index 516ded0c..82ef4c23 100644
--- a/blog/polymorphism/index.md
+++ b/blog/polymorphism/index.md
@@ -1,6 +1,6 @@
---
description: This post explores different patterns for implementing polymorphism in ORMs and how ZenStack can add the missing feature into Prisma.
-tags: [prisma, orm, database]
+tags: [prisma, orm, database, polymorphism]
authors: yiming
date: 2023-12-21
image: ./cover.png
diff --git a/blog/saas-backend/index.md b/blog/saas-backend/index.md
index 52bcb0be..2d6e18ab 100644
--- a/blog/saas-backend/index.md
+++ b/blog/saas-backend/index.md
@@ -1,7 +1,7 @@
---
title: 'How To Build a Scalable SaaS Backend in 10 Minutes With 100 Lines of Code'
description: Use schema as the single source of truth for the SaaS backend
-tags: [zenstack, saas, backend, access-control, nodejs, typescript]
+tags: [zenstack, saas, backend, authorization, nodejs]
authors: jiasheng
date: 2023-06-21
image: ./cover.png
diff --git a/blog/supabase-alternative/index.md b/blog/supabase-alternative/index.md
index a03e9d7a..21d6a326 100644
--- a/blog/supabase-alternative/index.md
+++ b/blog/supabase-alternative/index.md
@@ -1,7 +1,7 @@
---
title: Supabase RLS Alternative
description: Show the limitation of Supabase RLS(Row Level Security) with a multi-tenancy SaaS example and introduce ZenStack as an alternative.
-tags: [supabase, rls, auth, baas, zenstack]
+tags: [supabase, rls, auth, authorization, baas, zenstack]
authors: jiasheng
date: 2024-07-24
image: ./cover.png
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 18087dec..4510626c 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -46,11 +46,7 @@ const config = {
},
},
},
- blog: {
- showReadingTime: true,
- blogSidebarTitle: 'Recent posts',
- blogSidebarCount: 10,
- },
+ blog: false,
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
@@ -260,6 +256,14 @@ const config = {
},
};
},
+ [
+ './src/plugins/blog-plugin.js',
+ {
+ showReadingTime: true,
+ blogSidebarTitle: 'Recent posts',
+ blogSidebarCount: 10,
+ },
+ ],
],
markdown: {
diff --git a/src/components/blog/post-paginator.tsx b/src/components/blog/post-paginator.tsx
new file mode 100644
index 00000000..e9ba69ec
--- /dev/null
+++ b/src/components/blog/post-paginator.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import Link from '@docusaurus/Link';
+
+import clsx from 'clsx';
+
+export const PostPaginator = ({ posts, title }) => {
+ if (posts.length < 1) {
+ return null;
+ }
+
+ return (
+
+
+
{title}
+
+ {posts.map((post) => (
+
+
{post.title}
+
+
+ {post.description}
+
+
+ ))}
+
+ 🚀 Ready to build high-quality, scalable Prisma apps with built-in AuthZ and instant CRUD APIs ?
+
+
+ Get started with ZenStack's ultimate guide to build faster and smarter
+
+
+
+
+ );
+};
diff --git a/src/plugins/blog-plugin.js b/src/plugins/blog-plugin.js
new file mode 100644
index 00000000..157cd842
--- /dev/null
+++ b/src/plugins/blog-plugin.js
@@ -0,0 +1,78 @@
+const blogPluginExports = require('@docusaurus/plugin-content-blog');
+const utils = require('@docusaurus/utils');
+const path = require('path');
+
+const defaultBlogPlugin = blogPluginExports.default;
+const MIN_RELATED_POSTS = 10;
+
+function getMultipleRandomElement(arr, num) {
+ const shuffled = [...arr].sort(() => 0.5 - Math.random());
+
+ return shuffled.slice(0, num);
+}
+
+function getRelatedPosts(allBlogPosts, metadata) {
+ const currentTags = new Set(metadata.frontMatter.tags?.filter((tag) => tag?.toLowerCase() != 'zenstack'));
+
+ let relatedPosts = allBlogPosts.filter(
+ (post) =>
+ post.metadata.frontMatter.tags.some((tag) => currentTags.has(tag)) && post.metadata.title !== metadata.title
+ );
+
+ if (relatedPosts.length < MIN_RELATED_POSTS) {
+ remainingCount = MIN_RELATED_POSTS - relatedPosts.length;
+ const remainingPosts = getMultipleRandomElement(
+ allBlogPosts.filter((post) => !relatedPosts.includes(post) && post.metadata.title !== metadata.title),
+ remainingCount
+ );
+ relatedPosts = relatedPosts.concat(remainingPosts);
+ }
+
+ const filteredPostInfos = relatedPosts.map((post) => {
+ return {
+ title: post.metadata.title,
+ description: post.metadata.description,
+ permalink: post.metadata.permalink,
+ formattedDate: post.metadata.formattedDate,
+ authors: post.metadata.authors,
+ readingTime: post.metadata.readingTime,
+ date: post.metadata.date,
+ relatedWeight: post.metadata.frontMatter.tags.filter((tag) => currentTags.has(tag)).length * 3 + 1,
+ };
+ });
+
+ return filteredPostInfos;
+}
+
+async function blogPluginExtended(...pluginArgs) {
+ const blogPluginInstance = await defaultBlogPlugin(...pluginArgs);
+
+ return {
+ // Add all properties of the default blog plugin so existing functionality is preserved
+ ...blogPluginInstance,
+ contentLoaded: async function (data) {
+ await blogPluginInstance.contentLoaded(data);
+ const { content: blogContents, actions } = data;
+ const { blogPosts: allBlogPosts } = blogContents;
+ const { createData } = actions;
+ // Create routes for blog entries.
+ await Promise.all(
+ allBlogPosts.map(async (blogPost) => {
+ const { metadata } = blogPost;
+ const relatedPosts = getRelatedPosts(allBlogPosts, metadata);
+ await createData(
+ // Note that this created data path must be in sync with
+ // metadataPath provided to mdx-loader.
+ `${utils.docuHash(metadata.source)}.json`,
+ JSON.stringify({ ...metadata, relatedPosts }, null, 2)
+ );
+ })
+ );
+ },
+ };
+}
+
+module.exports = {
+ ...blogPluginExports,
+ default: blogPluginExtended,
+};
diff --git a/src/theme/BlogPostPage/index.js b/src/theme/BlogPostPage/index.js
index 5dc96748..c4da0bb2 100644
--- a/src/theme/BlogPostPage/index.js
+++ b/src/theme/BlogPostPage/index.js
@@ -9,17 +9,61 @@ import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
import TOC from '@theme/TOC';
import Unlisted from '@theme/Unlisted';
import GiscusComponent from '@site/src/components/GiscusComponent';
-function BlogPostPageContent({ sidebar, children }) {
+import { PostPaginator } from '@site/src/components/blog/post-paginator';
+
+function getMultipleRandomPosts(relatedPosts, number) {
+ // Create a copy of the original array to avoid modifying it
+ const weightedItems = [...relatedPosts];
+ const result = [];
+
+ // Calculate the total weight
+ let totalWeight = weightedItems.reduce((sum, item) => sum + item.relatedWeight, 0);
+
+ while (weightedItems.length > 0) {
+ // Generate a random value between 0 and the total weight
+ const randomValue = Math.random() * totalWeight;
+ let weightSum = 0;
+ let selectedIndex = -1;
+
+ // Find the item that corresponds to the random value
+ for (let i = 0; i < weightedItems.length; i++) {
+ weightSum += weightedItems[i].relatedWeight;
+ if (randomValue <= weightSum) {
+ selectedIndex = i;
+ break;
+ }
+ }
+
+ // If an item was selected, add it to the result and remove it from the original array
+ if (selectedIndex !== -1) {
+ const [selectedItem] = weightedItems.splice(selectedIndex, 1);
+ result.push(selectedItem);
+ totalWeight -= selectedItem.relatedWeight;
+ }
+ }
+
+ return result.slice(0, number);
+}
+
+function BlogPostPageContent({ children }) {
const { metadata, toc } = useBlogPost();
+ const { relatedPosts } = metadata;
+
const { nextItem, prevItem, frontMatter, unlisted } = metadata;
const {
hide_table_of_contents: hideTableOfContents,
toc_min_heading_level: tocMinHeadingLevel,
toc_max_heading_level: tocMaxHeadingLevel,
} = frontMatter;
+
+ const randomThreeRelatedPosts = getMultipleRandomPosts(relatedPosts, 3);
+
+ console.log('relatedPosts', relatedPosts);
+
+ //const url = '/blog/supabase-alternative/cover.png';
+
return (
0 ? (
@@ -27,7 +71,9 @@ function BlogPostPageContent({ sidebar, children }) {
}
>
{unlisted && }
+
{children}
+
{(nextItem || prevItem) && }