From 712fa949fd804a2c0b308dd79cafe4bd09384ac6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 9 May 2026 05:17:27 +0000 Subject: [PATCH 01/13] fix(website): rewrite llms.txt to follow llmstxt.org format - Move detailed install instructions into llms-full.txt - Make llms.txt a curated link list with H1, blockquote summary, and markdown link sections per the llmstxt.org spec Co-authored-by: Aiden Bai --- apps/website/public/llms-full.txt | 184 ++++++++++++++++++++++++++++ apps/website/public/llms.txt | 191 +++--------------------------- 2 files changed, 203 insertions(+), 172 deletions(-) create mode 100644 apps/website/public/llms-full.txt diff --git a/apps/website/public/llms-full.txt b/apps/website/public/llms-full.txt new file mode 100644 index 000000000..0ff3709f6 --- /dev/null +++ b/apps/website/public/llms-full.txt @@ -0,0 +1,184 @@ +# React Grab + +> Grab any element in your app and give it to Cursor, Claude Code, or other AI coding agents. + +## Usage + +Once installed, hover over any UI element in your browser and press: + +- **⌘C** (Cmd+C) on Mac +- **Ctrl+C** on Windows/Linux + +This copies the element's context (file name, React component, and HTML source code) to your clipboard ready to paste into your coding agent. For example: + +``` + + Forgot your password? + +in LoginForm at components/login-form.tsx:46:19 +``` + +## Installation + +### CLI (Recommended) + +Run this command at your project root. Use the `-y` flag to skip interactive prompts (required for non-interactive environments): + +```bash +npx grab@latest init -y +``` + +Init Options: +``` +Options: + -y, --yes Skip confirmation prompts + -f, --force Force overwrite existing config + -k, --key Activation key (e.g., "Meta+K", "Ctrl+Shift+G", "Space") + --skip-install Skip package installation + -c, --cwd Working directory (defaults to current directory) + +Examples: + npx grab@latest init -y # Auto-detect and install without prompts + npx grab@latest init -k "Meta+K" -y # Set activation key to Cmd+K / Win+K +``` + +The CLI will detect your framework and add the necessary scripts automatically. + +### Configuration + +Configure React Grab options (activation key, mode, etc.): + +```bash +npx grab@latest config +``` + +Config Options: +``` +Options: + -y, --yes Skip confirmation prompts + -k, --key Activation key (e.g., "Meta+K", "Ctrl+Shift+G", "Space") + -m, --mode Activation mode (toggle, hold) + --hold-duration Key hold duration in milliseconds (for hold mode) + --allow-input Allow activation inside input fields (true/false) + --context-lines Max context lines to include + --cdn CDN domain (e.g., unpkg.com, custom.react-grab.com) + -c, --cwd Working directory (defaults to current directory) + +Examples: + npx grab@latest config -k "Meta+K" # Set activation key to Cmd+K / Win+K + npx grab@latest config -k "Ctrl+Shift+G" # Set activation key to Ctrl+Shift+G + npx grab@latest config -m hold --hold-duration 150 # Use hold mode with 150ms delay + npx grab@latest config --context-lines 5 # Include 5 lines of context +``` + +To discover all available commands and flags: + +```bash +npx grab@latest --help +npx grab@latest init --help +npx grab@latest config --help +``` + +### Manual Installation + +If you prefer manual installation, use the correct package manager for your project: + +```bash +npm install react-grab@latest +# or +yarn add react-grab@latest +# or +pnpm add react-grab@latest +# or +bun add react-grab@latest +``` + +### Next.js (App router) + +Add this inside of your `app/layout.tsx`: + +```jsx +import Script from "next/script"; + +export default function RootLayout({ children }) { + return ( + + + {process.env.NODE_ENV === "development" && ( + + + +
+ + + +``` + +### Webpack + +First, install React Grab: + +```bash +npm install react-grab +``` + +Then add this at the top of your main entry file (e.g., `src/index.tsx` or `src/main.tsx`): + +```tsx +if (process.env.NODE_ENV === "development") { + import("react-grab"); +} +``` diff --git a/apps/website/public/llms.txt b/apps/website/public/llms.txt index 817165966..6fa4a72fc 100644 --- a/apps/website/public/llms.txt +++ b/apps/website/public/llms.txt @@ -1,184 +1,31 @@ # React Grab -Grab any element in your app and give it to Cursor, Claude Code, or other AI coding agents. +> React Grab lets you select any element in your app and hand it off to AI coding agents like Cursor, Claude Code, Codex, and Copilot. Press a hotkey, click an element, and the file path, component name, and HTML source get copied to your clipboard for instant pasting into your agent. -## Usage +React Grab is an open-source dev-only script that adds an element picker to React apps in development. It is framework agnostic for React (Next.js, Vite, Webpack, etc.), runs entirely client-side, and is removed from production builds. -Once installed, hover over any UI element in your browser and press: +The default activation is `⌘+C` on macOS and `Ctrl+C` on Windows/Linux. The activation key, mode, and other behavior are configurable via the `grab` CLI. -- **⌘C** (Cmd+C) on Mac -- **Ctrl+C** on Windows/Linux +## Docs -This copies the element's context (file name, React component, and HTML source code) to your clipboard ready to paste into your coding agent. For example: +- [Install guide](https://react-grab.com/install.md): Installation instructions for the CLI, Next.js (App & Pages router), Vite, and Webpack. +- [Full documentation](https://react-grab.com/llms-full.txt): Complete reference covering installation, configuration, usage, and CLI flags. +- [Changelog](https://react-grab.com/changelog.md): Release notes and version history. +- [Privacy policy](https://react-grab.com/privacy.md): How React Grab handles your data (it does not collect any). -``` - - Forgot your password? - -in LoginForm at components/login-form.tsx:46:19 -``` +## Source -## Installation +- [GitHub repository](https://github.com/aidenybai/react-grab): Source code, issues, and discussions. +- [README](https://raw.githubusercontent.com/aidenybai/react-grab/main/README.md): Project overview and quickstart. +- [Primitives reference](https://github.com/aidenybai/react-grab/tree/main?tab=readme-ov-file#primitives): Lower-level building blocks exported by `react-grab`. -### CLI (Recommended) +## Examples -Run this command at your project root. Use the `-y` flag to skip interactive prompts (required for non-interactive environments): +- [Homepage demo](https://react-grab.com/index.md): Interactive demo of the typical "find this button" agent flow. +- [Benchmark](https://react-bench.com): Independent benchmark showing 2× faster code-finding with React Grab context. -```bash -npx grab@latest init -y -``` +## Optional -Init Options: -``` -Options: - -y, --yes Skip confirmation prompts - -f, --force Force overwrite existing config - -k, --key Activation key (e.g., "Meta+K", "Ctrl+Shift+G", "Space") - --skip-install Skip package installation - -c, --cwd Working directory (defaults to current directory) - -Examples: - npx grab@latest init -y # Auto-detect and install without prompts - npx grab@latest init -k "Meta+K" -y # Set activation key to Cmd+K / Win+K -``` - -The CLI will detect your framework and add the necessary scripts automatically. - -### Configuration - -Configure React Grab options (activation key, mode, etc.): - -```bash -npx grab@latest config -``` - -Config Options: -``` -Options: - -y, --yes Skip confirmation prompts - -k, --key Activation key (e.g., "Meta+K", "Ctrl+Shift+G", "Space") - -m, --mode Activation mode (toggle, hold) - --hold-duration Key hold duration in milliseconds (for hold mode) - --allow-input Allow activation inside input fields (true/false) - --context-lines Max context lines to include - --cdn CDN domain (e.g., unpkg.com, custom.react-grab.com) - -c, --cwd Working directory (defaults to current directory) - -Examples: - npx grab@latest config -k "Meta+K" # Set activation key to Cmd+K / Win+K - npx grab@latest config -k "Ctrl+Shift+G" # Set activation key to Ctrl+Shift+G - npx grab@latest config -m hold --hold-duration 150 # Use hold mode with 150ms delay - npx grab@latest config --context-lines 5 # Include 5 lines of context -``` - -To discover all available commands and flags: - -```bash -npx grab@latest --help -npx grab@latest init --help -npx grab@latest config --help -``` - -### Manual Installation - -If you prefer manual installation, use the correct package manager for your project: - -```bash -npm install react-grab@latest -# or -yarn add react-grab@latest -# or -pnpm add react-grab@latest -# or -bun add react-grab@latest -``` - -### Next.js (App router) - -Add this inside of your `app/layout.tsx`: - -```jsx -import Script from "next/script"; - -export default function RootLayout({ children }) { - return ( - - - {process.env.NODE_ENV === "development" && ( - - - -
- - - -``` - -### Webpack - -First, install React Grab: - -```bash -npm install react-grab -``` - -Then add this at the top of your main entry file (e.g., `src/index.tsx` or `src/main.tsx`): - -```tsx -if (process.env.NODE_ENV === "development") { - import("react-grab"); -} -``` +- [Discord community](https://discord.com/invite/G7zxfUzkm7): Questions, feedback, and discussion. +- [npm package](https://www.npmjs.com/package/react-grab): Latest published version. +- [grab CLI on npm](https://www.npmjs.com/package/grab): Init and config CLI. From b407123ff6a3d85bf5a698a6d820e324e4a512de Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 9 May 2026 05:17:33 +0000 Subject: [PATCH 02/13] feat(website): add markdown mirrors for each page - /index.md, /privacy.md as static markdown mirrors - /changelog.md and /sitemap.md as dynamic route handlers - /404.md fallback markdown for missing-page agent responses Co-authored-by: Aiden Bai --- apps/website/app/changelog.md/route.ts | 41 +++++++++++++ apps/website/app/sitemap.md/route.ts | 85 ++++++++++++++++++++++++++ apps/website/public/404.md | 21 +++++++ apps/website/public/index.md | 41 +++++++++++++ apps/website/public/privacy.md | 60 ++++++++++++++++++ 5 files changed, 248 insertions(+) create mode 100644 apps/website/app/changelog.md/route.ts create mode 100644 apps/website/app/sitemap.md/route.ts create mode 100644 apps/website/public/404.md create mode 100644 apps/website/public/index.md create mode 100644 apps/website/public/privacy.md diff --git a/apps/website/app/changelog.md/route.ts b/apps/website/app/changelog.md/route.ts new file mode 100644 index 000000000..b057da52a --- /dev/null +++ b/apps/website/app/changelog.md/route.ts @@ -0,0 +1,41 @@ +import { readFileSync } from "fs"; +import { join } from "path"; +import { parseChangelog } from "@/utils/parse-changelog"; + +const BASE_URL = "https://react-grab.com"; + +export const dynamic = "force-static"; + +export const GET = (): Response => { + const changelogPath = join(process.cwd(), "..", "..", "packages", "react-grab", "CHANGELOG.md"); + const content = readFileSync(changelogPath, "utf-8"); + const entries = parseChangelog(content); + + const lines: string[] = []; + lines.push("# Changelog"); + lines.push(""); + lines.push("> Release notes and version history for React Grab."); + lines.push(""); + lines.push(`See the [HTML changelog](${BASE_URL}/changelog) for the rendered version.`); + lines.push(""); + + for (const entry of entries) { + lines.push(`## ${entry.version}`); + lines.push(""); + if (entry.changeType) { + lines.push(`*${entry.changeType}*`); + lines.push(""); + } + for (const change of entry.changes) { + lines.push(`- ${change}`); + } + lines.push(""); + } + + return new Response(lines.join("\n"), { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": "public, max-age=300, s-maxage=300", + }, + }); +}; diff --git a/apps/website/app/sitemap.md/route.ts b/apps/website/app/sitemap.md/route.ts new file mode 100644 index 000000000..7383dd339 --- /dev/null +++ b/apps/website/app/sitemap.md/route.ts @@ -0,0 +1,85 @@ +import { readdirSync, statSync } from "fs"; +import { join } from "path"; + +const BASE_URL = "https://react-grab.com"; +const EXCLUDED_PATHS = new Set(["api", "open-file"]); + +interface SitemapEntry { + url: string; + title: string; +} + +const PAGE_TITLES: Record = { + "/": "Home", + "/changelog": "Changelog", + "/privacy": "Privacy Policy", +}; + +const getRoutes = (directory: string, basePath = ""): string[] => { + const routes: string[] = []; + const entries = readdirSync(directory); + + for (const entry of entries) { + if (EXCLUDED_PATHS.has(entry)) continue; + if (entry.includes(".")) continue; + + const fullPath = join(directory, entry); + const stat = statSync(fullPath); + if (!stat.isDirectory()) continue; + + const routePath = basePath ? `${basePath}/${entry}` : entry; + const dirEntries = readdirSync(fullPath); + const hasPage = dirEntries.some((file) => file === "page.tsx" || file === "page.ts"); + + if (hasPage) { + routes.push(routePath); + } + routes.push(...getRoutes(fullPath, routePath)); + } + + return routes; +}; + +export const dynamic = "force-static"; + +export const GET = (): Response => { + const appDirectory = join(process.cwd(), "app"); + const routes = getRoutes(appDirectory); + + const entries: SitemapEntry[] = [{ url: BASE_URL, title: PAGE_TITLES["/"] ?? "Home" }]; + for (const route of routes) { + const path = `/${route}`; + entries.push({ + url: `${BASE_URL}${path}`, + title: PAGE_TITLES[path] ?? route.charAt(0).toUpperCase() + route.slice(1), + }); + } + + const lines: string[] = []; + lines.push("# React Grab Sitemap"); + lines.push(""); + lines.push("> All public pages on react-grab.com, with markdown alternates."); + lines.push(""); + lines.push("## Pages"); + lines.push(""); + for (const entry of entries) { + const mdUrl = entry.url === BASE_URL ? `${BASE_URL}/index.md` : `${entry.url}.md`; + lines.push(`- [${entry.title}](${entry.url}) — [markdown](${mdUrl})`); + } + lines.push(""); + lines.push("## Resources"); + lines.push(""); + lines.push(`- [llms.txt](${BASE_URL}/llms.txt)`); + lines.push(`- [llms-full.txt](${BASE_URL}/llms-full.txt)`); + lines.push(`- [Install guide](${BASE_URL}/install.md)`); + lines.push(`- [sitemap.xml](${BASE_URL}/sitemap.xml)`); + lines.push(`- [GitHub](https://github.com/aidenybai/react-grab)`); + lines.push(""); + + return new Response(lines.join("\n"), { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": "public, max-age=300, s-maxage=300", + }, + }); +}; diff --git a/apps/website/public/404.md b/apps/website/public/404.md new file mode 100644 index 000000000..246335ed3 --- /dev/null +++ b/apps/website/public/404.md @@ -0,0 +1,21 @@ +# Page Not Found + +> The page you requested does not exist on react-grab.com. + +## Available Pages + +- [Home](https://react-grab.com): Overview, install instructions, and quick start. +- [Changelog](https://react-grab.com/changelog): Release notes and version history. +- [Privacy policy](https://react-grab.com/privacy): How React Grab handles your data. + +## Resources for Agents + +- [llms.txt](https://react-grab.com/llms.txt): LLM-friendly site index. +- [llms-full.txt](https://react-grab.com/llms-full.txt): Full documentation. +- [Install guide (markdown)](https://react-grab.com/install.md): Step-by-step install. +- [Sitemap (markdown)](https://react-grab.com/sitemap.md): All pages. + +## External + +- [GitHub repository](https://github.com/aidenybai/react-grab) +- [Discord community](https://discord.com/invite/G7zxfUzkm7) diff --git a/apps/website/public/index.md b/apps/website/public/index.md new file mode 100644 index 000000000..e8b70d1ce --- /dev/null +++ b/apps/website/public/index.md @@ -0,0 +1,41 @@ +# React Grab + +> Grab any element in your app and give it to Cursor, Claude Code, or other AI coding agents. + +React Grab is an open-source dev-only script that adds an element picker to React apps in development. Hover any element, press a hotkey, and the file path, component name, and HTML source are copied to your clipboard ready to paste into your agent. + +## How It Works + +Once installed, hover over any UI element in your browser and press: + +- **⌘C** (Cmd+C) on Mac +- **Ctrl+C** on Windows/Linux + +The element's context (file name, React component, and HTML source) is copied to your clipboard. For example: + +``` + + Forgot your password? + +in LoginForm at components/login-form.tsx:46:19 +``` + +## Quick Install + +Run this command at your project root: + +```bash +npx grab@latest init -y +``` + +The CLI auto-detects your framework and configures everything. + +## Links + +- [Install guide](https://react-grab.com/install.md) +- [Full documentation](https://react-grab.com/llms-full.txt) +- [Changelog](https://react-grab.com/changelog.md) +- [Privacy policy](https://react-grab.com/privacy.md) +- [Sitemap](https://react-grab.com/sitemap.md) +- [GitHub repository](https://github.com/aidenybai/react-grab) +- [Discord community](https://discord.com/invite/G7zxfUzkm7) diff --git a/apps/website/public/privacy.md b/apps/website/public/privacy.md new file mode 100644 index 000000000..24bc3bebd --- /dev/null +++ b/apps/website/public/privacy.md @@ -0,0 +1,60 @@ +# Privacy Policy + +> Privacy policy for React Grab browser extension and website. React Grab does not collect, store, or transmit any personal data. + +## Overview + +React Grab is a developer tool that helps you inspect and copy React components from web pages. This privacy policy explains how the React Grab browser extension and website handle your data. + +## Data Collection + +React Grab does **NOT** collect, store, or transmit any personal data. Specifically: + +- We do not collect any personally identifiable information +- We do not track your browsing history +- We do not store any data about the websites you visit +- We do not use analytics or tracking services +- We do not use cookies for tracking purposes + +## How React Grab Works + +React Grab operates entirely locally in your browser. When you use the extension: + +- The extension injects code into web pages to enable element selection +- When you select an element, the HTML/JSX is copied to your clipboard locally +- No data is sent to external servers +- All processing happens on your device + +## Permissions + +The extension requires the following permissions: + +- **Access to all websites:** Required to inject the element selection functionality into any webpage you visit. +- **Storage:** Used only to store your extension preferences locally on your device. +- **Active Tab:** Needed to interact with the currently active tab when you use the keyboard shortcut. + +These permissions are used solely for the core functionality of the extension and are not used to collect or transmit any data. + +## Local Storage + +React Grab may store minimal settings locally on your device using browser storage APIs. This data never leaves your device and can be cleared by uninstalling the extension or clearing your browser data. + +## Third-Party Services + +React Grab does not integrate with any third-party analytics, tracking, or advertising services. The extension operates entirely offline and does not make any external network requests. + +## Open Source + +React Grab is open source software. You can review the complete source code on [GitHub](https://github.com/aidenybai/react-grab) to verify these privacy claims. + +## Changes to This Policy + +We may update this privacy policy from time to time. Any changes will be posted on this page with an updated revision date. + +## Contact + +If you have questions about this privacy policy, open an issue on our [GitHub repository](https://github.com/aidenybai/react-grab/issues) or join our [Discord community](https://discord.com/invite/G7zxfUzkm7). + +## Summary + +React Grab respects your privacy. We don't collect, store, or transmit any of your personal data. The extension works entirely locally on your device. From 0213fbc7ca4e75c8c16e79848091989852c3a5b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 9 May 2026 05:17:44 +0000 Subject: [PATCH 03/13] feat(website): add Next.js proxy for agent content negotiation - Detect AI user-agents (ChatGPT, Claude, Cursor, Perplexity, GPTBot, etc) and Accept: text/markdown header, then rewrite to the page's markdown mirror - Return 200 with /404.md (instead of an HTML 404 body) for unknown URLs requested by agents, since agents discard 404 bodies - www.react-grab.com -> react-grab.com permanent redirect to avoid cross-host redirects (still requires Vercel apex domain to be marked canonical for the audit to pass) - Set Content-Type: text/markdown and CORS headers on all .md and llms*.txt responses - Add Vary: Accept, User-Agent so caches respect content negotiation Co-authored-by: Aiden Bai --- apps/website/next.config.ts | 51 +++++++++++++--------- apps/website/proxy.ts | 85 +++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 apps/website/proxy.ts diff --git a/apps/website/next.config.ts b/apps/website/next.config.ts index 8e1d15ee7..3a108f7b5 100644 --- a/apps/website/next.config.ts +++ b/apps/website/next.config.ts @@ -19,6 +19,12 @@ const nextConfig: NextConfig = { }, redirects: async () => { return [ + { + source: "/:path*", + has: [{ type: "host", value: "www.react-grab.com" }], + destination: "https://react-grab.com/:path*", + permanent: true, + }, { source: "/docs", destination: "https://github.com/aidenybai/react-grab#readme", @@ -45,12 +51,28 @@ const nextConfig: NextConfig = { headers: async () => { return [ { - source: "/", + source: "/:path*", + headers: [{ key: "Vary", value: "Accept, User-Agent" }], + }, + { + source: "/:path*\\.md", + headers: [ + { key: "Content-Type", value: "text/markdown; charset=utf-8" }, + { key: "Access-Control-Allow-Origin", value: "*" }, + ], + }, + { + source: "/llms.txt", + headers: [ + { key: "Content-Type", value: "text/markdown; charset=utf-8" }, + { key: "Access-Control-Allow-Origin", value: "*" }, + ], + }, + { + source: "/llms-full.txt", headers: [ - { - key: "Vary", - value: "Accept", - }, + { key: "Content-Type", value: "text/markdown; charset=utf-8" }, + { key: "Access-Control-Allow-Origin", value: "*" }, ], }, ]; @@ -58,21 +80,10 @@ const nextConfig: NextConfig = { rewrites: async () => { return { beforeFiles: [ - { - source: "/", - destination: "/llms.txt", - has: [ - { - type: "header", - key: "accept", - value: "(.*)text/markdown(.*)", - }, - ], - }, - { - source: "/llm.txt", - destination: "/llms.txt", - }, + { source: "/llm.txt", destination: "/llms.txt" }, + { source: "/index.html.md", destination: "/index.md" }, + { source: "/privacy/index.md", destination: "/privacy.md" }, + { source: "/changelog/index.md", destination: "/changelog.md" }, ], }; }, diff --git a/apps/website/proxy.ts b/apps/website/proxy.ts new file mode 100644 index 000000000..e07cb045f --- /dev/null +++ b/apps/website/proxy.ts @@ -0,0 +1,85 @@ +import { NextResponse, type NextRequest } from "next/server"; + +const AGENT_UA_PATTERNS: RegExp[] = [ + /ChatGPT/i, + /OAI-SearchBot/i, + /OpenAI/i, + /GPTBot/i, + /Anthropic/i, + /Claude/i, + /Perplexity/i, + /Cursor/i, + /Cohere/i, + /CCBot/i, + /Bytespider/i, + /Amazonbot/i, + /Applebot-Extended/i, + /Diffbot/i, + /YouBot/i, + /MistralAI/i, + /OpenCode/i, + /aider/i, + /CodexCLI/i, +]; + +const PAGE_TO_MD: Record = { + "/": "/index.md", + "/privacy": "/privacy.md", + "/changelog": "/changelog.md", +}; + +const STATIC_ASSET_PATTERN = /\.(png|jpg|jpeg|svg|webp|ico|js|css|map|txt|xml|woff2?|ttf)$/i; + +const isAgentUserAgent = (userAgent: string): boolean => + AGENT_UA_PATTERNS.some((pattern) => pattern.test(userAgent)); + +const wantsMarkdown = (request: NextRequest): boolean => { + const accept = request.headers.get("accept") ?? ""; + if (accept.toLowerCase().includes("text/markdown")) return true; + const userAgent = request.headers.get("user-agent") ?? ""; + return isAgentUserAgent(userAgent); +}; + +export const proxy = (request: NextRequest): NextResponse => { + const url = request.nextUrl.clone(); + const { hostname, pathname } = url; + + if (hostname.startsWith("www.")) { + url.hostname = hostname.slice(4); + return NextResponse.redirect(url, 308); + } + + if ( + pathname.startsWith("/_next") || + pathname.startsWith("/api") || + pathname === "/script.js" || + pathname === "/sitemap.xml" || + pathname === "/robots.txt" || + pathname.endsWith(".md") || + pathname.endsWith(".txt") || + STATIC_ASSET_PATTERN.test(pathname) + ) { + const response = NextResponse.next(); + if (wantsMarkdown(request) || pathname.endsWith(".md") || pathname.endsWith(".txt")) { + response.headers.set("Vary", "Accept, User-Agent"); + } + return response; + } + + if (!wantsMarkdown(request)) { + return NextResponse.next(); + } + + const normalizedPath = pathname.replace(/\/+$/, "") || "/"; + const markdownPath = PAGE_TO_MD[normalizedPath] ?? "/404.md"; + + url.pathname = markdownPath; + const rewritten = NextResponse.rewrite(url); + rewritten.headers.set("Vary", "Accept, User-Agent"); + rewritten.headers.set("X-Robots-Tag", "noindex"); + return rewritten; +}; + +export const config = { + matcher: ["/((?!_next/static|_next/image|favicon\\.ico|script\\.js).*)"], +}; From 886aaf824c1de0bc08426b0344a4903de9c3d038 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 9 May 2026 05:17:52 +0000 Subject: [PATCH 04/13] feat(website): add JSON-LD, canonical URL, markdown alternate, skip link - WebSite + Organization + SoftwareApplication JSON-LD so agents can extract structured data without DOM parsing - alternates.canonical and metadataBase so duplicates are not indexed - pointing at /index.md and /llms.txt - Skip-to-content link for keyboard users Co-authored-by: Aiden Bai --- apps/website/app/layout.tsx | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx index 782295a10..66ebfefde 100644 --- a/apps/website/app/layout.tsx +++ b/apps/website/app/layout.tsx @@ -26,9 +26,16 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { + metadataBase: new URL("https://react-grab.com"), title: "React Grab", description: "Select an element → Give it to Cursor, Claude Code, etc → Make a change to your app", + alternates: { + canonical: "https://react-grab.com", + types: { + "text/markdown": "https://react-grab.com/index.md", + }, + }, icons: { icon: "https://react-grab.com/logo.png", shortcut: "https://react-grab.com/logo.png", @@ -53,6 +60,40 @@ export const metadata: Metadata = { }, }; +const STRUCTURED_DATA = { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "WebSite", + "@id": "https://react-grab.com/#website", + url: "https://react-grab.com", + name: "React Grab", + description: + "Select an element → Give it to Cursor, Claude Code, etc → Make a change to your app", + inLanguage: "en-US", + publisher: { "@id": "https://react-grab.com/#organization" }, + }, + { + "@type": "Organization", + "@id": "https://react-grab.com/#organization", + name: "React Grab", + url: "https://react-grab.com", + logo: "https://react-grab.com/logo.png", + sameAs: ["https://github.com/aidenybai/react-grab"], + }, + { + "@type": "SoftwareApplication", + name: "React Grab", + applicationCategory: "DeveloperApplication", + operatingSystem: "Web", + url: "https://react-grab.com", + description: + "Open-source dev-only script that adds an element picker to React apps so you can copy file paths, components, and HTML source for AI coding agents.", + offers: { "@type": "Offer", price: "0", priceCurrency: "USD" }, + }, + ], +}; + const RootLayout = ({ children, }: Readonly<{ @@ -60,9 +101,33 @@ const RootLayout = ({ }>) => { return ( + + + +