-
-
Notifications
You must be signed in to change notification settings - Fork 443
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Algolia DocSearch plugin (#1168)
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
- Loading branch information
Showing
10 changed files
with
796 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; |
Oops, something went wrong.