@@ -13,6 +13,8 @@ import Subheading from "@/components/Subheading";
1313import Link from "next/link" ;
1414import ArticleContent from "@/components/ArticleContent" ;
1515import AuthorCard from "@/components/AuthorCard" ;
16+ import JsonLd from "@/components/JsonLd" ;
17+ import type { Metadata } from "next" ;
1618
1719import {
1820 RiInstagramLine ,
@@ -46,24 +48,94 @@ export async function generateMetadata({
4648 params,
4749} : {
4850 params : Promise < { title : string } > ;
49- } ) {
50- const { title } = await params ; // Await the params object
51+ } ) : Promise < Metadata > {
52+ const { title } = await params ;
5153 try {
5254 const articlesRef = collection ( db , "articles" ) ;
5355 const q = query ( articlesRef , where ( "slug" , "==" , title ) ) ;
5456 const snapshot = await getDocs ( q ) ;
5557
5658 if ( snapshot . empty ) {
57- return { title : "Article Not Found | L.A.P" } ;
59+ return {
60+ title : "Article Not Found" ,
61+ description : "The requested article could not be found." ,
62+ } ;
5863 }
5964
6065 const articleData = snapshot . docs [ 0 ] . data ( ) ;
66+ const articleTitle = articleData . title || "Untitled Article" ;
67+ const articleDescription = articleData . description || "Read this article on L.A.P Docs." ;
68+ const articleImage = articleData . img ;
69+ const articleDate = articleData . date ?. toDate ( ) . toISOString ( ) ;
70+
71+ // Fetch author name if available
72+ let authorName = "L.A.P Team" ;
73+ if ( articleData . authorUID ) {
74+ const authorSnapshot = await getDocs (
75+ query ( collection ( db , "authors" ) , where ( "uid" , "==" , articleData . authorUID ) )
76+ ) ;
77+ if ( ! authorSnapshot . empty ) {
78+ authorName = authorSnapshot . docs [ 0 ] . data ( ) . name || authorName ;
79+ }
80+ }
81+
82+ // Generate keywords from Title and Description
83+ const stopWords = new Set ( [ "a" , "an" , "the" , "and" , "or" , "but" , "is" , "are" , "of" , "to" , "in" , "on" , "for" , "with" , "at" , "by" , "from" , "up" , "about" , "into" , "over" , "after" ] ) ;
84+
85+ const extractKeywords = ( text : string ) => {
86+ return text
87+ . toLowerCase ( )
88+ . replace ( / [ ^ a - z 0 - 9 \s ] / g, "" ) // Remove bad chars
89+ . split ( / \s + / )
90+ . filter ( word => word . length > 2 && ! stopWords . has ( word ) ) ;
91+ } ;
92+
93+ const titleKeywords = extractKeywords ( articleTitle ) ;
94+ const descKeywords = extractKeywords ( articleDescription ) ;
95+ const uniqueKeywords = Array . from ( new Set ( [
96+ articleData . label ,
97+ "Technology" ,
98+ "Tutorials" ,
99+ "L.A.P Docs" ,
100+ ...titleKeywords ,
101+ ...descKeywords
102+ ] ) ) ;
103+
61104 return {
62- title : `${ articleData . title } | L.A.P` ,
105+ title : articleTitle ,
106+ description : articleDescription ,
107+ keywords : uniqueKeywords ,
108+ authors : [ { name : authorName } ] ,
109+ alternates : {
110+ canonical : `/posts/${ articleData . slug } ` ,
111+ } ,
112+ openGraph : {
113+ title : articleTitle ,
114+ description : articleDescription ,
115+ url : `/posts/${ articleData . slug } ` ,
116+ siteName : "L.A.P Docs" ,
117+ type : "article" ,
118+ publishedTime : articleDate ,
119+ authors : [ authorName ] ,
120+ images : articleImage
121+ ? [
122+ {
123+ url : articleImage ,
124+ alt : articleData . imgAlt || articleTitle ,
125+ } ,
126+ ]
127+ : [ ] ,
128+ } ,
129+ twitter : {
130+ card : "summary_large_image" ,
131+ title : articleTitle ,
132+ description : articleDescription ,
133+ images : articleImage ? [ articleImage ] : [ ] ,
134+ } ,
63135 } ;
64136 } catch ( error ) {
65137 return {
66- title : "Error Loading Article | L.A.P " ,
138+ title : "Error Loading Article" ,
67139 } ;
68140 }
69141}
@@ -149,8 +221,36 @@ export default async function ArticleDetails({
149221 . filter ( ( article ) => article . id !== processedArticle . id )
150222 . slice ( 0 , 3 ) ;
151223
224+ const jsonLd = {
225+ "@context" : "https://schema.org" ,
226+ "@type" : "BlogPosting" ,
227+ headline : processedArticle . title ,
228+ description : processedArticle . description ,
229+ image : processedArticle . img ? [ processedArticle . img ] : [ ] ,
230+ datePublished : processedArticle . date . toISOString ( ) ,
231+ dateModified : processedArticle . date . toISOString ( ) , // Assuming modified is same as published if not tracked
232+ author : {
233+ "@type" : "Person" ,
234+ name : processedArticle . authorName ,
235+ url : authorData . slug ? `https://lap-docs.netlify.app/team/${ authorData . slug } ` : undefined ,
236+ } ,
237+ publisher : {
238+ "@type" : "Organization" ,
239+ name : "L.A.P Docs" ,
240+ logo : {
241+ "@type" : "ImageObject" ,
242+ url : "https://lap-docs.netlify.app/logos/LAP-Logo-Color.png" , // Verify this path
243+ } ,
244+ } ,
245+ mainEntityOfPage : {
246+ "@type" : "WebPage" ,
247+ "@id" : `https://lap-docs.netlify.app/posts/${ processedArticle . slug } ` ,
248+ } ,
249+ } ;
250+
152251 return (
153252 < main className = "max-w-[95rem] w-full mx-auto px-4 md:pt-8 sm:pt-4 xs:pt-2 lg:pb-4 md:pb-4 sm:pb-2 xs:pb-2" >
253+ < JsonLd data = { jsonLd } />
154254 < PostNavigation href = "/posts" > POSTS</ PostNavigation >
155255
156256 < article className = "grid md:grid-cols-2 gap-6 md:gap-6 pb-6 md:pb-24" >
0 commit comments