Skip to content

Commit 63699b2

Browse files
committed
feat(blog): migrate from RSS feed to WordPress REST API with application password authentication
1 parent 86fb422 commit 63699b2

File tree

2 files changed

+68
-106
lines changed

2 files changed

+68
-106
lines changed

components/business/home-blog.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @next/next/no-img-element */
12
import type { BlogPost } from "@/lib/blog";
23
import { getLatestBlogPosts } from "@/lib/blog";
34
import { cn } from "@/lib/utils";
@@ -40,7 +41,7 @@ export default async function HomeBlog({ className }: HomeBlogProps) {
4041
className="flex flex-col justify-between rounded-xl border border-border bg-card/40 overflow-hidden text-left transition-colors hover:border-primary"
4142
>
4243
{post.imageUrl ? (
43-
<div className="relative h-40 w-full">
44+
<div className="relative h-64 w-full">
4445
<img
4546
src={post.imageUrl}
4647
alt={post.title}

lib/blog.ts

Lines changed: 66 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,102 +5,64 @@ export interface BlogPost {
55
imageUrl?: string;
66
}
77

8-
function extractTagContent(xml: string, tag: string): string | undefined {
9-
const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i");
10-
const match = xml.match(regex);
11-
12-
if (!match?.[1]) return undefined;
13-
14-
const value = match[1].trim();
15-
16-
if (value.startsWith("<![CDATA[") && value.endsWith("]]>")) {
17-
return value.slice(9, -3).trim();
18-
}
19-
20-
return value;
8+
interface WordPressPost {
9+
id: number;
10+
date: string;
11+
link: string;
12+
title: {
13+
rendered: string;
14+
};
15+
excerpt: {
16+
rendered: string;
17+
protected: boolean;
18+
};
19+
jetpack_featured_media_url?: string;
2120
}
2221

23-
function extractImageUrl(xml: string): string | undefined {
24-
// First, try to find image in <figure class="wp-block-image"> or similar figure tags
25-
const figureMatch = xml.match(
26-
/<figure[^>]*class=["'][^"']*wp-block-image[^"']*["'][^>]*>([\s\S]*?)<\/figure>/i
27-
);
28-
if (figureMatch?.[1]) {
29-
const imgMatch = figureMatch[1].match(/<img[^>]*src=["']([^"']+)["'][^>]*>/i);
30-
if (imgMatch?.[1]) {
31-
return imgMatch[1];
32-
}
33-
}
34-
35-
// Fallback: try any figure tag
36-
const anyFigureMatch = xml.match(
37-
/<figure[^>]*>([\s\S]*?)<\/figure>/i
38-
);
39-
if (anyFigureMatch?.[1]) {
40-
const imgMatch = anyFigureMatch[1].match(/<img[^>]*src=["']([^"']+)["'][^>]*>/i);
41-
if (imgMatch?.[1]) {
42-
return imgMatch[1];
43-
}
44-
}
45-
46-
// Fallback: try media:content
47-
const mediaMatch = xml.match(
48-
/<media:content[^>]*url=["']([^"']+)["'][^>]*>/i
49-
);
50-
if (mediaMatch?.[1]) {
51-
return mediaMatch[1];
52-
}
53-
54-
// Fallback: try enclosure
55-
const enclosureMatch = xml.match(
56-
/<enclosure[^>]*url=["']([^"']+)["'][^>]*>/i
57-
);
58-
if (enclosureMatch?.[1]) {
59-
return enclosureMatch[1];
60-
}
61-
62-
// Fallback: try content:encoded
63-
const contentMatch = xml.match(
64-
/<content:encoded[^>]*>([\s\S]*?)<\/content:encoded>/i
65-
);
66-
const content = contentMatch?.[1];
67-
68-
if (content) {
69-
const imgMatch = content.match(/<img[^>]*src=["']([^"']+)["'][^>]*>/i);
70-
if (imgMatch?.[1]) {
71-
return imgMatch[1];
72-
}
73-
}
74-
75-
const covers = [
76-
"/images/covers/1.jpg",
77-
"/images/covers/2.jpg",
78-
"/images/covers/3.jpg",
79-
"/images/covers/4.jpg",
80-
"/images/covers/5.jpg",
81-
];
82-
83-
const randomIndex = Math.floor(Math.random() * covers.length);
84-
return covers[randomIndex];
22+
const WORDPRESS_POSTS_ENDPOINT =
23+
"https://rustfs.dev/wp-json/wp/v2/posts?per_page=5&orderby=date&order=desc&_fields=id,date,title,link,excerpt,jetpack_featured_media_url";
24+
25+
const BLOG_API_APPLICATION_USERNAME =
26+
process.env.BLOG_API_APPLICATION_USERNAME;
27+
const BLOG_API_APPLICATION_PASSWORD =
28+
process.env.BLOG_API_APPLICATION_PASSWORD;
29+
30+
const FALLBACK_COVERS = [
31+
"/images/covers/1.jpg",
32+
"/images/covers/2.jpg",
33+
"/images/covers/3.jpg",
34+
"/images/covers/4.jpg",
35+
"/images/covers/5.jpg",
36+
];
37+
38+
function getRandomCover(): string {
39+
const randomIndex = Math.floor(Math.random() * FALLBACK_COVERS.length);
40+
return FALLBACK_COVERS[randomIndex];
8541
}
8642

8743
export async function getLatestBlogPosts(limit = 3): Promise<BlogPost[]> {
8844
try {
89-
const response = await fetch("https://rustfs.dev/feed", {
45+
const headers: Record<string, string> = {
46+
"User-Agent":
47+
"Mozilla/5.0 (compatible; RustFSSiteBot/1.0; +https://rustfs.com)",
48+
Accept: "application/json",
49+
};
50+
51+
if (BLOG_API_APPLICATION_USERNAME && BLOG_API_APPLICATION_PASSWORD) {
52+
const basicToken = Buffer.from(
53+
`${BLOG_API_APPLICATION_USERNAME}:${BLOG_API_APPLICATION_PASSWORD}`
54+
).toString("base64");
55+
headers.Authorization = `Basic ${basicToken}`;
56+
}
57+
58+
const response = await fetch(WORDPRESS_POSTS_ENDPOINT, {
9059
next: { revalidate: 1800 },
91-
// Some WordPress/security setups block default Node/undici user agents from CI/CD.
92-
// Use a browser-like User-Agent and explicit Accept header to reduce false positives.
93-
headers: {
94-
"User-Agent":
95-
"Mozilla/5.0 (compatible; RustFSSiteBot/1.0; +https://rustfs.com)",
96-
Accept:
97-
"application/rss+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.7",
98-
},
60+
headers,
9961
});
10062

10163
if (!response.ok) {
10264
console.error(
103-
"[RustFS Blog] Failed to fetch feed",
65+
"[RustFS Blog] Failed to fetch posts from WordPress API",
10466
JSON.stringify({
10567
status: response.status,
10668
statusText: response.statusText,
@@ -109,45 +71,44 @@ export async function getLatestBlogPosts(limit = 3): Promise<BlogPost[]> {
10971
);
11072

11173
throw new Error(
112-
`[RustFS Blog] Failed to fetch feed: ${response.status} ${response.statusText}`
74+
`[RustFS Blog] Failed to fetch posts: ${response.status} ${response.statusText}`
11375
);
11476
}
11577

116-
const xml = await response.text();
78+
const data = (await response.json()) as WordPressPost[];
11779

11880
console.log(
119-
"[RustFS Blog] Feed fetched successfully",
81+
"[RustFS Blog] Posts fetched successfully from WordPress API",
12082
JSON.stringify({
12183
status: response.status,
122-
length: xml.length,
84+
totalItems: Array.isArray(data) ? data.length : 0,
12385
})
12486
);
12587

126-
const items = xml.match(/<item[\s\S]*?<\/item>/gi) ?? [];
88+
const safeData = Array.isArray(data) ? data : [];
12789

128-
console.log(
129-
"[RustFS Blog] Parsed items from feed",
130-
JSON.stringify({
131-
totalItems: items.length,
132-
limit,
133-
})
134-
);
90+
const posts: BlogPost[] = safeData.slice(0, limit).map((item) => {
91+
const title =
92+
(item.title && typeof item.title.rendered === "string"
93+
? item.title.rendered
94+
: ""
95+
).trim() || "Untitled";
13596

136-
const posts: BlogPost[] = items.slice(0, limit).map((itemXml) => {
137-
const title = extractTagContent(itemXml, "title") ?? "Untitled";
138-
const link = extractTagContent(itemXml, "link") ?? "#";
139-
const pubDateRaw = extractTagContent(itemXml, "pubDate");
140-
const imageUrl = extractImageUrl(itemXml);
97+
const link = item.link || "#";
14198

14299
let pubDate: string | undefined;
143-
144-
if (pubDateRaw) {
145-
const parsed = new Date(pubDateRaw);
100+
if (item.date) {
101+
const parsed = new Date(item.date);
146102
if (!Number.isNaN(parsed.getTime())) {
147103
pubDate = parsed.toISOString();
148104
}
149105
}
150106

107+
const imageUrl =
108+
(item.jetpack_featured_media_url &&
109+
item.jetpack_featured_media_url.trim()) ||
110+
getRandomCover();
111+
151112
return { title, link, pubDate, imageUrl };
152113
});
153114

0 commit comments

Comments
 (0)