Skip to content

Commit 7067f1f

Browse files
committed
feat: Enhance articles component with topic filtering and improve UI
- Refactored Articles component to include topic links and filtering by topic. - Updated article display to include topic paths and improved author references. - Changed button elements to links for better navigation and accessibility. - Enhanced SEO with new utility functions for building topic paths. - Added new API route for IndexNow integration to improve indexing. - Created a new page for displaying articles by topic. - Introduced content library for managing articles and authors. - Updated footer and header components for consistency and improved branding. - Added SVG for team title for better visual representation. - Updated package dependencies for better performance and security.
1 parent de85850 commit 7067f1f

26 files changed

Lines changed: 1459 additions & 945 deletions

app/api/indexnow/route.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import {
3+
INDEXNOW_KEY,
4+
SITE_HOST,
5+
SITE_URL,
6+
absoluteUrl,
7+
} from "@/lib/seo";
8+
9+
export const runtime = "nodejs";
10+
export const dynamic = "force-dynamic";
11+
12+
function normalizeUrl(value: unknown) {
13+
if (typeof value !== "string" || !value.trim()) {
14+
return null;
15+
}
16+
17+
try {
18+
const url = new URL(value, SITE_URL);
19+
return url.host === SITE_HOST ? url.toString() : null;
20+
} catch {
21+
return null;
22+
}
23+
}
24+
25+
export async function POST(request: NextRequest) {
26+
const secret = process.env.INDEXNOW_SECRET;
27+
if (secret && request.headers.get("x-indexnow-secret") !== secret) {
28+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
29+
}
30+
31+
let body: Record<string, unknown>;
32+
try {
33+
body = (await request.json()) as Record<string, unknown>;
34+
} catch {
35+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
36+
}
37+
38+
const rawUrlList = Array.isArray(body.urlList)
39+
? body.urlList
40+
: body.url
41+
? [body.url]
42+
: [];
43+
const urlList = rawUrlList
44+
.map((value) => normalizeUrl(value))
45+
.filter((value): value is string => Boolean(value))
46+
.slice(0, 10000);
47+
48+
if (urlList.length === 0) {
49+
return NextResponse.json(
50+
{ error: "Provide at least one same-host URL." },
51+
{ status: 400 },
52+
);
53+
}
54+
55+
const response = await fetch("https://api.indexnow.org/indexnow", {
56+
method: "POST",
57+
headers: {
58+
"Content-Type": "application/json; charset=utf-8",
59+
},
60+
body: JSON.stringify({
61+
host: SITE_HOST,
62+
key: INDEXNOW_KEY,
63+
keyLocation: absoluteUrl(`/${INDEXNOW_KEY}.txt`),
64+
urlList,
65+
}),
66+
});
67+
68+
if (!response.ok) {
69+
const message = await response.text();
70+
return NextResponse.json(
71+
{
72+
error: "IndexNow submission failed",
73+
status: response.status,
74+
message,
75+
},
76+
{ status: 502 },
77+
);
78+
}
79+
80+
return NextResponse.json({
81+
ok: true,
82+
submitted: urlList.length,
83+
});
84+
}

app/layout.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,6 @@ export const metadata: Metadata = {
4848
template: `%s | ${SITE_NAME}`,
4949
},
5050
description: SITE_DESCRIPTION,
51-
keywords: [
52-
SITE_NAME,
53-
"L.A.P Docs",
54-
"LAP Docs",
55-
"Technology",
56-
"Tutorials",
57-
"Guides",
58-
"Tech Simplified",
59-
"Programming",
60-
"Web Development",
61-
],
6251
authors: [{ name: "L.A.P Team", url: `${SITE_URL}/team` }],
6352
creator: "L.A.P Team",
6453
publisher: SITE_NAME,

app/not-found.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export default function NotFound() {
1818
<PageTitle
1919
className="sr-only"
2020
imgSrc="/images/titles/NotFound.svg"
21-
imgAlt="The words 'Not Found' in bold uppercase lettering"
21+
imageWidth={754}
22+
imageHeight={96}
23+
decorative
2224
>
2325
404 - Page Not Found
2426
</PageTitle>

app/page.tsx

Lines changed: 37 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Subheading from "@/components/Subheading";
88
import Sidebar from "@/components/Sidebar";
99
import { Suspense } from "react";
1010
import type { Metadata } from "next";
11-
import { db } from "@/lib/firebase";
11+
import { getAllAuthors, getPublishedArticles } from "@/lib/content";
1212
import {
1313
DEFAULT_OG_IMAGE_PATH,
1414
DEFAULT_TWITTER_IMAGE_PATH,
@@ -17,14 +17,6 @@ import {
1717
SITE_NAME,
1818
SITE_URL,
1919
} from "@/lib/seo";
20-
import {
21-
collection,
22-
getDocs,
23-
query,
24-
orderBy,
25-
limit,
26-
Timestamp,
27-
} from "firebase/firestore";
2820

2921
export const metadata: Metadata = {
3022
title: SITE_NAME,
@@ -62,14 +54,17 @@ type Article = {
6254
slug: string;
6355
description: string;
6456
authorName: string;
65-
date: string | Timestamp;
57+
date: string;
6658
read: string;
6759
label: string;
6860
img: string;
6961
imgAlt: string;
62+
topicPath: string;
7063
publish: boolean;
7164
};
7265

66+
export const dynamic = "force-dynamic";
67+
7368
type AuthorType = {
7469
id: string;
7570
slug: string;
@@ -82,64 +77,37 @@ type AuthorType = {
8277

8378
async function getData() {
8479
try {
85-
// Fetch latest articles
86-
const articlesQuery = query(
87-
collection(db, "articles"),
88-
orderBy("date", "desc"),
89-
limit(7),
90-
);
91-
const articlesSnapshot = await getDocs(articlesQuery);
92-
93-
// Fetch authors for mapping names
94-
const authorsSnapshot = await getDocs(collection(db, "authors"));
95-
const authorsMap = new Map(
96-
authorsSnapshot.docs.map((d) => [d.id, d.data().name]),
97-
);
98-
99-
const articles: Article[] = articlesSnapshot.docs
100-
.map((doc) => {
101-
const data = doc.data();
102-
return {
103-
id: doc.id,
104-
title: data.title || "",
105-
slug: data.slug || "",
106-
description: data.description || "",
107-
authorName:
108-
data.authorName ||
109-
authorsMap.get(data.authorUID) ||
110-
"Unknown Author",
111-
// Ensure date is a string. If it's a Timestamp, convert. If string, keep. Else current date.
112-
date:
113-
data.date instanceof Timestamp
114-
? data.date.toDate().toISOString()
115-
: typeof data.date === "string"
116-
? data.date
117-
: new Date().toISOString(),
118-
read: data.read || "",
119-
label: data.label || "",
120-
img: data.img || "",
121-
imgAlt: data.imgAlt || "",
122-
publish: data.publish || false,
123-
} as Article;
124-
})
125-
.filter((a) => a.publish === true);
126-
127-
// Fetch all authors for the authors section - explicitly pick fields
128-
const allAuthors = authorsSnapshot.docs.map((doc) => {
129-
const data = doc.data();
130-
return {
131-
id: doc.id,
132-
slug: data.slug || "",
133-
name: data.name || "",
134-
avatar: data.avatar || "",
135-
imgAlt: data.imgAlt || "",
136-
job: data.job || "",
137-
city: data.city || "",
138-
};
139-
});
80+
const [latestArticles, allAuthors] = await Promise.all([
81+
getPublishedArticles(7),
82+
getAllAuthors(),
83+
]);
84+
85+
const articles: Article[] = latestArticles.map((article) => ({
86+
id: article.id,
87+
title: article.title,
88+
slug: article.slug,
89+
description: article.description,
90+
authorName: article.authorName,
91+
date: article.date.toISOString(),
92+
read: article.read,
93+
label: article.label,
94+
img: article.img,
95+
imgAlt: article.imgAlt,
96+
topicPath: article.topicPath,
97+
publish: article.publish,
98+
}));
14099

141100
// Shuffle authors
142101
const shuffledAuthors = [...allAuthors]
102+
.map((author) => ({
103+
id: author.docId,
104+
slug: author.slug,
105+
name: author.name,
106+
avatar: author.avatar,
107+
imgAlt: author.imgAlt,
108+
job: author.job,
109+
city: author.city,
110+
}))
143111
.sort(() => 0.5 - Math.random())
144112
.slice(0, 4);
145113

@@ -158,7 +126,10 @@ export default async function Home() {
158126
<PageTitle
159127
className="sr-only"
160128
imgSrc="/images/titles/lap-docs.svg"
161-
imgAlt="The words 'L.A.P - Docs' in bold uppercase lettering"
129+
imageWidth={1520}
130+
imageHeight={216}
131+
priority
132+
decorative
162133
>
163134
L.A.P - Docs
164135
</PageTitle>
@@ -180,4 +151,3 @@ export default async function Home() {
180151
);
181152
}
182153

183-
export const revalidate = 60;

0 commit comments

Comments
 (0)