React Grab does NOT collect, store, or transmit any personal data. Specifically:
@@ -86,7 +92,7 @@ const PrivacyPage = () => {
-
How React Grab Works
+
How React Grab Works
React Grab operates entirely locally in your browser. When you use the extension:
@@ -99,7 +105,7 @@ const PrivacyPage = () => {
-
Permissions
+
Permissions
The extension requires the following permissions:
@@ -122,7 +128,7 @@ const PrivacyPage = () => {
-
Local Storage
+
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
@@ -131,7 +137,7 @@ const PrivacyPage = () => {
-
Third-Party Services
+
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
@@ -140,7 +146,7 @@ const PrivacyPage = () => {
-
Open Source
+
Open Source
React Grab is open source software. You can review the complete source code on{" "}
{
-
Changes to This Policy
+
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.
@@ -164,7 +170,7 @@ const PrivacyPage = () => {
-
Contact
+
Contact
If you have questions about this privacy policy, please open an issue on our{" "}
{
-
Summary
+
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.
@@ -197,7 +203,7 @@ const PrivacyPage = () => {
-
+
);
};
diff --git a/apps/website/app/sitemap.md/route.ts b/apps/website/app/sitemap.md/route.ts
new file mode 100644
index 000000000..1c3238184
--- /dev/null
+++ b/apps/website/app/sitemap.md/route.ts
@@ -0,0 +1,59 @@
+import { join } from "path";
+import { discoverPageRoutes } from "@/utils/discover-page-routes";
+
+const BASE_URL = "https://react-grab.com";
+
+interface SitemapEntry {
+ url: string;
+ title: string;
+}
+
+const PAGE_TITLES: Record = {
+ "/": "Home",
+ "/changelog": "Changelog",
+ "/privacy": "Privacy Policy",
+};
+
+export const dynamic = "force-static";
+
+export const GET = (): Response => {
+ const appDirectory = join(process.cwd(), "app");
+ const routes = discoverPageRoutes(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/app/sitemap.ts b/apps/website/app/sitemap.ts
index 1978a3223..061a6d101 100644
--- a/apps/website/app/sitemap.ts
+++ b/apps/website/app/sitemap.ts
@@ -1,44 +1,12 @@
import type { MetadataRoute } from "next";
-import { readdirSync, statSync } from "fs";
import { join } from "path";
+import { discoverPageRoutes } from "@/utils/discover-page-routes";
const BASE_URL = "https://react-grab.com";
-const EXCLUDED_PATHS = new Set(["api", "open-file"]);
-
-const getRoutes = (directory: string, basePath = ""): Array => {
- const routes: Array = [];
- const entries = readdirSync(directory);
-
- for (const entry of entries) {
- const fullPath = join(directory, entry);
- const routePath = basePath ? `${basePath}/${entry}` : entry;
-
- if (EXCLUDED_PATHS.has(entry)) {
- continue;
- }
-
- const stat = statSync(fullPath);
-
- if (stat.isDirectory()) {
- const hasPage = readdirSync(fullPath).some(
- (file) => file === "page.tsx" || file === "page.ts",
- );
-
- if (hasPage) {
- routes.push(routePath);
- }
-
- routes.push(...getRoutes(fullPath, routePath));
- }
- }
-
- return routes;
-};
-
const sitemap = (): MetadataRoute.Sitemap => {
const appDirectory = join(process.cwd(), "app");
- const routes = getRoutes(appDirectory);
+ const routes = discoverPageRoutes(appDirectory);
const sitemapEntries: MetadataRoute.Sitemap = [
{
diff --git a/apps/website/next.config.ts b/apps/website/next.config.ts
index 8e1d15ee7..98f42dd19 100644
--- a/apps/website/next.config.ts
+++ b/apps/website/next.config.ts
@@ -43,36 +43,31 @@ const nextConfig: NextConfig = {
];
},
headers: async () => {
+ const markdownContentType = { key: "Content-Type", value: "text/markdown; charset=utf-8" };
+ const allowAllOrigins = { key: "Access-Control-Allow-Origin", value: "*" };
+
return [
{
- source: "/",
- headers: [
- {
- key: "Vary",
- value: "Accept",
- },
- ],
+ source: "/:path*\\.md",
+ headers: [markdownContentType, allowAllOrigins],
+ },
+ {
+ source: "/llms.txt",
+ headers: [markdownContentType, allowAllOrigins],
+ },
+ {
+ source: "/llms-full.txt",
+ headers: [markdownContentType, allowAllOrigins],
},
];
},
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..9dee42607
--- /dev/null
+++ b/apps/website/proxy.ts
@@ -0,0 +1,78 @@
+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 { pathname } = url;
+
+ 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)
+ ) {
+ return NextResponse.next();
+ }
+
+ if (!wantsMarkdown(request)) {
+ const response = NextResponse.next();
+ response.headers.append("Vary", "Accept, User-Agent");
+ return response;
+ }
+
+ const normalizedPath = pathname.replace(/\/+$/, "") || "/";
+ const markdownPath = PAGE_TO_MD[normalizedPath] ?? "/404.md";
+
+ url.pathname = markdownPath;
+ const rewritten = NextResponse.rewrite(url);
+ rewritten.headers.append("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).*)"],
+};
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/install.md b/apps/website/public/install.md
index c7f94f27c..27a3dbfbc 100644
--- a/apps/website/public/install.md
+++ b/apps/website/public/install.md
@@ -90,6 +90,7 @@ Add to `pages/_document.tsx`:
```jsx
import { Html, Head, Main, NextScript } from "next/document";
+import Script from "next/script";
export default function Document() {
return (
diff --git a/apps/website/public/llms-full.txt b/apps/website/public/llms-full.txt
new file mode 100644
index 000000000..a6b7b43d0
--- /dev/null
+++ b/apps/website/public/llms-full.txt
@@ -0,0 +1,185 @@
+# 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" && (
+
+ )}
+
+ {children}
+
+ );
+}
+```
+
+### Next.js (Pages router)
+
+Add this into your `pages/_document.tsx`:
+
+```jsx
+import { Html, Head, Main, NextScript } from "next/document";
+import Script from "next/script";
+
+export default function Document() {
+ return (
+
+
+ {process.env.NODE_ENV === "development" && (
+
+ )}
+
+
+
+
+
+
+ );
+}
+```
+
+### Vite
+
+Add this to your `index.html`:
+
+```html
+
+
+
+
+
+
+
+
+
+
+```
+
+### 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" && (
-
- )}
-
- {children}
-
- );
-}
-```
-
-### Next.js (Pages router)
-
-Add this into your `pages/_document.tsx`:
-
-```jsx
-import { Html, Head, Main, NextScript } from "next/document";
-
-export default function Document() {
- return (
-
-
- {process.env.NODE_ENV === "development" && (
-
- )}
-
-
-
-
-
-
- );
-}
-```
-
-### Vite
-
-Add this to your `index.html`:
-
-```html
-
-
-
-
-
-
-
-
-
-
-```
-
-### 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.
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.
diff --git a/apps/website/utils/discover-page-routes.ts b/apps/website/utils/discover-page-routes.ts
new file mode 100644
index 000000000..6ab92a5e7
--- /dev/null
+++ b/apps/website/utils/discover-page-routes.ts
@@ -0,0 +1,29 @@
+import { readdirSync, statSync } from "fs";
+import { join } from "path";
+
+export const SITEMAP_EXCLUDED_PATHS = new Set(["api", "open-file"]);
+
+export const discoverPageRoutes = (directory: string, basePath = ""): string[] => {
+ const routes: string[] = [];
+ const entries = readdirSync(directory);
+
+ for (const entry of entries) {
+ if (SITEMAP_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(...discoverPageRoutes(fullPath, routePath));
+ }
+
+ return routes;
+};