Skip to content

Commit f8f8d87

Browse files
committed
feat: Implement core page structures, sitemap, OpenGraph images, and JSON-LD for improved SEO.
1 parent 19ff494 commit f8f8d87

8 files changed

Lines changed: 377 additions & 34 deletions

File tree

app/layout.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,25 @@ import Footer from "@/components/Footer";
66
import Script from 'next/script';
77

88
export const metadata: Metadata = {
9-
title: "L.A.P Docs",
9+
metadataBase: new URL("https://lap-docs.netlify.app/"),
10+
title: {
11+
default: "L.A.P Docs",
12+
template: "%s | L.A.P Docs",
13+
},
1014
description: "Simplicity in Tech. L.A.P aims to make tech simple as possible for everyone.",
15+
openGraph: {
16+
title: "L.A.P Docs",
17+
description: "Simplicity in Tech. L.A.P aims to make tech simple as possible for everyone.",
18+
url: "https://lap-docs.netlify.app/",
19+
siteName: "L.A.P Docs",
20+
locale: "en_US",
21+
type: "website",
22+
},
23+
twitter: {
24+
card: "summary_large_image",
25+
title: "L.A.P Docs",
26+
description: "Simplicity in Tech. L.A.P aims to make tech simple as possible for everyone.",
27+
},
1128
};
1229

1330
export default function RootLayout({

app/opengraph-image.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ImageResponse } from "next/og";
2+
3+
export const runtime = "edge";
4+
5+
export const alt = "L.A.P Docs";
6+
export const size = {
7+
width: 1200,
8+
height: 630,
9+
};
10+
11+
export const contentType = "image/png";
12+
13+
export default async function Image() {
14+
return new ImageResponse(
15+
(
16+
<div
17+
style={{
18+
background: "linear-gradient(to bottom right, #000000, #111111)",
19+
height: "100%",
20+
width: "100%",
21+
display: "flex",
22+
flexDirection: "column",
23+
alignItems: "center",
24+
justifyContent: "center",
25+
color: "white",
26+
fontFamily: "sans-serif",
27+
}}
28+
>
29+
<div
30+
style={{
31+
display: "flex",
32+
flexDirection: "column",
33+
alignItems: "center",
34+
justifyContent: "center",
35+
border: "4px solid white",
36+
padding: "40px 80px",
37+
borderRadius: "20px",
38+
background: "black",
39+
}}
40+
>
41+
<div style={{ fontSize: 120, fontWeight: "bold", letterSpacing: "-0.05em" }}>
42+
L.A.P
43+
</div>
44+
<div style={{ fontSize: 40, marginTop: "20px", color: "#CCCCCC", textTransform: "uppercase", letterSpacing: "0.2em" }}>
45+
Docs
46+
</div>
47+
</div>
48+
49+
<div style={{ position: "absolute", bottom: "40px", fontSize: 24, color: "#444" }}>
50+
Simplicity in Tech
51+
</div>
52+
</div>
53+
),
54+
{
55+
...size,
56+
}
57+
);
58+
}

app/page.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,43 @@ import NewsTicker from "@/components/NewsTicker/NewsTicker";
66
import PageTitle from "@/components/PageTitle";
77
import Subheading from "@/components/Subheading";
88
import { Suspense } from "react";
9+
import type { Metadata } from "next";
10+
import JsonLd from "@/components/JsonLd";
911

10-
export const metadata = {
12+
export const metadata: Metadata = {
1113
title: "L.A.P Docs | Home",
1214
description: "Simplified text documents about everything made on the L.A.P - tutorials youtube channel",
15+
openGraph: {
16+
title: "L.A.P Docs | Home",
17+
description: "Simplified text documents about everything made on the L.A.P - tutorials youtube channel",
18+
url: "https://lap-docs.netlify.app/",
19+
siteName: "L.A.P Docs",
20+
type: "website",
21+
locale: "en_US",
22+
},
23+
twitter: {
24+
card: "summary_large_image",
25+
title: "L.A.P Docs",
26+
description: "Simplified text documents about everything made on the L.A.P - tutorials youtube channel",
27+
},
1328
};
1429

1530
export default function Home() {
31+
const jsonLd = {
32+
"@context": "https://schema.org",
33+
"@type": "WebSite",
34+
name: "L.A.P Docs",
35+
url: "https://lap-docs.netlify.app/",
36+
potentialAction: {
37+
"@type": "SearchAction",
38+
target: "https://lap-docs.netlify.app/search?q={search_term_string}",
39+
"query-input": "required name=search_term_string",
40+
},
41+
};
42+
1643
return (
1744
<main className="flex flex-col min-h-screen max-w-[95rem] w-full mx-auto px-4 lg:pt-0 sm:pt-4 xs:pt-2 lg:pb-4 md:pb-4 sm:pb-2 xs:pb-2">
45+
<JsonLd data={jsonLd} />
1846
<PageTitle
1947
className="sr-only"
2048
imgSrc="/images/titles/lap-docs.svg"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { ImageResponse } from "next/og";
2+
import { db } from "@/lib/firebase";
3+
import { collection, query, where, getDocs } from "firebase/firestore";
4+
5+
export const runtime = "edge";
6+
7+
export const alt = "L.A.P Docs Article";
8+
export const size = {
9+
width: 1200,
10+
height: 630,
11+
};
12+
13+
export const contentType = "image/png";
14+
15+
export default async function Image({
16+
params,
17+
}: {
18+
params: Promise<{ title: string }>;
19+
}) {
20+
const { title } = await params;
21+
22+
// Default values
23+
let articleTitle = "L.A.P Docs";
24+
let authorName = "L.A.P Team";
25+
26+
try {
27+
// Note: Firestore Edge support might be limited depending on the SDK version and environment.
28+
// If this fails in edge runtime, we might need to use node runtime or a different fetching method.
29+
// For now, assuming standard usage or changing runtime if needed.
30+
31+
// However, Firestore JS SDK isn't always edge-friendly.
32+
// Let's rely on standard fetch if we had an API, but here we have direct DB access.
33+
// To be safe with "edge" runtime and Firebase, we might need to remove "runtime = edge"
34+
// or ensure using a compatible adapter.
35+
// Given the environment (Next.js 15), let's try 'nodejs' runtime if 'edge' is problematic,
36+
// but 'edge' is preferred for OG.
37+
// actually, let's stick to Node.js runtime for Firebase compatibility unless we know it works on Edge.
38+
// Removing `export const runtime = 'edge'` to default to Node.js (Serverless) which supports Firebase Admin/Client SDKs reliably.
39+
40+
const articlesRef = collection(db, "articles");
41+
const q = query(articlesRef, where("slug", "==", title));
42+
const snapshot = await getDocs(q);
43+
44+
if (!snapshot.empty) {
45+
const data = snapshot.docs[0].data();
46+
articleTitle = data.title || articleTitle;
47+
48+
if (data.authorUID) {
49+
const authorSnapshot = await getDocs(
50+
query(collection(db, "authors"), where("uid", "==", data.authorUID))
51+
);
52+
if (!authorSnapshot.empty) {
53+
authorName = authorSnapshot.docs[0].data().name || authorName;
54+
}
55+
}
56+
}
57+
} catch (error) {
58+
console.error("Error fetching OG data:", error);
59+
}
60+
61+
return new ImageResponse(
62+
(
63+
<div
64+
style={{
65+
background: "linear-gradient(to bottom right, #000000, #1a1a1a)",
66+
height: "100%",
67+
width: "100%",
68+
display: "flex",
69+
flexDirection: "column",
70+
alignItems: "flex-start",
71+
justifyContent: "space-between",
72+
padding: "60px",
73+
color: "white",
74+
fontFamily: "sans-serif",
75+
}}
76+
>
77+
<div style={{ display: "flex", alignItems: "center", gap: "20px" }}>
78+
{/* Logo or Brand Name */}
79+
<div style={{
80+
fontSize: 32,
81+
fontWeight: "bold",
82+
color: "#CCCCCC",
83+
border: "2px solid #CCCCCC",
84+
padding: "8px 16px",
85+
borderRadius: "50px"
86+
}}>
87+
L.A.P
88+
</div>
89+
</div>
90+
91+
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
92+
<div style={{ fontSize: 64, fontWeight: "bold", lineHeight: 1.1 }}>
93+
{articleTitle}
94+
</div>
95+
<div style={{ fontSize: 32, color: "#AAAAAA" }}>
96+
by {authorName}
97+
</div>
98+
</div>
99+
100+
<div style={{ fontSize: 24, color: "#666666" }}>
101+
lap-docs.netlify.app
102+
</div>
103+
</div>
104+
),
105+
{
106+
...size,
107+
}
108+
);
109+
}

app/posts/[title]/page.tsx

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import Subheading from "@/components/Subheading";
1313
import Link from "next/link";
1414
import ArticleContent from "@/components/ArticleContent";
1515
import AuthorCard from "@/components/AuthorCard";
16+
import JsonLd from "@/components/JsonLd";
17+
import type { Metadata } from "next";
1618

1719
import {
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-z0-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

Comments
 (0)