Skip to content

Commit 13c6f02

Browse files
committed
refactor: use try/catches during rss build
1 parent 43f26fa commit 13c6f02

File tree

2 files changed

+149
-137
lines changed

2 files changed

+149
-137
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: 109 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { parseString } from "xml2js"
22

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

126
/**
@@ -18,108 +12,118 @@ export const fetchRSS = async (xmlUrl: string | string[]) => {
1812
const urls = Array.isArray(xmlUrl) ? xmlUrl : [xmlUrl]
1913
const allItems: RSSItem[][] = []
2014
for (const url of urls) {
21-
const response = (await fetchXml(url)) as RSSResult | AtomResult
15+
try {
16+
const response = (await fetchXml(url)) as RSSResult | AtomResult
2217

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] : ""
18+
if ("rss" in response) {
19+
const [mainChannel] = response.rss.channel
20+
const [source] = mainChannel.title
21+
const [sourceUrl] = mainChannel.link
22+
const channelImage = mainChannel.image
23+
? mainChannel.image[0].url[0]
24+
: ""
2825

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

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]
61+
allItems.push(parsedRssItems)
62+
} else if ("feed" in response) {
63+
const [source] = response.feed.title
64+
const [sourceUrl] = response.feed.id
65+
const feedImage = response.feed.icon?.[0]
6966

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

119-
allItems.push(parsedAtomItems)
116+
allItems.push(parsedAtomItems)
117+
}
118+
} catch (error) {
119+
console.error(
120+
`Failed to fetch or parse RSS feed from ${url}:`,
121+
error instanceof Error ? error.message : error
122+
)
123+
continue
120124
}
121125
}
122-
return allItems as RSSItem[][]
126+
return allItems
123127
}
124128

125129
/**
@@ -132,14 +136,11 @@ export const fetchXml = async (url: string) => {
132136
try {
133137
const response = await fetch(url)
134138
const xml = await response.text()
135-
let returnObject: Record<string, unknown> = {}
136-
parseString(xml, (err, result) => {
137-
if (err) {
138-
throw err // Throw the error to be caught by the outer try-catch
139-
}
140-
returnObject = result
139+
return await new Promise<Record<string, unknown>>((resolve, reject) => {
140+
parseString(xml, (err, result) => {
141+
err ? reject(err) : resolve(result)
142+
})
141143
})
142-
return returnObject
143144
} catch (error) {
144145
console.error("Error fetching or parsing XML:", url, error)
145146
throw error

0 commit comments

Comments
 (0)