From 05390016b2318aa9d4cd30cc01f220c8e258688f Mon Sep 17 00:00:00 2001 From: RJ Date: Fri, 7 Nov 2025 16:55:58 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9B=2003=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/about/page.tsx | 47 +++++++++++ app/blog/[...slug]/not-found.tsx | 23 ++++++ app/blog/[...slug]/page.tsx | 129 +++++++++++++++++++++++++++++++ app/blog/page.tsx | 95 +++++++++++++++++++++++ app/layout.tsx | 52 ++++++++++++- app/page.tsx | 61 +++++++++++++-- lib/markdown.ts | 15 ++-- lib/seo.ts | 33 ++++++++ 8 files changed, 441 insertions(+), 14 deletions(-) create mode 100644 app/about/page.tsx create mode 100644 app/blog/[...slug]/not-found.tsx create mode 100644 app/blog/[...slug]/page.tsx create mode 100644 app/blog/page.tsx create mode 100644 lib/seo.ts diff --git a/app/about/page.tsx b/app/about/page.tsx new file mode 100644 index 0000000..d8f383c --- /dev/null +++ b/app/about/page.tsx @@ -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 ( +
+

Despre Mine

+ +
+

+ Bun venit pe blogul meu! Sunt un dezvoltator pasionat de tehnologie, + specializat în dezvoltarea web modernă cu Next.js, React și TypeScript. +

+ +

Ce vei găsi aici

+
    +
  • Tutoriale despre dezvoltare web
  • +
  • Ghiduri practice pentru Next.js și React
  • +
  • Sfaturi despre design și UX
  • +
  • Experiențe din proiecte reale
  • +
+ +

Tehnologii folosite

+

Acest blog este construit cu:

+
    +
  • Next.js 15 - Framework React pentru producție
  • +
  • TypeScript - Pentru type safety
  • +
  • Tailwind CSS - Pentru stilizare rapidă
  • +
  • Markdown - Pentru conținut
  • +
+ +

Contact

+

+ Mă poți contacta pe{' '} + + email@example.com + {' '} + sau mă poți găsi pe rețelele sociale. +

+
+
+ ) +} diff --git a/app/blog/[...slug]/not-found.tsx b/app/blog/[...slug]/not-found.tsx new file mode 100644 index 0000000..973ee8c --- /dev/null +++ b/app/blog/[...slug]/not-found.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link' + +export default function NotFound() { + return ( +
+
+

404

+

Articolul nu a fost găsit

+

+ Ne pare rău, dar articolul pe care îl cauți nu există sau a fost mutat. +

+
+ + Vezi toate articolele + + + Pagina principală + +
+
+
+ ) +} diff --git a/app/blog/[...slug]/page.tsx b/app/blog/[...slug]/page.tsx new file mode 100644 index 0000000..e694a90 --- /dev/null +++ b/app/blog/[...slug]/page.tsx @@ -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 { + 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 ( +
+
+ + {author.charAt(0).toUpperCase()} + +
+
+

{author}

+

+ Publicat {formatRelativeDate(date)} • {formatDate(date)} +

+
+
+ ) +} + +function RelatedPosts({ posts }: { posts: any[] }) { + if (posts.length === 0) return null + + return ( +
+

Articole similare

+
+ {posts.map((post) => ( + +

{post.frontmatter.title}

+

{post.frontmatter.description}

+

{formatDate(post.frontmatter.date)}

+ + ))} +
+
+ ) +} + +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 ( +
+
+ {post.frontmatter.image && ( + {post.frontmatter.title} + )} +

{post.frontmatter.title}

+

{post.frontmatter.description}

+ {post.frontmatter.tags && post.frontmatter.tags.length > 0 && ( +
+ {post.frontmatter.tags.map((tag: string) => ( + + #{tag} + + ))} +
+ )} + +
+ +
+
+ Timp estimat de citire: {post.readingTime} minute +
+
+
+ + + + +
+ ) +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..f12d075 --- /dev/null +++ b/app/blog/page.tsx @@ -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 ( +
+
+ {post.frontmatter.image && ( +
+ {post.frontmatter.title} +
+ )} +
+
+ + + {post.readingTime} min citire + + {post.frontmatter.author} +
+

+ + {post.frontmatter.title} + +

+

{post.frontmatter.description}

+ {post.frontmatter.tags && post.frontmatter.tags.length > 0 && ( +
+ {post.frontmatter.tags.map((tag: string) => ( + + #{tag} + + ))} +
+ )} + + Citește articolul complet + + + + +
+
+
+ ) +} + +function BlogFilters({ totalPosts }: { totalPosts: number }) { + return ( +
+
+
+

Articole Blog

+

+ {totalPosts} {totalPosts === 1 ? 'articol' : 'articole'} publicate +

+
+
+
+ ) +} + +export default async function BlogPage() { + const posts = await getAllPosts() + + if (posts.length === 0) { + return ( +
+

Blog

+

Nu există articole publicate încă.

+ + Înapoi la pagina principală + +
+ ) + } + + return ( +
+ +
+ {posts.map((post) => ( + + ))} +
+
+ ) +} diff --git a/app/layout.tsx b/app/layout.tsx index 705e90f..da72438 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,28 @@ import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import Link from 'next/link' import './globals.css' +const inter = Inter({ subsets: ['latin'] }) + export const metadata: Metadata = { - title: 'My Blog', - description: 'Personal blog built with Next.js 15', + title: { + 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({ @@ -12,8 +31,33 @@ export default function RootLayout({ children: React.ReactNode }) { return ( - - {children} + + +
+
+ +
+
+ {children} +
+
+
+ © 2025 Blog & Portofoliu. Toate drepturile rezervate. +
+
+
+ ) } diff --git a/app/page.tsx b/app/page.tsx index 536de54..ae28317 100644 --- a/app/page.tsx +++ b/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 ( -
-

Welcome to My Blog

-

Built with Next.js 15, TypeScript, and Tailwind CSS

-
+
+
+

Bun venit pe Blog

+

+ Explorează articole despre dezvoltare web, design și tehnologie. + Învață din experiențe practice și tutoriale detaliate. +

+
+ + Vezi toate articolele + + + Despre mine + +
+
+ +
+

Articole Recente

+
+ {featuredPosts.map((post) => ( +
+ {post.frontmatter.image && ( + {post.frontmatter.title} + )} +

+ + {post.frontmatter.title} + +

+

{post.frontmatter.description}

+
+ {formatDate(post.frontmatter.date)} + {post.readingTime} min citire +
+
+ ))} +
+
+ +
+

Vrei să afli mai multe?

+

Explorează arhiva completă de articole

+ + Vezi toate articolele → + +
+
) } diff --git a/lib/markdown.ts b/lib/markdown.ts index aa5608a..f540562 100644 --- a/lib/markdown.ts +++ b/lib/markdown.ts @@ -52,12 +52,13 @@ export function validateFrontmatter(data: any): FrontMatter { }; } -export function getPostBySlug(slug: string[]): Post { - const sanitized = slug.map(s => sanitizePath(s)); +export function getPostBySlug(slug: string | string[]): Post | null { + const slugArray = Array.isArray(slug) ? slug : [slug]; + const sanitized = slugArray.map(s => sanitizePath(s)); const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md'; if (!fs.existsSync(fullPath)) { - throw new Error('Post not found'); + return null; } 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$/, ''); try { const post = getPostBySlug(slug.split('/')); - if (!post.frontmatter.draft) { + if (post && !post.frontmatter.draft) { posts.push(includeContent ? post : { ...post, content: '' }); } } 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()); } -export function getRelatedPosts(currentSlug: string, category: string, tags: string[], limit = 3): Post[] { +export async function getRelatedPosts(currentSlug: string, limit = 3): Promise { + const currentPost = getPostBySlug(currentSlug); + if (!currentPost) return []; + const allPosts = getAllPosts(false); + const { category, tags } = currentPost.frontmatter; const scored = allPosts .filter(post => post.slug !== currentSlug) diff --git a/lib/seo.ts b/lib/seo.ts new file mode 100644 index 0000000..c1434ff --- /dev/null +++ b/lib/seo.ts @@ -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) +}