feat: add tkdodo past query and router articles to blog feed#1000
feat: add tkdodo past query and router articles to blog feed#1000KevinVandy wants to merge 2 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds support for surfacing external blog posts (TkDodo's RSS feed) alongside internal posts. New server utilities parse RSS feeds and classify posts by library. A scraper script downloads hero images and generates a slug-to-image mapping. Blog server functions are extended with merged internal/external post endpoints and author normalization. UI components and route loaders are updated to handle ChangesExternal Blog Posts Integration
Sequence Diagram(s)sequenceDiagram
participant Browser
participant RouteLoader
participant fetchBlogIndexPosts
participant getBlogCardPosts
participant getPublishedPosts
participant getExternalBlogPosts
participant ExternalRSSFeed
Browser->>RouteLoader: navigate to /blog/
RouteLoader->>fetchBlogIndexPosts: call server fn
fetchBlogIndexPosts->>getBlogCardPosts: assemble merged list
getBlogCardPosts->>getPublishedPosts: fetch internal posts
getBlogCardPosts->>getExternalBlogPosts: fetch external posts (cached)
getExternalBlogPosts->>ExternalRSSFeed: HTTP GET RSS (with timeout)
ExternalRSSFeed-->>getExternalBlogPosts: RSS XML
getExternalBlogPosts-->>getBlogCardPosts: BlogCardPost[] (external)
getPublishedPosts-->>getBlogCardPosts: BlogCardPost[] (internal)
getBlogCardPosts-->>fetchBlogIndexPosts: sorted merged BlogCardPost[]
fetchBlogIndexPosts-->>RouteLoader: BlogCardPost[] + cache-control headers
RouteLoader-->>Browser: render BlogCard (internal Link or external anchor)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
scripts/scrape-tkdodo-blog-images.ts (1)
277-283: ⚡ Quick winKeep the batch running when one post fetch fails.
A single network/parser error currently aborts the whole run. Wrap each iteration in
try/catchso remaining posts still produce mappings.Suggested fix
for (const item of items) { - const entry = await scrapePostImage(item) - - if (entry) { - entries.push(entry) - } + try { + const entry = await scrapePostImage(item) + if (entry) { + entries.push(entry) + } + } catch (error) { + console.warn(`[skip] ${getExternalPostSlug(item)}: failed to scrape`, error) + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/scrape-tkdodo-blog-images.ts` around lines 277 - 283, The loop iterating over items does not handle errors from the scrapePostImage function call, causing any network or parser error to abort the entire batch. Wrap the scrapePostImage function call and the entry push logic inside a try/catch block within the for loop. In the catch block, log the error with context (such as the current item being processed) but allow the loop to continue processing the remaining items instead of crashing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/utils/external-blog-posts.server.ts`:
- Around line 191-214: The externalUrl assignment uses feedItem.link directly
without validating the URL scheme, which is a security risk. Before the return
statement that creates the post object (after the getExternalPostSlug call), add
validation to ensure feedItem.link uses a safe scheme (http: or https:). If the
URL does not match these schemes, return an empty array to drop the invalid
item. This validation should be done by checking the URL scheme before passing
feedItem.link to addSearchParams.
---
Nitpick comments:
In `@scripts/scrape-tkdodo-blog-images.ts`:
- Around line 277-283: The loop iterating over items does not handle errors from
the scrapePostImage function call, causing any network or parser error to abort
the entire batch. Wrap the scrapePostImage function call and the entry push
logic inside a try/catch block within the for loop. In the catch block, log the
error with context (such as the current item being processed) but allow the loop
to continue processing the remaining items instead of crashing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 281cd017-bfee-4b7e-9642-eea304dee620
⛔ Files ignored due to path filters (36)
public/blog-assets/tkdodosblog/automatic-query-invalidation-after-mutations.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/breaking-react-querys-api-on-purpose.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/concurrent-optimistic-updates-in-react-query.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/context-inheritance-in-tan-stack-router.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/creating-query-abstractions.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/effective-react-query-keys.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/how-infinite-queries-work.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/inside-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/leveraging-the-query-function-context.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/mastering-mutations-in-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/offline-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/placeholder-and-initial-data-in-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/practical-react-query.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/react-query-and-forms.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-and-react-context.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-and-type-script.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-api-design-lessons-learned.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/react-query-as-a-state-manager.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-data-transformations.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-error-handling.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-fa-qs.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-meets-react-router.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-render-optimizations.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/react-query-selectors-supercharged.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/react-query-the-bad-parts.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/seeding-the-query-cache.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/status-checks-in-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/tan-stack-router-and-query.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/testing-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/the-beauty-of-tan-stack-router.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/the-query-options-api.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/thinking-in-react-query.pngis excluded by!**/*.pngpublic/blog-assets/tkdodosblog/type-safe-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/using-web-sockets-with-react-query.jpegis excluded by!**/*.jpegpublic/blog-assets/tkdodosblog/why-you-want-react-query.jpgis excluded by!**/*.jpgpublic/blog-assets/tkdodosblog/you-might-not-need-react-query.jpegis excluded by!**/*.jpeg
📒 Files selected for processing (11)
public/blog-assets/tkdodosblog/tkdodosblog.webpscripts/scrape-tkdodo-blog-images.tssrc/components/BlogCard.tsxsrc/components/RecentPostsWidget.tsxsrc/components/home/HomeSocialProofSection.tsxsrc/routes/_library/$libraryId/$version.docs.blog.tsxsrc/routes/blog.index.tsxsrc/utils/blog.functions.tssrc/utils/blog.tssrc/utils/external-blog-post-images.generated.tssrc/utils/external-blog-posts.server.ts
| if ( | ||
| !feedItem.title || | ||
| !feedItem.link || | ||
| !feedItem.published || | ||
| !libraries.length | ||
| ) { | ||
| return [] | ||
| } | ||
|
|
||
| const slug = getExternalPostSlug(source, feedItem) | ||
|
|
||
| return [ | ||
| { | ||
| slug, | ||
| title: feedItem.title, | ||
| published: feedItem.published, | ||
| excerpt: feedItem.excerpt, | ||
| headerImage: source.headerImages?.[slug] ?? source.defaultHeaderImage, | ||
| authors: normalizeBlogAuthors(source.authors), | ||
| library: libraries.join(','), | ||
| externalUrl: addSearchParams( | ||
| feedItem.link, | ||
| source.externalUrlSearchParams, | ||
| ), |
There was a problem hiding this comment.
Validate external feed URLs before assigning externalUrl.
Line 211 forwards feed-controlled links to UI href without scheme validation. Restrict to http:/https: (and optionally expected hosts) before returning a post; otherwise drop the item.
Suggested fix
+function toSafeExternalUrl(
+ url: string,
+ searchParams: Record<string, string> | undefined,
+) {
+ try {
+ const nextUrl = new URL(url)
+ if (nextUrl.protocol !== 'https:' && nextUrl.protocol !== 'http:') {
+ return undefined
+ }
+
+ for (const [key, value] of Object.entries(searchParams ?? {})) {
+ nextUrl.searchParams.set(key, value)
+ }
+
+ return nextUrl.toString()
+ } catch {
+ return undefined
+ }
+}
+
function parseRssFeed(
source: ExternalBlogSource,
feed: string,
): Array<BlogCardPost> {
@@
- return [
+ const externalUrl = toSafeExternalUrl(
+ feedItem.link,
+ source.externalUrlSearchParams,
+ )
+
+ if (!externalUrl) {
+ return []
+ }
+
+ return [
{
@@
- externalUrl: addSearchParams(
- feedItem.link,
- source.externalUrlSearchParams,
- ),
+ externalUrl,
source: source.name,
},
]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if ( | |
| !feedItem.title || | |
| !feedItem.link || | |
| !feedItem.published || | |
| !libraries.length | |
| ) { | |
| return [] | |
| } | |
| const slug = getExternalPostSlug(source, feedItem) | |
| return [ | |
| { | |
| slug, | |
| title: feedItem.title, | |
| published: feedItem.published, | |
| excerpt: feedItem.excerpt, | |
| headerImage: source.headerImages?.[slug] ?? source.defaultHeaderImage, | |
| authors: normalizeBlogAuthors(source.authors), | |
| library: libraries.join(','), | |
| externalUrl: addSearchParams( | |
| feedItem.link, | |
| source.externalUrlSearchParams, | |
| ), | |
| function toSafeExternalUrl( | |
| url: string, | |
| searchParams: Record<string, string> | undefined, | |
| ) { | |
| try { | |
| const nextUrl = new URL(url) | |
| if (nextUrl.protocol !== 'https:' && nextUrl.protocol !== 'http:') { | |
| return undefined | |
| } | |
| for (const [key, value] of Object.entries(searchParams ?? {})) { | |
| nextUrl.searchParams.set(key, value) | |
| } | |
| return nextUrl.toString() | |
| } catch { | |
| return undefined | |
| } | |
| } | |
| if ( | |
| !feedItem.title || | |
| !feedItem.link || | |
| !feedItem.published || | |
| !libraries.length | |
| ) { | |
| return [] | |
| } | |
| const slug = getExternalPostSlug(source, feedItem) | |
| const externalUrl = toSafeExternalUrl( | |
| feedItem.link, | |
| source.externalUrlSearchParams, | |
| ) | |
| if (!externalUrl) { | |
| return [] | |
| } | |
| return [ | |
| { | |
| slug, | |
| title: feedItem.title, | |
| published: feedItem.published, | |
| excerpt: feedItem.excerpt, | |
| headerImage: source.headerImages?.[slug] ?? source.defaultHeaderImage, | |
| authors: normalizeBlogAuthors(source.authors), | |
| library: libraries.join(','), | |
| externalUrl, | |
| source: source.name, | |
| }, | |
| ] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/external-blog-posts.server.ts` around lines 191 - 214, The
externalUrl assignment uses feedItem.link directly without validating the URL
scheme, which is a security risk. Before the return statement that creates the
post object (after the getExternalPostSlug call), add validation to ensure
feedItem.link uses a safe scheme (http: or https:). If the URL does not match
these schemes, return an empty array to drop the invalid item. This validation
should be done by checking the URL scheme before passing feedItem.link to
addSearchParams.
Summary by CodeRabbit
qquery terms.