Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Dependencies
/node_modules
/docsearch-export/

npm-debug.log*
yarn-debug.log*
Expand Down
1 change: 1 addition & 0 deletions website/.prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pnpm-lock.yaml
docsearch-export/
30 changes: 26 additions & 4 deletions website/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineConfig, HeadConfig } from 'vitepress';
import githubLinksPlugin from './plugins/github-links';
import { renderSearchContent } from './plugins/local-search';
import { searchKeywordsPlugin } from './plugins/search-keywords';
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import matter from 'gray-matter';
Expand Down Expand Up @@ -165,6 +166,12 @@ export default defineConfig({
{ name: 'docsearch:doc_type', content: pageData.frontmatter.docType }
]);
}
if (
pageData.frontmatter.search === false ||
pageData.frontmatter.noindex === true
) {
head.push(['meta', { name: 'docsearch:exclude', content: 'true' }]);
}

// Dynamic Open Graph and Twitter meta tags
const isHome = new URL(canonicalUrl).pathname === '/';
Expand Down Expand Up @@ -334,6 +341,7 @@ export default defineConfig({
});
md.use(tabsMarkdownPlugin);
md.use(groupIconMdPlugin);
md.use(searchKeywordsPlugin);
}
},
vite: {
Expand Down Expand Up @@ -383,13 +391,13 @@ export default defineConfig({
code: 'CESI65QJ',
placement: 'taskfiledev'
},
search: isProduction
search: isPublicDeploy
? {
provider: 'algolia',
options: {
appId: '7IZIJ13AI7',
apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc',
indexName: 'taskfile'
indexName: isLatest ? 'taskfile' : 'taskfile-next'
}
}
: {
Expand Down Expand Up @@ -483,7 +491,7 @@ export default defineConfig({
}
},
sitemap: {
hostname: 'https://taskfile.dev'
hostname: isLatest ? 'https://taskfile.dev' : 'https://next.taskfile.dev'
},
buildEnd({ outDir }) {
const robots = isProduction
Expand All @@ -494,7 +502,21 @@ export default defineConfig({
'Sitemap: https://taskfile.dev/sitemap.xml',
''
]
: ['User-agent: *', 'Disallow: /', ''];
: isPublicDeploy
? [
'User-agent: Algolia Crawler',
'Allow: /docs/',
'Allow: /sitemap.xml',
'Allow: /$',
'Disallow: /',
'',
'User-agent: *',
'Disallow: /',
'',
'Sitemap: https://next.taskfile.dev/sitemap.xml',
''
]
: ['User-agent: *', 'Disallow: /', ''];
writeFileSync(resolve(outDir, 'robots.txt'), robots.join('\n'));
}
});
7 changes: 7 additions & 0 deletions website/.vitepress/plugins/local-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export function renderSearchContent(
let html = md.render(src, env);
if (env.frontmatter?.search === false) return '';

// Only this index renderer adds the alternate terms to section content.
// The actual page keeps them in an attribute, invisible to readers.
html = html.replace(
/(<h[1-6]\b[^>]*\bdata-search-keywords="([^"]*)"[^>]*>[\s\S]*?<\/h[1-6]>)/g,
'$1<p>$2</p>'
);

// Otherwise stripping the tags joins the language label to the first
// command ("shellbrew"), preventing searches for "brew install".
html = html.replace(/<span class="lang">[^<]*<\/span>/g, '');
Expand Down
36 changes: 36 additions & 0 deletions website/.vitepress/plugins/search-keywords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type MarkdownIt from 'markdown-it';

// Keep alternate search terms attached to the heading they describe. HTML
// attributes don't change the visible heading, its permalink or the outline.
export function searchKeywordsPlugin(md: MarkdownIt): void {
md.core.ruler.push('search-keywords', (state) => {
const keywords = state.env.frontmatter?.searchKeywords;
if (keywords === undefined) return;
const fail = (message: string): never => {
throw new Error(
`searchKeywords (${state.env.relativePath ?? 'page'}): ${message}`
);
};
if (!keywords || typeof keywords !== 'object' || Array.isArray(keywords)) {
fail('expected a mapping of heading IDs to lists of search terms');
}

const headings = new Map(
state.tokens
.filter((token) => token.type === 'heading_open')
.map((token) => [token.attrGet('id'), token])
);
for (const [anchor, terms] of Object.entries(keywords)) {
const heading = headings.get(anchor);
if (!heading) return fail(`heading #${anchor} does not exist`);
if (
!Array.isArray(terms) ||
!terms.length ||
terms.some((term) => typeof term !== 'string' || !term.trim())
) {
return fail(`#${anchor} needs a non-empty list of strings`);
}
heading.attrSet('data-search-keywords', terms.join(' '));
}
});
}
Loading