Skip to content

Commit

Permalink
Add Algolia DocSearch plugin (#1168)
Browse files Browse the repository at this point in the history
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
  • Loading branch information
delucis and HiDeoo committed Nov 29, 2023
1 parent 537b82f commit 8155d1a
Show file tree
Hide file tree
Showing 10 changed files with 796 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/witty-pans-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight-docsearch': minor
---

Adds a Starlight plugin to support using Algolia DocSearch as the Starlight search provider.
2 changes: 0 additions & 2 deletions docs/src/content/docs/guides/overriding-components.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
title: Overriding Components
description: Learn how to override Starlight鈥檚 built-in components to add custom elements to your documentation site鈥檚 UI.
sidebar:
badge: New
---

Starlight鈥檚 default UI and configuration options are designed to be flexible and work for a range of content. Much of Starlight's default appearance can be customized with [CSS](/guides/css-and-tailwind/) and [configuration options](/guides/customization/).
Expand Down
166 changes: 166 additions & 0 deletions docs/src/content/docs/guides/site-search.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
title: Site Search
description: Learn about Starlight鈥檚 built-in site search features and how to customize them.
sidebar:
badge: New
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

By default, Starlight sites include full-text search powered by [Pagefind](https://pagefind.app/), which is a fast and low-bandwidth search tool for static sites.

No configuration is required to enable search. Build and deploy your site, then use the search bar in the site header to find content.

## Hide content in search results

### Exclude a page

To exclude a page from your search index, add [`pagefind: false`](/reference/frontmatter/#pagefind) to the page鈥檚 frontmatter:

```md title="src/content/docs/not-indexed.md" ins={3}
---
title: Content to hide from search
pagefind: false
---
```

### Exclude part of a page

Pagefind will ignore content inside an element with the [`data-pagefind-ignore`](https://pagefind.app/docs/indexing/#removing-individual-elements-from-the-index) attribute.

In the following example, the first paragraph will display in search results, but the contents of the `<div>` will not:

```md title="src/content/docs/partially-indexed.md" ins="data-pagefind-ignore"
---
title: Partially indexed page
---

This text will be discoverable via search.

<div data-pagefind-ignore>

This text will be hidden from search.

</div>
```

## Alternative search providers

### Algolia DocSearch

If you have access to [Algolia鈥檚 DocSearch program](https://docsearch.algolia.com/) and want to use it instead of Pagefind, you can use the official Starlight DocSearch plugin.

1. Install `@astrojs/starlight-docsearch`:

<Tabs>

<TabItem label="npm">

```sh
npm install @astrojs/starlight-docsearch
```

</TabItem>

<TabItem label="pnpm">

```sh
pnpm install @astrojs/starlight-docsearch
```

</TabItem>

<TabItem label="Yarn">

```sh
yarn add @astrojs/starlight-docsearch
```

</TabItem>

</Tabs>

2. Add DocSearch to your Starlight [`plugins`](/reference/configuration/#plugins) config in `astro.config.mjs` and pass it your Algolia `appId`, `apiKey`, and `indexName`:

```js ins={4,10-16}
// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightDocSearch from '@astrojs/starlight-docsearch';

export default defineConfig({
integrations: [
starlight({
title: 'Site with DocSearch',
plugins: [
starlightDocSearch({
appId: 'YOUR_APP_ID',
apiKey: 'YOUR_SEARCH_API_KEY',
indexName: 'YOUR_INDEX_NAME',
}),
],
}),
],
});
```

With this updated configuration, the search bar on your site will now open an Algolia modal instead of the default search modal.

#### Translating the DocSearch UI

DocSearch only provides English UI strings by default.
Add translations of the modal UI for your language using Starlight鈥檚 built-in [internationalization system](/guides/i18n/#translate-starlights-ui).

1. Extend Starlight鈥檚 `i18n` content collection definition with the DocSearch schema in `src/content/config.ts`:

```js ins={4} ins=/{ extend: .+ }/
// src/content/config.ts
import { defineCollection } from 'astro:content';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
import { docSearchI18nSchema } from '@astrojs/starlight-docsearch/schema';

export const collections = {
docs: defineCollection({ schema: docsSchema() }),
i18n: defineCollection({
type: 'data',
schema: i18nSchema({ extend: docSearchI18nSchema() }),
}),
};
```

2. Add translations to your JSON files in `src/content/i18n/`.

These are the English defaults used by DocSearch:

```json title="src/content/i18n/en.json"
{
"docsearch.searchBox.resetButtonTitle": "Clear the query",
"docsearch.searchBox.resetButtonAriaLabel": "Clear the query",
"docsearch.searchBox.cancelButtonText": "Cancel",
"docsearch.searchBox.cancelButtonAriaLabel": "Cancel",

"docsearch.startScreen.recentSearchesTitle": "Recent",
"docsearch.startScreen.noRecentSearchesText": "No recent searches",
"docsearch.startScreen.saveRecentSearchButtonTitle": "Save this search",
"docsearch.startScreen.removeRecentSearchButtonTitle": "Remove this search from history",
"docsearch.startScreen.favoriteSearchesTitle": "Favorite",
"docsearch.startScreen.removeFavoriteSearchButtonTitle": "Remove this search from favorites",

"docsearch.errorScreen.titleText": "Unable to fetch results",
"docsearch.errorScreen.helpText": "You might want to check your network connection.",

"docsearch.footer.selectText": "to select",
"docsearch.footer.selectKeyAriaLabel": "Enter key",
"docsearch.footer.navigateText": "to navigate",
"docsearch.footer.navigateUpKeyAriaLabel": "Arrow up",
"docsearch.footer.navigateDownKeyAriaLabel": "Arrow down",
"docsearch.footer.closeText": "to close",
"docsearch.footer.closeKeyAriaLabel": "Escape key",
"docsearch.footer.searchByText": "Search by",

"docsearch.noResultsScreen.noResultsText": "No results for",
"docsearch.noResultsScreen.suggestedQueryText": "Try searching for",
"docsearch.noResultsScreen.reportMissingResultsText": "Believe this query should return results?",
"docsearch.noResultsScreen.reportMissingResultsLinkText": "Let us know."
}
```
139 changes: 139 additions & 0 deletions packages/docsearch/DocSearch.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
import type { Props } from '@astrojs/starlight/props';
import '@docsearch/css/dist/modal.css';
import type docsearch from '@docsearch/js';
import './variables.css';
const { labels } = Astro.props;
type DocSearchTranslationProps = Pick<
Parameters<typeof docsearch>[0],
'placeholder' | 'translations'
>;
const pick = (keyStart: string) =>
Object.fromEntries(
Object.entries(labels)
.filter(([key]) => key.startsWith(keyStart))
.map(([key, value]) => [key.replace(keyStart, ''), value])
);
const docsearchTranslations: DocSearchTranslationProps = {
placeholder: labels['search.label'],
translations: {
button: { buttonText: labels['search.label'], buttonAriaLabel: labels['search.label'] },
modal: {
searchBox: pick('docsearch.searchBox.'),
startScreen: pick('docsearch.startScreen.'),
errorScreen: pick('docsearch.errorScreen.'),
footer: pick('docsearch.footer.'),
noResultsScreen: pick('docsearch.noResultsScreen.'),
},
},
};
---

<sl-doc-search data-translations={JSON.stringify(docsearchTranslations)}>
<button type="button" class="DocSearch DocSearch-Button" aria-label={labels['search.label']}>
<span class="DocSearch-Button-Container">
<svg width="20" height="20" class="DocSearch-Search-Icon" viewBox="0 0 20 20">
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fill-rule="evenodd"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
<span class="DocSearch-Button-Placeholder">{labels['search.label']}</span>
</span>
<span class="DocSearch-Button-Keys"></span>
</button>
</sl-doc-search>

<style is:global>
.DocSearch-Button {
display: flex;
align-items: center;
gap: 0.5rem;
border: 0;
background-color: transparent;
color: var(--sl-color-gray-1);
cursor: pointer;
height: 2.5rem;
font-size: var(--sl-text-xl);
}
.DocSearch-Button-Container {
display: contents;
}
.DocSearch-Search-Icon {
width: 0.875em;
height: 0.875em;
stroke-width: 0.125rem;
}
.DocSearch-Button-Placeholder,
.DocSearch-Button-Keys,
.DocSearch-Button-Key {
display: none;
}

@media (min-width: 50rem) {
sl-doc-search {
width: 100%;
}

.DocSearch-Button {
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
padding-inline-start: 0.75rem;
padding-inline-end: 1rem;
background-color: var(--sl-color-black);
color: var(--sl-color-gray-2);
font-size: var(--sl-text-sm);
width: 100%;
max-width: 22rem;
}
.DocSearch-Button:hover {
border-color: var(--sl-color-gray-2);
color: var(--sl-color-white);
}

.DocSearch-Button-Placeholder,
.DocSearch-Button-Keys {
display: flex;
}
.DocSearch-Button-Keys {
margin-inline-start: auto;
}
.DocSearch-Button-Keys::before {
content: '';
width: 1em;
height: 1em;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17 2H7a5 5 0 0 0-5 5v10a5 5 0 0 0 5 5h10a5 5 0 0 0 5-5V7a5 5 0 0 0-5-5Zm3 15a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v10Z'%3E%3C/path%3E%3Cpath d='M15.293 6.707a1 1 0 1 1 1.414 1.414l-8.485 8.486a1 1 0 0 1-1.414-1.415l8.485-8.485Z'%3E%3C/path%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M17 2H7a5 5 0 0 0-5 5v10a5 5 0 0 0 5 5h10a5 5 0 0 0 5-5V7a5 5 0 0 0-5-5Zm3 15a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v10Z'%3E%3C/path%3E%3Cpath d='M15.293 6.707a1 1 0 1 1 1.414 1.414l-8.485 8.486a1 1 0 0 1-1.414-1.415l8.485-8.485Z'%3E%3C/path%3E%3C/svg%3E");
-webkit-mask-size: 100%;
mask-size: 100%;
background-color: currentColor;
}
}
</style>

<script>
import config from 'virtual:starlight/docsearch-config';

class StarlightDocSearch extends HTMLElement {
constructor() {
super();
window.addEventListener('DOMContentLoaded', async () => {
const { default: docsearch } = await import('@docsearch/js');
const options: Parameters<typeof docsearch>[0] = { ...config, container: 'sl-doc-search' };
try {
const translations = JSON.parse(this.dataset.translations || '{}');
Object.assign(options, translations);
} catch {}
docsearch(options);
});
}
}
customElements.define('sl-doc-search', StarlightDocSearch);
</script>
75 changes: 75 additions & 0 deletions packages/docsearch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { StarlightPlugin } from '@astrojs/starlight/types';
import type { AstroUserConfig, ViteUserConfig } from 'astro';
import { z } from 'astro/zod';

/** Config options users must provide for DocSearch to work. */
const DocSearchConfigSchema = z.object({
appId: z.string(),
apiKey: z.string(),
indexName: z.string(),
});
export type DocSearchConfig = z.input<typeof DocSearchConfigSchema>;

/** Starlight DocSearch plugin. */
export default function starlightDocSearch(userConfig: DocSearchConfig): StarlightPlugin {
const opts = DocSearchConfigSchema.parse(userConfig);
return {
name: 'starlight-docsearch',
hooks: {
setup({ addIntegration, config, logger, updateConfig }) {
// If the user has already has a custom override for the Search component, don't override it.
if (config.components?.Search) {
logger.warn(
'It looks like you already have a `Search` component override in your Starlight configuration.'
);
logger.warn(
'To render `@astrojs/starlight-docsearch`, remove the override for the `Search` component.\n'
);
} else {
// Otherwise, add the Search component override to the user's configuration.
updateConfig({
pagefind: false,
components: {
...config.components,
Search: '@astrojs/starlight-docsearch/DocSearch.astro',
},
});
}

// Add an Astro integration that injects a Vite plugin to expose
// the DocSearch config via a virtual module.
addIntegration({
name: 'starlight-docsearch',
hooks: {
'astro:config:setup': ({ updateConfig }) => {
updateConfig({
vite: {
plugins: [vitePluginDocSearch(opts)],
},
} satisfies AstroUserConfig);
},
},
});
},
},
};
}

/** Vite plugin that exposes the DocSearch config via virtual modules. */
function vitePluginDocSearch(config: DocSearchConfig): VitePlugin {
const moduleId = 'virtual:starlight/docsearch-config';
const resolvedModuleId = `\0${moduleId}`;
const moduleContent = `export default ${JSON.stringify(config)}`;

return {
name: 'vite-plugin-starlight-docsearch-config',
load(id) {
return id === resolvedModuleId ? moduleContent : undefined;
},
resolveId(id) {
return id === moduleId ? resolvedModuleId : undefined;
},
};
}

type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number];
Loading

0 comments on commit 8155d1a

Please sign in to comment.