diff --git a/.env.example b/.env.example index 27268fe28..ead937ffa 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,12 @@ -ALGOLIA_API_KEY=algolia_api_key -ALGOLIA_APPLICATION_ID=algolia_application_id +TYPESENSE_URL=https://your-cluster.a1.typesense.net +TYPESENSE_PUBLIC_API_KEY=your_search_only_api_key +TYPESENSE_PRIVATE_API_KEY=typesense_private_api_key_required_for_indexing_only +# Optional. Overrides the branch-derived read alias. +# TYPESENSE_COLLECTION=directus-docs DIRECTUS_URL=https://marketing-directus-url.com GOOGLE_TAG_MANAGER_ID=GTM-PTLT3GH POSTHOG_API_HOST=https://directus.io/ingest POSTHOG_API_KEY=phc_secret_key_here NUXT_PUBLIC_SITE_URL=https://directus.io +# Optional. Fine-grained PAT, public repos read-only. Required for code search and raises GitHub raw rate limits. +# GITHUB_TOKEN=github_pat_... diff --git a/.github/workflows/search-index.yml b/.github/workflows/search-index.yml new file mode 100644 index 000000000..12f49ec83 --- /dev/null +++ b/.github/workflows/search-index.yml @@ -0,0 +1,76 @@ +name: Search Index + +on: + push: + branches: + - main + paths: &index-paths + - 'content/**' + - 'scripts/index-docs.ts' + - 'scripts/index-docs-chunker.ts' + - 'shared/utils/parseTypesenseUrl.ts' + - 'shared/utils/docsSections.ts' + - 'app/utils/slugify.ts' + - 'server/data/synonyms.ts' + - 'lib/typesenseAlias.ts' + - 'pnpm-lock.yaml' + - '.github/workflows/search-index.yml' + pull_request: + branches: + - main + paths: *index-paths + workflow_dispatch: + +concurrency: + group: search-index-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + preview-index: + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + env: + TYPESENSE_URL: ${{ secrets.TYPESENSE_URL }} + TYPESENSE_PUBLIC_API_KEY: ${{ secrets.TYPESENSE_PUBLIC_API_KEY }} + TYPESENSE_PRIVATE_API_KEY: ${{ secrets.TYPESENSE_PRIVATE_API_KEY }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Index preview collection + run: pnpm index:docs + + prod-index: + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + env: + TYPESENSE_URL: ${{ secrets.TYPESENSE_URL }} + TYPESENSE_PUBLIC_API_KEY: ${{ secrets.TYPESENSE_PUBLIC_API_KEY }} + TYPESENSE_PRIVATE_API_KEY: ${{ secrets.TYPESENSE_PRIVATE_API_KEY }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Index production collection + run: pnpm index:docs diff --git a/.nuxtrc b/.nuxtrc new file mode 100644 index 000000000..ce850a63b --- /dev/null +++ b/.nuxtrc @@ -0,0 +1 @@ +setups.@nuxt/test-utils="4.0.3" diff --git a/README.md b/README.md index 6fdffce77..8675d90f9 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,14 @@ pnpm build ### Repository Tooling -The repository includes scripts that keep docs routes stable when files move. +The repository includes scripts that keep docs routes stable when files move and that index the docs into Typesense. ```bash pnpm stable-ids:ensure # Add missing stableId frontmatter pnpm stable-ids:check # Validate stableId frontmatter pnpm redirects:sync # Update redirects.json for moved pages pnpm redirects:check # Check redirect coverage without writing files +pnpm index:docs # Build the search index in Typesense pnpm typecheck:scripts # Type check repository scripts ``` @@ -109,9 +110,45 @@ The documentation automatically deploys to Vercel when changes are merged into t - [GitHub Issues](https://github.com/directus/docs/issues) (Report Bugs) - [Roadmap](https://roadmap.directus.io) (Roadmap & Feature Requests) -## Making changes to Algolia Search +## 🔍 Search -The docs make use of the Algolia Crawler to index the content. The crawler is found at the bottom left in the Algolia dashboard under `Data Sources > Crawler > directus`. To make changes on how the crawler works, go to the `Editor` tab and make your changes. By default the crawler runs once a day but you can also manually run it. In order to tweak the ranking of search results, go to the `Search > Configure > Index > Configuration > Ranking and Sorting` tab. +Search is powered by [Typesense](https://typesense.org). The browser palette (`UCommandPalette`-based) lives at `app/components/DocsSearchPalette.vue` and queries Typesense directly via `app/services/typesenseService.ts`. The official `typesense` npm client is used by the indexer only. + +### Indexing + +The indexer at `scripts/index-docs.ts` walks `/content`, chunks each Markdown page, attaches synonyms, and pushes everything to Typesense. OpenAPI indexing is deferred to a later branch. Run it locally with: + +```bash +pnpm index:docs +``` + +CI runs the same command on every push to `main` (production index) and on every PR commit (per-branch preview index). See `.github/workflows/search-index.yml`. + +### Collection naming + +Indexes use a blue/green slot pattern with a stable alias: + +- `main` -> alias `directus-docs`, slots `directus-docs-a` / `directus-docs-b` +- Branch `bry/foo` -> alias `directus-docs-preview-bry-foo`, slots `...-a` / `...-b` +- Local branch runs use the same branch-derived alias as CI + +Each indexer run writes to whichever slot the alias is not currently pointing at, swaps the alias, then deletes the previous slot. + +For one-off writes, override the index target with `TYPESENSE_INDEX_TARGET=...`. + +The browser reads from `TYPESENSE_COLLECTION` when set. Otherwise it derives the same branch alias as the indexer. The app reads the alias, never the `-a` / `-b` slot name. + +### Ranking + +Section boosts and personalization live in `buildPersonalizedSortBy` in `app/composables/useDocsSearch.ts`. The same `sectionPriority` array drives both the Typesense `_eval` boost order and the chip-bar render order in the palette. + +### Synonyms + +Search synonyms live in `server/data/synonyms.ts` and are pushed to Typesense on every indexer run. Two formats: `multiway` (equivalent terms) and `oneway` (directional shorthand -> canonical, e.g. `db -> database`). Header comment in the file explains both. + +### Search-friendly content + +Write H2s and first paragraphs so they work as standalone search results.
diff --git a/app/app.config.ts b/app/app.config.ts index eafd0a7e4..00f158570 100644 --- a/app/app.config.ts +++ b/app/app.config.ts @@ -1,27 +1,69 @@ export default defineAppConfig({ search: { - backend: 'algolia', + backend: 'typesense', }, ui: { colors: { primary: 'purple', secondary: 'pink', - neutral: 'slate', + neutral: 'neutral', }, + button: { + slots: { + base: 'rounded-l-full rounded-r-full', + }, + }, + + container: { + base: '@max-[40rem]/docs-pane:px-4! @min-[40rem]/docs-pane:px-6! @min-[64rem]/docs-pane:px-8!', + }, + header: { + slots: { + left: '@max-[40rem]/docs-pane:flex-none! @min-[40rem]/docs-pane:flex-1!', + center: '@max-[40rem]/docs-pane:hidden! @min-[40rem]/docs-pane:flex!', + right: '@max-[40rem]/docs-pane:flex-none! @min-[40rem]/docs-pane:flex-1!', + toggle: '@max-[40rem]/docs-pane:flex! @min-[40rem]/docs-pane:hidden!', + content: '@max-[40rem]/docs-pane:block! @min-[40rem]/docs-pane:hidden!', + overlay: '@max-[40rem]/docs-pane:block! @min-[40rem]/docs-pane:hidden!', + }, + }, + contentToc: { + slots: { + bottom: '@max-[64rem]/docs-pane:hidden! @min-[64rem]/docs-pane:flex!', + }, + }, content: { callout: { // Fix background color of pre > code blocks wrapper: '[&_pre>code]:!bg-transparent', }, }, - page: { + contentSurround: { + slots: { + root: 'flex flex-col-reverse sm:grid sm:grid-cols-2 gap-8', + }, + }, + pageHeader: { slots: { - root: 'lg:gap-8', + wrapper: '@max-[40rem]/docs-pane:flex-col! @max-[40rem]/docs-pane:items-stretch! @max-[40rem]/docs-pane:justify-start! @min-[40rem]/docs-pane:flex-row! @min-[40rem]/docs-pane:items-center! @min-[40rem]/docs-pane:justify-between!', + title: 'text-3xl sm:text-4xl text-pretty font-display font-medium text-highlighted', }, }, prose: { + h1: { + base: 'font-display font-medium', + }, + h2: { + base: 'font-display font-medium', + }, + h3: { + base: 'font-display font-medium', + }, + h4: { + base: 'font-display font-medium', + }, pre: { slots: { base: 'text-xs/4', @@ -92,7 +134,7 @@ export default defineAppConfig({ { label: 'Security', to: '/guides/security/best-practices', - icon: 'i-ph-shield-check', + icon: 'material-symbols:verified-user-outline', }, { label: 'AI', @@ -112,17 +154,17 @@ export default defineAppConfig({ { label: 'Cloud', to: '/cloud/getting-started/introduction', - icon: 'i-ph-cloud', + icon: 'material-symbols:cloud-outline', }, { label: 'Self-Hosting', to: '/self-hosting/overview', - icon: 'i-ph-hard-drives', + icon: 'material-symbols:dns-outline', }, { label: 'Configuration', to: '/configuration/intro', - icon: 'i-ph-gear', + icon: 'material-symbols:settings-outline', }, ], }, @@ -132,22 +174,22 @@ export default defineAppConfig({ { label: 'Frameworks', to: '/frameworks', - icon: 'i-ph-brackets-curly', + icon: 'material-symbols:data-object', }, { label: 'Tutorials', to: '/tutorials', - icon: 'i-ph-article', + icon: 'material-symbols:article-outline', }, { label: 'Community', to: '/community/overview/welcome', - icon: 'i-ph-hand-heart', + icon: 'material-symbols:volunteer-activism-outline', }, { label: 'Releases', to: '/releases', - icon: 'i-ph-notebook', + icon: 'material-symbols:menu-book-outline', }, ], }, diff --git a/app/app.vue b/app/app.vue index 84522c811..38430ad3a 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,10 +1,8 @@