Skip to content

feat(website): export blog markdown for agents#1934

Open
rohitg00 wants to merge 7 commits into
mainfrom
docs/blog-md-llms-index
Open

feat(website): export blog markdown for agents#1934
rohitg00 wants to merge 7 commits into
mainfrom
docs/blog-md-llms-index

Conversation

@rohitg00

@rohitg00 rohitg00 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Export each published blog post to agent-readable markdown at https://iii.dev/blog/<slug>.md, plus https://iii.dev/blog/index.md.
  • Image paths in exported markdown are rewritten to the built /blog/_astro/ URLs.
  • Index all posts in generated llms.txt and AGENTS.md (deploy-time), repo AGENTS.md, sitemap, and agents-appendix.md.

Test plan

  • pnpm --filter iii-website test — 67/67 pass (includes redirect + blog-md export tests)
  • pnpm --filter iii-blog build + tsx scripts/generate-blog-md.ts — 7 posts + index.md exported
  • pnpm --filter iii-website build — llms.txt / sitemap include blog .md URLs and /privacy

Summary by CodeRabbit

  • New Features
    • Blog posts are now exported as agent-friendly markdown pages, including a navigable blog/index.md.
    • Agent resources now include a “Blog (knowledge base for coding agents)” section with direct links to the blog index and individual posts.
  • Bug Fixes
    • Improved blog deployment caching so markdown/XML/HTML are revalidated appropriately.
    • Updated sitemap entries to use /blog/<slug>.md URLs (and adjusted priorities).
  • Documentation
    • Refreshed blog deployment guidance and added blog index references to agent documentation.
  • Tests
    • Expanded coverage for markdown export behavior, generated link formatting, and agent resource inclusion.

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
iii-website Ready Ready Preview, Comment Jul 14, 2026 10:57am
tech-spec Error Error Jul 14, 2026 10:57am

Request Review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Terraform plan — infra/terraform/website

Click to expand
aws_cloudfront_origin_access_control.site: Refreshing state... [id=E3GRB5YVVA4332]
aws_iam_openid_connect_provider.github: Refreshing state... [id=arn:aws:iam::600627348446:oidc-provider/token.actions.githubusercontent.com]
aws_cloudfront_response_headers_policy.site: Refreshing state... [id=033b1d66-8d53-48a1-a817-8280d8a60114]
data.aws_route53_zone.iii_dev: Reading...
aws_acm_certificate.site: Refreshing state... [id=arn:aws:acm:us-east-1:600627348446:certificate/b8e26c06-08b6-4b90-bd59-1f83bc231db2]
aws_sns_topic.alarms: Refreshing state... [id=arn:aws:sns:us-east-1:600627348446:iii-website-prod-alarms]
data.aws_caller_identity.current: Reading...
aws_cloudfront_key_value_store.routes: Refreshing state... [id=iii-website-prod-routes]
aws_s3_bucket.site: Refreshing state... [id=iii-website-prod-us-east-1]
data.aws_caller_identity.current: Read complete after 1s [id=600627348446]
data.aws_iam_policy_document.github_tf_plan_trust: Reading...
data.aws_iam_policy_document.github_trust: Reading...
data.aws_iam_policy_document.github_tf_plan_trust: Read complete after 0s [id=2702237784]
data.aws_iam_policy_document.github_trust: Read complete after 0s [id=932063200]
aws_iam_role.github_tf_plan: Refreshing state... [id=iii-infra-github-tf-plan]
aws_iam_role.github_deploy_website: Refreshing state... [id=iii-website-prod-github-deploy]
aws_sns_topic_subscription.email: Refreshing state... [id=arn:aws:sns:us-east-1:600627348446:iii-website-prod-alarms:9ac7d89d-3735-493b-bce4-2b367f5c5211]
aws_cloudwatch_metric_alarm.acm_days_to_expiry: Refreshing state... [id=iii-website-prod-acm-days-to-expiry]
aws_cloudfront_function.redirects: Refreshing state... [id=iii-website-prod-redirects]
aws_iam_role_policy_attachment.github_tf_plan_readonly: Refreshing state... [id=iii-infra-github-tf-plan-20260414113923643200000001]
data.aws_route53_zone.iii_dev: Read complete after 1s [id=Z05516132AI1ZGB3NLC6D]
aws_route53_record.cert_validation["iii.dev"]: Refreshing state... [id=Z05516132AI1ZGB3NLC6D__7853369f3aea55a5d242d7c68506980e.iii.dev._CNAME]
aws_route53_record.cert_validation["www.iii.dev"]: Refreshing state... [id=Z05516132AI1ZGB3NLC6D__2bb3298c0bcca2494fce5e36c3d1427d.www.iii.dev._CNAME]
aws_route53_record.cert_validation["iii-preview.iii.dev"]: Refreshing state... [id=Z05516132AI1ZGB3NLC6D__0e43dc21d86b26a03742fb15aa5b4558.iii-preview.iii.dev._CNAME]
aws_acm_certificate_validation.site: Refreshing state... [id=2026-04-14 00:00:29.305 +0000 UTC]
aws_s3_bucket_versioning.site: Refreshing state... [id=iii-website-prod-us-east-1]
aws_s3_bucket_public_access_block.site: Refreshing state... [id=iii-website-prod-us-east-1]
aws_s3_bucket_server_side_encryption_configuration.site: Refreshing state... [id=iii-website-prod-us-east-1]
aws_cloudfront_distribution.site: Refreshing state... [id=E3N35RPQE4YVFQ]
data.aws_iam_policy_document.site_bucket: Reading...
aws_route53_record.apex_a[0]: Refreshing state... [id=Z05516132AI1ZGB3NLC6D_iii.dev_A]
aws_route53_record.www_a[0]: Refreshing state... [id=Z05516132AI1ZGB3NLC6D_www.iii.dev_A]
aws_route53_record.preview_aaaa: Refreshing state... [id=Z05516132AI1ZGB3NLC6D_iii-preview.iii.dev_AAAA]
aws_cloudwatch_metric_alarm.cf_5xx_rate: Refreshing state... [id=iii-website-prod-cf-5xx-rate]
aws_route53_record.www_aaaa[0]: Refreshing state... [id=Z05516132AI1ZGB3NLC6D_www.iii.dev_AAAA]
aws_route53_record.preview_a: Refreshing state... [id=Z05516132AI1ZGB3NLC6D_iii-preview.iii.dev_A]
aws_route53_record.apex_aaaa[0]: Refreshing state... [id=Z05516132AI1ZGB3NLC6D_iii.dev_AAAA]
data.aws_iam_policy_document.github_deploy_website: Reading...
data.aws_iam_policy_document.site_bucket: Read complete after 0s [id=2990320740]
data.aws_iam_policy_document.github_deploy_website: Read complete after 0s [id=3408297174]
aws_s3_bucket_policy.site: Refreshing state... [id=iii-website-prod-us-east-1]
aws_iam_policy.github_deploy_website: Refreshing state... [id=arn:aws:iam::600627348446:policy/iii-website-prod-github-deploy]
aws_iam_role_policy_attachment.github_deploy_website: Refreshing state... [id=iii-website-prod-github-deploy-20260414000337121000000003]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # aws_cloudfront_function.redirects will be updated in-place
  ~ resource "aws_cloudfront_function" "redirects" {
      ~ code                         = <<-EOT
            // Viewer-request handler for the default (S3) behavior. Tested in redirects.test.js.
            
            import cf from 'cloudfront'
            
            function redirect(location) {
              return {
                statusCode: 301,
                statusDescription: 'Moved Permanently',
                headers: {
                  location: { value: location },
                  'cache-control': { value: 'public, max-age=3600' },
                },
              }
            }
            
            // Return a real 404 for unknown extensionless paths instead of the previous
            // SPA fallback to /index.html. The old behavior served homepage HTML with
            // status 200 for any unknown path (e.g. /workers/iii-queue from broken docs
            // links), which Google indexed as soft-duplicate-of-homepage. Returning 404
            // here lets Search Console's "Not found (404)" bucket reflect reality and
            // stops generating "Duplicate without user-selected canonical" entries.
            function notFound() {
              return {
                statusCode: 404,
                statusDescription: 'Not Found',
                headers: {
                  'content-type': { value: 'text/html; charset=utf-8' },
                  'cache-control': { value: 'public, max-age=300' },
                },
                body: '<!doctype html><meta charset="utf-8"><title>404 — iii</title><meta name="robots" content="noindex"><style>body{font-family:system-ui,sans-serif;max-width:40rem;margin:4rem auto;padding:0 1rem;color:#1a1a1a}a{color:#1a5fbf}</style><h1>404 — page not found</h1><p>That URL doesn\'t exist on iii.dev.</p><p><a href="/">home</a> · <a href="/docs">docs</a> · <a href="/blog/">blog</a> · <a href="/manifesto">manifesto</a></p>',
              }
            }
            
            // CloudFront Functions deliver request.querystring as
            //   { key: { value: string, multiValue?: [{ value: string }, ...] } }
            // where repeated params spill into multiValue. We re-encode and rejoin so the
            // host-redirect path below preserves the original query (otherwise `?a=1&a=2`
            // would silently drop on the 301).
            function serializeQuerystring(qs) {
              if (!qs) return ''
              var parts = []
              for (var key in qs) {
                if (!Object.prototype.hasOwnProperty.call(qs, key)) continue
                var entry = qs[key]
                if (!entry) continue
                var encodedKey = encodeURIComponent(key)
                var primary = entry.value == null ? '' : entry.value
                parts.push(encodedKey + '=' + encodeURIComponent(primary))
                if (entry.multiValue && entry.multiValue.length) {
                  for (var i = 0; i < entry.multiValue.length; i++) {
                    var extra = entry.multiValue[i]
                    var extraValue = extra && extra.value != null ? extra.value : ''
                    parts.push(encodedKey + '=' + encodeURIComponent(extraValue))
                  }
                }
              }
              return parts.length ? '?' + parts.join('&') : ''
            }
            
            // Pretty-URL route map (/<page> → /<page>.html) lives in a CloudFront
            // KeyValueStore, populated by deploy-website.yml from the set of website/*.html
            // files. Adding a page needs no code change and no `terraform apply` — see
            // infra/terraform/website/README.md. The function is associated with exactly
            // one store, so cf.kvs() needs no store id.
            var routes = cf.kvs()
            
            // biome-ignore lint/correctness/noUnusedVariables: CloudFront Function entry point
            // biome-ignore lint/complexity/useOptionalChain: cloudfront-js-2.0 does NOT support optional chaining
            async function handler(event) {
              var request = event.request
              var uri = request.uri
              var host = request.headers && request.headers.host ? request.headers.host.value : undefined
            
              if (host === 'www.iii.dev') {
                return redirect(`https://iii.dev${uri}${serializeQuerystring(request.querystring)}`)
              }
            
              if (uri.indexOf('/.well-known/') === 0) return request
            
          +   var redirectHost = host || 'iii.dev'
          + 
          +   // Legacy privacy URL — canonical path is /privacy (privacy.html).
          +   if (uri === '/privacy-policy' || uri === '/privacy-policy.html') {
          +     return redirect('https://' + redirectHost + '/privacy' + serializeQuerystring(request.querystring))
          +   }
          + 
              // /blog/* — Astro emits build.format: 'directory' with trailingSlash:
              // 'always', so canonical URLs are /blog/<slug>/. CloudFront's
              // default_root_object only applies to the apex, so we rewrite directory
              // URLs to .../index.html and 301 extensionless paths to the canonical
              // trailing-slash form. Must run before the SPA fallback so /blog/<slug>
              // doesn't get hijacked into /index.html.
          -   var redirectHost = host || 'iii.dev'
              if (uri === '/blog') {
                return redirect('https://' + redirectHost + '/blog/' + serializeQuerystring(request.querystring))
              }
              if (uri.indexOf('/blog/') === 0) {
                if (uri.charAt(uri.length - 1) === '/') {
                  request.uri = uri + 'index.html'
                  return request
                }
                var lastSlashB = uri.lastIndexOf('/')
                var lastSegmentB = uri.substring(lastSlashB + 1)
                if (lastSegmentB.indexOf('.') === -1) {
                  return redirect('https://' + redirectHost + uri + '/' + serializeQuerystring(request.querystring))
                }
                return request
              }
            
              // Extensionless top-level paths resolve to a pretty page via the KVS route
          -   // map (e.g. /privacy-policy → /privacy-policy.html), or return a real 404.
          +   // map (e.g. /privacy → /privacy.html), or return a real 404.
              // Previously the map was hardcoded here, so a new page needed a `terraform
              // apply` to republish the function; it now lives in a CloudFront
              // KeyValueStore that deploy-website.yml syncs from website/*.html, making a
              // new page a content-only change. See notFound() for why unknown paths 404
              // instead of falling back to /index.html.
              if (uri !== '/' && uri.charAt(uri.length - 1) !== '/') {
                const lastSlash = uri.lastIndexOf('/')
                const lastSegment = uri.substring(lastSlash + 1)
                if (lastSegment.indexOf('.') === -1) {
                  try {
                    request.uri = await routes.get(uri)
                    return request
                  } catch (err) {
                    return notFound()
                  }
                }
              }
            
              return request
            }
        EOT
        id                           = "iii-website-prod-redirects"
        name                         = "iii-website-prod-redirects"
        # (8 unchanged attributes hidden)
    }

  # aws_sns_topic_subscription.email will be created
  + resource "aws_sns_topic_subscription" "email" {
      + arn                             = (known after apply)
      + confirmation_timeout_in_minutes = 1
      + confirmation_was_authenticated  = (known after apply)
      + endpoint                        = "devops@motia.dev"
      + endpoint_auto_confirms          = false
      + filter_policy_scope             = (known after apply)
      + id                              = (known after apply)
      + owner_id                        = (known after apply)
      + pending_confirmation            = (known after apply)
      + protocol                        = "email"
      + raw_message_delivery            = false
      + topic_arn                       = "arn:aws:sns:us-east-1:600627348446:iii-website-prod-alarms"
    }

Plan: 1 to add, 1 to change, 0 to destroy.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds blog markdown export for coding agents, threads generated blog links into llms.txt and AGENTS.md, updates deployment and sitemap handling for .md blog URLs, and refreshes related documentation and tests.

Changes

Blog Markdown Export for Coding Agents

Layer / File(s) Summary
Blog markdown export core and tests
website/scripts/blog-posts.ts, website/scripts/generate-blog-md.ts, website/scripts/generate-blog-md.test.ts, website/scripts/blog-posts.test.ts
Extends blog metadata, exports per-post and index markdown, rewrites image URLs, and validates the export pipeline.
Blog links in llms.txt and AGENTS.md
website/scripts/generate-llms-agents.ts, website/scripts/generate-llms-agents.test.ts, website/scripts/agents-appendix.md
Adds generated blog links to both agent-facing outputs and tests the resulting sections.
Deployment export and blog cache rules
.github/workflows/deploy-website.yml
Runs markdown export during deployment and applies revalidation caching to .md files.
Sitemap and blog documentation
website/scripts/generate-sitemap.ts, website/sitemap.xml, AGENTS.md, blog/README.md
Updates blog sitemap URLs and timestamps and documents markdown blog artifacts and routing behavior.

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

Suggested reviewers: sergiofilhowz

Poem

A rabbit found a markdown trail,
Blog links hopped into every pail.
Fresh paths bloom in sitemap light,
Agents read the posts just right.
Thump, deploy, and all is bright! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change and tests, but it does not follow the required What/Why/Notes template sections. Rewrite the PR description using the required What, Why, and Notes headings, and add the missing motivation and reviewer notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: exporting blog markdown for agent use.
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 docs/blog-md-llms-index

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.

@rohitg00
rohitg00 marked this pull request as draft July 6, 2026 09:13

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

🧹 Nitpick comments (4)
website/scripts/generate-blog-md.test.ts (1)

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

Consider adding an .mdx source test case.

These tests only exercise .md source files. Given readBlogPosts supports .mdx too, a test writing an .mdx fixture would catch the extension-mismatch issue flagged in generate-blog-md.ts.

🤖 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 `@website/scripts/generate-blog-md.test.ts` around lines 14 - 43, Add coverage
for the .mdx path in exportBlogMarkdown/readBlogPosts by extending the
generate-blog-md.test.ts fixture setup to include an .mdx blog source file and
asserting it is exported correctly. Use the existing exportBlogMarkdown test and
related symbols to mirror the current .md case, but ensure the new case verifies
the extension handling so the mismatch in generate-blog-md.ts is caught.
website/scripts/generate-blog-md.ts (2)

50-53: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Unescaped title/description can break markdown link syntax.

post.title/post.description are interpolated raw into [title](url) links. A title or description containing ] or ) would produce malformed markdown in index.md, llms.txt, and AGENTS.md. Since content is authored internally, this is low risk today but worth a defensive check for frontmatter authors.

Also applies to: 77-80

🤖 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 `@website/scripts/generate-blog-md.ts` around lines 50 - 53, The generated blog
index entries in generate-blog-md.ts interpolate post.title and post.description
directly into markdown link text, which can break the syntax if either contains
] or ). Update the post mapping used for indexLines (and the same logic in the
other referenced output paths) to escape or sanitize these fields before
building the [title](url) string, using the existing post.slug-based URL as-is
and ensuring the rendered markdown stays valid for all authored content.

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

Duplicate link-line formatting logic.

The markdown link template `- [${post.title}](${SITE_ORIGIN}/blog/${post.slug}.md) — ${post.description}` is duplicated verbatim in exportBlogMarkdown and buildBlogLinksSection.

♻️ Proposed refactor
+function formatBlogLink(post: { title: string; slug: string; description: string }): string {
+  return `- [${post.title}](${SITE_ORIGIN}/blog/${post.slug}.md) — ${post.description}`
+}
+
 export async function exportBlogMarkdown(
   ...
-  const indexLines = posts.map(
-    (post) =>
-      `- [${post.title}](${SITE_ORIGIN}/blog/${post.slug}.md) — ${post.description}`,
-  )
+  const indexLines = posts.map(formatBlogLink)
   ...
 export async function buildBlogLinksSection(): Promise<string> {
   ...
-  const lines = posts.map(
-    (post) =>
-      `- [${post.title}](${SITE_ORIGIN}/blog/${post.slug}.md) — ${post.description}`,
-  )
+  const lines = posts.map(formatBlogLink)

Also applies to: 73-80

🤖 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 `@website/scripts/generate-blog-md.ts` around lines 50 - 53, The blog markdown
link-line template is duplicated in both exportBlogMarkdown and
buildBlogLinksSection, so extract the shared formatting into a single helper and
reuse it from both places. Update the logic around posts.map in
generate-blog-md.ts so the link text, URL, and description are built in one
symbol (for example a dedicated formatter function) and both sections call that
instead of repeating the same template.
website/scripts/generate-llms-agents.ts (1)

153-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the duplicated optional-section insertion pattern.

The [section.trimEnd(), section ? '' : undefined] pattern is duplicated verbatim between buildLlmsTxt (Lines 153-154) and buildAgentsMd (Lines 179-180). Worth extracting into a small shared helper, e.g. optionalSection(section: string): (string | undefined)[], to avoid divergence if this logic needs to change later.

Also note: when blogSection is empty (no visible posts), the array becomes [..., home.trimEnd(), '', '', tail.trimEnd()] after filtering — two consecutive empty strings produce an extra blank line compared to when blogSection is populated ([..., home.trimEnd(), '', content, '', tail.trimEnd()]). This is a harmless cosmetic inconsistency in the generated llms.txt/AGENTS.md output.

♻️ Proposed helper extraction
+function optionalSection(section: string): (string | undefined)[] {
+  return section ? [section.trimEnd(), ''] : []
+}
+
 export function buildLlmsTxt(html: string, blogSection = ''): string {
   ...
   const body = [
     '# iii',
     '',
     `> ${LLMS_TAGLINE}`,
     '',
     overview.trimEnd(),
     '',
     home.trimEnd(),
     '',
-    blogSection.trimEnd(),
-    blogSection ? '' : undefined,
+    ...optionalSection(blogSection),
     tail.trimEnd(),
     '',
   ].filter((chunk): chunk is string => chunk !== undefined).join('\n')

Also applies to: 179-188

🤖 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 `@website/scripts/generate-llms-agents.ts` around lines 153 - 160, The
duplicated optional-section pattern in buildLlmsTxt and buildAgentsMd should be
extracted into a shared helper to keep the empty-section handling consistent.
Introduce a small helper such as optionalSection and use it at both call sites
so the trimEnd/conditional-empty-string logic lives in one place, and keep the
resulting output behavior unchanged aside from the noted extra blank line when
blogSection is empty.
🤖 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.

Nitpick comments:
In `@website/scripts/generate-blog-md.test.ts`:
- Around line 14-43: Add coverage for the .mdx path in
exportBlogMarkdown/readBlogPosts by extending the generate-blog-md.test.ts
fixture setup to include an .mdx blog source file and asserting it is exported
correctly. Use the existing exportBlogMarkdown test and related symbols to
mirror the current .md case, but ensure the new case verifies the extension
handling so the mismatch in generate-blog-md.ts is caught.

In `@website/scripts/generate-blog-md.ts`:
- Around line 50-53: The generated blog index entries in generate-blog-md.ts
interpolate post.title and post.description directly into markdown link text,
which can break the syntax if either contains ] or ). Update the post mapping
used for indexLines (and the same logic in the other referenced output paths) to
escape or sanitize these fields before building the [title](url) string, using
the existing post.slug-based URL as-is and ensuring the rendered markdown stays
valid for all authored content.
- Around line 50-53: The blog markdown link-line template is duplicated in both
exportBlogMarkdown and buildBlogLinksSection, so extract the shared formatting
into a single helper and reuse it from both places. Update the logic around
posts.map in generate-blog-md.ts so the link text, URL, and description are
built in one symbol (for example a dedicated formatter function) and both
sections call that instead of repeating the same template.

In `@website/scripts/generate-llms-agents.ts`:
- Around line 153-160: The duplicated optional-section pattern in buildLlmsTxt
and buildAgentsMd should be extracted into a shared helper to keep the
empty-section handling consistent. Introduce a small helper such as
optionalSection and use it at both call sites so the
trimEnd/conditional-empty-string logic lives in one place, and keep the
resulting output behavior unchanged aside from the noted extra blank line when
blogSection is empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0f638eeb-29bd-43ab-9745-baa396b43f22

📥 Commits

Reviewing files that changed from the base of the PR and between 3781ff4 and dd1c364.

📒 Files selected for processing (18)
  • .github/workflows/deploy-website.yml
  • AGENTS.md
  • blog/README.md
  • infra/terraform/website/cloudfront_functions/redirects.js
  • infra/terraform/website/cloudfront_functions/redirects.test.js
  • website/index.html
  • website/privacy.html
  • website/scripts/agents-appendix.md
  • website/scripts/blog-posts.test.ts
  • website/scripts/blog-posts.ts
  • website/scripts/generate-blog-md.test.ts
  • website/scripts/generate-blog-md.ts
  • website/scripts/generate-llms-agents.test.ts
  • website/scripts/generate-llms-agents.ts
  • website/scripts/generate-sitemap.ts
  • website/scripts/routes-kvs.test.ts
  • website/scripts/routes.ts
  • website/sitemap.xml

@rohitg00
rohitg00 marked this pull request as ready for review July 6, 2026 09:21
@rohitg00 rohitg00 changed the title feat(website): export blog markdown for agents and rename privacy URL feat(website): export blog markdown for agents Jul 6, 2026

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

🧹 Nitpick comments (2)
website/scripts/generate-blog-md.test.ts (1)

60-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting shared tmp-dir setup for the two exportBlogMarkdown tests.

Both the .md (Line 29) and .mdx (Line 60) tests repeat the same mkdtemp/mkdir boilerplate. A small helper (e.g., setupTempBlogDirs()) would reduce duplication, though current tests remain readable as-is.

🤖 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 `@website/scripts/generate-blog-md.test.ts` around lines 60 - 88, The two
exportBlogMarkdown tests duplicate the same temporary directory setup, so
extract the shared mkdtemp/mkdir boilerplate into a small helper such as
setupTempBlogDirs and reuse it from both the .md and .mdx test cases. Keep the
helper close to exportBlogMarkdown in generate-blog-md.test.ts so the test
intent stays clear while reducing repeated setup code.
website/scripts/generate-blog-md.ts (1)

53-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential await in loop; consider parallelizing.

Each post's read/rewrite/write happens sequentially. Since these are independent I/O operations, Promise.all could reduce build time as the number of posts grows.

Proposed refactor
-  for (const post of posts) {
-    const sourcePath = path.join(contentDir, post.sourceFile)
-    const raw = await fs.readFile(sourcePath, 'utf8')
-    const exported = rewriteBlogImagePaths(raw, astroAssets)
-    await fs.writeFile(path.join(distDir, `${post.slug}.md`), exported, 'utf8')
-    count++
-  }
+  await Promise.all(
+    posts.map(async (post) => {
+      const sourcePath = path.join(contentDir, post.sourceFile)
+      const raw = await fs.readFile(sourcePath, 'utf8')
+      const exported = rewriteBlogImagePaths(raw, astroAssets)
+      await fs.writeFile(path.join(distDir, `${post.slug}.md`), exported, 'utf8')
+    }),
+  )
+  count = posts.length
🤖 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 `@website/scripts/generate-blog-md.ts` around lines 53 - 59, The blog
generation loop is performing independent read/rewrite/write work sequentially
in the generateBlogMd flow, which slows builds as post count grows. Refactor the
posts processing in generateBlogMd to run each post’s fs.readFile,
rewriteBlogImagePaths, and fs.writeFile concurrently with Promise.all (or an
equivalent parallel map), while keeping the same output path logic and count
tracking behavior in the surrounding function.
🤖 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.

Nitpick comments:
In `@website/scripts/generate-blog-md.test.ts`:
- Around line 60-88: The two exportBlogMarkdown tests duplicate the same
temporary directory setup, so extract the shared mkdtemp/mkdir boilerplate into
a small helper such as setupTempBlogDirs and reuse it from both the .md and .mdx
test cases. Keep the helper close to exportBlogMarkdown in
generate-blog-md.test.ts so the test intent stays clear while reducing repeated
setup code.

In `@website/scripts/generate-blog-md.ts`:
- Around line 53-59: The blog generation loop is performing independent
read/rewrite/write work sequentially in the generateBlogMd flow, which slows
builds as post count grows. Refactor the posts processing in generateBlogMd to
run each post’s fs.readFile, rewriteBlogImagePaths, and fs.writeFile
concurrently with Promise.all (or an equivalent parallel map), while keeping the
same output path logic and count tracking behavior in the surrounding function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 280e3e92-3206-459d-83e4-4858cd2b1bcc

📥 Commits

Reviewing files that changed from the base of the PR and between fc6563f and 64f45be.

📒 Files selected for processing (4)
  • website/scripts/blog-posts.ts
  • website/scripts/generate-blog-md.test.ts
  • website/scripts/generate-blog-md.ts
  • website/scripts/generate-llms-agents.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • website/scripts/generate-llms-agents.ts

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

🧹 Nitpick comments (1)
website/scripts/generate-blog-md.ts (1)

63-80: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate slug !== 'index' before starting concurrent I/O.

The slug === 'index' check is performed inside the Promise.all map (Line 71-73), after other posts' async reads/writes may already be in flight. Since Promise.all rejects on the first failure without cancelling already-started promises, other posts' <slug>.md files can still get written to distDir even though the export ultimately throws — leaving a partially-exported distDir on failure.

Move the collision check to a synchronous pass over posts before the Promise.all call to fail fast and avoid partial writes.

♻️ Proposed fix
   const astroAssets = await listAstroAssets(distDir)
 
+  const indexCollision = posts.find((post) => post.slug === 'index')
+  if (indexCollision) {
+    throw new Error(`blog post slug "index" collides with the generated blog/index.md`)
+  }
+
   await Promise.all(
     posts.map(async (post) => {
-      if (post.slug === 'index') {
-        throw new Error(`blog post slug "index" collides with the generated blog/index.md`)
-      }
       const sourcePath = path.join(contentDir, post.sourceFile)
🤖 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 `@website/scripts/generate-blog-md.ts` around lines 63 - 80, The blog export in
generate-blog-md.ts is starting async reads/writes in Promise.all before
validating that no post has the reserved slug "index", which can leave partial
files in distDir if one task fails. Add a synchronous pre-check over posts
before the Promise.all block in generate-blog-md.ts (near readBlogPosts,
listAstroAssets, and rewriteBlogImagePaths usage) to reject any post with slug
=== 'index' up front, then keep the per-post export logic unchanged.
🤖 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.

Nitpick comments:
In `@website/scripts/generate-blog-md.ts`:
- Around line 63-80: The blog export in generate-blog-md.ts is starting async
reads/writes in Promise.all before validating that no post has the reserved slug
"index", which can leave partial files in distDir if one task fails. Add a
synchronous pre-check over posts before the Promise.all block in
generate-blog-md.ts (near readBlogPosts, listAstroAssets, and
rewriteBlogImagePaths usage) to reject any post with slug === 'index' up front,
then keep the per-post export logic unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ac2f846e-7177-460d-a671-e482ffb724a5

📥 Commits

Reviewing files that changed from the base of the PR and between 64f45be and b22f562.

📒 Files selected for processing (2)
  • website/scripts/generate-blog-md.test.ts
  • website/scripts/generate-blog-md.ts

guibeira
guibeira previously approved these changes Jul 8, 2026
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.

2 participants