refactor(tags): centralize tag slug logic in client-safe util

This commit is contained in:
kbot
2026-07-06 20:15:54 -05:00
parent 7c8a4d8c52
commit ff58dd000c
5 changed files with 21 additions and 37 deletions

View File

@@ -1,6 +1,7 @@
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import { getPosts, getPost, tagToSlug } from '@/lib/posts' import { getPosts, getPost } from '@/lib/posts'
import { tagToSlug } from '@/lib/tags'
import { TableOfContents } from '@/components/posts/TableOfContents' import { TableOfContents } from '@/components/posts/TableOfContents'
import { ScrollToTop } from '@/components/ui/ScrollToTop' import { ScrollToTop } from '@/components/ui/ScrollToTop'
import { ReadingProgress } from '@/components/ui/ReadingProgress' import { ReadingProgress } from '@/components/ui/ReadingProgress'

View File

@@ -3,18 +3,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { m, useReducedMotion } from 'motion/react'; import { m, useReducedMotion } from 'motion/react';
import type { PostMeta } from '@/lib/posts'; import type { PostMeta } from '@/lib/posts';
import { tagToSlug } from '@/lib/tags';
const tagToClientSlug = (tag: string): string =>
tag
.normalize('NFKD')
.toLowerCase()
.replace(/[\u0300-\u036f]/g, '')
.replace(/\+/g, ' plus ')
.replace(/#/g, ' sharp ')
.replace(/[\\/]+/g, ' ')
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/-{2,}/g, '-')
.replace(/^-|-$/g, '');
export function PostCard({ slug, title, date, excerpt, tags = [], author, readingTime, index = 0, coverImage }: PostMeta & { index?: number }) { export function PostCard({ slug, title, date, excerpt, tags = [], author, readingTime, index = 0, coverImage }: PostMeta & { index?: number }) {
const shouldReduceMotion = useReducedMotion(); const shouldReduceMotion = useReducedMotion();
@@ -56,7 +45,7 @@ export function PostCard({ slug, title, date, excerpt, tags = [], author, readin
{tags && tags.length > 0 && ( {tags && tags.length > 0 && (
<div className="mt-5 flex flex-wrap gap-2"> <div className="mt-5 flex flex-wrap gap-2">
{tags.slice(0, 3).map((tag) => ( {tags.slice(0, 3).map((tag) => (
<Link key={tag} href={`/tags/${tagToClientSlug(tag)}/`} className="rounded-full border border-border bg-surface/70 px-3 py-1 text-xs text-ink-soft transition-colors duration-200 hover:border-accent/60 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"> <Link key={tag} href={`/tags/${tagToSlug(tag)}/`} className="rounded-full border border-border bg-surface/70 px-3 py-1 text-xs text-ink-soft transition-colors duration-200 hover:border-accent/60 hover:text-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">
{tag} {tag}
</Link> </Link>
))} ))}

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { tagToSlug } from '@/lib/tags'
import { PostCard } from './PostCard' import { PostCard } from './PostCard'
type SearchPost = { type SearchPost = {
@@ -37,15 +38,6 @@ const normalizeSearchText = (value: string): string =>
.toLowerCase() .toLowerCase()
.trim() .trim()
const tagToClientSlug = (tag: string): string =>
normalizeSearchText(tag)
.replace(/\+/g, ' plus ')
.replace(/#/g, ' sharp ')
.replace(/[\\/]+/g, ' ')
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/-{2,}/g, '-')
.replace(/^-|-$/g, '')
const getOrderedCharacterScore = (field: string, query: string): number => { const getOrderedCharacterScore = (field: string, query: string): number => {
if (!query) return 0 if (!query) return 0
@@ -100,7 +92,7 @@ const getTagFilters = (posts: SearchPost[]): TagFilter[] => {
const postTags = new Set<string>() const postTags = new Set<string>()
post.tags.forEach((tag) => { post.tags.forEach((tag) => {
const slug = tagToClientSlug(tag) const slug = tagToSlug(tag)
if (!slug || postTags.has(slug)) return if (!slug || postTags.has(slug)) return
postTags.add(slug) postTags.add(slug)
@@ -130,7 +122,7 @@ export function PostSearch({ posts }: PostSearchProps) {
const results = useMemo(() => { const results = useMemo(() => {
const scoredPosts = posts.reduce<ScoredPost[]>((matches, post, index) => { const scoredPosts = posts.reduce<ScoredPost[]>((matches, post, index) => {
if (selectedTag && !post.tags.some((tag) => tagToClientSlug(tag) === selectedTag)) { if (selectedTag && !post.tags.some((tag) => tagToSlug(tag) === selectedTag)) {
return matches return matches
} }

View File

@@ -2,6 +2,9 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import matter from 'gray-matter' import matter from 'gray-matter'
import { cache } from 'react' import { cache } from 'react'
import { tagToSlug } from '@/lib/tags'
export { tagToSlug } from '@/lib/tags'
const postsDirectory = path.join(process.cwd(), 'content/posts') const postsDirectory = path.join(process.cwd(), 'content/posts')
@@ -61,18 +64,6 @@ export const normalizePostData = (
readingTime, readingTime,
}) })
export const tagToSlug = (tag: string): string =>
tag
.normalize('NFKD')
.toLowerCase()
.replace(/[\u0300-\u036f]/g, '')
.replace(/\+/g, ' plus ')
.replace(/#/g, ' sharp ')
.replace(/[\\/]+/g, ' ')
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/-{2,}/g, '-')
.replace(/^-|-$/g, '')
const getMdxFiles = cache(async () => { const getMdxFiles = cache(async () => {
try { try {
const files = await fs.promises.readdir(postsDirectory) const files = await fs.promises.readdir(postsDirectory)

11
lib/tags.ts Normal file
View File

@@ -0,0 +1,11 @@
export const tagToSlug = (tag: string): string =>
tag
.normalize('NFKD')
.toLowerCase()
.replace(/[\u0300-\u036f]/g, '')
.replace(/\+/g, ' plus ')
.replace(/#/g, ' sharp ')
.replace(/[\\/]+/g, ' ')
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/-{2,}/g, '-')
.replace(/^-|-$/g, '')