Skip to content

feat(blogs): markdown content pipeline, token fixes, and a redesign - #17

Merged
hallelx2 merged 3 commits into
mainfrom
halleluyaholudele/blogs-redesign-markdown
Aug 4, 2026
Merged

feat(blogs): markdown content pipeline, token fixes, and a redesign#17
hallelx2 merged 3 commits into
mainfrom
halleluyaholudele/blogs-redesign-markdown

Conversation

@hallelx2

@hallelx2 hallelx2 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Replaces the hardcoded blog with a markdown content pipeline, and rebuilds the pages on the design tokens.

Content is markdown, shaped for a database

Posts lived in a BLOG_POSTS array inside a 59 KB client component, with three invented authors carrying Unsplash stock photos. Nothing could be edited, scheduled or migrated without editing TSX.

They now live in content/posts/*.md with frontmatter, loaded through lib/posts.ts. The Post type is shaped as a database row deliberately — slug as primary key, publishedAt as an ISO string so it sorts and indexes without parsing, body kept as raw markdown so the renderer can change without a migration.

Pages call getAllPosts / getPost and know nothing about the filesystem, so moving to Postgres means reimplementing two functions, not touching every page. Reading time is computed from the body — never hand-written, because a hand-written one is wrong the moment the post is edited.

Frontmatter parsing and markdown rendering are written rather than pulled in: this app sits in a pnpm workspace, so a new dependency edits a lockfile shared by every app in the monorepo.

Raw HTML never renders — verified, not assumed

Nothing calls dangerouslySetInnerHTML, so every value reaches the DOM as a React child and gets escaped. A post containing <script> renders it as visible characters.

Links were the hole that left — [text](javascript:…) is valid markdown. safeUrl allowlists http, https, mailto, tel and relative paths; anything else collapses to #.

I proved it by building a fixture full of payloads and inspecting the emitted HTML:

check result
live <script> tag none — escaped to text
onerror attribute none — the only real <img> is src="#"
<iframe> none
javascript: / data: / vbscript: in href or src none
hostile links neutralised 4 → #
relative + https links intact

Two token bugs against DESIGN.md

--color-bg-base        #ffffff → #fcfcfd    canvas is "never pure"
--color-text-secondary #3f3f36 → #3f3f46    typo

The base layer also hardcoded background-color: #ffffff !important, overriding the token entirely — so the near-white canvas the brand depends on was never rendering. Added the tokens DESIGN.md specifies but the stylesheet never encoded: hairline, grid line, display tracking (-0.03em), eyebrow tracking (0.16em).

.signal-gradient is a named utility on purpose — DESIGN.md permits the blue→pink gradient on exactly one line per view, and a named class makes a second occurrence visible in review.

Diagrams are components, not SVG

Hand-positioned SVG text does not reflow, does not use the site fonts, is not selectable or translatable, and needs re-authoring whenever a token changes. components/Diagrams.tsx uses the same CSS as the page. For OG cards, screenshot the component rather than maintaining a second artwork that drifts.

Verification

Typecheck clean, production build clean, all 7 posts prerendered as static HTML, series ordering and prev/next resolved from the part field.

Not verified: the visual result. Screenshots require the browser pane to be displayed, so I checked structure and content programmatically only. Worth a look at next dev before merging.

Summary by Sourcery

Replace the bespoke, client-side blog page with a markdown-backed content pipeline and redesigned article/index layouts grounded in shared design tokens.

New Features:

  • Introduce filesystem-backed blog posts with frontmatter, loaded via a Post model and helper functions for listing and fetching posts.
  • Add per-post pages with static generation, markdown rendering, series navigation, and computed reading-time metadata.
  • Add reusable Figure and diagram components so posts can embed structured, styled visuals.

Bug Fixes:

  • Correct background and secondary text color tokens to match DESIGN.md and remove hardcoded white backgrounds that overrode the base canvas.
  • Add hairline and grid line tokens plus tracking utilities to ensure typography and grid styling consistently follow the design system.

Enhancements:

  • Rework the blogs homepage into a static, token-driven engineering notes index instead of an interactive sandbox-style client app.
  • Implement a custom markdown-to-React renderer that enforces safe, escaped output and link URL allowlisting.
  • Add diagram registry components to replace static SVG artwork with token-aware, responsive diagram rendering.

Build:

  • Add markdown content and post-loading utilities structured so future migration to a database requires only backend changes, not page components.

Documentation:

  • Author a seven-part engineering notes series as markdown content describing PDF parsing, pdftable, and benchmarks, with internal prev/next navigation.

Chores:

  • Add blog asset SVGs and workspace metadata files needed for the new content pipeline and layouts.

Summary by CodeRabbit

  • New Features
    • Added a redesigned blog homepage with featured posts, metrics, navigation, and footer content.
    • Added individual blog post pages with metadata, reading time, series navigation, and Markdown rendering.
    • Added seven articles covering PDF structure, text extraction, table reconstruction, architecture, testing, and future improvements.
    • Added diagrams, figures, responsive styling, and safer link handling for article content.

The blog was a single 59 KB client component with posts hardcoded in a
BLOG_POSTS array and three fabricated authors carrying Unsplash stock
photos. Nothing about it could be edited, scheduled, or moved to a
database without editing TSX.

Content now lives in content/posts/*.md with frontmatter, loaded through
lib/posts.ts. The Post type is shaped as a database row on purpose --
slug as primary key, publishedAt as an ISO string so it sorts and indexes
without parsing, body kept as raw markdown so the renderer can change
without a migration. Pages call getAllPosts and getPost and know nothing
about the filesystem, so moving to Postgres means reimplementing two
functions rather than touching every page. Reading time is computed from
the body, never written by hand, because a hand-written one is wrong the
moment the post is edited.

Frontmatter parsing and markdown rendering are written rather than pulled
in. This app sits in a pnpm workspace, so a new dependency edits a
lockfile shared by every app in the monorepo -- blast radius well beyond
a blog. The supported subset is closed and under our control.

Two token bugs fixed against DESIGN.md, both of which had been quietly
flattening the brand:

  --color-bg-base       #ffffff -> #fcfcfd   canvas is "never pure"
  --color-text-secondary #3f3f36 -> #3f3f46  a typo

The base layer also hardcoded pure white with !important, overriding the
token entirely. It now reads from the token.

Added the tokens the design source specifies but the stylesheet never
encoded: hairline, grid line, display tracking (-0.03em), eyebrow
tracking (0.16em). Plus utilities for the rules that were previously
enforced by memory -- .signal-gradient exists as a named class precisely
because DESIGN.md permits the blue-to-pink gradient on ONE line per view,
and a named utility makes a second occurrence visible in review.

Diagrams are React components rather than hand-authored SVG. Absolutely
positioned SVG text does not reflow, does not use the site fonts, is not
selectable or translatable, and needs re-authoring whenever a token
changes. Components inherit Geist and restyle themselves. Where a raster
is genuinely needed for an OG card, screenshot the component rather than
maintaining a second artwork that will drift.

Verified: typecheck clean, production build clean, all 7 posts
prerendered as static HTML, series ordering and prev/next resolved from
the `part` field.
Raw HTML in a post was already inert. Nothing in the renderer calls
dangerouslySetInnerHTML, so every value reaches the DOM as a React child
and gets escaped -- a post containing <script> renders those as visible
characters in a paragraph. That is a property worth stating explicitly,
since most markdown pipelines do the opposite, so it is now documented at
the top of the module rather than left as an accident of implementation.

Links were the hole that left. [text](javascript:...) is valid markdown
and would have become a live javascript: URL; the same for data: and
vbscript:, and for image sources. safeUrl allowlists http, https, mailto,
tel and relative paths, and collapses anything else to "#" rather than
throwing, because one bad link in a post should not blank the page.

Verified by building a fixture full of payloads and inspecting the
emitted HTML: no live script tag, no iframe, no event-handler attribute,
no dangerous URL in any href or src, and the four hostile links all
neutralised to "#" while the relative and https links survived intact.
The payloads appear as escaped text, which is the correct outcome -- the
post still renders, it just cannot execute.

One note for whoever changes this next: introducing
dangerouslySetInnerHTML to support embeds gives the guarantee up and
needs sanitisation to replace it.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
vectorless-web Ignored Ignored Preview Aug 4, 2026 8:55pm

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

Replaces the old client-side, hardcoded blog UI with a file-backed markdown content pipeline and new post layouts, while fixing design token mismatches and encoding several typography utilities; also introduces safe markdown rendering and reusable diagram/figure components aligned with DESIGN.md.

Sequence diagram for serving a blog post from markdown

sequenceDiagram
  actor User
  participant NextRouter as NextRouter
  participant PostPage as PostPage
  participant PostsRepo as lib_posts
  participant MarkdownRenderer as markdown

  User->>NextRouter: request /posts/:slug
  NextRouter->>PostPage: render PostPage(params)
  PostPage->>PostsRepo: getPost(slug)
  PostsRepo-->>PostPage: Post
  PostPage->>PostsRepo: getSeriesNeighbours(slug)
  PostsRepo-->>PostPage: {prev,next}
  PostPage->>MarkdownRenderer: renderMarkdown(Post.body)
  MarkdownRenderer->>MarkdownRenderer: safeUrl(...) on links/images
  MarkdownRenderer-->>PostPage: ReactNode[]
  PostPage-->>NextRouter: HTML for article
  NextRouter-->>User: rendered post page
Loading

File-Level Changes

Change Details Files
Replace hardcoded client blog page with a markdown-driven index that uses a posts data layer and design-token-based layout.
  • Removed the previous client-only, highly interactive blog page with hardcoded posts, categories, demo modal, and SVG illustrations.
  • Introduced a server component home page that reads posts via getAllPosts, sets metadata, and renders a hero section plus a series-aware index using new typography utilities.
  • Simplified navigation/header/footer to match the engineering-notes concept and rely on tokens like eyebrow, signal-gradient, and stat for styling.
apps/blogs/app/page.tsx
Introduce a markdown-based content model and posts loader shaped like a database row, including reading time and series navigation.
  • Added a Post interface representing a row-like post record with slug, title, summary, ISO publishedAt, optional part/series, tags, computed readingMinutes, and raw markdown body.
  • Implemented filesystem-backed getAllPosts, getPost, and getSeriesNeighbours functions that parse frontmatter from content/posts/*.md, compute reading time, and sort posts either by series part or published date.
  • Defined a minimal frontmatter parser instead of using gray-matter, keeping dependencies out of the shared pnpm workspace lockfile.
apps/blogs/lib/posts.ts
Add a custom markdown-to-React renderer that enforces HTML safety and uses design tokens for typography and tables.
  • Implemented renderMarkdown to parse a constrained markdown subset (headings, paragraphs, lists, fenced code, tables, blockquotes, images, inline code/bold/italic/links) into React elements without using dangerouslySetInnerHTML.
  • Added safeUrl URL allowlist for link/image href/src values, only permitting http/https/mailto/tel/relative URLs and collapsing others to # to neutralize javascript:/data:/vbscript: URLs.
  • Styled markdown outputs (code blocks, tables, blockquotes, lists, inline elements) using design tokens like eyebrow, stat, brand colors, and the dark-surface constraint for code panels.
apps/blogs/lib/markdown.tsx
Add a per-post page template that renders markdown, uses the posts data layer, and exposes series-aware prev/next navigation.
  • Created dynamic route handler for /posts/[slug] that statically generates params from getAllPosts, builds metadata from post frontmatter, and uses getPost to fetch a single post.
  • Rendered post header with series/part or date, summary, meta row (date, reading time, tags), and markdown body via renderMarkdown.
  • Implemented series navigation at the bottom using getSeriesNeighbours to provide prev/next links when part data is present, with consistent header/footer styling.
apps/blogs/app/posts/[slug]/page.tsx
Add a reusable Figure component and component-based diagrams that share site typography and tokens instead of raw SVG art.
  • Introduced a Figure component that wraps either an image or children in a framed, hairline-bordered container with an optional caption styled per DESIGN.md.
  • Implemented several diagram components (DriftDiagram, EncodingChain, BenchmarkBars) using regular React + CSS layout instead of SVG text, with labels in Instrument Serif/Geist and stat styling.
  • Provided a DIAGRAMS registry mapping names to diagram components so markdown or other code can reference diagrams symbolically.
apps/blogs/components/Figure.tsx
apps/blogs/components/Diagrams.tsx
Convert blog content into markdown posts representing a seven-part series and wire them into the new pipeline.
  • Added multiple markdown files under content/posts with frontmatter fields (slug, title, summary, publishedAt, series, part, tags) and bodies referencing diagrams and assets.
  • Ensured series metadata (Reading a PDF like a printer) and part indices form a coherent ordered series for the index and prev/next navigation.
  • Linked posts to internal /posts/... routes in markdown content for intra-series navigation and referenced static SVG assets under public/blog-assets.
apps/blogs/content/posts/01-a-pdf-is-not-a-document.md
apps/blogs/content/posts/02-thinking-like-a-printer.md
apps/blogs/content/posts/03-where-tables-actually-live.md
apps/blogs/content/posts/04-building-pdftable.md
apps/blogs/content/posts/05-the-bugs-that-never-crashed.md
apps/blogs/content/posts/06-measuring-instead-of-believing.md
apps/blogs/content/posts/07-what-is-still-missing.md
apps/blogs/public/blog-assets/00-hero.svg
apps/blogs/public/blog-assets/01-pdf-anatomy.svg
apps/blogs/public/blog-assets/02-advance-drift.svg
apps/blogs/public/blog-assets/03-encoding-chain.svg
apps/blogs/public/blog-assets/04-table-strategies.svg
apps/blogs/public/blog-assets/05-clipped-paren.svg
apps/blogs/public/blog-assets/06-benchmark-ceiling.svg
apps/blogs/public/blog-assets/07-hybrid-architecture.svg
Fix design token inconsistencies and add utilities for grid, hairline borders, gradients, and typography tracking.
  • Corrected --color-bg-base to the near-white canvas value from DESIGN.md and removed the hardcoded background-color: #ffffff !important on html, body in favor of token-based background.
  • Fixed the typo in --color-text-secondary and added new color tokens for hairline borders and grid lines, plus tracking tokens for display and eyebrow text.
  • Added utility classes for grid-paper backgrounds (using --color-grid-line), signal-gradient text, eyebrow mono uppercase labels, tracking-display, and stat with tabular numerals; set body font-weight to 300 for the specified extreme weight contrast and enabled font smoothing.
apps/blogs/app/globals.css
Repository/ops additions related to the blogs app launch.
  • Added a .claude/launch.json file for tooling/launch configuration associated with the blogs app.
  • Updated TypeScript build info for the blogs app to reflect the new source layout and static generation.
apps/blogs/.claude/launch.json
apps/blogs/tsconfig.tsbuildinfo

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hallelx2, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3dfbef9-9d54-4700-b5a1-9e1eacf23d2d

📥 Commits

Reviewing files that changed from the base of the PR and between 26e476e and c93ffbf.

📒 Files selected for processing (4)
  • apps/blogs/content/posts/02-thinking-like-a-printer.md
  • apps/blogs/content/posts/06-measuring-instead-of-believing.md
  • apps/blogs/lib/markdown.tsx
  • apps/blogs/tsconfig.tsbuildinfo
📝 Walkthrough

Walkthrough

The blogs app now uses filesystem-backed Markdown posts. It renders a server-side index and static post routes with metadata, series navigation, safe Markdown handling, reusable figures and diagrams, and updated design tokens.

Changes

Blogs publishing experience

Layer / File(s) Summary
Post content pipeline
apps/blogs/lib/posts.ts, apps/blogs/content/posts/*
Adds filesystem-backed post loading, frontmatter parsing, reading-time calculation, slug lookup, series navigation, and seven Markdown articles.
Markdown and figure rendering
apps/blogs/lib/markdown.tsx, apps/blogs/components/Figure.tsx, apps/blogs/components/Diagrams.tsx
Adds safe Markdown-to-React rendering, framed figures, and reusable glyph, encoding, and benchmark diagrams.
Server-rendered blog routes
apps/blogs/app/page.tsx, apps/blogs/app/posts/[slug]/page.tsx
Replaces the interactive index with server-rendered post listings and adds static post pages with metadata, missing-post handling, rendered content, and series links.
Blog presentation and development setup
apps/blogs/app/globals.css, apps/blogs/.claude/launch.json, apps/blogs/tsconfig.tsbuildinfo
Adds design tokens and typography utilities, updates base styling, adds a development launch configuration, and records TypeScript build metadata.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Visitor
  participant Home
  participant PostPage
  participant PostStore
  participant MarkdownRenderer
  Visitor->>Home: Open blog index
  Home->>PostStore: getAllPosts()
  PostStore-->>Home: Return sorted posts
  Home-->>Visitor: Render lead post and index
  Visitor->>PostPage: Open post slug
  PostPage->>PostStore: getPost() and getSeriesNeighbours()
  PostPage->>MarkdownRenderer: renderMarkdown()
  MarkdownRenderer-->>PostPage: Return React content
  PostPage-->>Visitor: Render post page
Loading

Possibly related PRs

  • hallelx2/vectorless#6: Both changes modify the blog index, but this PR replaces the interactive interface.
  • hallelx2/vectorless#7: This PR replaces the same blog interface with a server-rendered posts experience.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: the Markdown content pipeline, design token fixes, and blog redesign.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch halleluyaholudele/blogs-redesign-markdown

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.

The app shipped both the SVG files and the React diagram components,
with every post still pointing at the images -- so the components were
dead code and the incoherence would have survived review.

Posts now select a component with a fenced block:

    ```diagram
    drift
    ```

A fence rather than new syntax, so the file stays valid markdown: it
still reads correctly on GitHub and in an editor, degrading to a short
code block rather than to noise. An unknown key renders a visible
message in place instead of nothing, so a typo cannot hide until someone
reads the whole post.

Three diagrams converted -- glyph-width drift, the encoding chain, and
the benchmark bars. Verified in the built HTML that they emit real
markup: the AFM widths 222 and 833 are live text nodes, so the figures
are selectable, translatable and sharp at any zoom, which is the point
of not shipping them as images.

Four diagrams still reference SVG and are listed in the PR rather than
silently left behind.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (3)
apps/blogs/tsconfig.tsbuildinfo (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move apps/blogs/tsconfig.tsbuildinfo out of source control.

apps/blogs/tsconfig.json enables incremental, but this repo does not ignore *.tsbuildinfo. Move the file’s tsBuildInfoFile to .turbo/ or another ignored path, or ignore *.tsbuildinfo, then regenerate the build metadata instead of committing apps/blogs/tsconfig.tsbuildinfo. [apps/blogs/tsbuildinfo] is already tracked alongside the unignored apps/web/tsconfig.tsbuildinfo`, so apply the same ignore policy consistently.

🤖 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 `@apps/blogs/tsconfig.tsbuildinfo` at line 1, Remove the generated
apps/blogs/tsconfig.tsbuildinfo artifact from source control and apply a
consistent ignore policy for incremental TypeScript metadata, either by
configuring tsBuildInfoFile under an ignored .turbo path or ignoring
*.tsbuildinfo. Update the relevant tsconfig configuration and regenerate
metadata without retaining tracked tsbuildinfo files, including the similarly
tracked apps/web artifact.
apps/blogs/lib/posts.ts (1)

105-122: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize the post load to avoid repeated directory reads and parsing.

getPost and getSeriesNeighbours each call getAllPosts, which re-reads and re-parses every Markdown file. The post route calls both, so one page render parses the whole corpus twice. Cache the parsed list in module scope.

♻️ Proposed cache
+let cache: Post[] | undefined;
+
 export function getAllPosts(): Post[] {
+  if (cache) return cache;
   if (!fs.existsSync(CONTENT_DIR)) return [];
   const posts = fs
     .readdirSync(CONTENT_DIR)
     .filter((f) => f.endsWith(".md"))
     .map(toPost);
 
   const everyPostIsPartOfASeries = posts.length > 0 && posts.every((p) => p.part != null);
-  return posts.sort((a, b) =>
+  cache = posts.sort((a, b) =>
     everyPostIsPartOfASeries
       ? (a.part ?? 0) - (b.part ?? 0)
       : b.publishedAt.localeCompare(a.publishedAt),
   );
+  return cache;
 }
🤖 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 `@apps/blogs/lib/posts.ts` around lines 105 - 122, Memoize the parsed posts in
module scope so repeated calls do not re-read or re-parse the Markdown files.
Update getAllPosts to populate and reuse the cache, while preserving the
existing empty-directory handling, sorting, and behavior of getPost and
getSeriesNeighbours.
apps/blogs/app/page.tsx (1)

11-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

formatDate is duplicated with a different month format.

apps/blogs/app/posts/[slug]/page.tsx lines 18-24 defines the same helper with month: "long". Move one implementation into a shared module, and pass the month style as an argument if both formats are intended.

🤖 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 `@apps/blogs/app/page.tsx` around lines 11 - 17, Consolidate the duplicated
formatDate helper from the blog page components into a shared module, then
update both callers to import and use it. Preserve each page’s intended month
formatting by making the month style configurable when necessary, including the
existing "short" and "long" behaviors.
🤖 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 `@apps/blogs/app/page.tsx`:
- Around line 127-129: Update the post index rendering in the visible span to
return an empty cell when post.part is absent, rather than applying padStart to
an empty string; preserve the two-digit formatting for posts with a defined
part.
- Around line 90-94: Update the series label rendering in the page component so
its part count filters posts to those whose series matches the current series
before counting. Keep the existing conditional rendering and label format
unchanged, using the posts collection rather than the total posts.length.

In `@apps/blogs/components/Diagrams.tsx`:
- Around line 33-78: The fixed-width comparison rows in DriftDiagram are clipped
on narrow viewports because Figure hides overflow. Wrap the comparison content,
including the “word ends here” label and error marker, in an overflow-x-auto
container so all glyphs remain horizontally reachable without changing the
diagram’s scale.
- Around line 26-36: Update DriftDiagram’s drift measurement display to match
the defined TRUE_WIDTHS and guessTotal values: report the error as 0.221em, or
explicitly calculate and label the equivalent point value using a stated 12pt
font size. Remove the inconsistent “~0.3pt off” wording while preserving the
existing drift calculation.

In `@apps/blogs/content/posts/07-what-is-still-missing.md`:
- Line 102: Update the links in the post’s evaluation note so they no longer
point to unpublished relative routes; remove those links or replace them with
working canonical public URLs for the evaluations and benchmarks resources.

In `@apps/blogs/lib/markdown.tsx`:
- Around line 159-166: Update renderMarkdown’s Markdown image handling to
recognize an explicit diagram directive or source-to-component mapping for the
registered DIAGRAMS entries, rendering the resolved component directly instead
of wrapping it in Figure. Preserve Figure rendering for ordinary image sources,
and update Markdown posts to use the new diagram syntax for DriftDiagram,
EncodingChain, and BenchmarkBars.
- Around line 62-111: Update the token rendering branches in inline so emphasis,
strong, and link label content are passed back through inline using the existing
keyPrefix mechanism, enabling nested links and code spans; keep code span
contents rendered as plain text. Apply the recursive parsing to the label inside
the link branch while preserving safeUrl and external-link handling.

In `@apps/blogs/lib/posts.ts`:
- Around line 96-98: Update toPost in apps/blogs/lib/posts.ts at lines 96-98 to
validate frontmatter runtime types: normalize tags to an array using the
specified array/string/non-empty fallback behavior, and set part only when
data.part is a number. No direct change is needed in
apps/blogs/app/posts/[slug]/page.tsx lines 64-67; its existing tags length guard
and join are correct once Post.tags is guaranteed to be an array.
- Around line 124-130: Update getSeriesNeighbours to filter getAllPosts() to
posts sharing the target post’s series, then order that filtered collection by
part before selecting neighbours. Preserve the empty result for an unknown slug
and ensure prev/next are derived from the series-local ordering rather than the
global getAllPosts sort.

---

Nitpick comments:
In `@apps/blogs/app/page.tsx`:
- Around line 11-17: Consolidate the duplicated formatDate helper from the blog
page components into a shared module, then update both callers to import and use
it. Preserve each page’s intended month formatting by making the month style
configurable when necessary, including the existing "short" and "long"
behaviors.

In `@apps/blogs/lib/posts.ts`:
- Around line 105-122: Memoize the parsed posts in module scope so repeated
calls do not re-read or re-parse the Markdown files. Update getAllPosts to
populate and reuse the cache, while preserving the existing empty-directory
handling, sorting, and behavior of getPost and getSeriesNeighbours.

In `@apps/blogs/tsconfig.tsbuildinfo`:
- Line 1: Remove the generated apps/blogs/tsconfig.tsbuildinfo artifact from
source control and apply a consistent ignore policy for incremental TypeScript
metadata, either by configuring tsBuildInfoFile under an ignored .turbo path or
ignoring *.tsbuildinfo. Update the relevant tsconfig configuration and
regenerate metadata without retaining tracked tsbuildinfo files, including the
similarly tracked apps/web artifact.
🪄 Autofix

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 Plus

Run ID: 96070762-1e19-4dbb-9836-ca8c9971333d

📥 Commits

Reviewing files that changed from the base of the PR and between f89d610 and 26e476e.

⛔ Files ignored due to path filters (8)
  • apps/blogs/public/blog-assets/00-hero.svg is excluded by !**/*.svg
  • apps/blogs/public/blog-assets/01-pdf-anatomy.svg is excluded by !**/*.svg
  • apps/blogs/public/blog-assets/02-advance-drift.svg is excluded by !**/*.svg
  • apps/blogs/public/blog-assets/03-encoding-chain.svg is excluded by !**/*.svg
  • apps/blogs/public/blog-assets/04-table-strategies.svg is excluded by !**/*.svg
  • apps/blogs/public/blog-assets/05-clipped-paren.svg is excluded by !**/*.svg
  • apps/blogs/public/blog-assets/06-benchmark-ceiling.svg is excluded by !**/*.svg
  • apps/blogs/public/blog-assets/07-hybrid-architecture.svg is excluded by !**/*.svg
📒 Files selected for processing (16)
  • apps/blogs/.claude/launch.json
  • apps/blogs/app/globals.css
  • apps/blogs/app/page.tsx
  • apps/blogs/app/posts/[slug]/page.tsx
  • apps/blogs/components/Diagrams.tsx
  • apps/blogs/components/Figure.tsx
  • apps/blogs/content/posts/01-a-pdf-is-not-a-document.md
  • apps/blogs/content/posts/02-thinking-like-a-printer.md
  • apps/blogs/content/posts/03-where-tables-actually-live.md
  • apps/blogs/content/posts/04-building-pdftable.md
  • apps/blogs/content/posts/05-the-bugs-that-never-crashed.md
  • apps/blogs/content/posts/06-measuring-instead-of-believing.md
  • apps/blogs/content/posts/07-what-is-still-missing.md
  • apps/blogs/lib/markdown.tsx
  • apps/blogs/lib/posts.ts
  • apps/blogs/tsconfig.tsbuildinfo

Comment thread apps/blogs/app/page.tsx
Comment on lines +90 to +94
{series && (
<div className="eyebrow mb-8">
{series} · {posts.length} parts
</div>
</motion.div>
) : (
/* ━━━ MAGAZINE INDEX VIEW ━━━ */
<motion.div
key="magazine-index"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{/* ─── HERO SECTION ─── */}
<section className="relative min-h-[85vh] flex items-end overflow-hidden">
{/* Gradient backdrop */}
<div className="absolute -top-40 left-1/2 -translate-x-1/2 w-[1200px] h-[700px] bg-[radial-gradient(ellipse_at_center,rgba(20,86,240,0.07)_0%,rgba(234,94,193,0.035)_40%,transparent_70%)] blur-[50px] pointer-events-none" />
{/* Grid paper */}
<div className="absolute inset-0 grid-paper [mask-image:radial-gradient(ellipse_at_center,black_25%,transparent_70%)] pointer-events-none opacity-60" />

<div className="relative z-10 w-full max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 pb-16 pt-36 md:pt-44">
<div className="max-w-[920px]">
{/* Chip */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="inline-flex items-center gap-2.5 px-3.5 py-1.5 rounded-full border border-border-gray bg-white/80 backdrop-blur-sm mb-8 shadow-sm"
>
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-blue opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-brand-blue" />
</span>
<span className="font-data text-[10px] font-medium text-text-muted tracking-[0.16em] uppercase">
Vectorless Engineering Journal
</span>
</motion.div>

{/* Headline */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.1 }}
className="font-serif text-[48px] sm:text-[64px] md:text-[80px] lg:text-[92px] font-normal leading-[0.92] tracking-tight text-[#0A0A0A] mb-8"
>
Retrieval,{' '}
<span className="italic font-light text-transparent bg-clip-text bg-gradient-to-r from-brand-blue via-primary-500 to-brand-pink">
rethought.
</span>
</motion.h1>

{/* Subtitle */}
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.2 }}
className="text-lg md:text-xl font-light text-text-secondary leading-relaxed max-w-[640px]"
>
A technical journal on the design of structure-preserving retrieval architectures, no-chunking
models, and deterministic RAG systems for AI agents.
</motion.p>
</div>

{/* Decorative bottom border with date */}
<div className="mt-16 pt-6 border-t border-[#0A0A0A] flex items-center justify-between">
<div className="font-data text-[10px] uppercase tracking-[0.2em] text-text-muted">
Vol. 01 — {new Date().getFullYear()}
</div>
<div className="hidden sm:flex items-center gap-4 font-data text-[10px] uppercase tracking-[0.16em] text-text-muted">
<span>{BLOG_POSTS.length} Articles Published</span>
<span className="w-1 h-1 rounded-full bg-brand-pink" />
<span>Updated Weekly</span>
</div>
</div>
</div>
</section>

{/* ─── CATEGORIES + SANDBOX BAR ─── */}
<section className="sticky top-0 z-30 bg-white/90 backdrop-blur-lg border-b border-border-gray">
<div className="max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-1">
{categories.map((cat) => {
const isActive = selectedCategory === cat;
return (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-1.5 rounded-full text-xs font-data uppercase tracking-wider transition-all duration-200 cursor-pointer ${
isActive
? 'bg-[#0A0A0A] text-white font-semibold shadow-sm'
: 'text-text-muted hover:text-[#0A0A0A] hover:bg-black/[0.04]'
}`}
>
{cat}
</button>
);
})}
</div>

<button
onClick={openSandbox}
className="group inline-flex items-center gap-2 bg-brand-blue hover:bg-primary-600 text-white px-5 py-2.5 rounded-full font-data text-[10px] uppercase tracking-wider transition-all duration-300 shadow-sm cursor-pointer self-start sm:self-auto"
>
<Terminal className="w-3.5 h-3.5" />
<span>Try Retrieval</span>
<span className="w-1.5 h-1.5 rounded-full bg-brand-pink animate-pulse" />
</button>
</div>
</section>

{/* ─── FEATURED ARTICLE (FULL WIDTH) ─── */}
<AnimatePresence mode="wait">
{selectedCategory === 'All' && featuredPost && (
<motion.section
key="featured-hero"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.35 }}
className="border-b border-border-gray"
>
<div
onClick={() => setActiveArticle(featuredPost)}
className="group max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-16 md:py-20 cursor-pointer"
>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-start">
<div className="lg:col-span-7 flex flex-col justify-between min-h-[260px]">
<div>
<div className="flex items-center gap-3 font-data text-[10px] uppercase tracking-[0.16em] text-text-muted mb-5">
<span className="text-brand-blue font-semibold">{featuredPost.category}</span>
<span className="w-1 h-1 rounded-full bg-border-gray" />
<span>{featuredPost.date}</span>
<span className="w-1 h-1 rounded-full bg-border-gray" />
<span>{featuredPost.readTime}</span>
</div>

<h2 className="text-[32px] md:text-[44px] font-serif text-[#0A0A0A] group-hover:text-brand-blue transition-colors duration-300 leading-[1.06] tracking-tight mb-6">
{featuredPost.title}
</h2>

<p className="text-base md:text-lg text-text-secondary font-light leading-relaxed max-w-[600px] mb-8">
{featuredPost.snippet}
</p>
</div>

<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-white shadow-sm">
<img
src={featuredPost.author.avatarUrl}
alt={featuredPost.author.name}
className="object-cover w-full h-full"
referrerPolicy="no-referrer"
/>
</div>
<div className="flex flex-col">
<span className="text-xs font-semibold text-[#0A0A0A]">{featuredPost.author.name}</span>
<span className="text-[9px] font-data text-text-muted uppercase tracking-wider">
{featuredPost.author.role}
</span>
</div>
</div>

<div className="hidden sm:flex items-center gap-2 text-xs font-data uppercase tracking-[0.18em] font-semibold text-[#0A0A0A] group-hover:text-brand-blue transition-colors">
<span>Read article</span>
<ArrowRight className="w-4 h-4 text-brand-pink group-hover:translate-x-1.5 transition-transform duration-300" />
</div>
</div>
</div>

<div className="lg:col-span-5 w-full">
<BlueprintIllustration type={featuredPost.imageType} large />
</div>
</div>
</div>
</motion.section>
)}
</AnimatePresence>

{/* ─── ARTICLE GRID ─── */}
<section className="max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-12 md:py-16">
{selectedCategory === 'All' && (
<div className="flex items-center justify-between mb-8">
<h2 className="font-data text-[11px] uppercase tracking-[0.2em] text-text-muted font-medium">
Latest Articles
</h2>
<div className="h-px flex-1 bg-border-gray ml-6" />
</div>
)}

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-gray border border-border-gray rounded-xl overflow-hidden">
<AnimatePresence mode="popLayout">
{displayPosts.map((post, idx) => (
<motion.article
key={post.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, delay: idx * 0.03 }}
onClick={() => setActiveArticle(post)}
className="group flex flex-col cursor-pointer bg-white hover:bg-[#FAFBFF] transition-colors duration-300"
>
<div className="flex flex-col h-full p-7 md:p-8">
{/* Meta */}
<div className="flex items-center justify-between font-data text-[9px] uppercase tracking-[0.16em] text-text-muted mb-5">
<span className="text-brand-blue font-semibold">{post.category}</span>
<span>{post.date}</span>
</div>

{/* Headline */}
<h3 className="text-xl md:text-2xl font-serif text-[#0A0A0A] group-hover:text-brand-blue transition-colors duration-300 leading-tight mb-4">
{post.title}
</h3>

{/* Snippet */}
<p className="text-[13px] text-text-secondary leading-relaxed font-light mb-6 flex-grow">
{post.snippet}
</p>

{/* Blueprint */}
<div className="mb-6">
<BlueprintIllustration type={post.imageType} />
</div>

{/* Author + Read link */}
<div className="flex items-center justify-between pt-5 border-t border-border-light">
<div className="flex items-center gap-2.5">
<div className="relative w-7 h-7 rounded-full overflow-hidden border border-border-gray">
<img
src={post.author.avatarUrl}
alt={post.author.name}
className="object-cover w-full h-full"
referrerPolicy="no-referrer"
/>
</div>
<div className="flex flex-col">
<span className="text-[11px] font-semibold text-[#0A0A0A]">{post.author.name}</span>
<span className="text-[8px] font-data text-text-muted uppercase tracking-wider">
{post.readTime}
</span>
</div>
</div>

<ArrowUpRight className="w-4 h-4 text-text-muted group-hover:text-brand-blue group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all duration-300" />
</div>
</div>
</motion.article>
))}
</AnimatePresence>
</div>

{/* Empty State */}
{displayPosts.length === 0 && (
<div className="py-24 text-center border border-dashed border-border-gray rounded-2xl flex flex-col items-center justify-center gap-4 bg-[#FCFCFD] mt-4">
<Newspaper className="w-8 h-8 text-text-muted/40" />
<span className="font-display font-medium text-text-secondary">No matching articles found</span>
<button
onClick={() => setSelectedCategory('All')}
className="text-xs font-data uppercase tracking-wider text-brand-blue hover:underline cursor-pointer"
>
Show all articles
</button>
</div>
)}
</section>

{/* ─── NEWSLETTER CTA ─── */}
<section className="border-t border-border-gray">
<div className="max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-20 md:py-28">
<div className="max-w-[600px] mx-auto text-center">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-border-gray bg-white mb-6">
<Sparkles className="w-3 h-3 text-brand-pink" />
<span className="font-data text-[10px] uppercase tracking-[0.16em] text-text-muted font-medium">
Stay in the loop
</span>
</div>

<h2 className="font-serif text-[32px] md:text-[42px] leading-[1.08] tracking-tight text-[#0A0A0A] mb-4">
Engineering updates,{' '}
<span className="italic text-transparent bg-clip-text bg-gradient-to-r from-brand-blue to-brand-pink">
delivered.
</span>
</h2>

<p className="text-base text-text-secondary font-light leading-relaxed mb-8 max-w-[440px] mx-auto">
Get new posts on retrieval architectures, product releases, and the future of document AI — straight to
your inbox.
</p>

<div className="flex flex-col sm:flex-row items-stretch gap-3 max-w-[460px] mx-auto">
<input
type="email"
placeholder="you@company.com"
className="flex-1 px-4 py-3 border border-border-gray rounded-full text-sm bg-[#FCFCFD] focus:outline-none focus:border-brand-blue focus:ring-1 focus:ring-brand-blue/20 transition-all placeholder:text-text-muted/60"
/>
<button className="bg-[#0A0A0A] text-white px-6 py-3 rounded-full text-sm font-medium hover:bg-brand-blue transition-colors duration-300 cursor-pointer whitespace-nowrap">
Subscribe →
</button>
</div>

<p className="mt-4 text-[11px] text-text-muted font-data">
No spam. Unsubscribe anytime.
</p>
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The part count includes posts outside the series.

posts.length counts every loaded post. If a post without series is added, the label reports a part count that is too high. Count only posts in the same series.

♻️ Proposed fix
           {series && (
             <div className="eyebrow mb-8">
-              {series} · {posts.length} parts
+              {series} · {posts.filter((p) => p.series === series).length} parts
             </div>
           )}
📝 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.

Suggested change
{series && (
<div className="eyebrow mb-8">
{series} · {posts.length} parts
</div>
</motion.div>
) : (
/* ━━━ MAGAZINE INDEX VIEW ━━━ */
<motion.div
key="magazine-index"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{/* ─── HERO SECTION ─── */}
<section className="relative min-h-[85vh] flex items-end overflow-hidden">
{/* Gradient backdrop */}
<div className="absolute -top-40 left-1/2 -translate-x-1/2 w-[1200px] h-[700px] bg-[radial-gradient(ellipse_at_center,rgba(20,86,240,0.07)_0%,rgba(234,94,193,0.035)_40%,transparent_70%)] blur-[50px] pointer-events-none" />
{/* Grid paper */}
<div className="absolute inset-0 grid-paper [mask-image:radial-gradient(ellipse_at_center,black_25%,transparent_70%)] pointer-events-none opacity-60" />
<div className="relative z-10 w-full max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 pb-16 pt-36 md:pt-44">
<div className="max-w-[920px]">
{/* Chip */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="inline-flex items-center gap-2.5 px-3.5 py-1.5 rounded-full border border-border-gray bg-white/80 backdrop-blur-sm mb-8 shadow-sm"
>
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-blue opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-brand-blue" />
</span>
<span className="font-data text-[10px] font-medium text-text-muted tracking-[0.16em] uppercase">
Vectorless Engineering Journal
</span>
</motion.div>
{/* Headline */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.1 }}
className="font-serif text-[48px] sm:text-[64px] md:text-[80px] lg:text-[92px] font-normal leading-[0.92] tracking-tight text-[#0A0A0A] mb-8"
>
Retrieval,{' '}
<span className="italic font-light text-transparent bg-clip-text bg-gradient-to-r from-brand-blue via-primary-500 to-brand-pink">
rethought.
</span>
</motion.h1>
{/* Subtitle */}
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.2 }}
className="text-lg md:text-xl font-light text-text-secondary leading-relaxed max-w-[640px]"
>
A technical journal on the design of structure-preserving retrieval architectures, no-chunking
models, and deterministic RAG systems for AI agents.
</motion.p>
</div>
{/* Decorative bottom border with date */}
<div className="mt-16 pt-6 border-t border-[#0A0A0A] flex items-center justify-between">
<div className="font-data text-[10px] uppercase tracking-[0.2em] text-text-muted">
Vol. 01 {new Date().getFullYear()}
</div>
<div className="hidden sm:flex items-center gap-4 font-data text-[10px] uppercase tracking-[0.16em] text-text-muted">
<span>{BLOG_POSTS.length} Articles Published</span>
<span className="w-1 h-1 rounded-full bg-brand-pink" />
<span>Updated Weekly</span>
</div>
</div>
</div>
</section>
{/* ─── CATEGORIES + SANDBOX BAR ─── */}
<section className="sticky top-0 z-30 bg-white/90 backdrop-blur-lg border-b border-border-gray">
<div className="max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-1">
{categories.map((cat) => {
const isActive = selectedCategory === cat;
return (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-1.5 rounded-full text-xs font-data uppercase tracking-wider transition-all duration-200 cursor-pointer ${
isActive
? 'bg-[#0A0A0A] text-white font-semibold shadow-sm'
: 'text-text-muted hover:text-[#0A0A0A] hover:bg-black/[0.04]'
}`}
>
{cat}
</button>
);
})}
</div>
<button
onClick={openSandbox}
className="group inline-flex items-center gap-2 bg-brand-blue hover:bg-primary-600 text-white px-5 py-2.5 rounded-full font-data text-[10px] uppercase tracking-wider transition-all duration-300 shadow-sm cursor-pointer self-start sm:self-auto"
>
<Terminal className="w-3.5 h-3.5" />
<span>Try Retrieval</span>
<span className="w-1.5 h-1.5 rounded-full bg-brand-pink animate-pulse" />
</button>
</div>
</section>
{/* ─── FEATURED ARTICLE (FULL WIDTH) ─── */}
<AnimatePresence mode="wait">
{selectedCategory === 'All' && featuredPost && (
<motion.section
key="featured-hero"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.35 }}
className="border-b border-border-gray"
>
<div
onClick={() => setActiveArticle(featuredPost)}
className="group max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-16 md:py-20 cursor-pointer"
>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-start">
<div className="lg:col-span-7 flex flex-col justify-between min-h-[260px]">
<div>
<div className="flex items-center gap-3 font-data text-[10px] uppercase tracking-[0.16em] text-text-muted mb-5">
<span className="text-brand-blue font-semibold">{featuredPost.category}</span>
<span className="w-1 h-1 rounded-full bg-border-gray" />
<span>{featuredPost.date}</span>
<span className="w-1 h-1 rounded-full bg-border-gray" />
<span>{featuredPost.readTime}</span>
</div>
<h2 className="text-[32px] md:text-[44px] font-serif text-[#0A0A0A] group-hover:text-brand-blue transition-colors duration-300 leading-[1.06] tracking-tight mb-6">
{featuredPost.title}
</h2>
<p className="text-base md:text-lg text-text-secondary font-light leading-relaxed max-w-[600px] mb-8">
{featuredPost.snippet}
</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-white shadow-sm">
<img
src={featuredPost.author.avatarUrl}
alt={featuredPost.author.name}
className="object-cover w-full h-full"
referrerPolicy="no-referrer"
/>
</div>
<div className="flex flex-col">
<span className="text-xs font-semibold text-[#0A0A0A]">{featuredPost.author.name}</span>
<span className="text-[9px] font-data text-text-muted uppercase tracking-wider">
{featuredPost.author.role}
</span>
</div>
</div>
<div className="hidden sm:flex items-center gap-2 text-xs font-data uppercase tracking-[0.18em] font-semibold text-[#0A0A0A] group-hover:text-brand-blue transition-colors">
<span>Read article</span>
<ArrowRight className="w-4 h-4 text-brand-pink group-hover:translate-x-1.5 transition-transform duration-300" />
</div>
</div>
</div>
<div className="lg:col-span-5 w-full">
<BlueprintIllustration type={featuredPost.imageType} large />
</div>
</div>
</div>
</motion.section>
)}
</AnimatePresence>
{/* ─── ARTICLE GRID ─── */}
<section className="max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-12 md:py-16">
{selectedCategory === 'All' && (
<div className="flex items-center justify-between mb-8">
<h2 className="font-data text-[11px] uppercase tracking-[0.2em] text-text-muted font-medium">
Latest Articles
</h2>
<div className="h-px flex-1 bg-border-gray ml-6" />
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-px bg-border-gray border border-border-gray rounded-xl overflow-hidden">
<AnimatePresence mode="popLayout">
{displayPosts.map((post, idx) => (
<motion.article
key={post.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, delay: idx * 0.03 }}
onClick={() => setActiveArticle(post)}
className="group flex flex-col cursor-pointer bg-white hover:bg-[#FAFBFF] transition-colors duration-300"
>
<div className="flex flex-col h-full p-7 md:p-8">
{/* Meta */}
<div className="flex items-center justify-between font-data text-[9px] uppercase tracking-[0.16em] text-text-muted mb-5">
<span className="text-brand-blue font-semibold">{post.category}</span>
<span>{post.date}</span>
</div>
{/* Headline */}
<h3 className="text-xl md:text-2xl font-serif text-[#0A0A0A] group-hover:text-brand-blue transition-colors duration-300 leading-tight mb-4">
{post.title}
</h3>
{/* Snippet */}
<p className="text-[13px] text-text-secondary leading-relaxed font-light mb-6 flex-grow">
{post.snippet}
</p>
{/* Blueprint */}
<div className="mb-6">
<BlueprintIllustration type={post.imageType} />
</div>
{/* Author + Read link */}
<div className="flex items-center justify-between pt-5 border-t border-border-light">
<div className="flex items-center gap-2.5">
<div className="relative w-7 h-7 rounded-full overflow-hidden border border-border-gray">
<img
src={post.author.avatarUrl}
alt={post.author.name}
className="object-cover w-full h-full"
referrerPolicy="no-referrer"
/>
</div>
<div className="flex flex-col">
<span className="text-[11px] font-semibold text-[#0A0A0A]">{post.author.name}</span>
<span className="text-[8px] font-data text-text-muted uppercase tracking-wider">
{post.readTime}
</span>
</div>
</div>
<ArrowUpRight className="w-4 h-4 text-text-muted group-hover:text-brand-blue group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all duration-300" />
</div>
</div>
</motion.article>
))}
</AnimatePresence>
</div>
{/* Empty State */}
{displayPosts.length === 0 && (
<div className="py-24 text-center border border-dashed border-border-gray rounded-2xl flex flex-col items-center justify-center gap-4 bg-[#FCFCFD] mt-4">
<Newspaper className="w-8 h-8 text-text-muted/40" />
<span className="font-display font-medium text-text-secondary">No matching articles found</span>
<button
onClick={() => setSelectedCategory('All')}
className="text-xs font-data uppercase tracking-wider text-brand-blue hover:underline cursor-pointer"
>
Show all articles
</button>
</div>
)}
</section>
{/* ─── NEWSLETTER CTA ─── */}
<section className="border-t border-border-gray">
<div className="max-w-[1200px] mx-auto px-6 md:px-12 lg:px-16 py-20 md:py-28">
<div className="max-w-[600px] mx-auto text-center">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-border-gray bg-white mb-6">
<Sparkles className="w-3 h-3 text-brand-pink" />
<span className="font-data text-[10px] uppercase tracking-[0.16em] text-text-muted font-medium">
Stay in the loop
</span>
</div>
<h2 className="font-serif text-[32px] md:text-[42px] leading-[1.08] tracking-tight text-[#0A0A0A] mb-4">
Engineering updates,{' '}
<span className="italic text-transparent bg-clip-text bg-gradient-to-r from-brand-blue to-brand-pink">
delivered.
</span>
</h2>
<p className="text-base text-text-secondary font-light leading-relaxed mb-8 max-w-[440px] mx-auto">
Get new posts on retrieval architectures, product releases, and the future of document AI straight to
your inbox.
</p>
<div className="flex flex-col sm:flex-row items-stretch gap-3 max-w-[460px] mx-auto">
<input
type="email"
placeholder="you@company.com"
className="flex-1 px-4 py-3 border border-border-gray rounded-full text-sm bg-[#FCFCFD] focus:outline-none focus:border-brand-blue focus:ring-1 focus:ring-brand-blue/20 transition-all placeholder:text-text-muted/60"
/>
<button className="bg-[#0A0A0A] text-white px-6 py-3 rounded-full text-sm font-medium hover:bg-brand-blue transition-colors duration-300 cursor-pointer whitespace-nowrap">
Subscribe
</button>
</div>
<p className="mt-4 text-[11px] text-text-muted font-data">
No spam. Unsubscribe anytime.
</p>
</div>
)}
{series && (
<div className="eyebrow mb-8">
{series} · {posts.filter((p) => p.series === series).length} parts
</div>
)}
🤖 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 `@apps/blogs/app/page.tsx` around lines 90 - 94, Update the series label
rendering in the page component so its part count filters posts to those whose
series matches the current series before counting. Keep the existing conditional
rendering and label format unchanged, using the posts collection rather than the
total posts.length.

Comment thread apps/blogs/app/page.tsx
Comment on lines +127 to +129
<span className="stat text-[15px] text-text-muted md:w-8">
{String(post.part ?? "").padStart(2, "0")}
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A post without part renders as 00.

String(undefined ?? "") is "", and "".padStart(2, "0") is "00". The index then shows 00 as if the post were part zero. Render an empty cell instead.

♻️ Proposed fix
                 <span className="stat text-[15px] text-text-muted md:w-8">
-                  {String(post.part ?? "").padStart(2, "0")}
+                  {post.part != null ? String(post.part).padStart(2, "0") : ""}
                 </span>
📝 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.

Suggested change
<span className="stat text-[15px] text-text-muted md:w-8">
{String(post.part ?? "").padStart(2, "0")}
</span>
<span className="stat text-[15px] text-text-muted md:w-8">
{post.part != null ? String(post.part).padStart(2, "0") : ""}
</span>
🤖 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 `@apps/blogs/app/page.tsx` around lines 127 - 129, Update the post index
rendering in the visible span to return an empty cell when post.part is absent,
rather than applying padStart to an empty string; preserve the two-digit
formatting for posts with a defined part.

Comment on lines +26 to +36
const TRUE_WIDTHS = [
{ g: "i", w: 222 },
{ g: "m", w: 833 },
{ g: "W", w: 944 },
{ g: "i", w: 222 },
];

export function DriftDiagram() {
const scale = 0.28; // points-per-thousandth, chosen so the row fits
const trueTotal = TRUE_WIDTHS.reduce((a, b) => a + b.w, 0);
const guessTotal = TRUE_WIDTHS.length * 500;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the four-glyph drift measurement.

The defined widths sum to 2,221 units. Four flat guesses sum to 2,000 units. The error is 0.221em, which is 2.652pt at 12pt. The displayed ~0.3pt off has no matching font size and conflicts with these inputs. Show 0.221em or name the font size used for a point value.

Also applies to: 81-89

🤖 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 `@apps/blogs/components/Diagrams.tsx` around lines 26 - 36, Update
DriftDiagram’s drift measurement display to match the defined TRUE_WIDTHS and
guessTotal values: report the error as 0.221em, or explicitly calculate and
label the equivalent point value using a stated 12pt font size. Remove the
inconsistent “~0.3pt off” wording while preserving the existing drift
calculation.

Comment on lines +33 to +78
export function DriftDiagram() {
const scale = 0.28; // points-per-thousandth, chosen so the row fits
const trueTotal = TRUE_WIDTHS.reduce((a, b) => a + b.w, 0);
const guessTotal = TRUE_WIDTHS.length * 500;

return (
<Figure caption="Widths are stored in thousandths of an em. Guessing 500 for every glyph is not a small error on each one — it is a compounding error along the line.">
<div className="p-7">
<Eyebrow tone="blue">True AFM widths</Eyebrow>
<div className="mt-3 flex items-end">
{TRUE_WIDTHS.map((c, i) => (
<div key={i} style={{ width: c.w * scale }} className="shrink-0">
<div className="flex h-11 items-center justify-center rounded-sm border border-brand-blue/40 bg-brand-blue/[0.08] text-[17px] text-text-base">
{c.g}
</div>
<div className="stat mt-1.5 text-center text-[11px] text-text-muted">{c.w}</div>
</div>
))}
<div className="ml-3 self-center text-[12px] text-brand-blue">word ends here</div>
</div>

<div className="mt-8">
<Eyebrow tone="pink">Flat 500 guess — what pdftable did</Eyebrow>
<div className="mt-3 flex items-end">
{TRUE_WIDTHS.map((c, i) => (
<div key={i} style={{ width: 500 * scale }} className="shrink-0">
<div className="flex h-11 items-center justify-center rounded-sm border border-dashed border-brand-pink/50 bg-brand-pink/[0.06] text-[17px] text-text-base">
{c.g}
</div>
<div className="stat mt-1.5 text-center text-[11px] text-brand-pink">500</div>
</div>
))}
</div>
<div
className="relative mt-2 h-5"
style={{ width: Math.max(trueTotal, guessTotal) * scale }}
>
<div
className="absolute top-2 h-px bg-brand-pink"
style={{
left: Math.min(trueTotal, guessTotal) * scale,
width: Math.abs(trueTotal - guessTotal) * scale,
}}
/>
</div>
<div className="text-[12px] text-brand-pink">error after only four glyphs</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make DriftDiagram usable at narrow widths.

TRUE_WIDTHS produces a fixed 622px row before “word ends here” and padding. The flex row does not wrap. Figure at apps/blogs/components/Figure.tsx Line 25 clips overflow, so final glyphs and the error marker are unreachable on narrow viewports. Put the fixed-width comparison in an overflow-x-auto container, or use a responsive scale.

🤖 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 `@apps/blogs/components/Diagrams.tsx` around lines 33 - 78, The fixed-width
comparison rows in DriftDiagram are clipped on narrow viewports because Figure
hides overflow. Wrap the comparison content, including the “word ends here”
label and error marker, in an overflow-x-auto container so all glyphs remain
horizontally reachable without changing the diagram’s scale.


---

*Full evaluation reports, with commits, oracles and caveats, are in [`docs/evaluations/`](../evaluations/). Benchmarks that download their own datasets are in [`bench/`](../../bench/).*

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# List App Router pages. Verify routes for /evaluations and /bench are present.
fd -HI -t f '^page\.(ts|tsx|js|jsx)$' apps/blogs/app | sort

# Locate the source directories named by the prose.
fd -HI -t d . | rg '/(docs/evaluations|bench)$' || true

# Find site-local links or redirects that publish either path.
rg -n -C 3 -g '*.ts' -g '*.tsx' -g '*.md' '(?:/|\.{1,2}/)(evaluations|bench)/?' apps/blogs || true

Repository: hallelx2/vectorless

Length of output: 787


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Tracked paths near apps/blogs/app:\n'
git ls-files apps/blogs/app | sort

printf '\nTracked paths named evaluations or bench:\n'
git ls-files | rg '(^|/)evaluations($|/)|(^|/)bench($|/)' || true

printf '\nAll markdown/TS/TSX references to evaluations or bench:\n'
rg -n -C 2 -g '*.md' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' 'evaluations|bench|docs/evaluations' . || true

printf '\nCandidate app redirect/server config files:\n'
git ls-files apps/blogs app . | rg '(^|/)(middleware|redirects)\.(ts|tsx|js|jsx)$|(^|/)(layout|page)\.(ts|tsx|js|jsx)$|(^|/)next\.config\.(js|mjs|cjs|ts)$' || true

Repository: hallelx2/vectorless

Length of output: 6434


Replace these unresolvable relative links with canonical public URLs.

From the post slug route, ../evaluations/ resolves to /evaluations/ and ../../bench/ resolves to /bench/. These routes are not published in the blog app, so remove the links or replace them with working canonical URLs.

🤖 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 `@apps/blogs/content/posts/07-what-is-still-missing.md` at line 102, Update the
links in the post’s evaluation note so they no longer point to unpublished
relative routes; remove those links or replace them with working canonical
public URLs for the evaluations and benchmarks resources.

Comment on lines +62 to +111
const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(\[[^\]]+\]\([^)]+\))/g;
let last = 0;
let m: RegExpExecArray | null;
let i = 0;

while ((m = pattern.exec(text))) {
if (m.index > last) out.push(text.slice(last, m.index));
const token = m[0];
const key = `${keyPrefix}-i${i++}`;

if (token.startsWith("`")) {
out.push(
<code
key={key}
className="rounded bg-[color:var(--color-border-light)] px-1.5 py-0.5 font-data text-[0.85em] text-text-base"
>
{token.slice(1, -1)}
</code>,
);
} else if (token.startsWith("**")) {
out.push(
<strong key={key} className="font-medium text-text-base">
{token.slice(2, -2)}
</strong>,
);
} else if (token.startsWith("[")) {
const link = /\[([^\]]+)\]\(([^)]+)\)/.exec(token)!;
const href = safeUrl(link[2]);
const external = /^https?:/i.test(href);
out.push(
<a
key={key}
href={href}
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
className="text-brand-blue underline decoration-brand-blue/25 underline-offset-[3px] transition-colors hover:decoration-brand-blue"
>
{link[1]}
</a>,
);
} else {
out.push(
<em key={key} className="italic">
{token.slice(1, -1)}
</em>,
);
}
last = m.index + token.length;
}
if (last < text.length) out.push(text.slice(last));
return out;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Parse child inline syntax inside emphasis and link labels.

apps/blogs/content/posts/07-what-is-still-missing.md Line 102 starts and ends with *. The italic token consumes the full sentence. The branch then emits the nested links and code spans as text, so the document references appear literally and are not clickable. Recursively call inline for emphasis, strong text, and link labels. Keep code spans as text.

Proposed fix
-          {token.slice(2, -2)}
+          {inline(token.slice(2, -2), `${key}-strong`)}
...
-          {link[1]}
+          {inline(link[1], `${key}-link`)}
...
-          {token.slice(1, -1)}
+          {inline(token.slice(1, -1), `${key}-em`)}
📝 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.

Suggested change
const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(\[[^\]]+\]\([^)]+\))/g;
let last = 0;
let m: RegExpExecArray | null;
let i = 0;
while ((m = pattern.exec(text))) {
if (m.index > last) out.push(text.slice(last, m.index));
const token = m[0];
const key = `${keyPrefix}-i${i++}`;
if (token.startsWith("`")) {
out.push(
<code
key={key}
className="rounded bg-[color:var(--color-border-light)] px-1.5 py-0.5 font-data text-[0.85em] text-text-base"
>
{token.slice(1, -1)}
</code>,
);
} else if (token.startsWith("**")) {
out.push(
<strong key={key} className="font-medium text-text-base">
{token.slice(2, -2)}
</strong>,
);
} else if (token.startsWith("[")) {
const link = /\[([^\]]+)\]\(([^)]+)\)/.exec(token)!;
const href = safeUrl(link[2]);
const external = /^https?:/i.test(href);
out.push(
<a
key={key}
href={href}
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
className="text-brand-blue underline decoration-brand-blue/25 underline-offset-[3px] transition-colors hover:decoration-brand-blue"
>
{link[1]}
</a>,
);
} else {
out.push(
<em key={key} className="italic">
{token.slice(1, -1)}
</em>,
);
}
last = m.index + token.length;
}
if (last < text.length) out.push(text.slice(last));
return out;
const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(\[[^\]]+\]\([^)]+\))/g;
let last = 0;
let m: RegExpExecArray | null;
let i = 0;
while ((m = pattern.exec(text))) {
if (m.index > last) out.push(text.slice(last, m.index));
const token = m[0];
const key = `${keyPrefix}-i${i++}`;
if (token.startsWith("`")) {
out.push(
<code
key={key}
className="rounded bg-[color:var(--color-border-light)] px-1.5 py-0.5 font-data text-[0.85em] text-text-base"
>
{token.slice(1, -1)}
</code>,
);
} else if (token.startsWith("**")) {
out.push(
<strong key={key} className="font-medium text-text-base">
{inline(token.slice(2, -2), `${key}-strong`)}
</strong>,
);
} else if (token.startsWith("[")) {
const link = /\[([^\]]+)\]\(([^)]+)\)/.exec(token)!;
const href = safeUrl(link[2]);
const external = /^https?:/i.test(href);
out.push(
<a
key={key}
href={href}
{...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
className="text-brand-blue underline decoration-brand-blue/25 underline-offset-[3px] transition-colors hover:decoration-brand-blue"
>
{inline(link[1], `${key}-link`)}
</a>,
);
} else {
out.push(
<em key={key} className="italic">
{inline(token.slice(1, -1), `${key}-em`)}
</em>,
);
}
last = m.index + token.length;
}
if (last < text.length) out.push(text.slice(last));
return out;
🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 67-67: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 88-88: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 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 `@apps/blogs/lib/markdown.tsx` around lines 62 - 111, Update the token
rendering branches in inline so emphasis, strong, and link label content are
passed back through inline using the existing keyPrefix mechanism, enabling
nested links and code spans; keep code span contents rendered as plain text.
Apply the recursive parsing to the label inside the link branch while preserving
safeUrl and external-link handling.

Comment thread apps/blogs/lib/markdown.tsx
Comment thread apps/blogs/lib/posts.ts
Comment on lines +96 to +98
part: data.part as number | undefined,
series: data.series as string | undefined,
tags: (data.tags as string[]) ?? [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unvalidated frontmatter casts let tags become a string, which breaks post rendering. toPost casts parsed frontmatter values without checking their runtime type. parseFrontmatter returns an array only when the value uses [...] syntax, so tags: go yields the string "go". The ?? fallback does not replace a non-nullish wrong type, so the string reaches the post route and post.tags.join(", ") throws a TypeError during static generation. The same gap lets part become a string.

  • apps/blogs/lib/posts.ts#L96-L98: normalize each field before returning the post. Coerce tags with Array.isArray(data.tags) ? data.tags : typeof data.tags === "string" && data.tags ? [data.tags] : [], and accept part only when typeof data.part === "number".
  • apps/blogs/app/posts/[slug]/page.tsx#L64-L67: no change is needed once Post.tags is guaranteed to be an array; keep the existing post.tags.length guard and join.
📍 Affects 2 files
  • apps/blogs/lib/posts.ts#L96-L98 (this comment)
  • apps/blogs/app/posts/[slug]/page.tsx#L64-L67
🤖 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 `@apps/blogs/lib/posts.ts` around lines 96 - 98, Update toPost in
apps/blogs/lib/posts.ts at lines 96-98 to validate frontmatter runtime types:
normalize tags to an array using the specified array/string/non-empty fallback
behavior, and set part only when data.part is a number. No direct change is
needed in apps/blogs/app/posts/[slug]/page.tsx lines 64-67; its existing tags
length guard and join are correct once Post.tags is guaranteed to be an array.

Comment thread apps/blogs/lib/posts.ts
Comment on lines +124 to +130
/** Neighbours within a series, for prev/next links at the foot of a post. */
export function getSeriesNeighbours(slug: string): { prev?: Post; next?: Post } {
const posts = getAllPosts();
const i = posts.findIndex((p) => p.slug === slug);
if (i === -1) return {};
return { prev: posts[i - 1], next: posts[i + 1] };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

getSeriesNeighbours does not scope to a series, and reverses prev/next outside series mode.

getAllPosts sorts ascending by part only when every post has a part. If one non-series post is added, the sort falls back to publishedAt descending. Then posts[i - 1] is the newer post, so the page labels it "Previous", and neighbours can come from a different series.

Filter by post.series and order by part inside this function, so the result does not depend on the global sort mode.

♻️ Proposed fix
 export function getSeriesNeighbours(slug: string): { prev?: Post; next?: Post } {
-  const posts = getAllPosts();
-  const i = posts.findIndex((p) => p.slug === slug);
-  if (i === -1) return {};
-  return { prev: posts[i - 1], next: posts[i + 1] };
+  const all = getAllPosts();
+  const current = all.find((p) => p.slug === slug);
+  if (!current) return {};
+
+  const siblings = current.series
+    ? all
+        .filter((p) => p.series === current.series && p.part != null)
+        .sort((a, b) => (a.part ?? 0) - (b.part ?? 0))
+    : [...all].sort((a, b) => a.publishedAt.localeCompare(b.publishedAt));
+
+  const i = siblings.findIndex((p) => p.slug === slug);
+  if (i === -1) return {};
+  return { prev: siblings[i - 1], next: siblings[i + 1] };
 }
📝 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.

Suggested change
/** Neighbours within a series, for prev/next links at the foot of a post. */
export function getSeriesNeighbours(slug: string): { prev?: Post; next?: Post } {
const posts = getAllPosts();
const i = posts.findIndex((p) => p.slug === slug);
if (i === -1) return {};
return { prev: posts[i - 1], next: posts[i + 1] };
}
/** Neighbours within a series, for prev/next links at the foot of a post. */
export function getSeriesNeighbours(slug: string): { prev?: Post; next?: Post } {
const all = getAllPosts();
const current = all.find((p) => p.slug === slug);
if (!current) return {};
const siblings = current.series
? all
.filter((p) => p.series === current.series && p.part != null)
.sort((a, b) => (a.part ?? 0) - (b.part ?? 0))
: [...all].sort((a, b) => a.publishedAt.localeCompare(b.publishedAt));
const i = siblings.findIndex((p) => p.slug === slug);
if (i === -1) return {};
return { prev: siblings[i - 1], next: siblings[i + 1] };
}
🤖 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 `@apps/blogs/lib/posts.ts` around lines 124 - 130, Update getSeriesNeighbours
to filter getAllPosts() to posts sharing the target post’s series, then order
that filtered collection by part before selecting neighbours. Preserve the empty
result for an unknown slug and ensure prev/next are derived from the
series-local ordering rather than the global getAllPosts sort.

@hallelx2
hallelx2 merged commit 34f2b90 into main Aug 4, 2026
8 checks passed
@hallelx2
hallelx2 deleted the halleluyaholudele/blogs-redesign-markdown branch August 4, 2026 20:57
hallelx2 added a commit that referenced this pull request Aug 4, 2026
apps/blogs/tsconfig.tsbuildinfo was committed by mistake in #17 -- a
`git add apps/blogs` swept it in with the redesign.

It is TypeScript incremental build state: machine-local, rewritten on
every compile, and a source of pointless diffs and merge conflicts for
anyone else building the app.

apps/web had the same file already tracked, from before. Leaving it while
adding *.tsbuildinfo to .gitignore would be incoherent, so both go.

Adding the ignore pattern as well as removing the files, because
.gitignore covered node_modules/ and .next/ but not this -- which is why
nothing stopped it landing. Untracked with `update-index --force-remove`
so the files stay on disk and nobody loses their incremental cache.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant