Skip to content

Commit 2e1ab49

Browse files
committed
Merge branch 'dev' into 500-404-patches
2 parents 3ecc123 + 66396e7 commit 2e1ab49

File tree

3 files changed

+160
-141
lines changed

3 files changed

+160
-141
lines changed

src/lib/api/fetchPosts.ts

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,47 @@ import { fetchXml } from "./fetchRSS"
44

55
export const fetchAttestantPosts = async () => {
66
const BASE_URL = "https://www.attestant.io/posts/"
7-
const htmlData = (await fetchXml(BASE_URL)) as HTMLResult
7+
const allItems: RSSItem[] = []
88

9-
// Extract div containing list of posts from deeply nested HTML structure
10-
const postsContainer =
11-
htmlData.html.body[0].div[0].div[1].div[0].div[0].div[0].div
9+
try {
10+
const htmlData = (await fetchXml(BASE_URL)) as HTMLResult
1211

13-
const posts: RSSItem[] = postsContainer
14-
.map(({ a }) => {
15-
const [
16-
{
17-
$: { href },
18-
h4: [{ _: title }],
19-
div: [{ _: content }, { _: pubDate }],
20-
},
21-
] = a
22-
const { href: link } = new URL(href, BASE_URL)
23-
return {
24-
title,
25-
link,
26-
content,
27-
source: "Attestant",
28-
sourceUrl: BASE_URL,
29-
sourceFeedUrl: BASE_URL,
30-
imgSrc: "/images/attestant-logo.svg",
31-
pubDate,
32-
}
33-
})
34-
.sort(
35-
(a: RSSItem, b: RSSItem) =>
36-
new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime()
12+
// Extract div containing list of posts from deeply nested HTML structure
13+
const postsContainer =
14+
htmlData.html.body[0].div[0].div[1].div[0].div[0].div[0].div
15+
16+
const sortedPosts = postsContainer
17+
.map(({ a }) => {
18+
const [
19+
{
20+
$: { href },
21+
h4: [{ _: title }],
22+
div: [{ _: content }, { _: pubDate }],
23+
},
24+
] = a
25+
const { href: link } = new URL(href, BASE_URL)
26+
return {
27+
title,
28+
link,
29+
content,
30+
source: "Attestant",
31+
sourceUrl: BASE_URL,
32+
sourceFeedUrl: BASE_URL,
33+
imgSrc: "/images/attestant-logo.svg",
34+
pubDate,
35+
}
36+
})
37+
.sort(
38+
(a: RSSItem, b: RSSItem) =>
39+
new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime()
40+
)
41+
allItems.push(...sortedPosts)
42+
} catch (error) {
43+
console.error(
44+
"Error fetching Attestant posts:",
45+
error instanceof Error ? error.message : error
3746
)
38-
return posts
47+
}
48+
49+
return allItems
3950
}

src/lib/api/fetchRSS.ts

Lines changed: 118 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { parseString } from "xml2js"
22

3-
import type {
4-
AtomElement,
5-
AtomResult,
6-
RSSChannel,
7-
RSSItem,
8-
RSSResult,
9-
} from "../types"
3+
import { RSS_DISPLAY_COUNT } from "../constants"
4+
import type { AtomElement, AtomResult, RSSItem, RSSResult } from "../types"
105
import { isValidDate } from "../utils/date"
116

127
/**
@@ -18,108 +13,125 @@ export const fetchRSS = async (xmlUrl: string | string[]) => {
1813
const urls = Array.isArray(xmlUrl) ? xmlUrl : [xmlUrl]
1914
const allItems: RSSItem[][] = []
2015
for (const url of urls) {
21-
const response = (await fetchXml(url)) as RSSResult | AtomResult
16+
try {
17+
const response = (await fetchXml(url)) as RSSResult | AtomResult
2218

23-
if ("rss" in response) {
24-
const [mainChannel] = response.rss.channel as RSSChannel[]
25-
const [source] = mainChannel.title
26-
const [sourceUrl] = mainChannel.link
27-
const channelImage = mainChannel.image ? mainChannel.image[0].url[0] : ""
19+
if ("rss" in response) {
20+
const [mainChannel] = response.rss.channel
21+
const [source] = mainChannel.title
22+
const [sourceUrl] = mainChannel.link
23+
const channelImage = mainChannel.image
24+
? mainChannel.image[0].url[0]
25+
: ""
2826

29-
const parsedRssItems = mainChannel.item
30-
// Filter out items with invalid dates
31-
.filter((item) => {
32-
if (!item.pubDate) return false
33-
const [pubDate] = item.pubDate
34-
return isValidDate(pubDate)
35-
})
36-
// Sort by pubDate (most recent is first in array
37-
.sort((a, b) => {
38-
const dateA = new Date(a.pubDate[0])
39-
const dateB = new Date(b.pubDate[0])
40-
return dateB.getTime() - dateA.getTime()
41-
})
42-
// Map to RSSItem object
43-
.map((item) => {
44-
const getImgSrc = () => {
45-
if (item["content:encoded"])
46-
return item["content:encoded"][0].match(
47-
/https?:\/\/[^"]*?\.(jpe?g|png|webp)/g
48-
)?.[0]
49-
if (item.enclosure) return item.enclosure[0].$.url
50-
if (item["media:content"]) return item["media:content"][0].$.url
51-
return channelImage
52-
}
53-
return {
54-
pubDate: item.pubDate[0],
55-
title: item.title[0],
56-
link: item.link[0],
57-
imgSrc: getImgSrc(),
58-
source,
59-
sourceUrl,
60-
sourceFeedUrl: url,
61-
} as RSSItem
62-
})
27+
const parsedRssItems = mainChannel.item
28+
// Filter out items with invalid dates
29+
.filter((item) => {
30+
if (!item.pubDate) return false
31+
const [pubDate] = item.pubDate
32+
return isValidDate(pubDate)
33+
})
34+
// Sort by pubDate (most recent is first in array
35+
.sort((a, b) => {
36+
const dateA = new Date(a.pubDate[0])
37+
const dateB = new Date(b.pubDate[0])
38+
return dateB.getTime() - dateA.getTime()
39+
})
40+
// Map to RSSItem object
41+
.map((item) => {
42+
const getImgSrc = () => {
43+
if (item["content:encoded"])
44+
return item["content:encoded"][0].match(
45+
/https?:\/\/[^"]*?\.(jpe?g|png|webp)/g
46+
)?.[0]
47+
if (item.enclosure) return item.enclosure[0].$.url
48+
if (item["media:content"]) return item["media:content"][0].$.url
49+
return channelImage
50+
}
51+
return {
52+
pubDate: item.pubDate[0],
53+
title: item.title[0],
54+
link: item.link[0],
55+
imgSrc: getImgSrc(),
56+
source,
57+
sourceUrl,
58+
sourceFeedUrl: url,
59+
}
60+
})
6361

64-
allItems.push(parsedRssItems)
65-
} else if ("feed" in response) {
66-
const [source] = response.feed.title
67-
const [sourceUrl] = response.feed.id
68-
const feedImage = response.feed.icon?.[0]
62+
allItems.push(parsedRssItems)
63+
} else if ("feed" in response) {
64+
const [source] = response.feed.title
65+
const [sourceUrl] = response.feed.id
66+
const feedImage = response.feed.icon?.[0]
6967

70-
const parsedAtomItems = response.feed.entry
71-
// Filter out items with invalid dates
72-
.filter((entry) => {
73-
if (!entry.updated) return false
74-
const [published] = entry.updated
75-
return isValidDate(published)
76-
})
77-
// Sort by published (most recent is first in array
78-
.sort((a, b) => {
79-
const dateA = new Date(a.updated[0])
80-
const dateB = new Date(b.updated[0])
81-
return dateB.getTime() - dateA.getTime()
82-
})
83-
// Map to RSSItem object
84-
.map((entry) => {
85-
const getString = (el?: AtomElement[]): string => {
86-
if (!el) return ""
87-
const [firstEl] = el
88-
if (typeof firstEl === "string") return firstEl
89-
return firstEl._ || ""
90-
}
91-
const getHref = (): string => {
92-
if (!entry.link) {
93-
console.warn(`No link found for RSS url: ${url}`)
94-
return ""
68+
const parsedAtomItems = response.feed.entry
69+
// Filter out items with invalid dates
70+
.filter((entry) => {
71+
if (!entry.updated) return false
72+
const [published] = entry.updated
73+
return isValidDate(published)
74+
})
75+
// Sort by published (most recent is first in array
76+
.sort((a, b) => {
77+
const dateA = new Date(a.updated[0])
78+
const dateB = new Date(b.updated[0])
79+
return dateB.getTime() - dateA.getTime()
80+
})
81+
// Map to RSSItem object
82+
.map((entry) => {
83+
const getString = (el?: AtomElement[]): string => {
84+
if (!el) return ""
85+
const [firstEl] = el
86+
if (typeof firstEl === "string") return firstEl
87+
return firstEl._ || ""
88+
}
89+
const getHref = (): string => {
90+
if (!entry.link) {
91+
console.warn(`No link found for RSS url: ${url}`)
92+
return ""
93+
}
94+
const link = entry.link[0]
95+
if (typeof link === "string") return link
96+
return link.$.href || ""
97+
}
98+
const getImgSrc = (): string => {
99+
const imgRegEx = /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g
100+
const contentMatch = getString(entry.content).match(imgRegEx)
101+
if (contentMatch) return contentMatch[0]
102+
const summaryMatch = getString(entry.summary).match(imgRegEx)
103+
if (summaryMatch) return summaryMatch[0]
104+
return feedImage || ""
105+
}
106+
return {
107+
pubDate: entry.updated[0],
108+
title: getString(entry.title),
109+
link: getHref(),
110+
imgSrc: getImgSrc(),
111+
source,
112+
sourceUrl,
113+
sourceFeedUrl: url,
95114
}
96-
const link = entry.link[0]
97-
if (typeof link === "string") return link
98-
return link.$.href || ""
99-
}
100-
const getImgSrc = (): string => {
101-
const imgRegEx = /https?:\/\/[^"]*?\.(jpe?g|png|webp)/g
102-
const contentMatch = getString(entry.content).match(imgRegEx)
103-
if (contentMatch) return contentMatch[0]
104-
const summaryMatch = getString(entry.summary).match(imgRegEx)
105-
if (summaryMatch) return summaryMatch[0]
106-
return feedImage || ""
107-
}
108-
return {
109-
pubDate: entry.updated[0],
110-
title: getString(entry.title),
111-
link: getHref(),
112-
imgSrc: getImgSrc(),
113-
source,
114-
sourceUrl,
115-
sourceFeedUrl: url,
116-
} as RSSItem
117-
})
115+
})
118116

119-
allItems.push(parsedAtomItems)
117+
allItems.push(parsedAtomItems)
118+
} else {
119+
throw new Error(
120+
`Error parsing XML, invalid RSSResult or AtomResult type: ${url}`
121+
)
122+
}
123+
} catch (error) {
124+
console.error(error instanceof Error ? error.message : error)
125+
// Do not break build for single fetch failure
126+
continue
120127
}
121128
}
122-
return allItems as RSSItem[][]
129+
130+
// Only break build if insufficient number of items fetched
131+
if (allItems.length < RSS_DISPLAY_COUNT)
132+
throw new Error("Insufficient number of RSS items fetched")
133+
134+
return allItems
123135
}
124136

125137
/**
@@ -135,16 +147,12 @@ export const fetchXml = async (url: string) => {
135147
credentials: "omit", // Don't send or receive cookies
136148
})
137149
const xml = await response.text()
138-
let returnObject: Record<string, unknown> = {}
139-
parseString(xml, (err, result) => {
140-
if (err) {
141-
throw err // Throw the error to be caught by the outer try-catch
142-
}
143-
returnObject = result
150+
return await new Promise<Record<string, unknown>>((resolve, reject) => {
151+
parseString(xml, (err, result) => {
152+
err ? reject(err) : resolve(result)
153+
})
144154
})
145-
return returnObject
146155
} catch (error) {
147-
console.error("Error fetching or parsing XML:", url, error)
148-
throw error
156+
throw new Error(`Error fetching or parsing XML: ${url}`)
149157
}
150158
}

src/lib/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ export const COMMUNITY_BLOGS: CommunityBlog[] = [
231231
feed: "https://medium.com/feed/ethereum-cat-herders",
232232
},
233233
{
234-
href: "http://geodework.com/blog",
235-
feed: "http://geodework.com/feed.xml",
234+
href: "https://geodework.com/blog",
235+
feed: "https://geodework.com/feed.xml",
236236
},
237237
]
238238

0 commit comments

Comments
 (0)