refactor(tags): centralize tag slug logic in client-safe util
This commit is contained in:
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
lib/posts.ts
15
lib/posts.ts
@@ -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
11
lib/tags.ts
Normal 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, '')
|
||||||
Reference in New Issue
Block a user