🪛 03 routing
This commit is contained in:
47
app/about/page.tsx
Normal file
47
app/about/page.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Despre',
|
||||||
|
description: 'Află mai multe despre mine și acest blog',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold mb-8">Despre Mine</h1>
|
||||||
|
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<p className="text-lg leading-relaxed mb-6">
|
||||||
|
Bun venit pe blogul meu! Sunt un dezvoltator pasionat de tehnologie,
|
||||||
|
specializat în dezvoltarea web modernă cu Next.js, React și TypeScript.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-semibold mt-8 mb-4">Ce vei găsi aici</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li>Tutoriale despre dezvoltare web</li>
|
||||||
|
<li>Ghiduri practice pentru Next.js și React</li>
|
||||||
|
<li>Sfaturi despre design și UX</li>
|
||||||
|
<li>Experiențe din proiecte reale</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-semibold mt-8 mb-4">Tehnologii folosite</h2>
|
||||||
|
<p>Acest blog este construit cu:</p>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li><strong>Next.js 15</strong> - Framework React pentru producție</li>
|
||||||
|
<li><strong>TypeScript</strong> - Pentru type safety</li>
|
||||||
|
<li><strong>Tailwind CSS</strong> - Pentru stilizare rapidă</li>
|
||||||
|
<li><strong>Markdown</strong> - Pentru conținut</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-semibold mt-8 mb-4">Contact</h2>
|
||||||
|
<p>
|
||||||
|
Mă poți contacta pe{' '}
|
||||||
|
<a href="mailto:email@example.com" className="text-primary-600 hover:text-primary-700">
|
||||||
|
email@example.com
|
||||||
|
</a>{' '}
|
||||||
|
sau mă poți găsi pe rețelele sociale.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
app/blog/[...slug]/not-found.tsx
Normal file
23
app/blog/[...slug]/not-found.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[60vh] flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Articolul nu a fost găsit</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Ne pare rău, dar articolul pe care îl cauți nu există sau a fost mutat.
|
||||||
|
</p>
|
||||||
|
<div className="space-x-4">
|
||||||
|
<Link href="/blog" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
|
||||||
|
Vezi toate articolele
|
||||||
|
</Link>
|
||||||
|
<Link href="/" className="inline-block px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition">
|
||||||
|
Pagina principală
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
129
app/blog/[...slug]/page.tsx
Normal file
129
app/blog/[...slug]/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { getAllPosts, getPostBySlug, getRelatedPosts } from '@/lib/markdown'
|
||||||
|
import { formatDate, formatRelativeDate } from '@/lib/utils'
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const posts = await getAllPosts()
|
||||||
|
return posts.map((post) => ({ slug: post.slug.split('/') }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string[] }> }): Promise<Metadata> {
|
||||||
|
const { slug } = await params
|
||||||
|
const slugPath = slug.join('/')
|
||||||
|
const post = getPostBySlug(slugPath)
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return { title: 'Articol negăsit' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: post.frontmatter.title,
|
||||||
|
description: post.frontmatter.description,
|
||||||
|
authors: [{ name: post.frontmatter.author }],
|
||||||
|
openGraph: {
|
||||||
|
title: post.frontmatter.title,
|
||||||
|
description: post.frontmatter.description,
|
||||||
|
type: 'article',
|
||||||
|
publishedTime: post.frontmatter.date,
|
||||||
|
authors: [post.frontmatter.author],
|
||||||
|
images: post.frontmatter.image ? [post.frontmatter.image] : [],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: post.frontmatter.title,
|
||||||
|
description: post.frontmatter.description,
|
||||||
|
images: post.frontmatter.image ? [post.frontmatter.image] : [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthorInfo({ author, date }: { author: string; date: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-4 py-6 border-y border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-xl font-bold text-primary-600 dark:text-primary-400">
|
||||||
|
{author.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">{author}</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Publicat {formatRelativeDate(date)} • {formatDate(date)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RelatedPosts({ posts }: { posts: any[] }) {
|
||||||
|
if (posts.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Articole similare</h2>
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<Link key={post.slug} href={`/blog/${post.slug}`} className="block p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-lg transition">
|
||||||
|
<h3 className="font-semibold mb-2 line-clamp-2">{post.frontmatter.title}</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">{post.frontmatter.description}</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-2">{formatDate(post.frontmatter.date)}</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string[] }> }) {
|
||||||
|
const { slug } = await params
|
||||||
|
const slugPath = slug.join('/')
|
||||||
|
const post = getPostBySlug(slugPath)
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const relatedPosts = await getRelatedPosts(slugPath)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="max-w-4xl mx-auto">
|
||||||
|
<header className="mb-8">
|
||||||
|
{post.frontmatter.image && (
|
||||||
|
<img src={post.frontmatter.image} alt={post.frontmatter.title} className="w-full h-64 md:h-96 object-cover rounded-lg mb-8" />
|
||||||
|
)}
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold mb-4">{post.frontmatter.title}</h1>
|
||||||
|
<p className="text-xl text-gray-600 dark:text-gray-400 mb-6">{post.frontmatter.description}</p>
|
||||||
|
{post.frontmatter.tags && post.frontmatter.tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-6">
|
||||||
|
{post.frontmatter.tags.map((tag: string) => (
|
||||||
|
<Link key={tag} href={`/tags/${tag.toLowerCase().replace(/\s+/g, '-')}`} className="px-3 py-1 bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 rounded-full text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition">
|
||||||
|
#{tag}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<AuthorInfo author={post.frontmatter.author} date={post.frontmatter.date} />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="prose dark:prose-invert max-w-none">
|
||||||
|
<div className="flex items-center justify-between text-sm text-gray-500 mb-6">
|
||||||
|
<span>Timp estimat de citire: {post.readingTime} minute</span>
|
||||||
|
</div>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex justify-between items-center mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<Link href="/blog" className="flex items-center text-primary-600 hover:text-primary-700 transition">
|
||||||
|
<svg className="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Înapoi la blog
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<RelatedPosts posts={relatedPosts} />
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
95
app/blog/page.tsx
Normal file
95
app/blog/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { Metadata } from 'next'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { getAllPosts } from '@/lib/markdown'
|
||||||
|
import { formatDate } from '@/lib/utils'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Blog',
|
||||||
|
description: 'Toate articolele din blog',
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostCard({ post }: { post: any }) {
|
||||||
|
return (
|
||||||
|
<article className="border-b border-gray-200 dark:border-gray-700 pb-8 mb-8 last:border-0">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
|
{post.frontmatter.image && (
|
||||||
|
<div className="lg:w-1/3">
|
||||||
|
<img src={post.frontmatter.image} alt={post.frontmatter.title} className="w-full h-48 lg:h-full object-cover rounded-lg" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={post.frontmatter.image ? 'lg:w-2/3' : 'w-full'}>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-500 mb-2">
|
||||||
|
<time dateTime={post.frontmatter.date}>{formatDate(post.frontmatter.date)}</time>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{post.readingTime} min citire</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{post.frontmatter.author}</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold mb-3">
|
||||||
|
<Link href={`/blog/${post.slug}`} className="hover:text-primary-600 transition">
|
||||||
|
{post.frontmatter.title}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">{post.frontmatter.description}</p>
|
||||||
|
{post.frontmatter.tags && post.frontmatter.tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
|
{post.frontmatter.tags.map((tag: string) => (
|
||||||
|
<span key={tag} className="px-3 py-1 bg-gray-100 dark:bg-gray-800 text-sm rounded-full">
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Link href={`/blog/${post.slug}`} className="inline-flex items-center text-primary-600 hover:text-primary-700 transition">
|
||||||
|
Citește articolul complet
|
||||||
|
<svg className="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlogFilters({ totalPosts }: { totalPosts: number }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-8">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold mb-2">Articole Blog</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
{totalPosts} {totalPosts === 1 ? 'articol' : 'articole'} publicate
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogPage() {
|
||||||
|
const posts = await getAllPosts()
|
||||||
|
|
||||||
|
if (posts.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h1 className="text-3xl font-bold mb-4">Blog</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-8">Nu există articole publicate încă.</p>
|
||||||
|
<Link href="/" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
|
||||||
|
Înapoi la pagina principală
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<BlogFilters totalPosts={posts.length} />
|
||||||
|
<div>
|
||||||
|
{posts.map((post) => (
|
||||||
|
<PostCard key={post.slug} post={post} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,28 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import Link from 'next/link'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'My Blog',
|
title: {
|
||||||
description: 'Personal blog built with Next.js 15',
|
template: '%s | Blog & Portofoliu',
|
||||||
|
default: 'Blog & Portofoliu',
|
||||||
|
},
|
||||||
|
description: 'Blog personal despre dezvoltare web și design',
|
||||||
|
metadataBase: new URL('http://localhost:3000'),
|
||||||
|
authors: [{ name: 'Nume Autor' }],
|
||||||
|
keywords: ['blog', 'dezvoltare web', 'nextjs', 'react', 'typescript'],
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
locale: 'ro_RO',
|
||||||
|
siteName: 'Blog & Portofoliu',
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -12,8 +31,33 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="ro">
|
||||||
<body>{children}</body>
|
<body className={inter.className}>
|
||||||
|
<div className="min-h-screen bg-white dark:bg-gray-900">
|
||||||
|
<header className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<nav className="container mx-auto px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link href="/" className="text-2xl font-bold text-primary-600">
|
||||||
|
Blog
|
||||||
|
</Link>
|
||||||
|
<div className="flex space-x-6">
|
||||||
|
<Link href="/" className="hover:text-primary-600">Acasă</Link>
|
||||||
|
<Link href="/blog" className="hover:text-primary-600">Blog</Link>
|
||||||
|
<Link href="/about" className="hover:text-primary-600">Despre</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<footer className="border-t border-gray-200 dark:border-gray-700 mt-12">
|
||||||
|
<div className="container mx-auto px-4 py-6 text-center text-gray-600 dark:text-gray-400">
|
||||||
|
© 2025 Blog & Portofoliu. Toate drepturile rezervate.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
61
app/page.tsx
61
app/page.tsx
@@ -1,8 +1,59 @@
|
|||||||
export default function Home() {
|
import Link from 'next/link'
|
||||||
|
import { getAllPosts } from '@/lib/markdown'
|
||||||
|
import { formatDate } from '@/lib/utils'
|
||||||
|
|
||||||
|
export default async function HomePage() {
|
||||||
|
const allPosts = await getAllPosts()
|
||||||
|
const featuredPosts = allPosts.slice(0, 3)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen p-8">
|
<div className="space-y-12">
|
||||||
<h1 className="text-4xl font-bold text-primary-600">Welcome to My Blog</h1>
|
<section className="text-center py-12">
|
||||||
<p className="mt-4 text-gray-600">Built with Next.js 15, TypeScript, and Tailwind CSS</p>
|
<h1 className="text-5xl font-bold mb-4">Bun venit pe Blog</h1>
|
||||||
</main>
|
<p className="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||||
|
Explorează articole despre dezvoltare web, design și tehnologie.
|
||||||
|
Învață din experiențe practice și tutoriale detaliate.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 space-x-4">
|
||||||
|
<Link href="/blog" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
|
||||||
|
Vezi toate articolele
|
||||||
|
</Link>
|
||||||
|
<Link href="/about" className="inline-block px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition">
|
||||||
|
Despre mine
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-3xl font-bold mb-8">Articole Recente</h2>
|
||||||
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{featuredPosts.map((post) => (
|
||||||
|
<article key={post.slug} className="border border-gray-200 dark:border-gray-700 rounded-lg p-6 hover:shadow-lg transition">
|
||||||
|
{post.frontmatter.image && (
|
||||||
|
<img src={post.frontmatter.image} alt={post.frontmatter.title} className="w-full h-48 object-cover rounded-lg mb-4" />
|
||||||
|
)}
|
||||||
|
<h3 className="text-xl font-semibold mb-2">
|
||||||
|
<Link href={`/blog/${post.slug}`} className="hover:text-primary-600 transition">
|
||||||
|
{post.frontmatter.title}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">{post.frontmatter.description}</p>
|
||||||
|
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||||
|
<span>{formatDate(post.frontmatter.date)}</span>
|
||||||
|
<span>{post.readingTime} min citire</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="text-center py-12 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">Vrei să afli mai multe?</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-400 mb-6">Explorează arhiva completă de articole</p>
|
||||||
|
<Link href="/blog" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
|
||||||
|
Vezi toate articolele →
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,13 @@ export function validateFrontmatter(data: any): FrontMatter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostBySlug(slug: string[]): Post {
|
export function getPostBySlug(slug: string | string[]): Post | null {
|
||||||
const sanitized = slug.map(s => sanitizePath(s));
|
const slugArray = Array.isArray(slug) ? slug : [slug];
|
||||||
|
const sanitized = slugArray.map(s => sanitizePath(s));
|
||||||
const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md';
|
const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md';
|
||||||
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
if (!fs.existsSync(fullPath)) {
|
||||||
throw new Error('Post not found');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||||
@@ -89,7 +90,7 @@ export function getAllPosts(includeContent = false): Post[] {
|
|||||||
const slug = prefix ? `${prefix}/${file.replace(/\.md$/, '')}` : file.replace(/\.md$/, '');
|
const slug = prefix ? `${prefix}/${file.replace(/\.md$/, '')}` : file.replace(/\.md$/, '');
|
||||||
try {
|
try {
|
||||||
const post = getPostBySlug(slug.split('/'));
|
const post = getPostBySlug(slug.split('/'));
|
||||||
if (!post.frontmatter.draft) {
|
if (post && !post.frontmatter.draft) {
|
||||||
posts.push(includeContent ? post : { ...post, content: '' });
|
posts.push(includeContent ? post : { ...post, content: '' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -106,8 +107,12 @@ export function getAllPosts(includeContent = false): Post[] {
|
|||||||
return posts.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime());
|
return posts.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRelatedPosts(currentSlug: string, category: string, tags: string[], limit = 3): Post[] {
|
export async function getRelatedPosts(currentSlug: string, limit = 3): Promise<Post[]> {
|
||||||
|
const currentPost = getPostBySlug(currentSlug);
|
||||||
|
if (!currentPost) return [];
|
||||||
|
|
||||||
const allPosts = getAllPosts(false);
|
const allPosts = getAllPosts(false);
|
||||||
|
const { category, tags } = currentPost.frontmatter;
|
||||||
|
|
||||||
const scored = allPosts
|
const scored = allPosts
|
||||||
.filter(post => post.slug !== currentSlug)
|
.filter(post => post.slug !== currentSlug)
|
||||||
|
|||||||
33
lib/seo.ts
Normal file
33
lib/seo.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export interface SEOData {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
url: string
|
||||||
|
image?: string
|
||||||
|
type?: 'website' | 'article'
|
||||||
|
author?: string
|
||||||
|
publishedTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateStructuredData(data: SEOData) {
|
||||||
|
const structuredData: any = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': data.type === 'article' ? 'BlogPosting' : 'WebSite',
|
||||||
|
headline: data.title,
|
||||||
|
description: data.description,
|
||||||
|
url: data.url,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.image) {
|
||||||
|
structuredData.image = data.image
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'article') {
|
||||||
|
structuredData.author = {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: data.author || 'Unknown',
|
||||||
|
}
|
||||||
|
structuredData.datePublished = data.publishedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(structuredData)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user