@nikala-ui/folio-algolia 0.1.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.
- package/README.md +148 -0
- package/dist/adapter.d.ts +4 -0
- package/dist/adapter.js +22 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +47 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +22 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/records.d.ts +3 -0
- package/dist/records.js +17 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# @nikala-ui/folio-algolia
|
|
2
|
+
|
|
3
|
+
Algolia search adapter for [Folio](https://github.com/nikala-ui/folio)
|
|
4
|
+
documentation sites.
|
|
5
|
+
|
|
6
|
+
The package connects Folio's search contract to an Algolia index. It keeps
|
|
7
|
+
site configuration in `docs.config.ts`, sends only search requests from the
|
|
8
|
+
browser, and maps Algolia hits back to the canonical pages already known by
|
|
9
|
+
Folio.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Folio `0.13.2` or newer;
|
|
14
|
+
- an Algolia index containing the same page URLs used by Folio;
|
|
15
|
+
- an Algolia Search-Only API key.
|
|
16
|
+
|
|
17
|
+
Never expose an Algolia Admin API key or a key that can write/delete records
|
|
18
|
+
in a browser application.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun add @nikala-ui/folio @nikala-ui/folio-algolia
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The package also works with npm, pnpm, and yarn.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Keep the adapter import and provider selection in `docs.config.ts`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { algoliaAdapter } from "@nikala-ui/folio-algolia";
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
search: {
|
|
37
|
+
enabled: true,
|
|
38
|
+
provider: algoliaAdapter,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The adapter implements Folio's `SearchAdapter` contract. The configuration
|
|
44
|
+
contains no Algolia request logic and no indexing code.
|
|
45
|
+
|
|
46
|
+
## Environment variables
|
|
47
|
+
|
|
48
|
+
Create a `.env` file in the documentation project:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
VITE_ALGOLIA_APP_ID=your_application_id
|
|
52
|
+
VITE_ALGOLIA_SEARCH_KEY=your_search_only_key
|
|
53
|
+
VITE_ALGOLIA_INDEX=your_index_name
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The adapter also accepts the equivalent server/runtime names:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ALGOLIA_APP_ID=your_application_id
|
|
60
|
+
ALGOLIA_SEARCH_KEY=your_search_only_key
|
|
61
|
+
ALGOLIA_INDEX=your_index_name
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`VITE_*` variables are required when the adapter runs in the browser. They
|
|
65
|
+
are public by design; use only a Search-Only key.
|
|
66
|
+
|
|
67
|
+
## Index records
|
|
68
|
+
|
|
69
|
+
Use the Folio page URL as Algolia's stable object ID. The helper preserves the
|
|
70
|
+
page metadata needed by Folio:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { toAlgoliaRecord } from "@nikala-ui/folio-algolia";
|
|
74
|
+
|
|
75
|
+
const record = toAlgoliaRecord(page);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The generated record contains `objectID`, `url`, `slug`, `title`,
|
|
79
|
+
`description`, `metadata` (frontmatter), and `headings` (table-of-contents
|
|
80
|
+
text).
|
|
81
|
+
|
|
82
|
+
Indexing is intentionally not part of this package. Run indexing from a
|
|
83
|
+
separate trusted server or CI job with an Algolia Admin key.
|
|
84
|
+
|
|
85
|
+
## Manual adapter construction
|
|
86
|
+
|
|
87
|
+
Use the factory when credentials come from another runtime configuration
|
|
88
|
+
system:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createAlgoliaAdapter } from "@nikala-ui/folio-algolia";
|
|
92
|
+
|
|
93
|
+
const adapter = createAlgoliaAdapter({
|
|
94
|
+
appId: "your_application_id",
|
|
95
|
+
apiKey: "your_search_only_key",
|
|
96
|
+
indexName: "your_index_name",
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For environment-based setup, the equivalent factory is available:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createAlgoliaAdapterFromEnv } from "@nikala-ui/folio-algolia";
|
|
104
|
+
|
|
105
|
+
const adapter = createAlgoliaAdapterFromEnv();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Most Folio sites should use the exported `algoliaAdapter` instance instead.
|
|
109
|
+
|
|
110
|
+
## Matching behavior
|
|
111
|
+
|
|
112
|
+
Folio passes the query and its local page catalog to the adapter. Algolia
|
|
113
|
+
performs the remote search, then the adapter matches returned hits by
|
|
114
|
+
`objectID`, `url`, or `slug`. Hits that do not belong to the current Folio
|
|
115
|
+
site are ignored, preventing an index from displaying routes that do not exist
|
|
116
|
+
in the running documentation project.
|
|
117
|
+
|
|
118
|
+
The empty query never makes a network request and returns Folio's current page
|
|
119
|
+
catalog.
|
|
120
|
+
|
|
121
|
+
## Errors
|
|
122
|
+
|
|
123
|
+
Remote failures throw `AlgoliaSearchError`, which includes the HTTP status when
|
|
124
|
+
one is available:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { AlgoliaSearchError } from "@nikala-ui/folio-algolia";
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Folio owns the UI behavior for displaying or handling search failures.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bun install
|
|
136
|
+
bun run typecheck
|
|
137
|
+
bun test tests
|
|
138
|
+
bun run build
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The package contains the Folio adapter contract, Algolia query client,
|
|
142
|
+
environment resolution, and record mapping. Index creation, crawling, admin
|
|
143
|
+
credentials, and deployment belong in the consuming project or a separate
|
|
144
|
+
indexing service.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AlgoliaAdapterOptions, FolioSearchAdapter } from "./types.js";
|
|
2
|
+
export declare function createAlgoliaAdapter(options: AlgoliaAdapterOptions): FolioSearchAdapter;
|
|
3
|
+
export declare function createAlgoliaAdapterFromEnv(): FolioSearchAdapter;
|
|
4
|
+
export declare const algoliaAdapter: FolioSearchAdapter;
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getAlgoliaEnv } from "./env.js";
|
|
2
|
+
import { searchAlgolia } from "./client.js";
|
|
3
|
+
export function createAlgoliaAdapter(options) {
|
|
4
|
+
return {
|
|
5
|
+
name: "algolia",
|
|
6
|
+
async search({ query, pages }) {
|
|
7
|
+
const normalizedQuery = query.trim();
|
|
8
|
+
if (!normalizedQuery)
|
|
9
|
+
return pages;
|
|
10
|
+
return searchAlgolia(options, normalizedQuery, pages);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createAlgoliaAdapterFromEnv() {
|
|
15
|
+
return createAlgoliaAdapter(getAlgoliaEnv());
|
|
16
|
+
}
|
|
17
|
+
export const algoliaAdapter = {
|
|
18
|
+
name: "algolia",
|
|
19
|
+
search(context) {
|
|
20
|
+
return createAlgoliaAdapterFromEnv().search(context);
|
|
21
|
+
},
|
|
22
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AlgoliaAdapterOptions, FolioPage } from "./types.js";
|
|
2
|
+
export declare class AlgoliaSearchError extends Error {
|
|
3
|
+
readonly status?: number;
|
|
4
|
+
constructor(message: string, status?: number);
|
|
5
|
+
}
|
|
6
|
+
export declare function searchAlgolia(options: Required<Pick<AlgoliaAdapterOptions, "appId" | "apiKey" | "indexName">> & Pick<AlgoliaAdapterOptions, "endpoint" | "hitsPerPage" | "fetch">, query: string, pages: FolioPage[]): Promise<FolioPage[]>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { findFolioPage } from "./records.js";
|
|
2
|
+
export class AlgoliaSearchError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "AlgoliaSearchError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function createSearchUrl(endpoint, indexName) {
|
|
11
|
+
return `${endpoint.replace(/\/$/, "")}/1/indexes/${encodeURIComponent(indexName)}/query`;
|
|
12
|
+
}
|
|
13
|
+
export async function searchAlgolia(options, query, pages) {
|
|
14
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
15
|
+
const endpoint = options.endpoint ?? `https://${options.appId}-dsn.algolia.net`;
|
|
16
|
+
const response = await fetchImpl(createSearchUrl(endpoint, options.indexName), {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"X-Algolia-Application-Id": options.appId,
|
|
21
|
+
"X-Algolia-API-Key": options.apiKey,
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
params: new URLSearchParams({
|
|
25
|
+
query,
|
|
26
|
+
hitsPerPage: String(options.hitsPerPage ?? 20),
|
|
27
|
+
}).toString(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new AlgoliaSearchError(`Algolia search failed with status ${response.status}`, response.status);
|
|
32
|
+
}
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
if (!Array.isArray(data.hits)) {
|
|
35
|
+
throw new AlgoliaSearchError("Algolia search returned an invalid response");
|
|
36
|
+
}
|
|
37
|
+
const results = [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (const hit of data.hits) {
|
|
40
|
+
const page = findFolioPage(hit, pages);
|
|
41
|
+
if (page && !seen.has(page.url)) {
|
|
42
|
+
seen.add(page.url);
|
|
43
|
+
results.push(page);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
package/dist/env.d.ts
ADDED
package/dist/env.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function getRuntimeEnv() {
|
|
2
|
+
const runtime = globalThis;
|
|
3
|
+
return runtime.process?.env
|
|
4
|
+
?? runtime.Bun?.env
|
|
5
|
+
?? import.meta.env
|
|
6
|
+
?? {};
|
|
7
|
+
}
|
|
8
|
+
export function requiredEnv(...names) {
|
|
9
|
+
const env = getRuntimeEnv();
|
|
10
|
+
for (const name of names) {
|
|
11
|
+
if (env[name])
|
|
12
|
+
return env[name];
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`Missing required environment variable: ${names.join(" or ")}`);
|
|
15
|
+
}
|
|
16
|
+
export function getAlgoliaEnv() {
|
|
17
|
+
return {
|
|
18
|
+
appId: requiredEnv("ALGOLIA_APP_ID", "VITE_ALGOLIA_APP_ID"),
|
|
19
|
+
apiKey: requiredEnv("ALGOLIA_SEARCH_KEY", "VITE_ALGOLIA_SEARCH_KEY"),
|
|
20
|
+
indexName: requiredEnv("ALGOLIA_INDEX", "VITE_ALGOLIA_INDEX"),
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { algoliaAdapter, createAlgoliaAdapter, createAlgoliaAdapterFromEnv } from "./adapter.js";
|
|
2
|
+
export { AlgoliaSearchError, searchAlgolia } from "./client.js";
|
|
3
|
+
export { getAlgoliaEnv, requiredEnv } from "./env.js";
|
|
4
|
+
export { findFolioPage, toAlgoliaRecord } from "./records.js";
|
|
5
|
+
export type { AlgoliaAdapterOptions, AlgoliaHit, AlgoliaSearchResponse, FolioPage, FolioSearchAdapter, FolioSearchContext, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { algoliaAdapter, createAlgoliaAdapter, createAlgoliaAdapterFromEnv } from "./adapter.js";
|
|
2
|
+
export { AlgoliaSearchError, searchAlgolia } from "./client.js";
|
|
3
|
+
export { getAlgoliaEnv, requiredEnv } from "./env.js";
|
|
4
|
+
export { findFolioPage, toAlgoliaRecord } from "./records.js";
|
package/dist/records.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function toAlgoliaRecord(page) {
|
|
2
|
+
return {
|
|
3
|
+
objectID: page.url,
|
|
4
|
+
slug: page.slug,
|
|
5
|
+
url: page.url,
|
|
6
|
+
title: page.title,
|
|
7
|
+
description: page.description,
|
|
8
|
+
metadata: page.frontmatter,
|
|
9
|
+
headings: page.toc.map((item) => item.text),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function findFolioPage(hit, pages) {
|
|
13
|
+
const identifier = hit.objectID ?? hit.url ?? hit.slug;
|
|
14
|
+
if (!identifier)
|
|
15
|
+
return undefined;
|
|
16
|
+
return pages.find((page) => page.url === identifier || page.slug === identifier);
|
|
17
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PageData } from "@nikala-ui/folio";
|
|
2
|
+
export interface AlgoliaAdapterOptions {
|
|
3
|
+
appId: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
indexName: string;
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
hitsPerPage?: number;
|
|
8
|
+
fetch?: typeof globalThis.fetch;
|
|
9
|
+
}
|
|
10
|
+
export interface AlgoliaHit {
|
|
11
|
+
objectID?: string;
|
|
12
|
+
slug?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface AlgoliaSearchResponse {
|
|
16
|
+
hits?: AlgoliaHit[];
|
|
17
|
+
}
|
|
18
|
+
export interface FolioSearchContext {
|
|
19
|
+
query: string;
|
|
20
|
+
pages: PageData[];
|
|
21
|
+
}
|
|
22
|
+
export interface FolioSearchAdapter {
|
|
23
|
+
name: string;
|
|
24
|
+
search: (context: FolioSearchContext) => PageData[] | Promise<PageData[]>;
|
|
25
|
+
}
|
|
26
|
+
export type FolioPage = PageData;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nikala-ui/folio-algolia",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Algolia search adapter for Folio documentation sites",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": ["folio", "algolia", "search", "documentation", "solidjs"],
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/nikala-ui/folio-algolia.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/nikala-ui/folio-algolia#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nikala-ui/folio-algolia/issues"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": ["dist"],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
29
|
+
"test": "bun test tests",
|
|
30
|
+
"check": "bun run typecheck && bun run test && bun run build",
|
|
31
|
+
"package:check": "bun run check && npm pack --dry-run"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@nikala-ui/folio": ">=0.13.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@nikala-ui/folio": "^0.13.2",
|
|
38
|
+
"typescript": "^7.0.2"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|