feat: blog tags (#334)

* feat: blog tags

* feat: fit limit passed to function
This commit is contained in:
Kalidou Diagne
2025-04-18 17:30:22 +03:00
committed by GitHub
parent a9ec3101e7
commit dc415e1c5b
17 changed files with 223 additions and 161 deletions

View File

@@ -3,11 +3,17 @@ RUN apk add --no-cache git curl
WORKDIR /builder
COPY package.json yarn.lock ./
RUN corepack enable
RUN yarn install
# Copy manifests, lock file, and yarn config
COPY package.json yarn.lock .yarnrc.yml ./
# Enable corepack and install dependencies using the lockfile
RUN corepack enable
RUN yarn install --immutable
# Copy the rest of the application code
COPY . .
# Build the application
RUN yarn build
# Create image by copying build artifacts

View File

@@ -1,4 +1,3 @@
import Image from "next/image"
import Link from "next/link"
import { siteConfig } from "@/config/site"
@@ -22,20 +21,13 @@ export default async function AboutPage({ params: { lang } }: any) {
}) as any[]) ?? []
return (
<Divider.Section className="bg-white">
<PageHeader
title={t("title")}
subtitle={t("description")}
image={
<Image
width={280}
height={280}
className="mx-auto h-[210px] w-[210px] lg:ml-auto lg:h-[320px] lg:w-[320px]"
src="/logos/pse-logo-bg.svg"
alt="pse logo"
/>
}
actions={
<div className="flex flex-col">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-4 py-10 w-full max-w-[978px] mx-auto">
<Label.PageTitle label={t("title")} />
<h6 className="font-sans text-base font-normal text-tuatara-950 md:text-[18px] md:leading-[27px] md:max-w-[700px]">
{t("description")}
</h6>
<Link
href={siteConfig.links.discord}
target="_blank"
@@ -53,56 +45,57 @@ export default async function AboutPage({ params: { lang } }: any) {
</div>
</Button>
</Link>
}
/>
<div className="flex justify-center">
<AppContent className="container flex w-full max-w-[978px] flex-col gap-8 py-10 md:py-16">
<Label.Section
className="text-center"
label={t("our-principles-title")}
/>
<Accordion
type="multiple"
items={[
...principles.map((principle: any, index: number) => {
return {
label: principle.title,
value: index.toString(),
children: (
<span className="flex flex-col gap-6 break-words pb-12 font-sans text-lg font-normal leading-[150%]">
{principle.description.map(
(description: string, index: number) => {
return <p key={index}>{description}</p>
}
)}
</span>
),
}
}),
]}
/>
</AppContent>
</div>
<Divider.Section className="bg-white">
<div className="flex justify-center">
<AppContent className="container flex w-full max-w-[978px] flex-col gap-8 py-10 md:py-16">
<Label.Section
className="text-center"
label={t("our-principles-title")}
/>
<Accordion
type="multiple"
items={[
...principles.map((principle: any, index: number) => {
return {
label: principle.title,
value: index.toString(),
children: (
<span className="flex flex-col gap-6 break-words pb-12 font-sans text-lg font-normal leading-[150%]">
{principle.description.map(
(description: string, index: number) => {
return <p key={index}>{description}</p>
}
)}
</span>
),
}
}),
]}
/>
</AppContent>
</div>
<Banner title={t("banner.title")} subtitle={t("banner.subtitle")}>
<Link
href={siteConfig.links.discord}
target="_blank"
rel="noreferrer"
passHref
>
<Button>
<div className="flex items-center gap-2">
<Icons.discord fill="white" className="h-4" />
<span className="text-[14px] uppercase">
{common("joinOurDiscord")}
</span>
<Icons.externalUrl fill="white" className="h-5" />
</div>
</Button>
</Link>
</Banner>
</Divider.Section>
<Banner title={t("banner.title")} subtitle={t("banner.subtitle")}>
<Link
href={siteConfig.links.discord}
target="_blank"
rel="noreferrer"
passHref
>
<Button>
<div className="flex items-center gap-2">
<Icons.discord fill="white" className="h-4" />
<span className="text-[14px] uppercase">
{common("joinOurDiscord")}
</span>
<Icons.externalUrl fill="white" className="h-5" />
</div>
</Button>
</Link>
</Banner>
</Divider.Section>
</div>
)
}

View File

@@ -1,10 +1,12 @@
import { blogArticleCardTagCardVariants } from "@/components/blog/blog-article-card"
import { BlogContent } from "@/components/blog/blog-content"
import { AppContent } from "@/components/ui/app-content"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Markdown } from "@/components/ui/markdown"
import { getArticles, getArticleById } from "@/lib/blog"
import { Metadata } from "next"
import Link from "next/link"
export const generateStaticParams = async () => {
const articles = await getArticles()
@@ -81,6 +83,18 @@ export default function BlogArticle({ params }: any) {
{post?.tldr && <Markdown>{post?.tldr}</Markdown>}
</div>
) : null}
{(post?.tags ?? [])?.length > 0 && (
<div className="flex flex-col gap-2">
<span className="text-sm italic text-tuatara-950">Tags:</span>
<div className="flex gap-2">
{post?.tags?.map((tag) => (
<Link key={tag} href={`/${params.lang}/blog?tag=${tag}`}>
<Button size="xs">{tag}</Button>
</Link>
))}
</div>
</div>
)}
</AppContent>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { useTranslation } from "@/app/i18n"
import { BlogArticles } from "@/components/blog/blog-articles"
import { AppContent } from "@/components/ui/app-content"
import { Label } from "@/components/ui/label"
import { getArticles } from "@/lib/blog"
import { Metadata } from "next"
export const metadata: Metadata = {
@@ -9,14 +10,30 @@ export const metadata: Metadata = {
description: "",
}
const BlogPage = async ({ params: { lang } }: any) => {
interface BlogPageProps {
params: { lang: string }
searchParams?: { [key: string]: string | string[] | undefined }
}
const BlogPage = async ({ params: { lang }, searchParams }: BlogPageProps) => {
const { t } = await useTranslation(lang, "blog-page")
// Get the tag from searchParams
const tag = searchParams?.tag as string | undefined
// Fetch articles, filtering by tag if present
const articles = await getArticles({ tag })
return (
<div className="flex flex-col">
<div className="w-full bg-cover-gradient border-b border-tuatara-300">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-4 py-10 w-full">
<Label.PageTitle label={t("title")} />
{tag && (
<h2 className="text-xl font-semibold text-tuatara-800">
{`Filtered by tag: "${tag}"`}
</h2>
)}
<h6 className="font-sans text-base font-normal text-tuatara-950 md:text-[18px] md:leading-[27px] md:max-w-[700px]">
{t("subtitle")}
</h6>
@@ -24,7 +41,8 @@ const BlogPage = async ({ params: { lang } }: any) => {
</div>
<AppContent className="flex flex-col gap-10 py-10">
<BlogArticles />
{/* Pass fetched articles and lang to the component */}
<BlogArticles articles={articles} lang={lang} />
</AppContent>
</div>
)

View File

@@ -20,7 +20,7 @@ export default async function ProjectsPage({ params: { lang } }: any) {
return (
<div className="flex flex-col">
<div className="w-full bg-cover-gradient border-b border-tuatara-300">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-4 py-10 w-full">
<Label.PageTitle label={t("title")} />
<h6 className="font-sans text-base font-normal text-tuatara-950 md:text-[18px] md:leading-[27px] md:max-w-[700px]">

View File

@@ -17,14 +17,9 @@ export const metadata: Metadata = {
const ResearchPage = async ({ params: { lang } }: any) => {
const { t } = await useTranslation(lang, "research-page")
return (
<div className="flex flex-col gap-10 lg:gap-32 pb-[128px]">
<div
className="w-full"
style={{
background: "linear-gradient(180deg, #C2E8F5 -17.44%, #FFF 62.5%)",
}}
>
<AppContent className="flex flex-col gap-4 pt-10 w-full lg:px-[200px]">
<div className="flex flex-col gap-10 lg:gap-32 pb-[128px] ">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-4 py-10 w-full">
<Label.PageTitle label={t("title")} />
<h6 className="font-sans text-base font-normal text-tuatara-950 md:text-[18px] md:leading-[27px] md:max-w-[700px]">
{t("subtitle")}

View File

@@ -1,7 +1,6 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import Image from "next/image"
import Link from "next/link"
import { LangProps } from "@/types/common"
@@ -173,21 +172,13 @@ export default function ResourcePage({ params: { lang } }: LangProps) {
const { t: common } = useTranslation(lang, "common")
return (
<Divider.Section className="bg-white">
<PageHeader
title={t("title")}
subtitle={t("subtitle")}
image={
<div className="m-auto flex h-[320px] w-full max-w-[280px] items-center justify-center md:m-0 md:h-full md:w-full lg:max-w-[343px]">
<Image
width={343}
height={320}
src="/icons/resource-illustration.webp"
alt="illustrations"
/>
</div>
}
actions={
<div className="flex flex-col">
<div className="w-full bg-page-header-gradient">
<AppContent className="flex flex-col gap-4 py-10 w-full">
<Label.PageTitle label={t("title")} />
<h6 className="font-sans text-base font-normal text-tuatara-950 md:text-[18px] md:leading-[27px] md:max-w-[700px]">
{t("subtitle")}
</h6>
<Link
href={siteConfig.addGithubResource}
target="_blank"
@@ -204,58 +195,60 @@ export default function ResourcePage({ params: { lang } }: LangProps) {
</div>
</Button>
</Link>
}
/>
<div className="flex justify-center">
<AppContent className="grid grid-cols-1 gap-6 py-10 md:grid-cols-[3fr_1fr] md:pb-20 lg:grid-cols-[4fr_1fr]">
<div className="flex flex-col gap-6">
<article className="flex flex-col space-y-8 ">
<ResourcesContent
components={{
ResourceItem: (props: ResourceItemProps) => (
<ResourceItem {...props} />
),
ResourceCard: (props: ResourceCardProps) => (
<ResourceCard {...props} />
),
}}
/>
</article>
</div>
<section className="relative hidden md:block ">
<div className="sticky right-0 top-16 ml-auto">
<ResourceNav lang={lang} />
</div>
</section>
</AppContent>
</div>
<Banner
title={
<h3 className="py-2 font-display text-[18px] font-bold text-tuatara-950 md:text-3xl">
{t("addResourceQuestion")}
</h3>
}
>
<Link
href={siteConfig.links.discord}
className="pb-6"
target="_blank"
rel="noreferrer"
passHref
>
<Button>
<div className="flex items-center gap-1">
<Icons.discord size={18} />
<span className="text-[14px] uppercase">
{common("connectWithUsOnPlatform", {
platform: "Discord",
})}
</span>
<Icons.externalUrl size={20} />
<Divider.Section className="bg-white">
<div className="flex justify-center">
<AppContent className="grid grid-cols-1 gap-6 py-10 md:grid-cols-[3fr_1fr] md:pb-20 lg:grid-cols-[4fr_1fr]">
<div className="flex flex-col gap-6">
<article className="flex flex-col space-y-8 ">
<ResourcesContent
components={{
ResourceItem: (props: ResourceItemProps) => (
<ResourceItem {...props} />
),
ResourceCard: (props: ResourceCardProps) => (
<ResourceCard {...props} />
),
}}
/>
</article>
</div>
</Button>
</Link>
</Banner>
</Divider.Section>
<section className="relative hidden md:block ">
<div className="sticky right-0 top-16 ml-auto">
<ResourceNav lang={lang} />
</div>
</section>
</AppContent>
</div>
<Banner
title={
<h3 className="py-2 font-display text-[18px] font-bold text-tuatara-950 md:text-3xl">
{t("addResourceQuestion")}
</h3>
}
>
<Link
href={siteConfig.links.discord}
className="pb-6"
target="_blank"
rel="noreferrer"
passHref
>
<Button>
<div className="flex items-center gap-1">
<Icons.discord size={18} />
<span className="text-[14px] uppercase">
{common("connectWithUsOnPlatform", {
platform: "Discord",
})}
</span>
<Icons.externalUrl size={20} />
</div>
</Button>
</Link>
</Banner>
</Divider.Section>
</div>
)
}

View File

@@ -19,6 +19,7 @@ image: "/articles/my-new-article/cover.webp" # Image used as cover
tldr: "A brief summary of your article" #Short summary
date: "YYYY-MM-DD" # Publication date in ISO format
canonical: "mirror.xyz/my-new-article" # (Optional) The original source URL, this tells search engines the primary version of the content
tags: ["tag1", "tag2"] # (Optional) Add relevant tags as an array of strings to categorize the article
---
```

View File

@@ -1,15 +1,24 @@
import { Article, getArticles } from "@/lib/blog"
import { Article } from "@/lib/blog"
import Link from "next/link"
import { BlogArticleCard } from "./blog-article-card"
export const BlogArticles = () => {
const articles = getArticles()
interface BlogArticlesProps {
articles: Article[]
lang: string // Add lang prop for correct linking
}
export const BlogArticles = ({ articles, lang }: BlogArticlesProps) => {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{articles.length === 0 && (
<p className="col-span-full text-center text-gray-500">
No articles found for this tag.
</p>
)}
{articles.map(
({ id, title, image, tldr = "", date, authors, content }: Article) => {
const url = `/blog/${id}`
// Use lang parameter for correct article URL
const url = `/${lang}/blog/${id}`
return (
<Link
className="flex-1 w-full h-full group hover:opacity-90 transition-opacity duration-300 rounded-xl overflow-hidden bg-white shadow-sm border border-slate-900/10"

View File

@@ -8,7 +8,7 @@ import { Button } from "../ui/button"
import { Icons } from "../icons"
export async function BlogRecentArticles({ lang }: { lang: any }) {
const articles = getArticles(5)
const articles = getArticles({ limit: 5 })
const { t } = await useTranslation(lang, "blog-page")
const lastArticle = articles[0]

View File

@@ -11,6 +11,7 @@ type PageHeaderProps = {
image?: ReactNode
contentWidth?: number
showDivider?: boolean
size?: "small" | "large"
}
const PageHeader = ({
@@ -20,6 +21,7 @@ const PageHeader = ({
children,
image,
showDivider = true,
size = "small",
}: PageHeaderProps) => {
return (
<div className="flex h-full w-full items-center bg-cover-gradient md:h-[600px]">
@@ -27,7 +29,7 @@ const PageHeader = ({
<div className="flex flex-col items-start justify-between gap-10 md:flex-row md:gap-28">
<div className="flex w-full flex-col justify-center gap-6 md:max-w-[700px] md:gap-8 lg:gap-14">
<div className="flex flex-col gap-4 md:gap-8">
<Label.PageTitle label={title} />
<Label.PageTitle label={title} size={size} />
{subtitle && (
<h6 className="font-sans text-base font-normal text-tuatara-950 md:text-[18px] md:leading-[27px]">
{subtitle}

View File

@@ -55,7 +55,7 @@ export const ResearchList = ({ lang }: LangProps["params"]) => {
if (!isMounted) {
return (
<div className="flex flex-col gap-10 lg:px-[100px]">
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-6 overflow-hidden">
<div
className={cn(
@@ -90,7 +90,7 @@ export const ResearchList = ({ lang }: LangProps["params"]) => {
<div className="relative grid items-start justify-between grid-cols-1">
<div
data-section="active-researchs"
className="flex flex-col justify-between gap-10 lg:px-[100px]"
className="flex flex-col justify-between gap-10"
>
<div className={cn("flex w-full flex-col gap-10")}>
{!hasActiveFilters && (

View File

@@ -22,7 +22,7 @@ export const HomepageHeader = ({ lang }: { lang: any }) => {
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.8, cubicBezier: "easeOut" }}
>
<Label.PageTitle label={t("headerTitle")} />
<Label.PageTitle size="large" label={t("headerTitle")} />
</motion.h1>
}
subtitle={t("headerSubtitle")}

View File

@@ -25,6 +25,7 @@ const buttonVariants = cva(
},
size: {
default: "h-10 py-2 px-4",
xs: "h-8 px-2 rounded-md",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
},

View File

@@ -3,6 +3,7 @@ import { cn } from "@/lib/utils"
interface LabelProps {
label: React.ReactNode
className?: string
size?: "small" | "large"
}
const SectionTitle = ({ label, className = "" }: LabelProps) => {
@@ -18,11 +19,16 @@ const SectionTitle = ({ label, className = "" }: LabelProps) => {
)
}
const MainPageTitle = ({ label, className = "" }: LabelProps) => {
const MainPageTitle = ({
label,
className = "",
size = "small",
}: LabelProps) => {
return (
<span
className={cn(
"text-4xl font-bold break-words font-display text-tuatara-950 lg:text-6xl xl:text-7xl",
"text-4xl font-bold break-words font-display text-tuatara-950 ",
size === "small" ? "lg:text-5xl" : "lg:text-6xl xl:text-7xl",
className
)}
>

View File

@@ -15,12 +15,14 @@ export interface Article {
publicKey?: string
hash?: string
canonical?: string
tags?: string[]
}
const articlesDirectory = path.join(process.cwd(), "articles")
// Get all articles from /articles
export function getArticles(limit: number = 1000) {
export function getArticles(options?: { limit?: number; tag?: string }) {
const { limit = 1000, tag } = options ?? {}
// Get file names under /articles
const fileNames = fs.readdirSync(articlesDirectory)
const allArticlesData = fileNames.map((fileName: string) => {
@@ -53,9 +55,18 @@ export function getArticles(limit: number = 1000) {
},
})
// Ensure tags are always an array, combining 'tags' and 'tag'
const tags = [
...(Array.isArray(matterResult.data?.tags)
? matterResult.data.tags
: []),
...(matterResult.data?.tag ? [matterResult.data.tag] : []),
]
return {
id,
...matterResult.data,
tags: tags, // Assign the combined and normalized tags array
content: matterResult.content,
}
} catch (error) {
@@ -66,14 +77,23 @@ export function getArticles(limit: number = 1000) {
title: `Error processing ${id}`,
content: "This article could not be processed due to an error.",
date: new Date().toISOString().split("T")[0],
tags: [],
}
}
})
let filteredArticles = allArticlesData.filter(Boolean) as Article[]
// Filter by tag if provided
if (tag) {
filteredArticles = filteredArticles.filter((article) =>
article.tags?.includes(tag)
)
}
// Sort posts by date
return allArticlesData
.filter(Boolean)
.sort((a: any, b: any) => {
return filteredArticles
.sort((a, b) => {
const dateA = new Date(a.date)
const dateB = new Date(b.date)
@@ -81,11 +101,13 @@ export function getArticles(limit: number = 1000) {
return dateB.getTime() - dateA.getTime()
})
.slice(0, limit)
.filter((article: any) => article.id !== "_article-template") as Article[]
.filter((article) => article.id !== "_article-template")
}
export function getArticleById(slug?: string) {
const articles = getArticles()
// Note: This might need adjustment if you expect getArticleById to also have tags
// Currently relies on the base getArticles() which fetches all tags
const articles = getArticles() // Fetch all articles to find the one by ID
return articles.find((article) => article.id === slug)
}

View File

@@ -31,6 +31,8 @@ module.exports = {
"linear-gradient(180deg, #C2E8F5 -17.44%, #FFF 17.72%)",
"research-card-gradient":
"linear-gradient(84deg, #FFF -1.95%, #EAFAFF 59.98%, #FFF 100.64%)",
"page-header-gradient":
"linear-gradient(180deg, #C2E8F5 -17.44%, #FFF 62.5%)",
},
colors: {
corduroy: "#4A5754",