diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index ec1256fb079..4c8fc8d6e0d 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -23,6 +23,7 @@ import { BLOG_FEEDS, BLOGS_WITHOUT_FEED, CALENDAR_DISPLAY_COUNT, + LOCALES_CODES, RSS_DISPLAY_COUNT, } from "@/lib/constants" @@ -128,6 +129,8 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { ) } +export const generateStaticParams = async () => LOCALES_CODES + export async function generateMetadata({ params, }: { diff --git a/src/lib/api/fetchPosts.ts b/src/lib/api/fetchPosts.ts index 31b1bea95a6..827379ee217 100644 --- a/src/lib/api/fetchPosts.ts +++ b/src/lib/api/fetchPosts.ts @@ -4,36 +4,47 @@ import { fetchXml } from "./fetchRSS" export const fetchAttestantPosts = async () => { const BASE_URL = "https://www.attestant.io/posts/" - const htmlData = (await fetchXml(BASE_URL)) as HTMLResult + const allItems: RSSItem[] = [] - // Extract div containing list of posts from deeply nested HTML structure - const postsContainer = - htmlData.html.body[0].div[0].div[1].div[0].div[0].div[0].div + try { + const htmlData = (await fetchXml(BASE_URL)) as HTMLResult - const posts: RSSItem[] = postsContainer - .map(({ a }) => { - const [ - { - $: { href }, - h4: [{ _: title }], - div: [{ _: content }, { _: pubDate }], - }, - ] = a - const { href: link } = new URL(href, BASE_URL) - return { - title, - link, - content, - source: "Attestant", - sourceUrl: BASE_URL, - sourceFeedUrl: BASE_URL, - imgSrc: "/images/attestant-logo.svg", - pubDate, - } - }) - .sort( - (a: RSSItem, b: RSSItem) => - new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime() + // Extract div containing list of posts from deeply nested HTML structure + const postsContainer = + htmlData.html.body[0].div[0].div[1].div[0].div[0].div[0].div + + const sortedPosts = postsContainer + .map(({ a }) => { + const [ + { + $: { href }, + h4: [{ _: title }], + div: [{ _: content }, { _: pubDate }], + }, + ] = a + const { href: link } = new URL(href, BASE_URL) + return { + title, + link, + content, + source: "Attestant", + sourceUrl: BASE_URL, + sourceFeedUrl: BASE_URL, + imgSrc: "/images/attestant-logo.svg", + pubDate, + } + }) + .sort( + (a: RSSItem, b: RSSItem) => + new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime() + ) + allItems.push(...sortedPosts) + } catch (error) { + console.error( + "Error fetching Attestant posts:", + error instanceof Error ? error.message : error ) - return posts + } + + return allItems } diff --git a/src/lib/api/fetchRSS.ts b/src/lib/api/fetchRSS.ts index 7978adbbf6c..0f5a6124e78 100644 --- a/src/lib/api/fetchRSS.ts +++ b/src/lib/api/fetchRSS.ts @@ -1,12 +1,7 @@ import { parseString } from "xml2js" -import type { - AtomElement, - AtomResult, - RSSChannel, - RSSItem, - RSSResult, -} from "../types" +import { RSS_DISPLAY_COUNT } from "../constants" +import type { AtomElement, AtomResult, RSSItem, RSSResult } from "../types" import { isValidDate } from "../utils/date" /** @@ -18,108 +13,125 @@ export const fetchRSS = async (xmlUrl: string | string[]) => { const urls = Array.isArray(xmlUrl) ? xmlUrl : [xmlUrl] const allItems: RSSItem[][] = [] for (const url of urls) { - const response = (await fetchXml(url)) as RSSResult | AtomResult + try { + const response = (await fetchXml(url)) as RSSResult | AtomResult - if ("rss" in response) { - const [mainChannel] = response.rss.channel as RSSChannel[] - const [source] = mainChannel.title - const [sourceUrl] = mainChannel.link - const channelImage = mainChannel.image ? mainChannel.image[0].url[0] : "" + if ("rss" in response) { + const [mainChannel] = response.rss.channel + const [source] = mainChannel.title + const [sourceUrl] = mainChannel.link + const channelImage = mainChannel.image + ? mainChannel.image[0].url[0] + : "" - const parsedRssItems = mainChannel.item - // Filter out items with invalid dates - .filter((item) => { - if (!item.pubDate) return false - const [pubDate] = item.pubDate - return isValidDate(pubDate) - }) - // Sort by pubDate (most recent is first in array - .sort((a, b) => { - const dateA = new Date(a.pubDate[0]) - const dateB = new Date(b.pubDate[0]) - return dateB.getTime() - dateA.getTime() - }) - // Map to RSSItem object - .map((item) => { - const getImgSrc = () => { - if (item["content:encoded"]) - return item["content:encoded"][0].match( - /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g - )?.[0] - if (item.enclosure) return item.enclosure[0].$.url - if (item["media:content"]) return item["media:content"][0].$.url - return channelImage - } - return { - pubDate: item.pubDate[0], - title: item.title[0], - link: item.link[0], - imgSrc: getImgSrc(), - source, - sourceUrl, - sourceFeedUrl: url, - } as RSSItem - }) + const parsedRssItems = mainChannel.item + // Filter out items with invalid dates + .filter((item) => { + if (!item.pubDate) return false + const [pubDate] = item.pubDate + return isValidDate(pubDate) + }) + // Sort by pubDate (most recent is first in array + .sort((a, b) => { + const dateA = new Date(a.pubDate[0]) + const dateB = new Date(b.pubDate[0]) + return dateB.getTime() - dateA.getTime() + }) + // Map to RSSItem object + .map((item) => { + const getImgSrc = () => { + if (item["content:encoded"]) + return item["content:encoded"][0].match( + /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g + )?.[0] + if (item.enclosure) return item.enclosure[0].$.url + if (item["media:content"]) return item["media:content"][0].$.url + return channelImage + } + return { + pubDate: item.pubDate[0], + title: item.title[0], + link: item.link[0], + imgSrc: getImgSrc(), + source, + sourceUrl, + sourceFeedUrl: url, + } + }) - allItems.push(parsedRssItems) - } else if ("feed" in response) { - const [source] = response.feed.title - const [sourceUrl] = response.feed.id - const feedImage = response.feed.icon?.[0] + allItems.push(parsedRssItems) + } else if ("feed" in response) { + const [source] = response.feed.title + const [sourceUrl] = response.feed.id + const feedImage = response.feed.icon?.[0] - const parsedAtomItems = response.feed.entry - // Filter out items with invalid dates - .filter((entry) => { - if (!entry.updated) return false - const [published] = entry.updated - return isValidDate(published) - }) - // Sort by published (most recent is first in array - .sort((a, b) => { - const dateA = new Date(a.updated[0]) - const dateB = new Date(b.updated[0]) - return dateB.getTime() - dateA.getTime() - }) - // Map to RSSItem object - .map((entry) => { - const getString = (el?: AtomElement[]): string => { - if (!el) return "" - const [firstEl] = el - if (typeof firstEl === "string") return firstEl - return firstEl._ || "" - } - const getHref = (): string => { - if (!entry.link) { - console.warn(`No link found for RSS url: ${url}`) - return "" + const parsedAtomItems = response.feed.entry + // Filter out items with invalid dates + .filter((entry) => { + if (!entry.updated) return false + const [published] = entry.updated + return isValidDate(published) + }) + // Sort by published (most recent is first in array + .sort((a, b) => { + const dateA = new Date(a.updated[0]) + const dateB = new Date(b.updated[0]) + return dateB.getTime() - dateA.getTime() + }) + // Map to RSSItem object + .map((entry) => { + const getString = (el?: AtomElement[]): string => { + if (!el) return "" + const [firstEl] = el + if (typeof firstEl === "string") return firstEl + return firstEl._ || "" + } + const getHref = (): string => { + if (!entry.link) { + console.warn(`No link found for RSS url: ${url}`) + return "" + } + const link = entry.link[0] + if (typeof link === "string") return link + return link.$.href || "" + } + const getImgSrc = (): string => { + const imgRegEx = /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g + const contentMatch = getString(entry.content).match(imgRegEx) + if (contentMatch) return contentMatch[0] + const summaryMatch = getString(entry.summary).match(imgRegEx) + if (summaryMatch) return summaryMatch[0] + return feedImage || "" + } + return { + pubDate: entry.updated[0], + title: getString(entry.title), + link: getHref(), + imgSrc: getImgSrc(), + source, + sourceUrl, + sourceFeedUrl: url, } - const link = entry.link[0] - if (typeof link === "string") return link - return link.$.href || "" - } - const getImgSrc = (): string => { - const imgRegEx = /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g - const contentMatch = getString(entry.content).match(imgRegEx) - if (contentMatch) return contentMatch[0] - const summaryMatch = getString(entry.summary).match(imgRegEx) - if (summaryMatch) return summaryMatch[0] - return feedImage || "" - } - return { - pubDate: entry.updated[0], - title: getString(entry.title), - link: getHref(), - imgSrc: getImgSrc(), - source, - sourceUrl, - sourceFeedUrl: url, - } as RSSItem - }) + }) - allItems.push(parsedAtomItems) + allItems.push(parsedAtomItems) + } else { + throw new Error( + `Error parsing XML, invalid RSSResult or AtomResult type: ${url}` + ) + } + } catch (error) { + console.error(error instanceof Error ? error.message : error) + // Do not break build for single fetch failure + continue } } - return allItems as RSSItem[][] + + // Only break build if insufficient number of items fetched + if (allItems.length < RSS_DISPLAY_COUNT) + throw new Error("Insufficient number of RSS items fetched") + + return allItems } /** @@ -135,16 +147,12 @@ export const fetchXml = async (url: string) => { credentials: "omit", // Don't send or receive cookies }) const xml = await response.text() - let returnObject: Record = {} - parseString(xml, (err, result) => { - if (err) { - throw err // Throw the error to be caught by the outer try-catch - } - returnObject = result + return await new Promise>((resolve, reject) => { + parseString(xml, (err, result) => { + err ? reject(err) : resolve(result) + }) }) - return returnObject } catch (error) { - console.error("Error fetching or parsing XML:", url, error) - throw error + throw new Error(`Error fetching or parsing XML: ${url}`) } } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d6b0aecbe46..083a0dc1419 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -231,8 +231,8 @@ export const COMMUNITY_BLOGS: CommunityBlog[] = [ feed: "https://medium.com/feed/ethereum-cat-herders", }, { - href: "http://geodework.com/blog", - feed: "http://geodework.com/feed.xml", + href: "https://geodework.com/blog", + feed: "https://geodework.com/feed.xml", }, ]