@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 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;
@@ -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
+ };
@@ -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
@@ -0,0 +1,6 @@
1
+ export declare function requiredEnv(...names: string[]): string;
2
+ export declare function getAlgoliaEnv(): {
3
+ appId: string;
4
+ apiKey: string;
5
+ indexName: string;
6
+ };
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
+ }
@@ -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";
@@ -0,0 +1,3 @@
1
+ import type { AlgoliaHit, FolioPage } from "./types.js";
2
+ export declare function toAlgoliaRecord(page: FolioPage): Record<string, unknown>;
3
+ export declare function findFolioPage(hit: AlgoliaHit, pages: FolioPage[]): FolioPage | undefined;
@@ -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
+ }
@@ -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
+ }