Add PostList component and split blog posts into sections#74
Add PostList component and split blog posts into sections#74fabian-hiller merged 2 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a PostList component and reworks the blog index to load and group posts into "Recent" and yearly sections; updates PostMeta to wrap author avatars conditionally and force UTC in date formatting. Changes
Sequence DiagramsequenceDiagram
actor User
participant BlogIndex
participant RouteLoader as Route Loader
participant PostOrganizer as Organizer
participant PostList
participant PostCover
participant PostMeta
User->>BlogIndex: Navigate to /blog
BlogIndex->>RouteLoader: usePosts() (routeLoader$)
RouteLoader->>RouteLoader: Glob MDX, extract frontmatter, compute href
RouteLoader->>PostOrganizer: Return raw posts
PostOrganizer->>PostOrganizer: Classify into "Recent" and yearly PostSection[]
PostOrganizer->>BlogIndex: Return PostSection[]
BlogIndex->>PostList: Render each PostSection (heading + posts)
PostList->>PostCover: Render cover for each post
PostList->>PostMeta: Render authors & published date
PostList->>User: Display post cards
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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.
Pull request overview
This PR refactors the blog index page to use a new reusable PostList component and groups blog posts into sections (recent posts + yearly buckets) instead of rendering one flat list.
Changes:
- Added
PostListcomponent and re-exported it from the components barrel. - Updated
/blogroute loader to return post sections (recent + by year) and updated the page UI to render those sections. - Enhanced
PostMetato link author avatars on post pages and format dates in UTC.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| website/src/routes/blog/index.tsx | Builds “recent” + “posts of {year}” sections and renders them via PostList. |
| website/src/components/index.ts | Exports the new PostList component. |
| website/src/components/PostMeta.tsx | Makes author avatars linkable (post variant) and uses UTC for date formatting. |
| website/src/components/PostList.tsx | New shared component for rendering the blog post card grid. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
website/src/routes/blog/index.tsx (1)
46-53:⚠️ Potential issue | 🟡 MinorNon-null assertions on frontmatter could cause runtime errors.
If any MDX file is missing frontmatter or required fields, the non-null assertions (
frontmatter!) will cause runtime errors. Consider adding validation or providing defaults.Suggested defensive approach
.map(async ([path, readFile]) => { const { frontmatter } = await readFile(); + if (!frontmatter?.cover || !frontmatter?.title || !frontmatter?.published || !frontmatter?.authors) { + console.warn(`Skipping post at ${path}: missing required frontmatter fields`); + return null; + } return { - cover: frontmatter!.cover, - title: frontmatter!.title, - published: frontmatter!.published, - authors: frontmatter!.authors, + cover: frontmatter.cover, + title: frontmatter.title, + published: frontmatter.published, + authors: frontmatter.authors, href: `./${path.split('/').slice(2, 3)[0]}/`, }; })Then filter out nulls after
Promise.all.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/src/routes/blog/index.tsx` around lines 46 - 53, The code uses non-null assertions on frontmatter returned by readFile (frontmatter!) which can throw if frontmatter or required fields are missing; update the mapping that builds the object (uses readFile and frontmatter) to defensively check that frontmatter exists and contains required keys (cover, title, published, authors) and either supply safe defaults or return null for that entry, then after Promise.all filter out null results so only valid posts are returned; reference the readFile call and the properties frontmatter.cover/frontmatter.title/frontmatter.published/frontmatter.authors and the href construction when adding the validation and defaults.
🧹 Nitpick comments (4)
website/src/components/PostMeta.tsx (1)
4-8: Type naming mismatch andinterfacepreference.The type is named
PostCoverPropsbut defines props forPostMeta. Additionally, per coding guidelines, preferinterfaceovertypefor object shapes.Suggested fix
-type PostCoverProps = { +interface PostMetaProps { variant: 'blog' | 'post'; authors: string[]; published: string; -}; +}Update the component definition to use the renamed interface:
-export const PostMeta = component$<PostCoverProps>( +export const PostMeta = component$<PostMetaProps>(As per coding guidelines: "Prefer
interfaceovertypefor defining object shapes in TypeScript".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/src/components/PostMeta.tsx` around lines 4 - 8, The props declaration currently uses a type named PostCoverProps while the component expects PostMeta props and your style guide prefers interface; rename PostCoverProps to PostMetaProps and convert the declaration from a type to an interface (interface PostMetaProps { variant: 'blog' | 'post'; authors: string[]; published: string; }) and update any references (e.g., the PostMeta component signature and its usages) to use PostMetaProps instead of PostCoverProps.website/src/components/index.ts (1)
27-27: Missing.tsextension, but consistent with existing pattern.The export follows the existing pattern in this file. If ESLint enforces
.tsextensions, all exports in this file would need updating. Consider addressing this as part of the PR or as a follow-up cleanup.Suggested fix for new line
-export * from './PostList'; +export * from './PostList.ts';As per coding guidelines: "Use ESM with
.tsextensions in imports (enforced by ESLint)".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/src/components/index.ts` at line 27, The export statement in website/src/components/index.ts uses './PostList' without the required .ts extension; update the export to './PostList.ts' (and make the same .ts extension change for other exports in this file such as any occurrences of export * from './XYZ') so imports comply with the ESM .ts-extension rule enforced by ESLint, or alternatively note this as a repo-wide cleanup if you prefer to change all exports in a follow-up.website/src/routes/blog/index.tsx (2)
20-34: Preferinterfaceovertypefor object shapes.Per coding guidelines,
interfaceis preferred for defining object shapes in TypeScript.Suggested fix
-type PostFrontmatter = { +interface PostFrontmatter { cover: string; title: string; published: string; authors: string[]; -}; +} -type PostData = PostFrontmatter & { - href: string; -}; +interface PostData extends PostFrontmatter { + href: string; +} -interface PostSection { +interface PostSection { heading: string; posts: PostData[]; }As per coding guidelines: "Prefer
interfaceovertypefor defining object shapes in TypeScript".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/src/routes/blog/index.tsx` around lines 20 - 34, Replace the object-shape type aliases with interfaces: change "type PostFrontmatter" to "interface PostFrontmatter" with the same properties, and change "type PostData = PostFrontmatter & { href: string }" to "interface PostData extends PostFrontmatter { href: string }"; keep the existing "interface PostSection" as-is. This preserves the same structure while following the guideline to prefer interface for object shapes (referencing PostFrontmatter, PostData, and PostSection).
58-59: Extract magic number to a named constant.The value
7776000000(90 days in milliseconds) should be a named constant for clarity and maintainability.Suggested fix
+ // 90 days in milliseconds + const RECENT_POSTS_CUTOFF_MS = 90 * 24 * 60 * 60 * 1000; + // Create variables for latest posts and posts by year - const latestCutoff = new Date(Date.now() - 7776000000); + const latestCutoff = new Date(Date.now() - RECENT_POSTS_CUTOFF_MS);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/src/routes/blog/index.tsx` around lines 58 - 59, Replace the magic number 7776000000 with a named constant to clarify it's 90 days in milliseconds: define a constant (e.g., RECENT_POSTS_WINDOW_MS or NINETY_DAYS_MS) and use it when computing latestCutoff (const latestCutoff = new Date(Date.now() - RECENT_POSTS_WINDOW_MS)); keep the constant near the top of the module so it's discoverable and add a short comment explaining it's a 90-day window.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@website/src/components/PostList.tsx`:
- Around line 1-4: The local imports at the top of PostList.tsx (imports
referencing Link, PostCover, PostMeta) are missing the required ESM .ts
extensions; update the import specifiers for the local modules (the lines
importing './Link', './PostCover', and './PostMeta') to include the .ts
extension (e.g., './Link.ts') so they comply with the project's ESLint rule;
keep the import of component$ unchanged.
---
Outside diff comments:
In `@website/src/routes/blog/index.tsx`:
- Around line 46-53: The code uses non-null assertions on frontmatter returned
by readFile (frontmatter!) which can throw if frontmatter or required fields are
missing; update the mapping that builds the object (uses readFile and
frontmatter) to defensively check that frontmatter exists and contains required
keys (cover, title, published, authors) and either supply safe defaults or
return null for that entry, then after Promise.all filter out null results so
only valid posts are returned; reference the readFile call and the properties
frontmatter.cover/frontmatter.title/frontmatter.published/frontmatter.authors
and the href construction when adding the validation and defaults.
---
Nitpick comments:
In `@website/src/components/index.ts`:
- Line 27: The export statement in website/src/components/index.ts uses
'./PostList' without the required .ts extension; update the export to
'./PostList.ts' (and make the same .ts extension change for other exports in
this file such as any occurrences of export * from './XYZ') so imports comply
with the ESM .ts-extension rule enforced by ESLint, or alternatively note this
as a repo-wide cleanup if you prefer to change all exports in a follow-up.
In `@website/src/components/PostMeta.tsx`:
- Around line 4-8: The props declaration currently uses a type named
PostCoverProps while the component expects PostMeta props and your style guide
prefers interface; rename PostCoverProps to PostMetaProps and convert the
declaration from a type to an interface (interface PostMetaProps { variant:
'blog' | 'post'; authors: string[]; published: string; }) and update any
references (e.g., the PostMeta component signature and its usages) to use
PostMetaProps instead of PostCoverProps.
In `@website/src/routes/blog/index.tsx`:
- Around line 20-34: Replace the object-shape type aliases with interfaces:
change "type PostFrontmatter" to "interface PostFrontmatter" with the same
properties, and change "type PostData = PostFrontmatter & { href: string }" to
"interface PostData extends PostFrontmatter { href: string }"; keep the existing
"interface PostSection" as-is. This preserves the same structure while following
the guideline to prefer interface for object shapes (referencing
PostFrontmatter, PostData, and PostSection).
- Around line 58-59: Replace the magic number 7776000000 with a named constant
to clarify it's 90 days in milliseconds: define a constant (e.g.,
RECENT_POSTS_WINDOW_MS or NINETY_DAYS_MS) and use it when computing latestCutoff
(const latestCutoff = new Date(Date.now() - RECENT_POSTS_WINDOW_MS)); keep the
constant near the top of the module so it's discoverable and add a short comment
explaining it's a 90-day window.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 92ca0722-d123-4c38-bbac-83c70bccd399
📒 Files selected for processing (4)
website/src/components/PostList.tsxwebsite/src/components/PostMeta.tsxwebsite/src/components/index.tswebsite/src/routes/blog/index.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b38bf9316
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🧹 Nitpick comments (1)
website/src/components/PostMeta.tsx (1)
4-8: Type name mismatch and preferinterface.The type is named
PostCoverPropsbut is used for thePostMetacomponent. Additionally, coding guidelines specify preferringinterfaceovertypefor object shapes.♻️ Suggested fix
-type PostCoverProps = { +interface PostMetaProps { variant: 'blog' | 'post'; authors: string[]; published: string; -}; +}Also update line 13:
-export const PostMeta = component$<PostCoverProps>( +export const PostMeta = component$<PostMetaProps>(As per coding guidelines: "Prefer
interfaceovertypefor defining object shapes in TypeScript".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@website/src/components/PostMeta.tsx` around lines 4 - 8, Rename the prop shape to an interface and ensure the component uses it: replace the existing type alias PostCoverProps with an interface named PostMetaProps (preserving the fields variant, authors, published) and update the PostMeta component's prop annotation to use PostMetaProps instead of PostCoverProps; this converts "type PostCoverProps = { variant: 'blog' | 'post'; authors: string[]; published: string }" into "interface PostMetaProps { variant: 'blog' | 'post'; authors: string[]; published: string }" and swaps the usage in the PostMeta function signature to reference PostMetaProps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@website/src/components/PostMeta.tsx`:
- Around line 4-8: Rename the prop shape to an interface and ensure the
component uses it: replace the existing type alias PostCoverProps with an
interface named PostMetaProps (preserving the fields variant, authors,
published) and update the PostMeta component's prop annotation to use
PostMetaProps instead of PostCoverProps; this converts "type PostCoverProps = {
variant: 'blog' | 'post'; authors: string[]; published: string }" into
"interface PostMetaProps { variant: 'blog' | 'post'; authors: string[];
published: string }" and swaps the usage in the PostMeta function signature to
reference PostMetaProps.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5afc4410-2fcc-4e46-8868-33143286dbe4
📒 Files selected for processing (1)
website/src/components/PostMeta.tsx
Summary by CodeRabbit
New Features
Improvements