feat: add recommended blog post depending on current post tags (#514)
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
<script>
|
||||||
|
import Chip from '$lib/Chip/Chip.svelte';
|
||||||
|
import { formatDate } from '$lib/utils/formatDate';
|
||||||
|
|
||||||
|
export let post;
|
||||||
|
export let isRecommended = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !isRecommended}
|
||||||
|
<div class="mb-5 flex md:flex-row flex-col md:items-center gap-2">
|
||||||
|
<!-- Date -->
|
||||||
|
<p class="text-sm text-slate-500">{formatDate(post.date)}</p>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each post.tags as tag}
|
||||||
|
<Chip label={tag} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<a href="/blog/{post.slug}" class="group">
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
src={post.imageUrl}
|
||||||
|
alt={post.title}
|
||||||
|
class="w-70 h-48 object-cover rounded-md"
|
||||||
|
/>
|
||||||
|
<p class="font-bold text-lg py-2 h-[60px] line-clamp-2 group-hover:text-slate-500">
|
||||||
|
{@html post.title}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-slate-500 pt-2 mb-4 line-clamp-3">{post.description}</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-start gap-4 my-2">
|
||||||
|
<img loading="lazy" src={post.avatar} alt="avatar" class="w-10 h-10 rounded-full" />
|
||||||
|
<span>
|
||||||
|
<p class="font-semibold">{post.author}</p>
|
||||||
|
<p class="text-slate-500">{post.role}</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
export type Categories = 'sveltekit' | 'svelte';
|
export type Categories = 'sveltekit' | 'svelte';
|
||||||
|
|
||||||
|
export type RawBlogPost = {
|
||||||
|
metadata: BlogPost;
|
||||||
|
default: any;
|
||||||
|
};
|
||||||
|
|
||||||
export type BlogPost = {
|
export type BlogPost = {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Chip from '$lib/Chip/Chip.svelte';
|
|
||||||
import { formatDate } from '$lib/utils/formatDate';
|
|
||||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||||
|
import BlogListItem from '$lib/Blog/BlogListItem.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
</script>
|
</script>
|
||||||
@@ -27,41 +26,7 @@
|
|||||||
<ul class="flex flex-col items-start justify-start gap-2 w-[90%] md:w-[50%]">
|
<ul class="flex flex-col items-start justify-start gap-2 w-[90%] md:w-[50%]">
|
||||||
{#each data.posts as post}
|
{#each data.posts as post}
|
||||||
<li class="py-10 w-full border-b border-gray-200">
|
<li class="py-10 w-full border-b border-gray-200">
|
||||||
<div class="mb-5 flex md:flex-row flex-col md:items-center gap-2">
|
<BlogListItem {post} />
|
||||||
<!-- Date -->
|
|
||||||
<p class="text-sm text-slate-500">{formatDate(post.date)}</p>
|
|
||||||
|
|
||||||
<!-- Tags -->
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
{#each post.tags as tag}
|
|
||||||
<Chip label={tag} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href={`/blog/${post.slug}`} class="group">
|
|
||||||
<img
|
|
||||||
loading="lazy"
|
|
||||||
src={post.imageUrl}
|
|
||||||
alt={post.title}
|
|
||||||
class="w-70 h-48 object-cover rounded-md"
|
|
||||||
/>
|
|
||||||
<p class="font-bold text-lg py-2 group-hover:text-slate-500">
|
|
||||||
{@html post.title}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-slate-500 pt-2 pb-4">{post.description}</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-start gap-4 my-2">
|
|
||||||
<img loading="lazy" src={post.avatar} alt="avatar" class="w-10 h-10 rounded-full" />
|
|
||||||
<span>
|
|
||||||
<p class="font-semibold">{post.author}</p>
|
|
||||||
<p class="text-slate-500">{post.role}</p>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { BlogPost } from '$lib/types';
|
import type { BlogPost } from '$lib/types';
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageLoad} */
|
||||||
export async function load({ fetch }) {
|
export async function load({ fetch }) {
|
||||||
const response = await fetch('api/blog');
|
const response = await fetch('api/blog');
|
||||||
const posts: BlogPost[] = await response.json();
|
const posts: BlogPost[] = await response.json();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { ChevronLeft } from 'carbon-icons-svelte';
|
||||||
import Chip from '$lib/Chip/Chip.svelte';
|
import Chip from '$lib/Chip/Chip.svelte';
|
||||||
import { formatDate } from '$lib/utils/formatDate';
|
import { formatDate } from '$lib/utils/formatDate';
|
||||||
import { ChevronLeft } from 'carbon-icons-svelte';
|
import BlogListItem from '$lib/Blog/BlogListItem.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
</script>
|
</script>
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Post -->
|
<!-- Post -->
|
||||||
<div class="prose pt-2 pb-4 border-b border-gray-200">
|
<div class="prose pt-2 pb-4 border-b-2 border-gray-200">
|
||||||
<svelte:component this={data.content} />
|
<svelte:component this={data.content} />
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="flex gap-2 py-4">
|
<div class="flex gap-2 py-4">
|
||||||
@@ -34,6 +35,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if data.relatedPosts.length > 0}
|
||||||
|
<section class="mt-5">
|
||||||
|
<p class="text-xl font-semibold">Related Posts</p>
|
||||||
|
<ul class="flex items-start justify-start gap-3 overflow-x-scroll">
|
||||||
|
{#each data.relatedPosts as post}
|
||||||
|
<li class="py-10 min-w-[80%] sm:w-80 sm:min-w-0">
|
||||||
|
<BlogListItem {post} isRecommended />
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/blog"
|
href="/blog"
|
||||||
class="flex font-medium items-center gap-2 text-slate-700 text-lg underline my-10"
|
class="flex font-medium items-center gap-2 text-slate-700 text-lg underline my-10"
|
||||||
|
|||||||
@@ -1,45 +1,71 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
import type { BlogPost, RawBlogPost } from '$lib/types';
|
||||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||||
|
|
||||||
export async function load({ params }) {
|
/** @type {import('./$types').PageLoad} */
|
||||||
|
export async function load({ params, fetch }) {
|
||||||
try {
|
try {
|
||||||
const blog = await import(`../../../blog/${params.slug}.md`);
|
const response = await fetch('/api/blog');
|
||||||
|
const posts: BlogPost[] = await response.json();
|
||||||
|
|
||||||
const pageMetaTags = Object.freeze({
|
const blog: RawBlogPost = await import(`../../../blog/${params.slug}.md`);
|
||||||
title: blog.metadata.title,
|
|
||||||
description: blog.metadata.description,
|
|
||||||
openGraph: {
|
|
||||||
title: blog.metadata.title,
|
|
||||||
description: blog.metadata.description,
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: blog.metadata.imageUrl,
|
|
||||||
alt: blog.metadata.title,
|
|
||||||
width: 280,
|
|
||||||
height: 200,
|
|
||||||
secureUrl: blog.metadata.imageUrl,
|
|
||||||
type: 'image/jpeg'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
handle: '@classroomio',
|
|
||||||
site: '@classroomio',
|
|
||||||
cardType: 'summary_large_image' as const,
|
|
||||||
title: blog.metadata.title,
|
|
||||||
description: blog.metadata.description,
|
|
||||||
image: blog.metadata.imageUrl,
|
|
||||||
imageAlt: `${blog.metadata.title} OG`
|
|
||||||
}
|
|
||||||
}) satisfies MetaTagsProps;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: blog.default,
|
content: blog.default,
|
||||||
meta: blog.metadata,
|
meta: blog.metadata,
|
||||||
slug: params.slug,
|
slug: params.slug,
|
||||||
pageMetaTags
|
pageMetaTags: getPageMetaTags(blog),
|
||||||
|
relatedPosts: getRelatedPosts(posts, blog.metadata.tags, params.slug)
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(404, `Could not find ${params.slug}`);
|
error(404, `Could not find ${params.slug}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRelatedPosts(posts: BlogPost[], tags: string[], slug: string) {
|
||||||
|
const related = posts
|
||||||
|
.filter((post) => post.tags.some((tag) => tags.includes(tag)) && post.slug !== slug)
|
||||||
|
.map((post) => ({
|
||||||
|
...post,
|
||||||
|
rank: post.tags.filter((tag) => tags.includes(tag)).length
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.rank - a.rank);
|
||||||
|
|
||||||
|
const topPosts = related.filter((post) => post.rank > 1);
|
||||||
|
|
||||||
|
if (topPosts.length > 2) {
|
||||||
|
return topPosts.sort(() => Math.random() - 0.5).slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return related.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageMetaTags(blog: RawBlogPost): MetaTagsProps {
|
||||||
|
return Object.freeze({
|
||||||
|
title: blog.metadata.title,
|
||||||
|
description: blog.metadata.description,
|
||||||
|
openGraph: {
|
||||||
|
title: blog.metadata.title,
|
||||||
|
description: blog.metadata.description,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: blog.metadata.imageUrl,
|
||||||
|
alt: blog.metadata.title,
|
||||||
|
width: 280,
|
||||||
|
height: 200,
|
||||||
|
secureUrl: blog.metadata.imageUrl,
|
||||||
|
type: 'image/jpeg'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
handle: '@classroomio',
|
||||||
|
site: '@classroomio',
|
||||||
|
cardType: 'summary_large_image' as const,
|
||||||
|
title: blog.metadata.title,
|
||||||
|
description: blog.metadata.description,
|
||||||
|
image: blog.metadata.imageUrl,
|
||||||
|
imageAlt: `${blog.metadata.title} OG`
|
||||||
|
}
|
||||||
|
}) satisfies MetaTagsProps;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user