Skip to content

Commit ab23ead

Browse files
committed
feat(site): harden the crawler configuration
Safety and correctness settings that were missing from the first pass: saveBackup, and a safety check that refuses to publish an index which lost more than 10% of its records, so a crawl that breaks cannot empty search. url_without_anchor and type join attributesToRetrieve; the DocSearch client needs both to build a result link and group hits. startUrls goes back to the site root. /docs/ does not exist on the released site yet, so the crawler had no reachable entry point until the refactor is promoted. A section is inferred from the URL for the same reason, as a fallback for pages that do not carry the frontmatter yet. Code blocks are copied into crawler-only paragraphs so a search can find an exact Taskfile key or flag. Measured at +10% indexed content, and the level weighting still puts headings ahead of code. The two deprecation actions collapse into one, with pageRank computed from the path. The frontmatter field is renamed docsearch:doc_type to match what the extractor reads.
1 parent b79ffd1 commit ab23ead

3 files changed

Lines changed: 114 additions & 43 deletions

File tree

website/.vitepress/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export default defineConfig({
165165
if (pageData.frontmatter.docType) {
166166
head.push([
167167
'meta',
168-
{ name: 'docsearch:type', content: pageData.frontmatter.docType }
168+
{ name: 'docsearch:doc_type', content: pageData.frontmatter.docType }
169169
])
170170
}
171171

website/docsearch.config.js

Lines changed: 111 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,36 @@
77
// When you change the configuration in the dashboard, change it here too.
88
//
99
// The API key below is the crawler's *write* key and is deliberately not in
10-
// this repository. Set it in the dashboard, or in the crawler's environment.
10+
// this repository. Keep the existing key when pasting this file into the
11+
// dashboard; never replace this placeholder in Git.
1112
//
1213
// Every selector here was checked against the generated HTML, not assumed.
1314

1415
new Crawler({
1516
appId: '7IZIJ13AI7',
16-
apiKey: process.env.ALGOLIA_CRAWLER_WRITE_KEY,
17+
apiKey: '<ALGOLIA_CRAWLER_WRITE_KEY>',
1718
indexPrefix: '',
1819
rateLimit: 8,
1920
maxDepth: 10,
20-
schedule: 'every 1 day',
21+
schedule: 'at 9:50 AM on Thursday',
22+
ignoreCanonicalTo: true,
23+
saveBackup: true,
24+
25+
safetyChecks: {
26+
beforeIndexPublishing: {
27+
maxLostRecordsPercentage: 10
28+
},
29+
// The dashboard's current config.d.ts exposes this at the safetyChecks
30+
// level, rather than inside beforeIndexPublishing.
31+
maxFailedUrls: 5
32+
},
2133

2234
// Only the released site. next.taskfile.dev serves the same URLs from the
2335
// upcoming release, and both sites share the single `taskfile` index, so
2436
// crawling it as well would give every page a duplicate record.
25-
startUrls: ['https://taskfile.dev/docs/'],
37+
// The root exists both before and after the documentation refactor. The old
38+
// production site has no /docs/ landing page yet.
39+
startUrls: ['https://taskfile.dev/'],
2640
sitemaps: ['https://taskfile.dev/sitemap.xml'],
2741
// Only /docs is indexed, so there is no reason to fetch the blog, the
2842
// homepage or /adopters on every crawl.
@@ -40,11 +54,68 @@ new Crawler({
4054
{
4155
indexName: 'taskfile',
4256
pathsToMatch: ['https://taskfile.dev/docs/**'],
43-
recordExtractor: ({ $, helpers }) => {
57+
recordExtractor: ({ $, helpers, url }) => {
4458
// The banner the llms plugin injects sits inside .vp-doc, ahead of the
4559
// h1. It is display:none for readers and must not become content.
4660
$('[data-nosnippet]').remove();
4761

62+
// DocSearch expects content selectors to target paragraphs or list
63+
// items. Copy code blocks into crawler-only paragraphs so experienced
64+
// users can search for exact Taskfile keys and command syntax without
65+
// changing the page rendered to readers.
66+
$('.vp-doc pre code').each((_, element) => {
67+
const code = $(element).text().trim();
68+
if (!code) return;
69+
const paragraph = $('<p></p>').addClass('docsearch-code').text(code);
70+
$(element).closest('pre').after(paragraph);
71+
});
72+
73+
// Frontmatter metadata is available after the refactor. Infer the same
74+
// values from the URL while the old monolithic guide is still live, so
75+
// this configuration can be installed before the website PR merges.
76+
// Remove this URL inference once the refactored documentation is live
77+
// and every indexed page exposes the DocSearch metadata.
78+
const pathname = url.pathname.replace(/\/+$/, '') || '/';
79+
const inferredSection = (() => {
80+
if (pathname === '/docs') return 'Overview';
81+
if (
82+
/^\/docs\/(installation|getting-started|integrations)$/.test(
83+
pathname
84+
)
85+
) {
86+
return 'Getting Started';
87+
}
88+
if (/^\/docs\/reference\//.test(pathname)) return 'Reference';
89+
if (/^\/docs\/(contributing|releasing|styleguide)$/.test(pathname)) {
90+
return 'Contributing';
91+
}
92+
if (
93+
/^\/docs\/(experiments|deprecations|security)(\/|$)/.test(
94+
pathname
95+
) ||
96+
/^\/docs\/(changelog|faq|taskfile-versions|community)$/.test(
97+
pathname
98+
)
99+
) {
100+
return 'Project';
101+
}
102+
return 'Guide';
103+
})();
104+
const section =
105+
$('meta[name="docsearch:section"]').attr('content') ||
106+
inferredSection;
107+
const docType =
108+
$('meta[name="docsearch:doc_type"]').attr('content') ||
109+
({
110+
Overview: 'overview',
111+
Reference: 'reference',
112+
Contributing: 'contributing',
113+
Project: 'project',
114+
Guide: 'guide',
115+
'Getting Started': 'guide'
116+
}[section] ??
117+
'guide');
118+
48119
return helpers.docsearch({
49120
recordProps: {
50121
// Not a heading on the page: the section the page belongs to,
@@ -53,8 +124,10 @@ new Crawler({
53124
// DOM instead, which ties the index to the theme's markup and
54125
// breaks silently when that markup changes.
55126
lvl0: {
56-
selectors: 'meta[name="docsearch:section"]',
57-
defaultValue: 'Documentation'
127+
// Algolia documents an empty selector as the way to provide a
128+
// raw, dynamically computed lvl0 through defaultValue.
129+
selectors: '',
130+
defaultValue: section
58131
},
59132
// Everything below is scoped to .vp-doc. VitePress renders the
60133
// sidebar's section labels as <h2 class="text"> inside
@@ -64,55 +137,44 @@ new Crawler({
64137
lvl2: '.vp-doc h2',
65138
lvl3: '.vp-doc h3',
66139
lvl4: '.vp-doc h4',
67-
content: '.vp-doc p, .vp-doc li, .vp-doc td',
68-
// Faceted so a search can be narrowed to reference pages, or
69-
// weighted differently later.
70-
docType: {
71-
selectors: 'meta[name="docsearch:type"]',
72-
defaultValue: 'guide'
73-
}
74-
},
75-
aggregateContent: true,
76-
recordVersion: 'v3'
77-
});
78-
}
79-
},
80-
{
81-
// Deprecation notices describe what to stop doing. They should be
82-
// findable by name, but never ahead of the page documenting the
83-
// replacement.
84-
indexName: 'taskfile',
85-
pathsToMatch: ['https://taskfile.dev/docs/deprecations/**'],
86-
pageRank: -10,
87-
recordExtractor: ({ $, helpers }) => {
88-
$('[data-nosnippet]').remove();
89-
return helpers.docsearch({
90-
recordProps: {
91-
lvl0: {
92-
selectors: 'meta[name="docsearch:section"]',
93-
defaultValue: 'Deprecations'
140+
lvl5: '.vp-doc h5',
141+
lvl6: '.vp-doc h6',
142+
content: '.vp-doc p, .vp-doc li, .vp-doc td, .vp-doc th',
143+
section: { defaultValue: section },
144+
doc_type: { defaultValue: docType },
145+
lang: {
146+
defaultValue: $('html').attr('lang') || 'en-US'
94147
},
95-
lvl1: '.vp-doc h1',
96-
lvl2: '.vp-doc h2',
97-
lvl3: '.vp-doc h3',
98-
content: '.vp-doc p, .vp-doc li, .vp-doc td'
148+
// Deprecation notices remain findable, but don't outrank the page
149+
// that documents the supported replacement.
150+
pageRank: pathname.startsWith('/docs/deprecations/') ? '-10' : '0'
99151
},
152+
indexHeadings: true,
100153
aggregateContent: true,
101154
recordVersion: 'v3'
102155
});
103156
}
104157
}
105158
],
106159

160+
// These settings initialize a new index. Algolia doesn't apply them to an
161+
// existing index, so import the same values in the taskfile index settings
162+
// when this configuration changes them.
107163
initialIndexSettings: {
108164
taskfile: {
109-
attributesForFaceting: ['type', 'lang', 'docType'],
165+
hitsPerPage: 20,
166+
maxValuesPerFacet: 100,
167+
attributesForFaceting: ['type', 'lang', 'section', 'doc_type'],
110168
attributesToRetrieve: [
111169
'hierarchy',
112170
'content',
113171
'anchor',
114172
'url',
115-
'docType'
173+
'url_without_anchor',
174+
'type',
175+
'lang',
176+
'section',
177+
'doc_type'
116178
],
117179
attributesToHighlight: ['hierarchy', 'content'],
118180
attributesToSnippet: ['content:10'],
@@ -123,6 +185,8 @@ new Crawler({
123185
'unordered(hierarchy.lvl2)',
124186
'unordered(hierarchy.lvl3)',
125187
'unordered(hierarchy.lvl4)',
188+
'unordered(hierarchy.lvl5)',
189+
'unordered(hierarchy.lvl6)',
126190
'content'
127191
],
128192
distinct: true,
@@ -150,7 +214,13 @@ new Crawler({
150214
ignorePlurals: true,
151215
advancedSyntax: true,
152216
attributeCriteriaComputedByMinProximity: true,
153-
removeWordsIfNoResults: 'allOptional'
217+
removeWordsIfNoResults: 'allOptional',
218+
separatorsToIndex: '_',
219+
paginationLimitedTo: 1000,
220+
exactOnSingleWordQuery: 'attribute',
221+
queryType: 'prefixLast',
222+
snippetEllipsisText: '',
223+
alternativesAsExact: ['ignorePlurals', 'singleWordSynonym']
154224
}
155225
}
156226
});

website/src/next/docs/contributing.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ Search is provided by Algolia DocSearch. The crawler runs on Algolia's side and
132132
is configured through their dashboard, but `website/docsearch.config.js` holds
133133
the same configuration in the repository so it can be read and reviewed. If you
134134
change one, change the other. A page's `section:` frontmatter is what the
135-
crawler shows as the breadcrumb on a search result, so a new page needs one.
135+
crawler shows as the breadcrumb on a search result, while `docType:` powers its
136+
search facet, so a new page needs both.
136137

137138
When making a change, consider whether a change to the [Usage
138139
Guide][usage-guide] is necessary. This document contains descriptions and

0 commit comments

Comments
 (0)