reroute-js 0.20.0 → 0.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/_/basic/package.json +6 -6
  2. package/_/basic/src/client/App.tsx +2 -2
  3. package/_/basic/src/index.ts +1 -1
  4. package/_/blog/package.json +7 -7
  5. package/_/blog/src/client/App.tsx +3 -3
  6. package/_/blog/src/index.ts +1 -1
  7. package/_/redirects/package.json +7 -7
  8. package/_/rss/package.json +7 -7
  9. package/_/search/package.json +28 -0
  10. package/_/search/reroute.config.ts +37 -0
  11. package/_/search/src/client/App.tsx +11 -0
  12. package/_/search/src/client/components/SearchBox.tsx +325 -0
  13. package/_/search/src/client/components/SearchResults.tsx +114 -0
  14. package/_/search/src/client/index.html +27 -0
  15. package/_/search/src/client/index.tsx +8 -0
  16. package/_/search/src/client/lib/markdown-components.tsx +72 -0
  17. package/_/search/src/client/routes/[404].tsx +49 -0
  18. package/_/search/src/client/routes/blog/[slug].tsx +109 -0
  19. package/_/search/src/client/routes/blog/content/building-rest-apis.md +155 -0
  20. package/_/search/src/client/routes/blog/content/getting-started-with-react.md +92 -0
  21. package/_/search/src/client/routes/blog/content/typescript-best-practices.md +138 -0
  22. package/_/search/src/client/routes/docs/[slug].tsx +108 -0
  23. package/_/search/src/client/routes/docs/content/api-reference.md +425 -0
  24. package/_/search/src/client/routes/docs/content/content-collections.md +300 -0
  25. package/_/search/src/client/routes/docs/content/installation.md +142 -0
  26. package/_/search/src/client/routes/docs/content/routing.md +235 -0
  27. package/_/search/src/client/routes/index.tsx +113 -0
  28. package/_/search/src/client/routes/search.tsx +88 -0
  29. package/_/search/src/index.ts +10 -0
  30. package/_/search/tsconfig.json +26 -0
  31. package/_/sitemap/package.json +7 -7
  32. package/_/store/package.json +7 -7
  33. package/_/store/src/client/App.tsx +2 -9
  34. package/_/store/src/index.ts +1 -1
  35. package/_/streaming/package.json +7 -7
  36. package/cli/bin.d.ts +1 -1
  37. package/cli/bin.js +93 -19
  38. package/cli/bin.js.map +6 -6
  39. package/cli/index.d.ts +1 -1
  40. package/cli/index.js +4 -4
  41. package/cli/index.js.map +1 -1
  42. package/cli/src/cli.d.ts +1 -1
  43. package/cli/src/commands/analyze.d.ts +1 -1
  44. package/cli/src/commands/build.d.ts +1 -1
  45. package/cli/src/commands/build.d.ts.map +1 -1
  46. package/cli/src/commands/dev.d.ts +1 -1
  47. package/cli/src/commands/gen.d.ts +1 -1
  48. package/cli/src/commands/gen.d.ts.map +1 -1
  49. package/cli/src/commands/index.d.ts +1 -1
  50. package/cli/src/commands/init.d.ts +1 -1
  51. package/cli/src/commands/lib/command.d.ts +1 -1
  52. package/cli/src/commands/lib/env.d.ts +1 -1
  53. package/cli/src/commands/lib/index.d.ts +1 -1
  54. package/cli/src/commands/lib/log.d.ts +1 -1
  55. package/cli/src/commands/lib/markdown/availability.d.ts +1 -1
  56. package/cli/src/commands/lib/markdown/index.d.ts +1 -1
  57. package/cli/src/commands/lib/markdown/processor.d.ts +1 -1
  58. package/cli/src/commands/lib/production.d.ts +1 -1
  59. package/cli/src/commands/lib/server.d.ts +1 -1
  60. package/cli/src/commands/lib/streaming/analyzer.d.ts +1 -1
  61. package/cli/src/commands/lib/streaming/suspense.d.ts +1 -1
  62. package/cli/src/commands/lib/tailwind.d.ts +1 -1
  63. package/cli/src/commands/lib/version.d.ts +1 -1
  64. package/cli/src/commands/og.d.ts +1 -1
  65. package/cli/src/commands/start.d.ts +1 -1
  66. package/cli/src/index.d.ts +1 -1
  67. package/core/index.d.ts +1 -1
  68. package/core/index.js +11 -4
  69. package/core/index.js.map +4 -4
  70. package/core/src/bundler/hash.d.ts +1 -1
  71. package/core/src/bundler/index.d.ts +1 -1
  72. package/core/src/config.d.ts +1 -1
  73. package/core/src/config.d.ts.map +1 -1
  74. package/core/src/content/discovery.d.ts +1 -1
  75. package/core/src/content/index.d.ts +1 -1
  76. package/core/src/content/metadata.d.ts +1 -1
  77. package/core/src/index.d.ts +1 -1
  78. package/core/src/llms/extractor.d.ts +1 -1
  79. package/core/src/llms/formatter.d.ts +1 -1
  80. package/core/src/llms/full-generator.d.ts +1 -1
  81. package/core/src/llms/index-generator.d.ts +1 -1
  82. package/core/src/llms/index.d.ts +1 -1
  83. package/core/src/og/discovery.d.ts +1 -1
  84. package/core/src/og/index.d.ts +1 -1
  85. package/core/src/og/meta.d.ts +1 -1
  86. package/core/src/og/render.d.ts +1 -1
  87. package/core/src/og/types.d.ts +1 -1
  88. package/core/src/robots/discovery.d.ts +1 -1
  89. package/core/src/robots/generator.d.ts +1 -1
  90. package/core/src/robots/index.d.ts +1 -1
  91. package/core/src/robots/policies.d.ts +1 -1
  92. package/core/src/rss/discovery.d.ts +1 -1
  93. package/core/src/rss/generator.d.ts +1 -1
  94. package/core/src/rss/index.d.ts +1 -1
  95. package/core/src/sitemap/discovery.d.ts +1 -1
  96. package/core/src/sitemap/generator.d.ts +1 -1
  97. package/core/src/sitemap/index.d.ts +1 -1
  98. package/core/src/ssr/index.d.ts +1 -1
  99. package/core/src/ssr/lib/cache.d.ts +1 -1
  100. package/core/src/ssr/lib/collections.d.ts +1 -1
  101. package/core/src/ssr/lib/compression.d.ts +1 -1
  102. package/core/src/ssr/lib/compute/content.d.ts +1 -1
  103. package/core/src/ssr/lib/compute/index.d.ts +1 -1
  104. package/core/src/ssr/lib/compute/layouts.d.ts +1 -1
  105. package/core/src/ssr/lib/compute/routes.d.ts +1 -1
  106. package/core/src/ssr/lib/data.d.ts +1 -1
  107. package/core/src/ssr/lib/html.d.ts +1 -1
  108. package/core/src/ssr/lib/html.d.ts.map +1 -1
  109. package/core/src/ssr/lib/imports.d.ts +1 -1
  110. package/core/src/ssr/lib/index.d.ts +1 -1
  111. package/core/src/ssr/lib/layouts.d.ts +1 -1
  112. package/core/src/ssr/lib/metadata.d.ts +1 -1
  113. package/core/src/ssr/lib/mime.d.ts +1 -1
  114. package/core/src/ssr/lib/modules.d.ts +1 -1
  115. package/core/src/ssr/lib/path.d.ts +1 -1
  116. package/core/src/ssr/lib/preload.d.ts +1 -1
  117. package/core/src/ssr/lib/scripts/collections.d.ts +1 -1
  118. package/core/src/ssr/lib/scripts/data.d.ts +1 -1
  119. package/core/src/ssr/lib/scripts/escape.d.ts +1 -1
  120. package/core/src/ssr/lib/scripts/feeds.d.ts +1 -1
  121. package/core/src/ssr/lib/scripts/index.d.ts +1 -1
  122. package/core/src/ssr/lib/seed.d.ts +1 -1
  123. package/core/src/ssr/lib/setup.d.ts +1 -1
  124. package/core/src/ssr/lib/styles.d.ts +1 -1
  125. package/core/src/ssr/lib/template.d.ts +1 -1
  126. package/core/src/ssr/lib/types.d.ts +1 -1
  127. package/core/src/ssr/render.d.ts +1 -1
  128. package/core/src/ssr/stream.d.ts +1 -1
  129. package/elysia/index.d.ts +1 -1
  130. package/elysia/index.js +14 -4
  131. package/elysia/index.js.map +5 -5
  132. package/elysia/src/index.d.ts +1 -1
  133. package/elysia/src/libs/cache.d.ts +1 -1
  134. package/elysia/src/libs/caching.d.ts +1 -1
  135. package/elysia/src/libs/http.d.ts +1 -1
  136. package/elysia/src/libs/image.d.ts +1 -1
  137. package/elysia/src/libs/index.d.ts +1 -1
  138. package/elysia/src/libs/llms.d.ts +1 -1
  139. package/elysia/src/libs/response.d.ts +1 -1
  140. package/elysia/src/libs/serving.d.ts +1 -1
  141. package/elysia/src/plugin.d.ts +1 -1
  142. package/elysia/src/plugin.d.ts.map +1 -1
  143. package/elysia/src/routes/artifacts.d.ts +1 -1
  144. package/elysia/src/routes/content.d.ts +1 -1
  145. package/elysia/src/routes/image.d.ts +1 -1
  146. package/elysia/src/routes/index.d.ts +1 -1
  147. package/elysia/src/routes/internal.d.ts +1 -1
  148. package/elysia/src/routes/llms.d.ts +1 -1
  149. package/elysia/src/routes/og.d.ts +1 -1
  150. package/elysia/src/routes/redirects.d.ts +1 -1
  151. package/elysia/src/routes/robots.d.ts +1 -1
  152. package/elysia/src/routes/rss.d.ts +1 -1
  153. package/elysia/src/routes/search.d.ts +1 -1
  154. package/elysia/src/routes/sitemap.d.ts +1 -1
  155. package/elysia/src/routes/ssr.d.ts +1 -1
  156. package/elysia/src/routes/static.d.ts +1 -1
  157. package/elysia/src/types.d.ts +1 -1
  158. package/package.json +1 -1
  159. package/react/index.d.ts +1 -1
  160. package/react/index.js +14 -17
  161. package/react/index.js.map +3 -3
  162. package/react/src/components/ClientOnly.d.ts +1 -1
  163. package/react/src/components/ContentRoute.d.ts +1 -1
  164. package/react/src/components/Image.d.ts +1 -1
  165. package/react/src/components/LazyRoute.d.ts +1 -1
  166. package/react/src/components/Link.d.ts +1 -1
  167. package/react/src/components/Markdown.d.ts +1 -1
  168. package/react/src/components/Outlet.d.ts +1 -1
  169. package/react/src/components/index.d.ts +1 -1
  170. package/react/src/hooks/index.d.ts +1 -1
  171. package/react/src/hooks/useContent.d.ts +1 -1
  172. package/react/src/hooks/useData.d.ts +1 -1
  173. package/react/src/hooks/useFeed.d.ts +1 -1
  174. package/react/src/hooks/useLayoutData.d.ts +1 -1
  175. package/react/src/hooks/useLlms.d.ts +1 -1
  176. package/react/src/hooks/useNavigate.d.ts +1 -1
  177. package/react/src/hooks/useParams.d.ts +1 -1
  178. package/react/src/hooks/useRouter.d.ts +1 -1
  179. package/react/src/hooks/useSearch.d.ts +1 -1
  180. package/react/src/hooks/useSearchParams.d.ts +1 -1
  181. package/react/src/hooks/useToc.d.ts +1 -1
  182. package/react/src/index.d.ts +1 -1
  183. package/react/src/lib/collection.d.ts +1 -1
  184. package/react/src/lib/content.d.ts +1 -1
  185. package/react/src/lib/head.d.ts +1 -1
  186. package/react/src/lib/index.d.ts +1 -1
  187. package/react/src/lib/lazy-route.d.ts +1 -1
  188. package/react/src/lib/route-loader.d.ts +1 -1
  189. package/react/src/providers/ContentProvider.d.ts +1 -1
  190. package/react/src/providers/RerouteProvider.d.ts +5 -7
  191. package/react/src/providers/RerouteProvider.d.ts.map +1 -1
  192. package/react/src/providers/RouterProvider.d.ts +1 -1
  193. package/react/src/providers/index.d.ts +1 -1
  194. package/react/src/types/any.d.ts +1 -1
  195. package/react/src/types/index.d.ts +1 -1
  196. package/react/src/types/router.d.ts +1 -1
@@ -4,12 +4,12 @@
4
4
  "version": "0.0.0",
5
5
  "scripts": {
6
6
  "types": "tsc --noEmit",
7
- "gen": "reroute gen",
8
- "start": "reroute start --prod",
9
- "dev": "reroute dev",
10
- "build": "reroute build",
11
- "kill": "bunx kill-port 3000",
12
- "analyze": "reroute analyze --prod --open"
7
+ "gen": "reroute gen",
8
+ "start": "reroute start --prod",
9
+ "dev": "reroute dev",
10
+ "build": "reroute build",
11
+ "kill": "bunx kill-port 3000",
12
+ "analyze": "reroute analyze --prod --open"
13
13
  },
14
14
  "private": true,
15
15
  "dependencies": {
@@ -1,10 +1,10 @@
1
1
  import { RerouteProvider } from 'reroute-js/react';
2
- import { artifacts } from '../../.reroute';
2
+ import { bundle } from '../../.reroute';
3
3
 
4
4
  interface AppProps {
5
5
  pathname?: string;
6
6
  }
7
7
 
8
8
  export default function App({ pathname }: AppProps = {}) {
9
- return <RerouteProvider from={{ pathname, ...artifacts }} />;
9
+ return <RerouteProvider pathname={pathname} {...bundle} />;
10
10
  }
@@ -9,6 +9,6 @@ const app = new Elysia()
9
9
  console.log('[api] log from my api')
10
10
  return { message: 'Hello from server' }
11
11
  })
12
- .listen(Number(Bun.env.PORT || '3000'));
12
+ .listen(Bun.env.PORT || '3000');
13
13
 
14
14
  console.log(`[app] Running at ${app.server?.hostname}:${app.server?.port}`);
@@ -3,13 +3,13 @@
3
3
  "description": "A Reroute application",
4
4
  "version": "0.0.0",
5
5
  "scripts": {
6
- "types": "tsc --noEmit",
7
- "gen": "reroute gen",
8
- "start": "PORT=3001 reroute start --prod",
9
- "dev": "PORT=3001 reroute dev",
10
- "build": "reroute build",
11
- "kill": "bunx kill-port 3001",
12
- "analyze": "reroute analyze --prod --open"
6
+ "types": "tsc --noEmit",
7
+ "gen": "reroute gen",
8
+ "start": "PORT=3001 reroute start --prod",
9
+ "dev": "PORT=3001 reroute dev",
10
+ "build": "reroute build",
11
+ "kill": "bunx kill-port 3001",
12
+ "analyze": "reroute analyze --prod --open"
13
13
  },
14
14
  "private": true,
15
15
  "dependencies": {
@@ -1,11 +1,11 @@
1
1
  import { RerouteProvider } from 'reroute-js/react';
2
- import { artifacts } from '../../.reroute';
2
+ import { bundle } from '../../.reroute';
3
3
 
4
4
  interface AppProps {
5
5
  pathname?: string;
6
6
  }
7
7
 
8
8
  export default function App({ pathname }: AppProps = {}) {
9
- console.log('[app] app mounted');
10
- return <RerouteProvider from={{ pathname, ...artifacts }} />;
9
+ console.log('[app] blog mounted');
10
+ return <RerouteProvider pathname={pathname} {...bundle} />;
11
11
  }
@@ -9,7 +9,7 @@ const app = new Elysia()
9
9
  console.log('[api] log from my api');
10
10
  return { message: 'Hello from server' };
11
11
  })
12
- .listen(Number(Bun.env.PORT || '3001'));
12
+ .listen(Bun.env.PORT || '3001');
13
13
 
14
14
  console.log(
15
15
  `[app] Blog running at ${app.server?.hostname}:${app.server?.port} on ${Bun.env.NODE_ENV || 'development'}`,
@@ -4,13 +4,13 @@
4
4
  "description": "A Reroute application",
5
5
  "private": true,
6
6
  "scripts": {
7
- "types": "tsc --noEmit",
8
- "gen": "reroute gen",
9
- "start": "PORT=3002 reroute start --prod",
10
- "dev": "PORT=3002 reroute dev",
11
- "build": "reroute build",
12
- "kill": "bunx kill-port 3002",
13
- "analyze": "reroute analyze --prod --open"
7
+ "types": "tsc --noEmit",
8
+ "gen": "reroute gen",
9
+ "start": "PORT=3002 reroute start --prod",
10
+ "dev": "PORT=3002 reroute dev",
11
+ "build": "reroute build",
12
+ "kill": "bunx kill-port 3002",
13
+ "analyze": "reroute analyze --prod --open"
14
14
  },
15
15
  "dependencies": {
16
16
  "reroute-js": "latest",
@@ -4,13 +4,13 @@
4
4
  "description": "A Reroute application",
5
5
  "private": true,
6
6
  "scripts": {
7
- "types": "tsc --noEmit",
8
- "gen": "reroute gen",
9
- "start": "PORT=3003 reroute start --prod",
10
- "dev": "PORT=3003 reroute dev",
11
- "build": "reroute build",
12
- "kill": "bunx kill-port 3003",
13
- "analyze": "reroute analyze --prod --open"
7
+ "types": "tsc --noEmit",
8
+ "gen": "reroute gen",
9
+ "start": "PORT=3003 reroute start --prod",
10
+ "dev": "PORT=3003 reroute dev",
11
+ "build": "reroute build",
12
+ "kill": "bunx kill-port 3003",
13
+ "analyze": "reroute analyze --prod --open"
14
14
  },
15
15
  "dependencies": {
16
16
  "reroute-js": "latest",
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "reroute-search-example",
3
+ "description": "Search functionality example with content indexing",
4
+ "version": "0.0.0",
5
+ "scripts": {
6
+ "types": "tsc --noEmit",
7
+ "gen": "reroute gen",
8
+ "start": "PORT=3004 reroute start --prod",
9
+ "dev": "PORT=3004 reroute dev",
10
+ "build": "reroute build",
11
+ "kill": "bunx kill-port 3004"
12
+ },
13
+ "private": true,
14
+ "dependencies": {
15
+ "reroute-js": "latest",
16
+ "elysia": "^1.4.12",
17
+ "react": "^19.2.0",
18
+ "react-dom": "^19.2.0",
19
+ "sharp": "^0.34.4"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest",
23
+ "@types/react": "^19.2.2",
24
+ "@types/react-dom": "^19.2.2",
25
+ "typescript": "^5"
26
+ }
27
+ }
28
+
@@ -0,0 +1,37 @@
1
+ import { defineConfig } from 'reroute-js/core';
2
+
3
+ export default defineConfig({
4
+ options: {
5
+ app: './src/client/App',
6
+ maxAge: 3600,
7
+ compression: true,
8
+ },
9
+ markdown: {
10
+ components: './src/client/lib/markdown-components',
11
+ enableGfm: true,
12
+ theme: 'github-dark',
13
+ },
14
+ // 🔍 Search configuration
15
+ search: {
16
+ // Enable search index generation
17
+ enabled: true,
18
+
19
+ // Index specific collections (or omit to index all)
20
+ collections: ['blog', 'docs'],
21
+
22
+ // Include full content text in search (default: false)
23
+ // Set to false for lightweight index with only headings + metadata
24
+ includeContent: true,
25
+
26
+ // Maximum content length to index (if includeContent is true)
27
+ contentLimit: 5000,
28
+
29
+ // Meta fields to index from frontmatter
30
+ metaFields: ['title', 'description', 'excerpt', 'tags', 'category'],
31
+
32
+ // Heading level range to extract
33
+ minHeadingLevel: 1,
34
+ maxHeadingLevel: 6,
35
+ },
36
+ });
37
+
@@ -0,0 +1,11 @@
1
+ import { RerouteProvider } from 'reroute-js/react';
2
+ import { bundle } from '../../.reroute';
3
+
4
+ interface AppProps {
5
+ pathname?: string;
6
+ }
7
+
8
+ export default function App({ pathname }: AppProps = {}) {
9
+ return <RerouteProvider pathname={pathname} {...bundle} />;
10
+ }
11
+
@@ -0,0 +1,325 @@
1
+
2
+ import { useNavigate, useRouter, useSearch } from 'reroute-js/react';
3
+ import { useEffect, useState } from 'react';
4
+ import SearchResults from './SearchResults';
5
+
6
+ interface SearchBoxProps {
7
+ initialQuery?: string;
8
+ }
9
+
10
+ function SearchBox({ initialQuery = '' }: SearchBoxProps = {}) {
11
+ const router = useRouter();
12
+
13
+ // Initialize from URL search params (works on both server and client)
14
+ const urlQuery = router.searchParams.get('q') || initialQuery || '';
15
+ const [query, setQuery] = useState(urlQuery);
16
+ const [selectedCollections, setSelectedCollections] = useState<string[]>([]);
17
+ const [page, setPage] = useState(1);
18
+ const navigate = useNavigate();
19
+ const [mounted, setMounted] = useState(false);
20
+
21
+ // Mark as mounted after first render
22
+ useEffect(() => {
23
+ setMounted(true);
24
+ }, []);
25
+
26
+ // Sync search query to URL as user types (client-side only, no history entries)
27
+ useEffect(() => {
28
+ if (!mounted || typeof window === 'undefined') {
29
+ return;
30
+ }
31
+
32
+ const url = new URL(window.location.href);
33
+ const currentQ = url.searchParams.get('q') || '';
34
+
35
+ // Only update if different to avoid loops
36
+ if (currentQ !== query) {
37
+ if (query) {
38
+ url.searchParams.set('q', query);
39
+ } else {
40
+ url.searchParams.delete('q');
41
+ }
42
+ window.history.replaceState({}, '', url.toString());
43
+ }
44
+ }, [query, mounted]);
45
+
46
+ const { results, isLoading, error, total, totalPages, pageSize } = useSearch({
47
+ query,
48
+ page,
49
+ pageSize: 10,
50
+ collections: selectedCollections.length > 0 ? selectedCollections : undefined,
51
+ debounce: 400,
52
+ });
53
+
54
+ const toggleCollection = (collection: string) => {
55
+ setSelectedCollections((prev) =>
56
+ prev.includes(collection)
57
+ ? prev.filter((c) => c !== collection)
58
+ : [...prev, collection],
59
+ );
60
+ setPage(1); // Reset to first page when filters change
61
+ };
62
+
63
+ return (
64
+ <div
65
+ style={{
66
+ background: '#fff',
67
+ padding: '2rem',
68
+ borderRadius: '12px',
69
+ boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
70
+ }}
71
+ >
72
+ {/* Search Input */}
73
+ <div style={{ marginBottom: '1.5rem' }}>
74
+ <label
75
+ htmlFor="search-input"
76
+ style={{
77
+ display: 'block',
78
+ marginBottom: '0.5rem',
79
+ fontWeight: 600,
80
+ fontSize: '1.125rem',
81
+ }}
82
+ >
83
+ Search Content
84
+ </label>
85
+ <input
86
+ id="search-input"
87
+ type="search"
88
+ value={query}
89
+ onChange={(e) => {
90
+ setQuery(e.target.value);
91
+ setPage(1); // Reset to first page on new search
92
+ }}
93
+ onKeyDown={(e) => {
94
+ if (e.key === 'Enter' && query.trim()) {
95
+ const params = new URLSearchParams();
96
+ params.set('q', query);
97
+ if (selectedCollections.length > 0) {
98
+ params.set('collections', selectedCollections.join(','));
99
+ }
100
+ navigate(`/search?${params.toString()}`);
101
+ }
102
+ }}
103
+ placeholder="Type to search articles, docs, and more..."
104
+ style={{
105
+ width: '100%',
106
+ padding: '0.875rem 1rem',
107
+ fontSize: '1rem',
108
+ border: '2px solid #e0e0e0',
109
+ borderRadius: '8px',
110
+ outline: 'none',
111
+ transition: 'border-color 0.2s',
112
+ }}
113
+ onFocus={(e) => {
114
+ e.currentTarget.style.borderColor = '#0066cc';
115
+ }}
116
+ onBlur={(e) => {
117
+ e.currentTarget.style.borderColor = '#e0e0e0';
118
+ }}
119
+ data-testid="search-input"
120
+ />
121
+ </div>
122
+
123
+ {/* Collection Filters */}
124
+ <div style={{ marginBottom: '1.5rem' }}>
125
+ <label
126
+ style={{
127
+ display: 'block',
128
+ marginBottom: '0.5rem',
129
+ fontWeight: 600,
130
+ fontSize: '0.875rem',
131
+ color: '#666',
132
+ }}
133
+ >
134
+ Filter by Collection
135
+ </label>
136
+ <div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
137
+ <button
138
+ type="button"
139
+ onClick={() => toggleCollection('blog')}
140
+ style={{
141
+ padding: '0.5rem 1rem',
142
+ border: selectedCollections.includes('blog')
143
+ ? '2px solid #0066cc'
144
+ : '2px solid #e0e0e0',
145
+ background: selectedCollections.includes('blog')
146
+ ? '#e6f2ff'
147
+ : '#fff',
148
+ color: selectedCollections.includes('blog') ? '#0066cc' : '#666',
149
+ borderRadius: '20px',
150
+ cursor: 'pointer',
151
+ fontSize: '0.875rem',
152
+ fontWeight: 500,
153
+ transition: 'all 0.2s',
154
+ }}
155
+ >
156
+ {selectedCollections.includes('blog') ? '✓ ' : ''}Blog
157
+ </button>
158
+ <button
159
+ type="button"
160
+ onClick={() => toggleCollection('docs')}
161
+ style={{
162
+ padding: '0.5rem 1rem',
163
+ border: selectedCollections.includes('docs')
164
+ ? '2px solid #0066cc'
165
+ : '2px solid #e0e0e0',
166
+ background: selectedCollections.includes('docs')
167
+ ? '#e6f2ff'
168
+ : '#fff',
169
+ color: selectedCollections.includes('docs') ? '#0066cc' : '#666',
170
+ borderRadius: '20px',
171
+ cursor: 'pointer',
172
+ fontSize: '0.875rem',
173
+ fontWeight: 500,
174
+ transition: 'all 0.2s',
175
+ }}
176
+ >
177
+ {selectedCollections.includes('docs') ? '✓ ' : ''}Docs
178
+ </button>
179
+ </div>
180
+ </div>
181
+
182
+ {/* Search Status */}
183
+ {isLoading && (
184
+ <div
185
+ style={{
186
+ padding: '1rem',
187
+ background: '#f0f9ff',
188
+ border: '1px solid #bae6fd',
189
+ borderRadius: '8px',
190
+ color: '#0369a1',
191
+ }}
192
+ data-testid="search-loading"
193
+ >
194
+ 🔍 Searching...
195
+ </div>
196
+ )}
197
+
198
+ {error && (
199
+ <div
200
+ style={{
201
+ padding: '1rem',
202
+ background: '#fef2f2',
203
+ border: '1px solid #fecaca',
204
+ borderRadius: '8px',
205
+ color: '#dc2626',
206
+ }}
207
+ data-testid="search-error"
208
+ >
209
+ ❌ Error: {error.message}
210
+ </div>
211
+ )}
212
+
213
+ {/* Results Count */}
214
+ {!isLoading && query && !error && (
215
+ <div
216
+ style={{
217
+ padding: '0.75rem 0',
218
+ marginBottom: '1rem',
219
+ borderBottom: '1px solid #e0e0e0',
220
+ fontSize: '0.875rem',
221
+ color: '#666',
222
+ }}
223
+ data-testid={total > 0 ? 'search-results-count' : 'search-no-results'}
224
+ >
225
+ {total > 0 ? (
226
+ <span>
227
+ Found <strong>{total}</strong> result{total !== 1 ? 's' : ''}{' '}
228
+ for <strong>"{query}"</strong>
229
+ {totalPages > 1 && (
230
+ <span style={{ marginLeft: '0.5rem', color: '#999' }}>
231
+ (Page {page} of {totalPages})
232
+ </span>
233
+ )}
234
+ </span>
235
+ ) : (
236
+ <span>
237
+ No results found for <strong>"{query}"</strong>
238
+ </span>
239
+ )}
240
+ </div>
241
+ )}
242
+
243
+ {/* Results */}
244
+ {!isLoading && results.length > 0 && (
245
+ <>
246
+ <SearchResults results={results} />
247
+
248
+ {/* Pagination Controls */}
249
+ {totalPages > 1 && (
250
+ <div
251
+ style={{
252
+ marginTop: '2rem',
253
+ padding: '1rem 0',
254
+ borderTop: '1px solid #e0e0e0',
255
+ display: 'flex',
256
+ justifyContent: 'center',
257
+ alignItems: 'center',
258
+ gap: '1rem',
259
+ }}
260
+ >
261
+ <button
262
+ type="button"
263
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
264
+ disabled={page === 1}
265
+ style={{
266
+ padding: '0.5rem 1rem',
267
+ border: '2px solid #e0e0e0',
268
+ background: page === 1 ? '#f5f5f5' : '#fff',
269
+ color: page === 1 ? '#999' : '#333',
270
+ borderRadius: '6px',
271
+ cursor: page === 1 ? 'not-allowed' : 'pointer',
272
+ fontSize: '0.875rem',
273
+ fontWeight: 500,
274
+ transition: 'all 0.2s',
275
+ }}
276
+ >
277
+ ← Previous
278
+ </button>
279
+ <span style={{ fontSize: '0.875rem', color: '#666' }}>
280
+ Page <strong>{page}</strong> of <strong>{totalPages}</strong>
281
+ </span>
282
+ <button
283
+ type="button"
284
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
285
+ disabled={page === totalPages}
286
+ style={{
287
+ padding: '0.5rem 1rem',
288
+ border: '2px solid #e0e0e0',
289
+ background: page === totalPages ? '#f5f5f5' : '#fff',
290
+ color: page === totalPages ? '#999' : '#333',
291
+ borderRadius: '6px',
292
+ cursor: page === totalPages ? 'not-allowed' : 'pointer',
293
+ fontSize: '0.875rem',
294
+ fontWeight: 500,
295
+ transition: 'all 0.2s',
296
+ }}
297
+ >
298
+ Next →
299
+ </button>
300
+ </div>
301
+ )}
302
+ </>
303
+ )}
304
+
305
+ {/* Empty State */}
306
+ {!isLoading && !query && (
307
+ <div
308
+ style={{
309
+ padding: '3rem 1rem',
310
+ textAlign: 'center',
311
+ color: '#999',
312
+ }}
313
+ >
314
+ <div style={{ fontSize: '3rem', marginBottom: '1rem' }}>🔍</div>
315
+ <p style={{ fontSize: '1.125rem' }}>
316
+ Start typing to search through blog posts and documentation
317
+ </p>
318
+ </div>
319
+ )}
320
+ </div>
321
+ );
322
+ }
323
+
324
+ export default SearchBox;
325
+
@@ -0,0 +1,114 @@
1
+ import { Link, type SearchResultEntry } from 'reroute-js/react';
2
+
3
+ interface SearchResultsProps {
4
+ results: SearchResultEntry[];
5
+ }
6
+
7
+ function SearchResults({ results }: SearchResultsProps) {
8
+ return (
9
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }} data-testid="search-results-list">
10
+ {results.map((result) => (
11
+ <article
12
+ key={result.id}
13
+ style={{
14
+ padding: '1.5rem',
15
+ background: '#fafafa',
16
+ border: '1px solid #e0e0e0',
17
+ borderRadius: '8px',
18
+ transition: 'all 0.2s',
19
+ }}
20
+ onMouseEnter={(e) => {
21
+ e.currentTarget.style.background = '#f5f5f5';
22
+ e.currentTarget.style.borderColor = '#0066cc';
23
+ }}
24
+ onMouseLeave={(e) => {
25
+ e.currentTarget.style.background = '#fafafa';
26
+ e.currentTarget.style.borderColor = '#e0e0e0';
27
+ }}
28
+ data-testid={`search-result-${result.id}`}
29
+ >
30
+ {/* Collection Badge */}
31
+ <div style={{ marginBottom: '0.5rem' }}>
32
+ <span
33
+ style={{
34
+ display: 'inline-block',
35
+ padding: '0.25rem 0.75rem',
36
+ background: result.collection === 'blog' ? '#dbeafe' : '#dcfce7',
37
+ color: result.collection === 'blog' ? '#1e40af' : '#15803d',
38
+ borderRadius: '12px',
39
+ fontSize: '0.75rem',
40
+ fontWeight: 600,
41
+ textTransform: 'uppercase',
42
+ letterSpacing: '0.5px',
43
+ }}
44
+ >
45
+ {result.collection}
46
+ </span>
47
+ </div>
48
+
49
+ {/* Title */}
50
+ <h3 style={{ marginBottom: '0.75rem' }}>
51
+ <Link
52
+ to={`/${result.collection}/${result.slug}`}
53
+ style={{
54
+ fontSize: '1.5rem',
55
+ fontWeight: 600,
56
+ color: '#0066cc',
57
+ textDecoration: 'none',
58
+ }}
59
+ onMouseEnter={(e) => {
60
+ e.currentTarget.style.textDecoration = 'underline';
61
+ }}
62
+ onMouseLeave={(e) => {
63
+ e.currentTarget.style.textDecoration = 'none';
64
+ }}
65
+ >
66
+ {result.title}
67
+ </Link>
68
+ </h3>
69
+
70
+ {/* Excerpt */}
71
+ {result.excerpt && (
72
+ <p
73
+ style={{
74
+ marginBottom: '1rem',
75
+ color: '#666',
76
+ fontSize: '0.9375rem',
77
+ lineHeight: 1.6,
78
+ }}
79
+ >
80
+ {result.excerpt}
81
+ </p>
82
+ )}
83
+
84
+ {/* Read More Link */}
85
+ <div style={{ marginTop: '1rem', paddingTop: '1rem', borderTop: '1px solid #e0e0e0' }}>
86
+ <Link
87
+ to={`/${result.collection}/${result.slug}`}
88
+ style={{
89
+ color: '#0066cc',
90
+ textDecoration: 'none',
91
+ fontSize: '0.875rem',
92
+ fontWeight: 600,
93
+ display: 'inline-flex',
94
+ alignItems: 'center',
95
+ gap: '0.25rem',
96
+ }}
97
+ onMouseEnter={(e) => {
98
+ e.currentTarget.style.textDecoration = 'underline';
99
+ }}
100
+ onMouseLeave={(e) => {
101
+ e.currentTarget.style.textDecoration = 'none';
102
+ }}
103
+ >
104
+ Read full article →
105
+ </Link>
106
+ </div>
107
+ </article>
108
+ ))}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ export default SearchResults;
114
+
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Search Example - Reroute</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+ body {
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
15
+ 'Helvetica Neue', Arial, sans-serif;
16
+ line-height: 1.6;
17
+ color: #333;
18
+ background: #f5f5f5;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <div id="root"></div>
24
+ <script type="module" src="/index.tsx"></script>
25
+ </body>
26
+ </html>
27
+
@@ -0,0 +1,8 @@
1
+ import { hydrateRoot } from 'react-dom/client';
2
+ import App from './App';
3
+
4
+ const root = document.getElementById('root');
5
+ if (root) {
6
+ hydrateRoot(root, <App />);
7
+ }
8
+