Skip to content

Add PostList component and split blog posts into sections#74

Merged
fabian-hiller merged 2 commits intomainfrom
improve-blog
Mar 17, 2026
Merged

Add PostList component and split blog posts into sections#74
fabian-hiller merged 2 commits intomainfrom
improve-blog

Conversation

@fabian-hiller
Copy link
Member

@fabian-hiller fabian-hiller commented Mar 16, 2026

Summary by CodeRabbit

  • New Features

    • Blog posts now display as card lists and are grouped into yearly archive sections, with a prominent "Recent posts" section for quick access.
    • Post list layout improved for clearer presentation of title, cover, authors, and date.
  • Improvements

    • Author avatars now behave contextually: clickable links on individual post views, non-link on blog listings.
    • Date formatting standardized to UTC.

Copilot AI review requested due to automatic review settings March 16, 2026 22:17
@vercel
Copy link

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
formisch Ready Ready Preview, Comment Mar 17, 2026 3:55am

Request Review

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 16, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
New UI component + export
website/src/components/PostList.tsx, website/src/components/index.ts
Adds PostList component and re-exports it from components barrel. Renders post cards (cover, title, PostMeta) by mapping a posts array.
Author avatar & date formatting
website/src/components/PostMeta.tsx
Replaces static avatar markup with conditional wrapper (anchor for post variant, div for blog variant), moves styling/zIndex to wrapper, and sets timeZone: 'UTC' for published date.
Blog index: loading & grouping posts
website/src/routes/blog/index.tsx
Replaces flat post list with loader that globs MDX, computes href, defines PostFrontmatter, PostData, PostSection types, and organizes posts into "Recent" and per-year sections; renders sections using PostList.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A PostList hops into view with cheer,
Sections sorted by year and recent near,
Avatars link or gently nest,
Dates set to UTC for time-true rest,
The blog blooms neat — a rabbit’s small cheer! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main changes: introducing a new PostList component and reorganizing blog posts into sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve-blog
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PostList component and re-exported it from the components barrel.
  • Updated /blog route loader to return post sections (recent + by year) and updated the page UI to render those sections.
  • Enhanced PostMeta to 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Non-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 and interface preference.

The type is named PostCoverProps but defines props for PostMeta. Additionally, per coding guidelines, prefer interface over type for 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 interface over type for 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 .ts extension, but consistent with existing pattern.

The export follows the existing pattern in this file. If ESLint enforces .ts extensions, 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 .ts extensions 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: Prefer interface over type for object shapes.

Per coding guidelines, interface is 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 interface over type for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 89961a0 and 4b38bf9.

📒 Files selected for processing (4)
  • website/src/components/PostList.tsx
  • website/src/components/PostMeta.tsx
  • website/src/components/index.ts
  • website/src/routes/blog/index.tsx

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
website/src/components/PostMeta.tsx (1)

4-8: Type name mismatch and prefer interface.

The type is named PostCoverProps but is used for the PostMeta component. Additionally, coding guidelines specify preferring interface over type for 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 interface over type for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4b38bf9 and 0997abd.

📒 Files selected for processing (1)
  • website/src/components/PostMeta.tsx

@fabian-hiller fabian-hiller merged commit 1cc1871 into main Mar 17, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants