{"_id":"@adunblock/server-tag-react-router","_rev":"4-bdc4ca0c741c5b024669834132e2df5a","name":"@adunblock/server-tag-react-router","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@adunblock/server-tag-react-router","version":"1.0.0","keywords":["react","react-router","client-side","script-loading","typescript"],"author":{"name":"Adunblock"},"license":"ISC","_id":"@adunblock/server-tag-react-router@1.0.0","maintainers":[{"name":"billadunblock","email":"bill@ad-unblock.com"}],"homepage":"https://github.com/adunblock/adunblock-server-tag#readme","bugs":{"url":"https://github.com/adunblock/adunblock-server-tag/issues"},"dist":{"shasum":"3cd0f45b70d06b9ec7e9977529d68f1142b3d117","tarball":"https://registry.npmjs.org/@adunblock/server-tag-react-router/-/server-tag-react-router-1.0.0.tgz","fileCount":10,"integrity":"sha512-nl1JoaSY6cOpnvQ64lLP0wqo+4q70bGli3UJY7EH1DUi4y3o0Jo76Pm8JGa8QOQCUP7mAdniCrxPCMtY3bQFWQ==","signatures":[{"sig":"MEYCIQDlXSlbY/Mf4PndQw1RviIEXec93iBvzaXQm62OFYK3ugIhAJviOqnIabsd2DecCUYMBHJJGJBRkK+Jp3OT1xuXgO11","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19808},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/esm/index.js","require":"./dist/index.js"}},"gitHead":"cd6cfd2abdc080ef0208f699d7cab9a76c7364d7","scripts":{"dev":"cd test-app-framework-mode && npm run dev","build":"npm run clean && tsc && tsc -p tsconfig.esm.json","clean":"rm -rf dist","test:e2e":"cd test-app-framework-mode && npm run test:e2e","test:build":"cd test-app-framework-mode && npm run build","test:start":"cd test-app-framework-mode && npm run start","prepublishOnly":"npm run build"},"_npmUser":{"name":"billadunblock","actor":{"name":"billadunblock","type":"user","email":"bill@ad-unblock.com"},"email":"bill@ad-unblock.com"},"repository":{"url":"git+https://github.com/adunblock/adunblock-server-tag.git","type":"git"},"_npmVersion":"11.0.0","description":"A React Router component for client-side script loading and rendering","directories":{},"_nodeVersion":"22.11.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5","@types/node":"^20","@types/react":"^19"},"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0","react-router-dom":">=6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/server-tag-react-router_1.0.0_1751334763995_0.748224846719945","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@adunblock/server-tag-react-router","version":"1.0.1","keywords":["react","react-router","client-side","script-loading","typescript"],"author":{"name":"Adunblock"},"license":"ISC","_id":"@adunblock/server-tag-react-router@1.0.1","maintainers":[{"name":"billadunblock","email":"bill@ad-unblock.com"}],"homepage":"https://github.com/adunblock/adunblock-server-tag#readme","bugs":{"url":"https://github.com/adunblock/adunblock-server-tag/issues"},"dist":{"shasum":"d6ccf4a84d61ed2adcaba7f57edffcd3b8d0786e","tarball":"https://registry.npmjs.org/@adunblock/server-tag-react-router/-/server-tag-react-router-1.0.1.tgz","fileCount":10,"integrity":"sha512-538dAYkenQABh6rlbzZ7xJcsPZ8is+t4VtzlsitP6iNRhz9C+MYvEHsWcRm61YjfNeLXqGYHduF2bovsp8VOYg==","signatures":[{"sig":"MEYCIQDTnIUzvkVfo70Fgw/i0u5xQ0vw4LmJt2+Tm9T83UBWEwIhAJB8CHQ8lEH8dHlKOnQerGITwT5fQcIBXp2TQ2xCCEt+","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19808},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/esm/index.js","require":"./dist/index.js"}},"gitHead":"e45bcbb8124d294566ec7fdf92a37df98c9c192f","scripts":{"dev":"cd test-app-framework-mode && npm run dev","build":"npm run clean && tsc && tsc -p tsconfig.esm.json","clean":"rm -rf dist","test:e2e":"cd test-app-framework-mode && npm run test:e2e","test:build":"cd test-app-framework-mode && npm run build","test:start":"cd test-app-framework-mode && npm run start","prepublishOnly":"npm run build"},"_npmUser":{"name":"billadunblock","email":"bill@ad-unblock.com"},"repository":{"url":"git+https://github.com/adunblock/adunblock-server-tag.git","type":"git"},"_npmVersion":"10.9.2","description":"A React Router component for client-side script loading and rendering","directories":{},"_nodeVersion":"22.17.1","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5","@types/node":"^20","@types/react":"^19"},"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0","react-router-dom":">=6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/server-tag-react-router_1.0.1_1755805165678_0.9784163482519288","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@adunblock/server-tag-react-router","version":"1.0.2","keywords":["react","react-router","client-side","script-loading","typescript"],"author":{"name":"Adunblock"},"license":"ISC","_id":"@adunblock/server-tag-react-router@1.0.2","maintainers":[{"name":"billadunblock","email":"bill@ad-unblock.com"}],"homepage":"https://github.com/adunblock/adunblock-server-tag#readme","bugs":{"url":"https://github.com/adunblock/adunblock-server-tag/issues"},"dist":{"shasum":"0a7fab24a97f2ce700b73decf1cd1ca4e3309474","tarball":"https://registry.npmjs.org/@adunblock/server-tag-react-router/-/server-tag-react-router-1.0.2.tgz","fileCount":10,"integrity":"sha512-ZFToInLDzGUmlR9YtJiiyimbQyJS01vXkAVoTdzM/wuVS1A89ZkpYTtGNChHW0EfbO2tUxemCgDqu1lyASSp6Q==","signatures":[{"sig":"MEUCIDkvvmjEKqRTdmlEbpO3eNpz+Mq0hRBKtBFMp9sRPGP4AiEArjTE2LiF9a6+FCBidDEnP2LMo3zokNyVr6SH5m07CZw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":21109},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/esm/index.js","require":"./dist/index.js"}},"gitHead":"bce08b4440c3a051bc49e3dbee52873a0ebf790e","scripts":{"dev":"cd test-app-framework-mode && npm run dev","build":"npm run clean && tsc && tsc -p tsconfig.esm.json","clean":"rm -rf dist","test:e2e":"cd test-app-framework-mode && npm run test:e2e","test:build":"cd test-app-framework-mode && npm run build","test:start":"cd test-app-framework-mode && npm run start","prepublishOnly":"npm run build"},"_npmUser":{"name":"billadunblock","email":"bill@ad-unblock.com"},"repository":{"url":"git+https://github.com/adunblock/adunblock-server-tag.git","type":"git"},"_npmVersion":"10.9.2","description":"A React Router component for client-side script loading and rendering","directories":{},"_nodeVersion":"22.17.1","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5","@types/node":"^20","@types/react":"^19"},"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0","react-router-dom":">=6.0.0"},"_npmOperationalInternal":{"tmp":"tmp/server-tag-react-router_1.0.2_1763235930838_0.4679955876978019","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@adunblock/server-tag-react-router","version":"1.1.0","description":"A React Router component for client-side script loading and rendering","main":"dist/index.js","module":"dist/esm/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/esm/index.js","require":"./dist/index.js"}},"scripts":{"build":"npm run clean && tsc && tsc -p tsconfig.esm.json","prepublishOnly":"npm run build","clean":"rm -rf dist","dev":"cd test-app-framework-mode && npm run dev","test:build":"cd test-app-framework-mode && npm run build","test:start":"cd test-app-framework-mode && npm run start","test:e2e":"cd test-app-framework-mode && npm run test:e2e"},"keywords":["react","react-router","client-side","script-loading","typescript"],"author":{"name":"Adunblock"},"license":"ISC","repository":{"type":"git","url":"git+https://github.com/adunblock/adunblock-server-tag.git"},"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0","react-router-dom":">=6.0.0"},"devDependencies":{"@types/node":"^20","@types/react":"^19","typescript":"^5"},"_id":"@adunblock/server-tag-react-router@1.1.0","gitHead":"d6edac9ccb68d4067d52754540ba247e27dc6774","bugs":{"url":"https://github.com/adunblock/adunblock-server-tag/issues"},"homepage":"https://github.com/adunblock/adunblock-server-tag#readme","_nodeVersion":"22.17.1","_npmVersion":"10.9.2","dist":{"integrity":"sha512-Hbk6kHOqTDNJkVJvknSCo5GkDueOmUSPqNZsVBLFp2IFT506fddgFSCy31S9xcLQ7FJrSXBDVWLnfRK3Aix24g==","shasum":"bc3eecc63c19df9e6c7c7da128259c1e0857455f","tarball":"https://registry.npmjs.org/@adunblock/server-tag-react-router/-/server-tag-react-router-1.1.0.tgz","fileCount":10,"unpackedSize":22693,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBp18i781BWxI5/M1mNLd47LnpFdvx51/1TbIeFBhTLMAiEA/y1jsjH/5Z0nSsRgGYoaBzbRbHiXw6KdSZen8anIeJQ="}]},"_npmUser":{"name":"billadunblock","email":"bill@ad-unblock.com"},"directories":{},"maintainers":[{"name":"billadunblock","email":"bill@ad-unblock.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/server-tag-react-router_1.1.0_1776446924864_0.41145115223018935"},"_hasShrinkwrap":false}},"time":{"created":"2025-07-01T01:52:43.892Z","modified":"2026-04-17T17:28:45.141Z","1.0.0":"2025-07-01T01:52:44.174Z","1.0.1":"2025-08-21T19:39:25.884Z","1.0.2":"2025-11-15T19:45:31.034Z","1.1.0":"2026-04-17T17:28:45.020Z"},"bugs":{"url":"https://github.com/adunblock/adunblock-server-tag/issues"},"author":{"name":"Adunblock"},"license":"ISC","homepage":"https://github.com/adunblock/adunblock-server-tag#readme","keywords":["react","react-router","client-side","script-loading","typescript"],"repository":{"type":"git","url":"git+https://github.com/adunblock/adunblock-server-tag.git"},"description":"A React Router component for client-side script loading and rendering","maintainers":[{"name":"billadunblock","email":"bill@ad-unblock.com"}],"readme":"# @adunblock/server-tag-react-router\n\nA React Router component for server-side script loading and rendering designed for React Router's **data mode** and **framework mode**. This package provides secure, cached script loading with TypeScript support for server-side rendering.\n\n## Features\n\n- 🚀 **Strictly Server-Side**: Scripts are fetched and rendered on the server. The loader will throw an error if called on the client.\n- 🔒 **Security**: Built-in URL validation (HTTP/HTTPS only)\n- ⚡ **Caching**: Configurable cache interval to reduce remote requests\n- 🎯 **Custom Rendering**: Override default script rendering with custom callbacks\n- 📘 **TypeScript**: Full TypeScript support with proper type definitions\n- ✅ **React Router Data/Framework Mode**: Built for React Router v7+ data and framework modes\n- 🔄 **SSR Optimized**: Designed for server-side rendering workflows\n\n## React Router Modes\n\nThis package is designed specifically for:\n- **Data Mode**: Using `createBrowserRouter` with loaders\n- **Framework Mode**: Using React Router with Vite plugin and SSR\n\n> **Note**: This package is for **server-side rendering only**. The script loader will throw an error if it runs in the browser, and it is not suitable for pure client-side applications.\n\n## Installation\n\n```bash\nnpm install @adunblock/server-tag-react-router\n```\n\n## Usage\n\nThis component is designed to be used with a React Router setup that supports server-side rendering (SSR). This can be a framework like Next.js or Remix (via \"Framework Mode\") or a custom server setup (via \"Data Mode\").\n\n### Data Mode with a Custom Server\n\nUsing `server-tag-react-router` in \"Data Mode\" requires a custom server-side rendering (SSR) setup. The `serverTagLoader` is designed to run exclusively on the server. Simply using `createBrowserRouter` in a client-only application will result in an error, as the loader would attempt to run in the browser.\n\nAs described in the [React Router Custom Server Rendering documentation](https://reactrouter.com/start/data/custom#server-rendering), a typical SSR setup involves:\n\n1.  **On the Server:** Using `createStaticHandler` and `createStaticRouter` to handle the request and render the initial HTML.\n2.  **On the Client:** Using `createBrowserRouter` to hydrate the server-rendered HTML.\n\nHere is a conceptual example of how you would integrate `serverTagLoader`.\n\n#### 1. Define Your Routes (Shared)\n\nThis route configuration is used by both the server and the client.\n\n```tsx\n// app/routes.tsx\nimport ServerTag, { serverTagLoader } from '@adunblock/server-tag-react-router';\n\n// Your page component\nfunction HomePage() {\n  return (\n    <div>\n      <ServerTag />\n      <h1>Home Page</h1>\n    </div>\n  );\n}\n\nexport const routes = [\n  {\n    path: \"/\",\n    element: <HomePage />,\n    loader: () => serverTagLoader({\n      remoteUrl: \"https://public.adunblocker.com/api/vendor_scripts\",\n      cacheInterval: 300 // Optional: cache for 5 minutes\n    })\n  }\n];\n```\n\n#### 2. Create a Server Entrypoint\n\nThis file contains the server-side rendering logic, typically run in an Express or Node.js environment.\n\n```tsx\n// entry.server.tsx\nimport { renderToString } from 'react-dom/server';\nimport {\n  createStaticHandler,\n  createStaticRouter,\n  StaticRouterProvider,\n} from 'react-router-dom/server';\nimport { routes } from './app/routes';\n\nexport async function render(req: Request) {\n  const { query, dataRoutes } = createStaticHandler(routes);\n  const context = await query(req);\n\n  if (context instanceof Response) {\n    return context;\n  }\n\n  const router = createStaticRouter(dataRoutes, context);\n  const html = renderToString(\n    <StaticRouterProvider router={router} context={context} />\n  );\n\n  return new Response(\"<!DOCTYPE html>\" + html, {\n    status: context.statusCode,\n    headers: { \"Content-Type\": \"text/html\" },\n  });\n}\n```\n*Note: A complete implementation would also need to serialize and send hydration data to the client.*\n\n#### 3. Create a Client Entrypoint\n\nThis file hydrates the server-rendered HTML in the browser.\n\n```tsx\n// entry.client.tsx\nimport { hydrateRoot } from 'react-dom/client';\nimport { createBrowserRouter, RouterProvider } from 'react-router-dom';\nimport { routes } from './app/routes';\n\n// In a full setup, you would pass hydrationData to createBrowserRouter\nconst router = createBrowserRouter(routes);\n\nhydrateRoot(document.getElementById('root')!, <RouterProvider router={router} />);\n```\n\n### Framework Mode Usage (Optimal Solution)\n\nFor React Router applications using a framework with SSR (e.g., Vite's SSR plugin), `ServerTag` offers a powerful and seamless way to manage both global and per-route scripts.\n\nSimply place the `<ServerTag />` component once in your root layout. It will automatically detect and render scripts loaded from **any active route's loader**, from the root down to the deepest child.\n\nThis allows for a flexible and optimal architecture:\n\n- **Global Scripts**: Define a loader in your root route to load scripts needed on every page.\n- **Per-Route Scripts**: Define loaders on specific routes to add scripts only for those pages.\n\n`ServerTag` will intelligently combine scripts from all sources and render them without duplicates.\n\n#### Example: Hybrid Approach\n\nThis example demonstrates loading a global analytics script from the root and a page-specific charting library for a dashboard page.\n\n**1. Update Your Root Layout**\n\nPlace `<ServerTag />` in the `<head>` of your root layout. This single component will handle rendering all scripts.\n\n```tsx\n// app/root.tsx\nimport ServerTag, { serverTagLoader } from '@adunblock/server-tag-react-router';\nimport { Outlet } from 'react-router-dom';\n\nexport async function loader() {\n  // Load global scripts for all pages\n  return await serverTagLoader({\n    remoteUrl: \"https://public.adunblocker.com/api/vendor_scripts\",\n  });\n}\n\nexport default function Root() {\n  return (\n    <html>\n      <head>\n        <ServerTag />\n        {/* ... other head elements */}\n      </head>\n      <body>\n        <Outlet />\n        {/* ... other body elements */}\n      </body>\n    </html>\n  );\n}\n```\n\n**2. Add a Page-Specific Loader**\n\nIn a specific route, add another loader. When combining with other page data, ensure that the object returned by `serverTagLoader` is spread into the final returned object from your loader. This makes the `js` array available at the top level of the loader data.\n\n```tsx\n// routes/dashboard.tsx\nimport { serverTagLoader } from '@adunblock/server-tag-react-router';\n\nexport async function loader() {\n  const [scriptData, dashboardData] = await Promise.all([\n    // Load scripts only for this page\n    serverTagLoader({\n      remoteUrl: `https://public.adunblocker.com/api/vendor_scripts`,\n    }),\n    // Load other page data\n    fetch('https://api.example.com/dashboard').then(r => r.json())\n  ]);\n\n  // Spread the scriptData to make the `js` property available\n  return { ...scriptData, dashboardData };\n}\n\nexport default function DashboardPage() {\n  // ... page component using dashboardData\n  return <div>Dashboard Content</div>;\n}\n```\n\nWith this setup, when a user navigates to the dashboard, `<ServerTag>` (in `root.tsx`) will render both the global scripts and the dashboard-specific scripts. On any other page, it will only render the global scripts.\n\n### Expected Remote Response Format\n\nThe remote URL should return a JSON response in this format (array directly):\n\n```json\n[\n  \"https://example.com/script1.js\",\n  \"https://example.com/script2.js\"\n]\n```\n\nThe default endpoint is `https://public.adunblocker.com/api/vendor_scripts`.\n\n> **Note**: For backward compatibility, the package also supports the legacy format `{\"js\": [...]}` but the new format (array directly) is preferred.\n\n### Custom Script Attributes\n\nPass extra attributes (including `data-*`) that should be rendered on every generated `<script>` tag:\n\n```tsx\nimport { ServerTag } from '@adunblock/server-tag-react-router';\n\nfunction MyPage() {\n  return (\n    <ServerTag\n      scriptAttributes={{\n        'data-code': 'abc123',\n        'data-source': 'server-tag',\n        defer: true,\n      }}\n    />\n  );\n}\n```\n\nRendering rules:\n\n- `true` → bare attribute (e.g. `defer`)\n- `false` / `undefined` / `null` → omitted\n- any other value → `key=\"value\"`\n\nWhen both `async` (prop) and `scriptAttributes.async` are set, the explicit `scriptAttributes` entry wins.\n\n### Custom Script Rendering\n\nOverride the default script rendering with a custom callback:\n\n```tsx\nimport ServerTag from '@adunblock/server-tag-react-router';\n\nfunction MyPage() {\n  return (\n    <div>\n      <ServerTag\n        async={false}\n        renderScript={({ js }) => (\n          <>\n            {js.map((src) => (\n              <script \n                key={src}\n                src={src}\n                defer\n                data-custom-attribute=\"server-loaded\"\n              />\n            ))}\n          </>\n        )}\n      />\n      <h1>My Page Content</h1>\n    </div>\n  );\n}\n```\n\n## API Reference\n\n### ServerTag Component\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `async` | `boolean` | `true` | Toggles the `async` attribute on the script tag. |\n| `scriptAttributes` | `ScriptAttributes` | `undefined` | Extra attributes applied to every generated `<script>` tag (e.g. `data-code`). |\n| `renderScript` | `function` | `undefined` | Custom script rendering function. |\n\n### Loader Function\n\n#### `serverTagLoader(config)`\n\nAsync function for loading scripts in React Router loaders. Works with both data mode and framework mode.\n\n**Parameters:**\n- `config.remoteUrl` (string, required): The URL to fetch script URLs from  \n- `config.cacheInterval` (number, optional): Cache duration in seconds (default: 300)\n\n**Returns:** `Promise<ServerTagLoaderData>`\n\n**Usage Examples:**\n\n```tsx\n// Simple loader\nexport const loader = () => serverTagLoader({\n  remoteUrl: \"https://public.adunblocker.com/api/vendor_scripts\"\n});\n\n// Combined with other data\nexport async function loader({ params }) {\n  const [scripts, data] = await Promise.all([\n    serverTagLoader({ \n      remoteUrl: `https://public.adunblocker.com/api/vendor_scripts`,\n      cacheInterval: 600 \n    }),\n    fetch('/api/data').then(r => r.json())\n  ]);\n  return { scripts, data };\n}\n```\n\n### Types\n\n```typescript\ntype ScriptAttributeValue = string | boolean | number | undefined;\n\ninterface ScriptAttributes {\n  [key: string]: ScriptAttributeValue;\n}\n\ninterface ServerTagProps {\n  async?: boolean;\n  scriptAttributes?: ScriptAttributes;\n  renderScript?: (jsFiles: { js: string[] }) => React.ReactNode;\n}\n\ninterface ServerTagLoaderArgs {\n  remoteUrl: string;\n  cacheInterval?: number;\n}\n\ninterface ServerTagLoaderData {\n  js: string[];\n}\n```\n\n## Development\n\n### Local Testing\n\n```bash\n# Install dependencies\nnpm install\n\n# Run development server\nnpm run dev\n\n# Build the package\nnpm run build\n\n# Run E2E tests\nnpm run test:e2e\n```\n\n### Project Structure\n\n```\nnodejs/react-router/\n├── src/\n│   ├── ServerTag.tsx    # Main component and loaders\n│   └── index.ts         # Package entry point\n├── test-app/            # React Router test application\n├── test-app/e2e/        # Playwright E2E tests\n└── dist/                # Built package files\n```\n\n## Server-Side Only Design\n\nThis package is designed exclusively for server-side rendering:\n\n- **Loaders run on the server** during route resolution\n- **Components render on the server** before hydration\n- **Scripts are included in the initial HTML** for optimal performance\n- **No client-side fetching** or dynamic script injection\n\n## Security\n\n- Only HTTP and HTTPS URLs are allowed\n- URL validation prevents malicious protocol usage\n- Server-side execution prevents client-side script injection\n- Built-in error handling for failed requests\n\n## Browser Compatibility\n\nThis package works with all browsers supported by React 19 and React Router DOM 7+ when used in server-side rendering contexts.\n\n## Migration from Client-Side Versions\n\nIf migrating from a client-side script loading solution:\n\n1. Move script fetching to React Router loaders\n2. Use `serverTagLoader` in your route configuration\n3. Replace client-side components with this server-side `ServerTag`","readmeFilename":"README.md"}