Skip to content

refactor(blog): split AEO/GEO content into a new /library section#5516

Merged
waleedlatif1 merged 5 commits into
stagingfrom
blog-library-split
Jul 8, 2026
Merged

refactor(blog): split AEO/GEO content into a new /library section#5516
waleedlatif1 merged 5 commits into
stagingfrom
blog-library-split

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Blog was mixing hand-written editorial posts with AEO/GEO-style content (listicles, comparisons, how-tos) in one undifferentiated pipeline
  • Extracted a generic shared content engine (lib/content: registry factory, MDX components, SEO builders) instantiated separately for lib/blog and lib/library
  • Moved the 6 AEO/GEO posts (best-zapier-alternatives, ai-agents-vs-rpa, ai-agent-vs-chatbot, openai-vs-n8n-vs-sim, ai-agent-ideas, how-to-create-an-ai-agent) into a new content/library/ tree
  • Built /library as an exact route-tree mirror of /blog (index, [slug], tags, authors, rss.xml, sitemap-images.xml), rendered through shared Content*Page components so both sections stay byte-identical in layout
  • Updated app/sitemap.ts, app/robots.ts, and the navbar Resources menu for the new section
  • Added permanent redirects from the 6 moved /blog/<slug> URLs to /library/<slug> to preserve existing SEO/indexing

Type of Change

  • Refactor

Testing

  • bun run type-check clean (only pre-existing unrelated @aws-sdk/client-textract errors)
  • bunx biome check . clean across full repo
  • Production build compiles and typechecks clean (build fails past that point only on a local env var, NEXT_PUBLIC_APP_URL, unrelated to this change)
  • Grepped for and confirmed zero remaining references to moved slugs under /blog/, zero dangling imports to deleted lib/blog files

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Blog was mixing hand-written editorial posts with listicle/comparison/how-to
posts optimized for answer engines. Extract a generic content engine
(lib/content) shared by both sections, move the 6 AEO/GEO posts into a new
/library route tree, and keep /blog editorial-only.

- lib/content: generic registry factory, MDX components, SEO builders
- lib/blog + lib/library: thin per-section instantiations over the shared engine
- app/(landing)/library: mirrors the blog route tree via shared Content*Page components
- app/sitemap.ts, app/robots.ts, navbar: updated for the new section
- next.config.ts: permanent redirects from moved /blog/<slug> URLs to /library/<slug>
@vercel

vercel Bot commented Jul 8, 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)
docs Skipped Skipped Jul 8, 2026 8:37pm

Request Review

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large surface area (URLs, canonicals, sitemaps, and shared index pagination) affects SEO and crawl behavior; behavior should stay equivalent for blog aside from the featured-post pagination fix.

Overview
Introduces /library as a second marketing content section (AEO/GEO listicles, comparisons, how-tos) alongside editorial /blog, both driven by the same stack: lib/content (registry factory, shared schemas/SEO/MDX) plus thin lib/blog / lib/library wrappers.

Blog routes are slimmed down to data + metadata; layout, loading skeletons, and SEO wiring move into reusable Content*Page components (ContentIndexPage, ContentPostPage, author/tags variants). ShareButton, Lightbox, and ContentImage are lifted to shared landing components for MDX in both sections.

Six posts live under content/library/ with /library/... canonicals and asset paths; author avatars move to /authors/. ContentIndexPage changes featured handling so featured items are excluded from the paginated list (no duplicates on later pages).

Discovery updates: Library in the Resources nav, app/sitemap.ts (library index, posts, authors), robots.ts (library image sitemap), library RSS and sitemap-images routes. MDX prose tokens shift from legacy --landing-* to platform light-mode tokens.

Reviewed by Cursor Bugbot for commit e32db6f. Configure here.

Comment thread apps/sim/lib/content/seo.ts
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors the blog content pipeline by extracting a generic shared lib/content engine (registry factory, MDX components, SEO builders, schema) and using it to power two independent content sections: the existing /blog (editorial posts) and a new /library (AEO/GEO-style content). The 6 AEO/GEO posts are moved to content/library/, permanent redirects preserve their old /blog/<slug> URLs, and the new /library route tree mirrors /blog exactly via shared Content*Page components.

  • Shared content engine: lib/content provides createContentRegistry, the unified frontmatter schema, MDX components, and parameterized SEO builders — both sections instantiate the same factory with their own contentDir.
  • Library route tree: /library, /library/[slug], /library/tags, /library/authors/[id], /library/rss.xml, and /library/sitemap-images.xml all mirror the blog routes and re-use the same shared Content*Page components.
  • Sitemap and robots updated: app/sitemap.ts now includes library pages and per-section author entries; app/robots.ts registers the new library/sitemap-images.xml.

Confidence Score: 5/5

Safe to merge — this is a structural refactor with no runtime logic changes; the shared component architecture is consistent, canonical URLs are correctly updated, and SEO redirects are in place.

The shared registry factory, SEO builders, and Content*Page components are well-parameterized. MDX frontmatter canonical URLs were updated to /library/ paths, the 6 permanent redirects preserve existing indexed URLs, and sitemap.ts/robots.ts are correctly extended. The only noteworthy design wrinkle is that invalidateCaches() clears the module-level author cache shared between both registries, but the function has no active call site in the codebase, so this has no current effect.

apps/sim/lib/content/registry-factory.ts — the invalidateCaches implementation has a subtle cross-registry side effect worth reviewing if cache invalidation is ever wired up.

Important Files Changed

Filename Overview
apps/sim/lib/content/registry-factory.ts Core content registry factory; authorsCacheByDir is a module-level map shared across registries, but invalidateCaches() deletes the shared key, inadvertently clearing the other registry's author cache too.
apps/sim/lib/content/seo.ts Parameterized SEO builders (metadata, JSON-LD, breadcrumbs) abstracted from the old blog-specific layer; clean and section-agnostic.
apps/sim/app/sitemap.ts Adds library posts and library author pages to the sitemap; introduces latestModified helper and a buildAuthorPages utility; logic is correct.
apps/sim/next.config.ts Adds permanent 301 redirects for all 6 moved slugs from /blog/* to /library/*; straightforward and correct.
apps/sim/app/(landing)/library/[slug]/page.tsx Library post detail page; mirrors blog slug page exactly, using shared ContentPostPage component with library-specific section constants.
apps/sim/app/(landing)/library/rss.xml/route.ts Library RSS feed; uses latestModified for lastBuildDate (fix applied from prior review thread), correct XML structure.
apps/sim/app/(landing)/components/content-index-page/content-index-page.tsx Shared index/list layout for both blog and library; featured posts, pagination, and filtering are all parameterized by basePath; well-structured.
apps/sim/app/(landing)/library/authors/[id]/page.tsx Library author page; mirrors blog author page exactly, filtering posts by p.author.id (primary author only) — same as the pre-existing blog behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Factory[createContentRegistry] -->|blog instance| BR[blogRegistry]
    Factory -->|library instance| LR[libraryRegistry]

    BR -->|shared authorsDir| AC[(authorsCacheByDir module-level)]
    LR -->|shared authorsDir| AC

    BR --> BM[cachedMeta - blog]
    LR --> LM[cachedMeta - library]

    BR --> BlogIndex[blog index and slug pages]
    BR --> BlogRSS[blog rss.xml]
    LR --> LibIndex[library index and slug pages]
    LR --> LibRSS[library rss.xml]
    LR --> LibSitemap[library sitemap-images.xml]

    BR --> Sitemap[sitemap.xml]
    LR --> Sitemap

    OldBlog[old blog slug URLs] -->|301 permanent redirect| LibSlug[library slug pages]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    Factory[createContentRegistry] -->|blog instance| BR[blogRegistry]
    Factory -->|library instance| LR[libraryRegistry]

    BR -->|shared authorsDir| AC[(authorsCacheByDir module-level)]
    LR -->|shared authorsDir| AC

    BR --> BM[cachedMeta - blog]
    LR --> LM[cachedMeta - library]

    BR --> BlogIndex[blog index and slug pages]
    BR --> BlogRSS[blog rss.xml]
    LR --> LibIndex[library index and slug pages]
    LR --> LibRSS[library rss.xml]
    LR --> LibSitemap[library sitemap-images.xml]

    BR --> Sitemap[sitemap.xml]
    LR --> Sitemap

    OldBlog[old blog slug URLs] -->|301 permanent redirect| LibSlug[library slug pages]
Loading

Reviews (4): Last reviewed commit: "fix(blog): RSS lastBuildDate reflects th..." | Re-trigger Greptile

Comment thread apps/sim/app/(landing)/library/rss.xml/route.ts Outdated
Comment thread apps/sim/lib/content/registry-factory.ts Outdated
Comment thread apps/sim/lib/library/registry.ts
Author pages for unknown/unmatched ids fell back to author?.id, which is
undefined when no post matches — collapsing canonical/OG URLs to
{basePath}/authors/ instead of {basePath}/authors/{id}. Pass the route id
explicitly so canonical always resolves correctly.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Fixed in ef7c0b1buildAuthorMetadata now takes the route id explicitly instead of falling back to author?.id, so canonical/OG URLs for author pages always resolve to {basePath}/authors/{id} even when no post matches that author.

… cache, dead-export docs

- RSS lastBuildDate now uses updated ?? date so edits after publication are
  reflected, matching the per-item pubDate semantics (blog + library)
- Author JSON is now cached once per authorsDir at module scope instead of
  once per registry instantiation, since blog and library point at the same
  directory
- Documented getNavPosts as reserved-but-unwired, matching the existing
  PLATFORM_MENU/SOLUTIONS_MENU convention in the navbar
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 91a7a3f. Configure here.

A featured post older than POSTS_PER_PAGE would appear both on page 1's
featured row (sorted to the front) and again on its natural date-sorted
page, since only page 1 excluded featured posts from the remaining list.
Carve featured posts out of the paginated pool up front so pagination stays
consistent and duplicate-free across all pages.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Addressed the pagination dedup bug Greptile flagged in its summary (confidence 4/5) — fixed in 43bc002. ContentIndexPage now carves featured posts out of the paginated pool up front instead of only excluding them from remaining on page 1, so a featured-but-older post can no longer appear on both the page-1 featured row and its natural date-sorted page.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/(landing)/blog/rss.xml/route.ts Outdated
…west item

lastBuildDate only looked at items[0]'s updated/date, so revising an older
post already in the feed wouldn't advance it. Extract the max-across-posts
logic sitemap.ts already had into a shared lib/content/utils helper
(latestModified) and use it in both RSS routes and the sitemap, so a
revision to any feed item is reflected.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e32db6f. Configure here.

@waleedlatif1 waleedlatif1 merged commit 004955d into staging Jul 8, 2026
18 checks passed
@waleedlatif1 waleedlatif1 deleted the blog-library-split branch July 8, 2026 20:55
waleedlatif1 added a commit that referenced this pull request Jul 9, 2026
…-*-page components

The blog/library split (#5516) moved the blog post/index/author JSX into
shared ContentPostPage/ContentIndexPage/ContentAuthorPage components while
this branch was in flight, so the original unoptimized removal (verified
local-only ogImage paths for both blog and library content) needs to land
on those shared components instead of the old per-route JSX.
waleedlatif1 added a commit that referenced this pull request Jul 9, 2026
… pages (#5522)

* fix(landing): fix Core Web Vitals regressions across public marketing pages

- root layout unconditionally rendered next-runtime-env's PublicEnvScript,
  which calls unstable_noStore() and silently forced every route in the app
  dynamic - marketing pages never got static/ISR caching despite their own
  revalidate. Gated it to self-hosted only; hosted now uses a static,
  build-time equivalent (app/_shell/public-env-script.tsx)
- removed real pointer-drag handlers from the hero's decorative workflow
  animation (was draggable despite being aria-hidden)
- disabled dragging/panning on the (currently unmounted) landing-preview
  ReactFlow canvas so it's static-by-default if it's ever wired in
- lazy-mount the Product Demo section's duplicate HeroVisual instance via
  next/dynamic + IntersectionObserver instead of loading it eagerly below
  the fold
- disabled Next.js Link prefetch on always-in-viewport /signup and /login
  CTAs (navbar, hero, mobile nav) so their JS isn't fetched on every
  pageview regardless of whether the visitor clicks
- removed `unoptimized` from local blog/integration images (including the
  priority LCP image on every blog post), letting next/image serve
  resized AVIF/WebP instead of full-size originals

* fix(landing): address review findings on the CWV PR

- revert prefetch={false} on below-fold CTAs (cta.tsx, enterprise.tsx) -
  contradicts the prefetch-on-approach rule this PR itself documents
- restore unoptimized on avatarUrl and the MDX body-image renderer, both of
  which can legitimately hold external URLs outside next.config.ts's
  image remotePatterns allow-list
- simplify handleAnchors to a single block argument now that positions are
  static (the second "live position" argument was always identical to the
  first after the drag-handler removal)
- extract the near-duplicate IntersectionObserver lazy-mount logic shared
  by landing-preview-mount.tsx and product-demo-visual-mount.tsx into a
  single apps/sim/app/(landing)/hooks/use-lazy-mount.ts hook
- import next-runtime-env's own exported PUBLIC_ENV_KEY constant instead of
  a hardcoded string literal, and match its case-insensitive NEXT_PUBLIC_
  filter exactly, removing any drift risk between the two implementations
- drop plain inline comments with no TSDoc home in favor of relying on the
  existing TSDoc/CLAUDE.md documentation

* fix(landing): carry the unoptimized-image fix onto the shared content-*-page components

The blog/library split (#5516) moved the blog post/index/author JSX into
shared ContentPostPage/ContentIndexPage/ContentAuthorPage components while
this branch was in flight, so the original unoptimized removal (verified
local-only ogImage paths for both blog and library content) needs to land
on those shared components instead of the old per-route JSX.

* fix(landing): document ogImage's local-path expectation, fix CLAUDE.md structure doc

- add a one-line comment on ContentFrontmatterSchema.ogImage documenting
  that it's rendered without unoptimized and expects a local path, matching
  the existing avatarUrl comment convention (a reviewer noted ogImage's
  schema is technically unconstrained and seo.ts has an http-prefix branch,
  though all current content is local)
- add the new hooks/ folder to the (landing) CLAUDE.md structure diagram and
  name use-lazy-mount.ts directly in the lazy-mount rule

* fix(landing): escape </script> breakout in the static public-env script

Greptile P1: a NEXT_PUBLIC_* value containing "</script>" would close the
inline script early and could inject markup/script into every hosted page.
Escape "<" in the serialized JSON before interpolating it, matching the
standard JSON-in-script-tag safeguard.
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