Skip to content

feat(landing): add canonical blog path#187

Merged
ian-pascoe merged 14 commits into
mainfrom
feat/landing-blog-path
Jul 1, 2026
Merged

feat(landing): add canonical blog path#187
ian-pascoe merged 14 commits into
mainfrom
feat/landing-blog-path

Conversation

@ian-pascoe

@ian-pascoe ian-pascoe commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a static, repo-owned /blog surface to the Astro landing app.
  • Add the first benchmark-backed launch essay at /blog/why-giant-mcp-tool-walls-dont-scale/.
  • Add canonical/social metadata, article presentation, CTAs, landing navigation links, and privacy-safe categorical blog observability.

Verification

  • pnpm --filter @caplets/landing test
  • pnpm --filter @caplets/landing typecheck
  • pnpm --filter @caplets/landing build
  • pnpm --filter @caplets/web-observability test
  • pnpm --filter @caplets/web-observability typecheck
  • pnpm format:check
  • pnpm lint
  • Pre-push pnpm verify completed successfully before pushing feat/landing-blog-path.

Notes

Plan: docs/plans/2026-07-01-003-feat-landing-blog-path-plan.md

Summary by CodeRabbit

  • New Features
    • Launched a full blog section with an index and individual post pages.
    • Added blog UI components (article layout with canonical link and a “Try” CTA card) plus “Blog” navigation in the header/footer.
    • Implemented canonical URL handling and expanded Open Graph/Twitter metadata for sharing.
  • Bug Fixes
    • Improved blog intent/telemetry categorization so blog navigation is consistently classified.
  • Tests
    • Added/extended automated tests for blog content validation, routing, SEO/social metadata, navigation markup, and analytics events.
  • Documentation
    • Added an implementation plan for the canonical /blog surface.
  • Style
    • Refreshed blog typography and spacing for long-form readability.

@ian-pascoe ian-pascoe added the no changeset No package changeset required label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 805c03e2-e087-4669-a3b9-c35a17c35c7a

📥 Commits

Reviewing files that changed from the base of the PR and between bf46a71 and 08f48cf.

📒 Files selected for processing (1)
  • apps/landing/src/content/blog/why-giant-mcp-tool-walls-dont-scale.md

📝 Walkthrough

Walkthrough

This PR adds a new /blog surface to the landing site with content-driven routes, shared article/layout components, navigation links, blog-aware observability, and supporting tests plus a planning document.

Changes

Landing Blog Feature

Layer / File(s) Summary
Content, URLs, and routes
apps/landing/src/content.config.ts, apps/landing/src/content/blog/why-giant-mcp-tool-walls-dont-scale.md, apps/landing/src/lib/blog.ts, apps/landing/src/pages/blog/index.astro, apps/landing/src/pages/blog/[slug].astro
Defines the blog collection, helpers, and static blog index/detail pages.
Article, CTA, layout, and nav
apps/landing/src/components/landing/BlogArticle.astro, apps/landing/src/components/landing/BlogCta.astro, apps/landing/src/styles/global.css, apps/landing/src/layouts/LandingLayout.astro, apps/landing/src/components/landing/Header.astro, apps/landing/src/components/landing/Footer.astro
Adds the blog article presentation, CTA block, SEO metadata, prose styling, and blog links in site navigation.
Observability classification
apps/landing/src/scripts/observability.ts, packages/web-observability/src/events.ts, packages/web-observability/src/privacy.ts
Adds blog route/category handling to landing telemetry and shared observability allowlists.
Tests and planning doc
AGENTS.md, apps/landing/test/blog-*.test.ts, apps/landing/test/observability.test.ts, packages/web-observability/test/web-observability.test.ts, docs/plans/2026-07-01-003-feat-landing-blog-path-plan.md
Adds coverage for content, routes, metadata, navigation, and observability, plus the implementation plan.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant BlogIndex as blog/index.astro
  participant BlogSlug as [slug].astro
  participant BlogLib as blog.ts
  participant Content as astro:content
  participant LandingLayout

  Browser->>BlogIndex: request /blog/
  BlogIndex->>BlogLib: getSortedBlogPosts()
  BlogLib->>Content: load blog collection
  Content-->>BlogLib: entries
  BlogLib-->>BlogIndex: sorted posts
  BlogIndex->>LandingLayout: render index with canonicalUrl
  Browser->>BlogSlug: request /blog/<slug>/
  BlogSlug->>BlogLib: getSortedBlogPosts()
  BlogSlug->>Content: render(entry)
  BlogSlug->>LandingLayout: render post with canonicalUrl
  LandingLayout-->>Browser: HTML
Loading

Poem

A hop, a skip, a brand-new page,
/blog/ is born, we set the stage. 🐇
Canonical links, tags aglow,
CTA cards in tidy row.
Bloggy clicks and careful signs,
this rabbit cheers the sorted lines!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is relevant and concise, though it undersells the broader new blog surface, content, and metadata work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/landing-blog-path

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.

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a repo-owned blog surface to the landing app. The main changes are:

  • New /blog index and static post pages.
  • Blog content collection, helpers, article UI, and CTA components.
  • Canonical, Open Graph, and Twitter metadata in the shared layout.
  • Header and footer links to the blog.
  • Blog-safe observability categories and route classification.
  • Tests for routing, metadata, navigation, content, and observability.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
apps/landing/src/scripts/observability.ts Classifies blog pageviews and click intents with source-page route and page categories.
apps/landing/src/pages/blog/[slug].astro Adds static blog post rendering with slug-derived canonical metadata.
apps/landing/src/pages/blog/index.astro Adds the blog index page with sorted public posts.
apps/landing/src/content.config.ts Defines the typed static blog content collection.
apps/landing/src/lib/blog.ts Adds shared blog URL, sorting, and collection helpers.
packages/web-observability/src/events.ts Adds blog route and event categories to the shared event contract.
packages/web-observability/src/privacy.ts Allows only categorical blog values through the web observability privacy filter.

Reviews (7): Last reviewed commit: "copy(landing): reframe MCP client moat" | Re-trigger Greptile

Comment thread apps/landing/src/scripts/observability.ts Outdated
Comment thread apps/landing/src/pages/blog/[slug].astro Outdated
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
apps/landing/src/content.config.ts (1)

12-12: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

canonicalPath is a manually duplicated source of truth.

The schema validates canonicalPath as a free-form string separate from the collection's own id/slug. Per the downstream [slug].astro route, the actual canonical <link> is derived independently via blogPostUrl(entry.slug), while BlogArticle renders entry.data.canonicalPath as the displayed "Canonical URL" text. Two independent sources for what should be the same value means a typo in frontmatter (or a future slug rename) can silently desync the displayed URL from the real canonical URL, which is an SEO-relevant correctness issue.

Consider deriving the displayed canonical path from the slug (e.g., pass blogPostUrl(entry.slug) into BlogArticle instead of a separate frontmatter field), or add a schema-level/test-level assertion that canonicalPath === blogPostUrl(id) for every entry.

🤖 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/landing/src/content.config.ts` at line 12, The canonicalPath field is
duplicating the slug-derived canonical URL and can drift out of sync. Update the
blog article flow so BlogArticle uses blogPostUrl(entry.slug) (or the equivalent
slug-derived value) instead of relying on entry.data.canonicalPath, and adjust
content.config.ts to remove the free-form source of truth or replace it with a
schema/test assertion that ties canonicalPath to the entry slug/id.
apps/landing/src/layouts/LandingLayout.astro (2)

40-42: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing twitter:image tag.

twitter:card is summary_large_image but no twitter:image is set. X falls back to og:image when absent, so this isn't broken, but explicitly setting it is the documented best practice for card redundancy — worth adding alongside the other twitter:* tags for consistency with this PR's social-metadata goal.

✏️ Suggested addition
     <meta name="twitter:card" content="summary_large_image" />
     <meta name="twitter:title" content={title} />
     <meta name="twitter:description" content={description} />
+    <meta name="twitter:image" content={ogImageUrl} />
🤖 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/landing/src/layouts/LandingLayout.astro` around lines 40 - 42, The
Twitter metadata block in LandingLayout should include an explicit twitter:image
because twitter:card is set to summary_large_image. Update the existing
twitter:* meta group in LandingLayout to add the image tag alongside
twitter:title and twitter:description, using the same image source already used
for the page’s social metadata.

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

Centralize the landing site URL

https://caplets.dev is duplicated in apps/landing/src/layouts/LandingLayout.astro and the blog pages. Move it to one shared source of truth, or set site in the landing Astro config and derive canonical/OG URLs from that instead.

🤖 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/landing/src/layouts/LandingLayout.astro` around lines 14 - 23, Move the
hardcoded landing origin out of LandingLayout.astro and the blog pages into one
shared source of truth, ideally the landing Astro config via the site setting.
Update LandingLayout and any blog URL builders to derive canonicalHref and
ogImageUrl from that shared site value instead of duplicating
"https://caplets.dev", using the existing canonicalUrl and ogImage props where
applicable.
apps/landing/src/pages/blog/index.astro (1)

18-18: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

featuredTitle duplicates the post's frontmatter title as a literal.

This hardcoded string will silently drift from the actual post title if the frontmatter (src/content/blog/why-giant-mcp-tool-walls-dont-scale.md) is ever updated, since nothing keeps them in sync. Deriving it from the already-fetched posts array avoids the duplication.

♻️ Suggested fix
-const featuredTitle = "Why Giant MCP Tool Walls Don’t Scale";
+const featuredTitle = posts[0]?.data.title ?? "our launch essay";
🤖 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/landing/src/pages/blog/index.astro` at line 18, The featured title in
the blog index is hardcoded and duplicates the frontmatter title, so it can
drift from the source post metadata. Update the logic in the blog page component
to derive featuredTitle from the already loaded posts array instead of using a
literal, using the relevant post entry by slug or position so it stays in sync
with the content source.
apps/landing/src/pages/blog/[slug].astro (1)

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

Duplicated collection-fetch/sort logic with pages/blog/index.astro.

The (await getCollection("blog")).map((entry) => ({ ...entry, slug: entry.id })) + sortBlogPostsNewestFirst(...) sequence is duplicated verbatim in pages/blog/index.astro (Lines 12-17). Consider extracting a shared getSortedBlogPosts() helper in lib/blog.ts so both routes share one implementation.

♻️ Suggested extraction (lib/blog.ts)
export async function getSortedBlogPosts() {
  const { getCollection } = await import("astro:content");
  return sortBlogPostsNewestFirst(
    (await getCollection("blog")).map((entry) => ({ ...entry, slug: entry.id })),
  );
}
🤖 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/landing/src/pages/blog/`[slug].astro around lines 11 - 23, The blog post
collection fetch and newest-first sort are duplicated between the slug page and
the blog index, so extract that shared logic into a helper such as
getSortedBlogPosts in lib/blog.ts. Update getStaticPaths in
pages/blog/[slug].astro and the index route to call the shared helper instead of
repeating the getCollection("blog") plus sortBlogPostsNewestFirst mapping
sequence.
🤖 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/landing/src/components/landing/Header.astro`:
- Around line 25-26: The new and modified nav links in Header.astro are missing
the focus-visible ring color class, causing inconsistent keyboard focus styling.
Update the affected anchor elements for the Blog and Remote links to match the
other site-header__section-link entries by restoring the
focus-visible:ring-outline/50 class alongside focus-visible:ring-3 and
focus-visible:outline-none, so the focus ring remains consistent across all nav
items.

In `@apps/landing/src/pages/blog/`[slug].astro:
- Around line 25-38: The blog post page is using two separate canonical path
sources: `canonicalUrl` is derived from `entry.slug`, while `BlogArticle`
receives `entry.data.canonicalPath` from frontmatter. Update the `[slug].astro`
page so `BlogArticle` uses the slug-derived path (or otherwise assert the two
values match at build time) to keep the canonical/OG URL consistent. Use the
existing `canonicalUrl`, `entry.slug`, and `BlogArticle` props as the
identifiers to locate the fix.

---

Nitpick comments:
In `@apps/landing/src/content.config.ts`:
- Line 12: The canonicalPath field is duplicating the slug-derived canonical URL
and can drift out of sync. Update the blog article flow so BlogArticle uses
blogPostUrl(entry.slug) (or the equivalent slug-derived value) instead of
relying on entry.data.canonicalPath, and adjust content.config.ts to remove the
free-form source of truth or replace it with a schema/test assertion that ties
canonicalPath to the entry slug/id.

In `@apps/landing/src/layouts/LandingLayout.astro`:
- Around line 40-42: The Twitter metadata block in LandingLayout should include
an explicit twitter:image because twitter:card is set to summary_large_image.
Update the existing twitter:* meta group in LandingLayout to add the image tag
alongside twitter:title and twitter:description, using the same image source
already used for the page’s social metadata.
- Around line 14-23: Move the hardcoded landing origin out of
LandingLayout.astro and the blog pages into one shared source of truth, ideally
the landing Astro config via the site setting. Update LandingLayout and any blog
URL builders to derive canonicalHref and ogImageUrl from that shared site value
instead of duplicating "https://caplets.dev", using the existing canonicalUrl
and ogImage props where applicable.

In `@apps/landing/src/pages/blog/`[slug].astro:
- Around line 11-23: The blog post collection fetch and newest-first sort are
duplicated between the slug page and the blog index, so extract that shared
logic into a helper such as getSortedBlogPosts in lib/blog.ts. Update
getStaticPaths in pages/blog/[slug].astro and the index route to call the shared
helper instead of repeating the getCollection("blog") plus
sortBlogPostsNewestFirst mapping sequence.

In `@apps/landing/src/pages/blog/index.astro`:
- Line 18: The featured title in the blog index is hardcoded and duplicates the
frontmatter title, so it can drift from the source post metadata. Update the
logic in the blog page component to derive featuredTitle from the already loaded
posts array instead of using a literal, using the relevant post entry by slug or
position so it stays in sync with the content source.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 56eb253d-9369-4c51-b50f-eaed2638c0ae

📥 Commits

Reviewing files that changed from the base of the PR and between 30f6152 and e6fa035.

📒 Files selected for processing (21)
  • apps/landing/src/components/landing/BlogArticle.astro
  • apps/landing/src/components/landing/BlogCta.astro
  • apps/landing/src/components/landing/Footer.astro
  • apps/landing/src/components/landing/Header.astro
  • apps/landing/src/content.config.ts
  • apps/landing/src/content/blog/why-giant-mcp-tool-walls-dont-scale.md
  • apps/landing/src/layouts/LandingLayout.astro
  • apps/landing/src/lib/blog.ts
  • apps/landing/src/pages/blog/[slug].astro
  • apps/landing/src/pages/blog/index.astro
  • apps/landing/src/scripts/observability.ts
  • apps/landing/src/styles/global.css
  • apps/landing/test/blog-content.test.ts
  • apps/landing/test/blog-metadata.test.ts
  • apps/landing/test/blog-navigation.test.ts
  • apps/landing/test/blog-routes.test.ts
  • apps/landing/test/observability.test.ts
  • docs/plans/2026-07-01-003-feat-landing-blog-path-plan.md
  • packages/web-observability/src/events.ts
  • packages/web-observability/src/privacy.ts
  • packages/web-observability/test/web-observability.test.ts

Comment thread apps/landing/src/components/landing/Header.astro Outdated
Comment thread apps/landing/src/pages/blog/[slug].astro Outdated
@ian-pascoe ian-pascoe merged commit 2f16e71 into main Jul 1, 2026
7 checks passed
@ian-pascoe ian-pascoe deleted the feat/landing-blog-path branch July 1, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changeset No package changeset required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant