Skip to content

Commit abee3c7

Browse files
authored
blog: add related posts for blog page (#350)
* blog: update supabase rls alternative * blog: add related posts for blog page * remove console.log * at least make 3 related posts * update style * add CTA to doc * display max 3 lines for description
1 parent 509613a commit abee3c7

File tree

10 files changed

+217
-13
lines changed

10 files changed

+217
-13
lines changed

blog/good-dx/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: 'How to make good DX(Developer Experience): Empathize'
33
description: Why DX matters and how to create great DX toolkit for developers.
4-
tags: [DX, zensatck]
4+
tags: [DX, zenstack, programming]
55
authors: jiasheng
66
date: 2023-03-24
77
image: ./cover.png

blog/microservice/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Where Did Microservices Go
33
description: Microservices have undergone a shift due to a combination of lessons learned and the emergence of new technologies.
4-
tags: [zenstack, Microservices]
4+
tags: [zenstack, microservices, fullstack, nextjs]
55
authors: jiasheng
66
date: 2023-04-22
77
image: ./cover.jpg

blog/ocp/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Use a real example to illustrate how to achieve good design by applying polymorphism from the database to the UI.
3-
tags: [prisma, orm, database, oop, design, typescript]
3+
tags: [orm, database, oop, design, typescript, polymorphism]
44
authors: jiasheng
55
image: ./cover.jpg
66
date: 2024-03-28

blog/polymorphism/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: This post explores different patterns for implementing polymorphism in ORMs and how ZenStack can add the missing feature into Prisma.
3-
tags: [prisma, orm, database]
3+
tags: [prisma, orm, database, polymorphism]
44
authors: yiming
55
date: 2023-12-21
66
image: ./cover.png

blog/saas-backend/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: 'How To Build a Scalable SaaS Backend in 10 Minutes With 100 Lines of Code'
33
description: Use schema as the single source of truth for the SaaS backend
4-
tags: [zenstack, saas, backend, access-control, nodejs, typescript]
4+
tags: [zenstack, saas, backend, authorization, nodejs]
55
authors: jiasheng
66
date: 2023-06-21
77
image: ./cover.png

blog/supabase-alternative/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Supabase RLS Alternative
33
description: Show the limitation of Supabase RLS(Row Level Security) with a multi-tenancy SaaS example and introduce ZenStack as an alternative.
4-
tags: [supabase, rls, auth, baas, zenstack]
4+
tags: [supabase, rls, auth, authorization, baas, zenstack]
55
authors: jiasheng
66
date: 2024-07-24
77
image: ./cover.png

docusaurus.config.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,7 @@ const config = {
4646
},
4747
},
4848
},
49-
blog: {
50-
showReadingTime: true,
51-
blogSidebarTitle: 'Recent posts',
52-
blogSidebarCount: 10,
53-
},
49+
blog: false,
5450
theme: {
5551
customCss: require.resolve('./src/css/custom.css'),
5652
},
@@ -260,6 +256,14 @@ const config = {
260256
},
261257
};
262258
},
259+
[
260+
'./src/plugins/blog-plugin.js',
261+
{
262+
showReadingTime: true,
263+
blogSidebarTitle: 'Recent posts',
264+
blogSidebarCount: 10,
265+
},
266+
],
263267
],
264268

265269
markdown: {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react';
2+
import Link from '@docusaurus/Link';
3+
4+
import clsx from 'clsx';
5+
6+
export const PostPaginator = ({ posts, title }) => {
7+
if (posts.length < 1) {
8+
return null;
9+
}
10+
11+
return (
12+
<div
13+
className={clsx(
14+
'mr-auto w-full',
15+
'py-10',
16+
'blog-sm:py-12',
17+
'blog-md:py-16',
18+
'max-w-[894px]',
19+
'blog-sm:max-w-screen-blog-sm',
20+
'blog-lg:max-w-screen-content-2xl'
21+
)}
22+
>
23+
<div className="blog-sm:px-6 w-full">
24+
<h2 className="mb-4 p-0 text-2xl font-semibold">{title}</h2>
25+
<div className="flex flex-col not-prose ">
26+
{posts.map((post) => (
27+
<Link
28+
to={post.permalink}
29+
rel="dofollow"
30+
key={post.permalink ?? post.id}
31+
style={{ color: 'var(--ifm-font-color-base)' }}
32+
className={clsx(
33+
'flex',
34+
'flex-col',
35+
'gap-2',
36+
'p-5',
37+
'mb-5',
38+
'rounded-lg',
39+
'border',
40+
'hover:bg-gray-100',
41+
'dark:hover:bg-gray-800',
42+
'not-prose',
43+
'no-underline',
44+
'border-solid',
45+
'border',
46+
'hover:no-underline',
47+
'group'
48+
)}
49+
>
50+
<div className={clsx('font-bold', 'group-hover:underline')}>{post.title}</div>
51+
52+
<p
53+
className={clsx('font-sm')}
54+
style={{
55+
display: '-webkit-box',
56+
'-webkit-line-clamp': '3',
57+
'-webkit-box-orient': 'vertical',
58+
overflow: 'hidden',
59+
'text-overflow': 'ellipsis',
60+
}}
61+
>
62+
{post.description}
63+
</p>
64+
</Link>
65+
))}
66+
<p className="mb-4 p-0 text-xl">
67+
🚀 Ready to build high-quality, scalable Prisma apps with built-in AuthZ and instant CRUD APIs ?
68+
</p>
69+
<Link className="mb-4 p-0 text-xl" to="/docs/welcome">
70+
Get started with ZenStack's ultimate guide to build faster and smarter
71+
</Link>
72+
</div>
73+
</div>
74+
</div>
75+
);
76+
};

src/plugins/blog-plugin.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const blogPluginExports = require('@docusaurus/plugin-content-blog');
2+
const utils = require('@docusaurus/utils');
3+
const path = require('path');
4+
5+
const defaultBlogPlugin = blogPluginExports.default;
6+
const MIN_RELATED_POSTS = 10;
7+
8+
function getMultipleRandomElement(arr, num) {
9+
const shuffled = [...arr].sort(() => 0.5 - Math.random());
10+
11+
return shuffled.slice(0, num);
12+
}
13+
14+
function getRelatedPosts(allBlogPosts, metadata) {
15+
const currentTags = new Set(metadata.frontMatter.tags?.filter((tag) => tag?.toLowerCase() != 'zenstack'));
16+
17+
let relatedPosts = allBlogPosts.filter(
18+
(post) =>
19+
post.metadata.frontMatter.tags.some((tag) => currentTags.has(tag)) && post.metadata.title !== metadata.title
20+
);
21+
22+
if (relatedPosts.length < MIN_RELATED_POSTS) {
23+
remainingCount = MIN_RELATED_POSTS - relatedPosts.length;
24+
const remainingPosts = getMultipleRandomElement(
25+
allBlogPosts.filter((post) => !relatedPosts.includes(post) && post.metadata.title !== metadata.title),
26+
remainingCount
27+
);
28+
relatedPosts = relatedPosts.concat(remainingPosts);
29+
}
30+
31+
const filteredPostInfos = relatedPosts.map((post) => {
32+
return {
33+
title: post.metadata.title,
34+
description: post.metadata.description,
35+
permalink: post.metadata.permalink,
36+
formattedDate: post.metadata.formattedDate,
37+
authors: post.metadata.authors,
38+
readingTime: post.metadata.readingTime,
39+
date: post.metadata.date,
40+
relatedWeight: post.metadata.frontMatter.tags.filter((tag) => currentTags.has(tag)).length * 3 + 1,
41+
};
42+
});
43+
44+
return filteredPostInfos;
45+
}
46+
47+
async function blogPluginExtended(...pluginArgs) {
48+
const blogPluginInstance = await defaultBlogPlugin(...pluginArgs);
49+
50+
return {
51+
// Add all properties of the default blog plugin so existing functionality is preserved
52+
...blogPluginInstance,
53+
contentLoaded: async function (data) {
54+
await blogPluginInstance.contentLoaded(data);
55+
const { content: blogContents, actions } = data;
56+
const { blogPosts: allBlogPosts } = blogContents;
57+
const { createData } = actions;
58+
// Create routes for blog entries.
59+
await Promise.all(
60+
allBlogPosts.map(async (blogPost) => {
61+
const { metadata } = blogPost;
62+
const relatedPosts = getRelatedPosts(allBlogPosts, metadata);
63+
await createData(
64+
// Note that this created data path must be in sync with
65+
// metadataPath provided to mdx-loader.
66+
`${utils.docuHash(metadata.source)}.json`,
67+
JSON.stringify({ ...metadata, relatedPosts }, null, 2)
68+
);
69+
})
70+
);
71+
},
72+
};
73+
}
74+
75+
module.exports = {
76+
...blogPluginExports,
77+
default: blogPluginExtended,
78+
};

src/theme/BlogPostPage/index.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,71 @@ import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata';
99
import TOC from '@theme/TOC';
1010
import Unlisted from '@theme/Unlisted';
1111
import GiscusComponent from '@site/src/components/GiscusComponent';
12-
function BlogPostPageContent({ sidebar, children }) {
12+
import { PostPaginator } from '@site/src/components/blog/post-paginator';
13+
14+
function getMultipleRandomPosts(relatedPosts, number) {
15+
// Create a copy of the original array to avoid modifying it
16+
const weightedItems = [...relatedPosts];
17+
const result = [];
18+
19+
// Calculate the total weight
20+
let totalWeight = weightedItems.reduce((sum, item) => sum + item.relatedWeight, 0);
21+
22+
while (weightedItems.length > 0) {
23+
// Generate a random value between 0 and the total weight
24+
const randomValue = Math.random() * totalWeight;
25+
let weightSum = 0;
26+
let selectedIndex = -1;
27+
28+
// Find the item that corresponds to the random value
29+
for (let i = 0; i < weightedItems.length; i++) {
30+
weightSum += weightedItems[i].relatedWeight;
31+
if (randomValue <= weightSum) {
32+
selectedIndex = i;
33+
break;
34+
}
35+
}
36+
37+
// If an item was selected, add it to the result and remove it from the original array
38+
if (selectedIndex !== -1) {
39+
const [selectedItem] = weightedItems.splice(selectedIndex, 1);
40+
result.push(selectedItem);
41+
totalWeight -= selectedItem.relatedWeight;
42+
}
43+
}
44+
45+
return result.slice(0, number);
46+
}
47+
48+
function BlogPostPageContent({ children }) {
1349
const { metadata, toc } = useBlogPost();
50+
const { relatedPosts } = metadata;
51+
1452
const { nextItem, prevItem, frontMatter, unlisted } = metadata;
1553
const {
1654
hide_table_of_contents: hideTableOfContents,
1755
toc_min_heading_level: tocMinHeadingLevel,
1856
toc_max_heading_level: tocMaxHeadingLevel,
1957
} = frontMatter;
58+
59+
const randomThreeRelatedPosts = getMultipleRandomPosts(relatedPosts, 3);
60+
61+
console.log('relatedPosts', relatedPosts);
62+
63+
//const url = '/blog/supabase-alternative/cover.png';
64+
2065
return (
2166
<BlogLayout
22-
sidebar={sidebar}
2367
toc={
2468
!hideTableOfContents && toc.length > 0 ? (
2569
<TOC toc={toc} minHeadingLevel={tocMinHeadingLevel} maxHeadingLevel={tocMaxHeadingLevel} />
2670
) : undefined
2771
}
2872
>
2973
{unlisted && <Unlisted />}
74+
3075
<BlogPostItem>{children}</BlogPostItem>
76+
<PostPaginator title="Related Articles" posts={randomThreeRelatedPosts}></PostPaginator>
3177
<GiscusComponent />
3278
{(nextItem || prevItem) && <BlogPostPaginator nextItem={nextItem} prevItem={prevItem} />}
3379
</BlogLayout>

0 commit comments

Comments
 (0)