@docslurp/contrib-astro 0.3.0 → 0.4.0

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.
@@ -1,13 +1,16 @@
1
1
  ---
2
+ import { assertBrowserSafeApiKey, assertSecureApiOrigin } from "@docslurp/sdk";
2
3
  /**
3
4
  * Browser key only: restrict it to this site's origin and the search scope.
4
- * workspaceId takes a slug, UUID or "default" (the key organization's default
5
+ * workspace takes an ID, slug or name, or "default" (the key organization's default
5
6
  * workspace) and defaults to "default"; apiUrl defaults to https://docslurp.io.
6
7
  */
7
- interface Props { clientKey: string; workspaceId?: string; apiUrl?: string; placeholder?: string; }
8
- const { clientKey, workspaceId = 'default', apiUrl = 'https://docslurp.io', placeholder = 'Search documents…' } = Astro.props;
8
+ interface Props { clientKey: string; workspace?: string; apiUrl?: string; placeholder?: string; }
9
+ const { clientKey, workspace = 'default', apiUrl = 'https://docslurp.io', placeholder = 'Search documents…' } = Astro.props;
10
+ assertBrowserSafeApiKey(clientKey, true);
11
+ assertSecureApiOrigin(apiUrl);
9
12
  ---
10
- <docslurp-search-box api-url={apiUrl} workspace-id={workspaceId} client-key={clientKey} placeholder={placeholder}></docslurp-search-box>
13
+ <docslurp-search-box api-url={apiUrl} workspace={workspace} client-key={clientKey} placeholder={placeholder}></docslurp-search-box>
11
14
  <noscript><p>Enable JavaScript to search the document collection.</p></noscript>
12
15
  <script>
13
16
  import { defineDocSlurpSearch } from '@docslurp/sdk/widget';
package/README.md CHANGED
@@ -10,13 +10,13 @@ import DocSlurpSearch from "@docslurp/contrib-astro";
10
10
  ---
11
11
  <!-- PUBLIC_DOCSLURP_CLIENT_KEY holds a browser key, safe to ship in page HTML. -->
12
12
  <DocSlurpSearch clientKey={import.meta.env.PUBLIC_DOCSLURP_CLIENT_KEY} />
13
- <!-- To search a named workspace: <DocSlurpSearch clientKey={...} workspaceId="product-manuals" /> -->
13
+ <!-- To search a named workspace: <DocSlurpSearch clientKey={...} workspace="product-manuals" /> -->
14
14
  ```
15
15
 
16
16
  | Prop | Default | Notes |
17
17
  | --- | --- | --- |
18
18
  | `clientKey` | Required | A browser key: search-only and restricted to your allowed websites. Server keys are refused in the browser. |
19
- | `workspaceId` | `default` | A workspace slug (such as `product-manuals`), UUID or `default`, the key organization's default workspace. See [Workspaces](https://docslurp.io/docs/http#workspaces). |
19
+ | `workspace` | `default` | A workspace ID, slug (such as `product-manuals`) or name, or `default`, the key organization's default workspace. See [Workspaces](https://docslurp.io/docs/http#workspaces). |
20
20
  | `apiUrl` | `https://docslurp.io` | The service origin. |
21
21
  | `placeholder` | `Search documents…` | Search box placeholder. |
22
22
 
@@ -38,28 +38,25 @@ Your server key is used only during the build. It never ends up in the generated
38
38
  import docslurpDocsIndex from '@docslurp/contrib-astro/build';
39
39
 
40
40
  integrations: [docslurpDocsIndex({
41
- routes: ['/docs', '/docs/getting-started'], // List every public page to index.
41
+ routes: ['/docs', '/docs/getting-started'], // Every public page to index: lowercase paths under /docs.
42
42
  site: 'https://docs.example.com', // Origin used for canonical page URLs.
43
43
  apiUrl: 'https://docslurp.io',
44
44
  apiKey: process.env.DOCSLURP_API_KEY, // Server key with the ingest scope.
45
- workspaceName: 'Product documentation',
46
- // To add to an existing workspace instead: workspaceId: 'existing-workspace-slug',
45
+ workspace: 'Product documentation', // ID, slug or name; created on the first build if none matches.
47
46
  })]
48
47
  ```
49
48
 
50
- Without `apiKey`, the build still writes `.docslurp/docs-index.json` and skips the upload. With a key, any indexing failure fails the build. A successful upload writes `docslurp-docs-search.json` to the client output with the workspace ID, so your search component can find it. Use `renderEnv` to pass environment variables that only the page renderer needs.
49
+ Without `apiKey`, the build still writes `.docslurp/docs-index.json` and skips the upload. With a key, any indexing failure fails the build. A successful upload writes `docslurp-docs-search.json` (`{ "workspace": "<workspace ID>" }`) to the client output, so your search component can find it. Use `renderEnv` to pass environment variables that only the page renderer needs. `apiUrl` and `site` default to `https://docslurp.io` and `workspace` to `DocSlurp documentation`, so set `site` and `workspace` for your own docs.
51
50
 
52
51
  Pages are indexed as text: the upload asks for `{ visionFallback: false, preserveImages: false, summarize: true }`, so rendered HTML skips page images and vision and gets per-page summaries. Pass `pipeline` with other pipeline overrides to change that.
53
52
 
54
53
  To index a site after it deploys (for example when the build has no API key), crawl the live pages instead:
55
54
 
56
55
  ```bash
57
- DOCSLURP_API_KEY=... DOCSLURP_DOCS_WORKSPACE_ID=<workspace id> \
56
+ DOCSLURP_API_KEY=... DOCSLURP_DOCS_WORKSPACE_ID=<workspace ID, slug or name> \
58
57
  node node_modules/@docslurp/contrib-astro/index-docs.mjs --site https://docs.example.com --routes routes.json --output .docslurp/docs-index.json
59
58
  ```
60
59
 
61
- `--routes` is a file or URL with a JSON array of paths. Only those paths on the site's own origin are fetched, and redirects are refused. Elements marked `data-search-exclude` (repeated boilerplate) are left out of the index.
60
+ Set `DOCSLURP_URL` for a self-hosted service (default `https://docslurp.io`). `--routes` is a file or URL with a JSON array of `/docs` paths. Only those paths on the site's own origin are fetched, and redirects are refused. Elements marked `data-search-exclude` (repeated boilerplate) are left out of the index.
62
61
 
63
62
  This integration does not render protected pages, discover dynamic routes, delete documents for removed pages, or support other Astro adapters, so list your public documentation routes explicitly. Keep public documentation in its own organization, and use keys from that organization for both indexing and search.
64
-
65
- For how DocSlurp indexes its own website, see [Dogfood docs search](https://github.com/DocSlurp/docslurp-oss/blob/main/docs/docs-search.md).
package/build.mjs CHANGED
@@ -2,7 +2,11 @@ import { spawn } from 'node:child_process';
2
2
  import { fileURLToPath } from 'node:url';
3
3
 
4
4
  /** Index rendered Astro Node standalone pages through DocSlurp's public API. */
5
- export default function docslurpDocsIndex({ routes, apiKey, apiUrl = 'https://docslurp.io', workspaceId, workspaceName = 'DocSlurp documentation', site = 'https://docslurp.io', renderEnv = {}, pipeline }) {
5
+ /**
6
+ * `workspace`: the workspace ID, slug or name to index into. A name that matches
7
+ * no workspace is created on the first build; later builds find it by name.
8
+ */
9
+ export default function docslurpDocsIndex({ routes, apiKey, apiUrl = 'https://docslurp.io', workspace = 'DocSlurp documentation', site = 'https://docslurp.io', renderEnv = {}, pipeline }) {
6
10
  let root;
7
11
  return {
8
12
  name: '@docslurp/contrib-astro/docs-index',
@@ -15,7 +19,7 @@ export default function docslurpDocsIndex({ routes, apiKey, apiUrl = 'https://do
15
19
  await new Promise((resolve, reject) => {
16
20
  const child = spawn(process.execPath, [fileURLToPath(new URL('./index-docs.mjs', import.meta.url)), '--entry', fileURLToPath(entry), '--output', fileURLToPath(output), '--public-config', fileURLToPath(new URL('docslurp-docs-search.json', dir))], {
17
21
  cwd: fileURLToPath(root), stdio: 'inherit',
18
- env: { ...process.env, ...renderEnv, ASTRO_NODE_AUTOSTART: 'disabled', DOCSLURP_API_KEY: apiKey ?? '', DOCSLURP_DOCS_INDEX_OPTIONS: JSON.stringify({ routes, apiUrl, workspaceId, workspaceName, site, ...(pipeline ? { pipeline } : {}) }) },
22
+ env: { ...process.env, ...renderEnv, ASTRO_NODE_AUTOSTART: 'disabled', DOCSLURP_API_KEY: apiKey ?? '', DOCSLURP_DOCS_INDEX_OPTIONS: JSON.stringify({ routes, apiUrl, workspace, site, ...(pipeline ? { pipeline } : {}) }) },
19
23
  });
20
24
  child.on('error', reject);
21
25
  child.on('exit', code => code === 0 ? resolve() : reject(new Error(`DocSlurp docs indexing exited with ${code}`)));
package/index-docs.mjs CHANGED
@@ -59,20 +59,18 @@ export async function crawlDocs({ site, routes }, fetchImpl = fetch) {
59
59
  */
60
60
  export const DEFAULT_DOCS_PIPELINE = Object.freeze({ visionFallback: false, preserveImages: false, summarize: true });
61
61
 
62
- export async function indexDocs(corpus, { apiUrl, workspaceId, workspaceName, pipeline = DEFAULT_DOCS_PIPELINE }, apiKey, fetchImpl = fetch) {
62
+ /**
63
+ * Uploads the rendered corpus into `workspace` (ID, slug or name): the server
64
+ * finds it, or creates a workspace with that name on the first build, so
65
+ * repeated builds keep landing in the same one.
66
+ */
67
+ export async function indexDocs(corpus, { apiUrl, workspace = 'DocSlurp documentation', pipeline = DEFAULT_DOCS_PIPELINE }, apiKey, fetchImpl = fetch) {
63
68
  const base = apiUrl.replace(/\/$/, '');
64
69
  const request = async (path, init = {}) => {
65
70
  const response = await fetchImpl(`${base}${path}`, { ...init, headers: { ...init.headers, Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(60_000) });
66
71
  if (!response.ok) throw new Error(`DocSlurp ${path}: HTTP ${response.status}`);
67
72
  return response.json();
68
73
  };
69
- let target = workspaceId;
70
- if (!target) {
71
- const { workspaces } = await request('/v1/workspaces');
72
- const matches = workspaces.filter(w => w.name === workspaceName);
73
- if (matches.length > 1) throw new Error('Multiple docs workspaces: set DOCSLURP_DOCS_WORKSPACE_ID');
74
- target = matches[0]?.id;
75
- }
76
74
  const form = new FormData();
77
75
  const canonicalIds = [];
78
76
  for (const doc of corpus) {
@@ -81,10 +79,9 @@ export async function indexDocs(corpus, { apiUrl, workspaceId, workspaceName, pi
81
79
  }
82
80
  form.set('canonicalIds', JSON.stringify(canonicalIds));
83
81
  if (pipeline) form.set('pipeline', JSON.stringify(pipeline));
84
- if (target) form.set('workspaceId', target);
85
- else { form.set('newWorkspace', 'true'); form.set('workspaceName', workspaceName); }
86
- const hash = createHash('sha256').update(JSON.stringify({ target: target ?? workspaceName, corpus, pipeline: pipeline ?? null })).digest('hex');
87
- const result = await request('/v1/ingest/bulk', { method: 'POST', headers: { 'Idempotency-Key': `docs-${hash}` }, body: form });
82
+ form.set('workspace', workspace);
83
+ const hash = createHash('sha256').update(JSON.stringify({ target: workspace, corpus, pipeline: pipeline ?? null })).digest('hex');
84
+ const result = await request('/v1/uploads/bulk', { method: 'POST', headers: { 'Idempotency-Key': `docs-${hash}` }, body: form });
88
85
  const deadline = Date.now() + 10 * 60_000;
89
86
  while (Date.now() < deadline) {
90
87
  const { run } = await request(`/v1/runs/${encodeURIComponent(result.run.id)}/progress`);
@@ -98,7 +95,7 @@ export async function indexDocs(corpus, { apiUrl, workspaceId, workspaceName, pi
98
95
  async function main() {
99
96
  const args = process.argv.slice(2);
100
97
  const value = name => args[args.indexOf(name) + 1];
101
- const options = { apiUrl: process.env.DOCSLURP_URL ?? 'https://docslurp.io', workspaceId: process.env.DOCSLURP_DOCS_WORKSPACE_ID || process.env.PUBLIC_DOCSLURP_DOCS_WORKSPACE_ID || undefined, workspaceName: 'DocSlurp documentation', ...(process.env.DOCSLURP_DOCS_PIPELINE ? { pipeline: JSON.parse(process.env.DOCSLURP_DOCS_PIPELINE) } : {}), ...JSON.parse(process.env.DOCSLURP_DOCS_INDEX_OPTIONS ?? '{}') };
98
+ const options = { apiUrl: process.env.DOCSLURP_URL ?? 'https://docslurp.io', workspace: process.env.DOCSLURP_DOCS_WORKSPACE_ID || process.env.PUBLIC_DOCSLURP_DOCS_WORKSPACE_ID || 'DocSlurp documentation', ...(process.env.DOCSLURP_DOCS_PIPELINE ? { pipeline: JSON.parse(process.env.DOCSLURP_DOCS_PIPELINE) } : {}), ...JSON.parse(process.env.DOCSLURP_DOCS_INDEX_OPTIONS ?? '{}') };
102
99
  const output = value('--output');
103
100
  let corpus;
104
101
  if (args.includes('--entry')) {
@@ -136,11 +133,11 @@ async function main() {
136
133
  if (key) {
137
134
  const indexed = await indexDocs(corpus, options, key);
138
135
  console.log('[docs-index]', indexed);
139
- if (args.includes('--public-config')) await writeFile(value('--public-config'), JSON.stringify({ workspaceId: indexed.workspaceId }));
136
+ if (args.includes('--public-config')) await writeFile(value('--public-config'), JSON.stringify({ workspace: indexed.workspaceId }));
140
137
  }
141
138
  else {
142
139
  console.log('[docs-index] No API key; remote indexing skipped');
143
- if (args.includes('--public-config')) await writeFile(value('--public-config'), JSON.stringify({ workspaceId: options.workspaceId ?? null }));
140
+ if (args.includes('--public-config')) await writeFile(value('--public-config'), JSON.stringify({ workspace: options.workspace ?? null }));
144
141
  }
145
142
  }
146
143
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docslurp/contrib-astro",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Astro search component for DocSlurp cited search, with a framework-free browser widget.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docslurp.io/docs/packages/contrib-astro",
@@ -27,7 +27,7 @@
27
27
  "index-docs.mjs"
28
28
  ],
29
29
  "dependencies": {
30
- "@docslurp/sdk": "^0.3.0"
30
+ "@docslurp/sdk": "^0.4.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -35,6 +35,6 @@
35
35
  "scripts": {
36
36
  "build": "pnpm --filter @docslurp/sdk build",
37
37
  "typecheck": "pnpm --filter @docslurp/sdk typecheck",
38
- "test": "node --test index-docs.test.mjs"
38
+ "test": "node ../../scripts/node-test.mjs index-docs.test.mjs"
39
39
  }
40
40
  }