diff --git a/website/.vitepress/components/GuideRedirect.vue b/website/.vitepress/components/GuideRedirect.vue new file mode 100644 index 0000000000..2a98182de8 --- /dev/null +++ b/website/.vitepress/components/GuideRedirect.vue @@ -0,0 +1,30 @@ + + + diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index e9c4837525..178410dcd6 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -144,6 +144,23 @@ export default defineConfig({ ).href; head.push(['link', { rel: 'canonical', href: canonicalUrl }]); + // The DocSearch crawler otherwise has to infer a record's section from the + // active sidebar link in the DOM. Stating it on the page is steadier: it + // survives a theme upgrade, and it is what hierarchy.lvl0 - the breadcrumb + // on every search result - should be set from. + if (pageData.frontmatter.section) { + head.push([ + 'meta', + { name: 'docsearch:section', content: pageData.frontmatter.section } + ]) + } + if (pageData.frontmatter.docType) { + head.push([ + 'meta', + { name: 'docsearch:doc_type', content: pageData.frontmatter.docType } + ]) + } + // Dynamic Open Graph and Twitter meta tags const isHome = new URL(canonicalUrl).pathname === '/'; let pageTitle = pageData.frontmatter.title || pageData.title || taskName; @@ -297,6 +314,11 @@ export default defineConfig({ srcDir: 'src', cleanUrls: true, srcExclude: [`${other}/**`, `${channel}/docs/**/template.md`], + // A function rather than the equivalent `{ '/:path*': ':path*' }`. + // vitepress-plugin-llms reuses this config to name its Markdown output, and + // on the object form it compiles the `:path*` array parameter back without + // separators, producing dist/docsreferencecli.md instead of + // dist/docs/reference/cli.md and breaking every relative link in them. rewrites: (id) => id.startsWith(`${channel}/`) ? id.slice(channel.length + 1) : id, markdown: { @@ -381,7 +403,9 @@ export default defineConfig({ { text: 'Home', link: '/' }, { text: 'Docs', - link: '/docs/guide', + // The landing page only exists on next until cmd/release promotes it; + // the released channel still has to enter the section at the guide. + link: isLatest ? '/docs/guide' : '/docs/', activeMatch: '^/docs' }, { text: 'Blog', link: '/blog', activeMatch: '^/blog' }, diff --git a/website/.vitepress/guideAnchors.ts b/website/.vitepress/guideAnchors.ts new file mode 100644 index 0000000000..d5ce370995 --- /dev/null +++ b/website/.vitepress/guideAnchors.ts @@ -0,0 +1,104 @@ +// Where each section of the old single-page guide went when it was +// split up. Netlify never sees the URL fragment, so a _redirects rule +// cannot route these; GuideRedirect.vue resolves them in the browser. +export const guideAnchors: Record = { + 'running-taskfiles': '/docs/guide/running-tasks', + 'supported-file-names': '/docs/guide/running-tasks#supported-file-names', + 'running-a-taskfile-from-a-subdirectory': + '/docs/guide/running-tasks#running-a-taskfile-from-a-subdirectory', + 'running-a-global-taskfile': + '/docs/guide/running-tasks#running-a-global-taskfile', + 'running-a-taskfile-from-stdin': + '/docs/guide/running-tasks#running-a-taskfile-from-stdin', + 'running-a-remote-taskfile': + '/docs/remote-taskfiles#specifying-a-remote-entrypoint', + 'environment-variables': '/docs/guide/environment', + task: '/docs/guide/environment#task', + 'env-files': '/docs/guide/environment#env-files', + 'including-other-taskfiles': '/docs/guide/includes', + 'remote-taskfiles': '/docs/guide/includes#remote-taskfiles', + 'os-specific-taskfiles': '/docs/guide/includes#os-specific-taskfiles', + 'directory-of-included-taskfile': + '/docs/guide/includes#directory-of-included-taskfile', + 'optional-includes': '/docs/guide/includes#optional-includes', + 'internal-includes': '/docs/guide/includes#internal-includes', + 'flatten-includes': '/docs/guide/includes#flatten-includes', + 'exclude-tasks-from-being-included': + '/docs/guide/includes#exclude-tasks-from-being-included', + 'vars-of-included-taskfiles': + '/docs/guide/includes#vars-of-included-taskfiles', + 'namespace-aliases': '/docs/guide/includes#namespace-aliases', + 'internal-tasks': '/docs/guide/defining-tasks#internal-tasks', + 'task-directory': '/docs/guide/defining-tasks#task-directory', + 'task-dependencies': '/docs/guide/dependencies#task-dependencies', + 'fail-fast-dependencies': '/docs/guide/dependencies#fail-fast-dependencies', + 'platform-specific-tasks-and-commands': + '/docs/guide/platforms#platform-specific-tasks-and-commands', + 'calling-another-task': '/docs/guide/dependencies#calling-another-task', + 'prevent-unnecessary-work': '/docs/guide/up-to-date', + 'by-fingerprinting-locally-generated-files-and-their-sources': + '/docs/guide/up-to-date#by-fingerprinting-locally-generated-files-and-their-sources', + 'using-programmatic-checks-to-indicate-a-task-is-up-to-date': + '/docs/guide/up-to-date#using-programmatic-checks-to-indicate-a-task-is-up-to-date', + 'using-programmatic-checks-to-cancel-the-execution-of-a-task-and-its-dependencies': + '/docs/guide/conditional-execution#using-programmatic-checks-to-cancel-the-execution-of-a-task-and-its-dependencies', + 'conditional-execution-with-if': + '/docs/guide/conditional-execution#conditional-execution-with-if', + 'task-level-if': '/docs/guide/conditional-execution#task-level-if', + 'command-level-if': '/docs/guide/conditional-execution#command-level-if', + 'using-templates-in-if-conditions': + '/docs/guide/conditional-execution#using-templates-in-if-conditions', + 'using-if-with-for-loops': + '/docs/guide/conditional-execution#using-if-with-for-loops', + 'if-vs-preconditions': + '/docs/guide/conditional-execution#if-vs-preconditions', + 'limiting-when-tasks-run': + '/docs/guide/conditional-execution#limiting-when-tasks-run', + 'ensuring-required-variables-are-set': + '/docs/guide/required-variables#ensuring-required-variables-are-set', + 'ensuring-required-variables-have-allowed-values': + '/docs/guide/required-variables#ensuring-required-variables-have-allowed-values', + 'using-variable-references-for-enum-values': + '/docs/guide/required-variables#using-variable-references-for-enum-values', + 'prompting-for-missing-variables-interactively': + '/docs/guide/required-variables#prompting-for-missing-variables-interactively', + variables: '/docs/guide/variables', + 'dynamic-variables': '/docs/guide/variables#dynamic-variables', + 'referencing-other-variables': + '/docs/guide/variables#referencing-other-variables', + 'parsing-json-yaml-into-map-variables': + '/docs/guide/variables#parsing-json-yaml-into-map-variables', + 'secret-variables': '/docs/guide/variables#secret-variables', + 'looping-over-values': '/docs/guide/loops', + 'looping-over-a-static-list': '/docs/guide/loops#looping-over-a-static-list', + 'looping-over-a-matrix': '/docs/guide/loops#looping-over-a-matrix', + 'looping-over-your-task-s-sources-or-generated-files': + '/docs/guide/loops#looping-over-your-task-s-sources-or-generated-files', + 'looping-over-variables': '/docs/guide/loops#looping-over-variables', + 'renaming-variables': '/docs/guide/loops#renaming-variables', + 'looping-over-tasks': '/docs/guide/loops#looping-over-tasks', + 'looping-over-dependencies': '/docs/guide/loops#looping-over-dependencies', + 'forwarding-cli-arguments-to-commands': + '/docs/guide/arguments#forwarding-cli-arguments-to-commands', + 'wildcard-arguments': '/docs/guide/arguments#wildcard-arguments', + 'doing-task-cleanup-with-defer': + '/docs/guide/dependencies#doing-task-cleanup-with-defer', + help: '/docs/guide/defining-tasks#help', + 'display-summary-of-task': + '/docs/guide/defining-tasks#display-summary-of-task', + 'task-aliases': '/docs/guide/defining-tasks#task-aliases', + 'overriding-task-name': '/docs/guide/defining-tasks#overriding-task-name', + 'warning-prompts': '/docs/guide/required-variables#warning-prompts', + 'silent-mode': '/docs/guide/output#silent-mode', + 'dry-run-mode': '/docs/guide/running-tasks#dry-run-mode', + 'ignore-errors': '/docs/guide/output#ignore-errors', + 'output-syntax': '/docs/guide/output#output-syntax', + 'ci-integration': '/docs/guide/output#ci-integration', + 'colored-output': '/docs/guide/output#colored-output', + 'error-annotations': '/docs/guide/output#error-annotations', + 'interactive-cli-application': + '/docs/guide/running-tasks#interactive-cli-application', + 'short-task-syntax': '/docs/guide/defining-tasks#short-task-syntax', + 'set-and-shopt': '/docs/guide/platforms#set-and-shopt', + 'watch-tasks': '/docs/guide/watch' +}; diff --git a/website/.vitepress/sidebar/next.ts b/website/.vitepress/sidebar/next.ts index c7171547bc..fc7bc1b73a 100644 --- a/website/.vitepress/sidebar/next.ts +++ b/website/.vitepress/sidebar/next.ts @@ -3,39 +3,107 @@ import type { DefaultTheme } from 'vitepress'; // Navigation for the `/docs` section. next.ts is the source of both sidebars; // cmd/release copies it over latest.ts alongside the content it describes. See // the "Documentation channels" section of website/src/next/docs/contributing.md. +// +// Grouped by what the reader is trying to do: get going, learn Task, look +// something up, follow the project. The DocSearch crawler puts the active +// sidebar section into hierarchy.lvl0, so these labels are also the breadcrumbs +// on every search result. export const sidebar: DefaultTheme.SidebarItem[] = [ { - text: 'Installation', - link: '/docs/installation' + text: 'Overview', + link: '/docs/' }, { text: 'Getting Started', - link: '/docs/getting-started' + items: [ + { + text: 'Installation', + link: '/docs/installation' + }, + { + text: 'Quick Start', + link: '/docs/getting-started' + }, + { + text: 'Editors and Integrations', + link: '/docs/integrations' + } + ] }, { text: 'Guide', - link: '/docs/guide' - }, - { - text: 'Remote Taskfiles', - link: '/docs/remote-taskfiles' + link: '/docs/guide/', + items: [ + { + text: 'Running tasks', + link: '/docs/guide/running-tasks' + }, + { + text: 'Defining tasks', + link: '/docs/guide/defining-tasks' + }, + { + text: 'Passing arguments', + link: '/docs/guide/arguments' + }, + { + text: 'Variables', + link: '/docs/guide/variables' + }, + { + text: 'Environment variables', + link: '/docs/guide/environment' + }, + { + text: 'Required variables and prompts', + link: '/docs/guide/required-variables' + }, + { + text: 'Dependencies and task calls', + link: '/docs/guide/dependencies' + }, + { + text: 'Skipping work that is up to date', + link: '/docs/guide/up-to-date' + }, + { + text: 'Conditional execution', + link: '/docs/guide/conditional-execution' + }, + { + text: 'Loops', + link: '/docs/guide/loops' + }, + { + text: 'Including other Taskfiles', + link: '/docs/guide/includes' + }, + { + text: 'Remote Taskfiles', + link: '/docs/remote-taskfiles' + }, + { + text: 'Output and logging', + link: '/docs/guide/output' + }, + { + text: 'Platform-specific behaviour', + link: '/docs/guide/platforms' + }, + { + text: 'Watch mode', + link: '/docs/guide/watch' + } + ] }, { text: 'Reference', - collapsed: true, + collapsed: false, items: [ { text: 'Taskfile Schema', link: '/docs/reference/schema' }, - { - text: 'Environment', - link: '/docs/reference/environment' - }, - { - text: 'Configuration', - link: '/docs/reference/config' - }, { text: 'CLI', link: '/docs/reference/cli' @@ -44,6 +112,14 @@ export const sidebar: DefaultTheme.SidebarItem[] = [ text: 'Templating', link: '/docs/reference/templating' }, + { + text: 'Environment', + link: '/docs/reference/environment' + }, + { + text: 'Configuration', + link: '/docs/reference/config' + }, { text: 'Package API', link: '/docs/reference/package' @@ -51,88 +127,96 @@ export const sidebar: DefaultTheme.SidebarItem[] = [ ] }, { - text: 'Experiments', + text: 'Project', collapsed: true, - link: '/docs/experiments/', items: [ { - text: 'Env Precedence (#1038)', - link: '/docs/experiments/env-precedence' + text: 'Changelog', + link: '/docs/changelog' }, { - text: 'Gentle Force (#1200)', - link: '/docs/experiments/gentle-force' + text: 'FAQ', + link: '/docs/faq' }, { - text: 'Remote Taskfiles (#1317)', - link: '/docs/experiments/remote-taskfiles' - } - ] - }, - { - text: 'Deprecations', - collapsed: true, - link: '/docs/deprecations/', - items: [ + text: 'Taskfile Versions', + link: '/docs/taskfile-versions' + }, + { + text: 'Community', + link: '/docs/community' + }, { - text: 'Completion Scripts', - link: '/docs/deprecations/completion-scripts' + text: 'Experiments', + collapsed: true, + link: '/docs/experiments/', + items: [ + { + text: 'Env Precedence (#1038)', + link: '/docs/experiments/env-precedence' + }, + { + text: 'Gentle Force (#1200)', + link: '/docs/experiments/gentle-force' + }, + { + text: 'Remote Taskfiles (#1317)', + link: '/docs/experiments/remote-taskfiles' + } + ] }, { - text: 'Template Functions', - link: '/docs/deprecations/template-functions' + text: 'Deprecations', + collapsed: true, + link: '/docs/deprecations/', + items: [ + { + text: 'Completion Scripts', + link: '/docs/deprecations/completion-scripts' + }, + { + text: 'Template Functions', + link: '/docs/deprecations/template-functions' + }, + { + text: 'Version 2 Schema (#1197)', + link: '/docs/deprecations/version-2-schema' + } + ] }, { - text: 'Version 2 Schema (#1197)', - link: '/docs/deprecations/version-2-schema' + text: 'Security', + collapsed: true, + link: '/docs/security/', + items: [ + { + text: 'Incident Response Plan', + link: '/docs/security/incident-response-plan' + }, + { + text: 'Threat Model', + link: '/docs/security/threat-model' + } + ] } ] }, - { - text: 'Taskfile Versions', - link: '/docs/taskfile-versions' - }, - { - text: 'Integrations', - link: '/docs/integrations' - }, - { - text: 'Community', - link: '/docs/community' - }, - { - text: 'Style Guide', - link: '/docs/styleguide' - }, { text: 'Contributing', - link: '/docs/contributing' - }, - { - text: 'Releasing', - link: '/docs/releasing' - }, - { - text: 'Security', collapsed: true, - link: '/docs/security/', items: [ { - text: 'Incident Response Plan', - link: '/docs/security/incident-response-plan' + text: 'Contributing', + link: '/docs/contributing' + }, + { + text: 'Style Guide', + link: '/docs/styleguide' }, { - text: 'Threat Model', - link: '/docs/security/threat-model' + text: 'Releasing', + link: '/docs/releasing' } ] - }, - { - text: 'Changelog', - link: '/docs/changelog' - }, - { - text: 'FAQ', - link: '/docs/faq' } ]; diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts index 495bf7dc67..e3509b2616 100644 --- a/website/.vitepress/theme/index.ts +++ b/website/.vitepress/theme/index.ts @@ -6,6 +6,7 @@ import AuthorCard from '../components/AuthorCard.vue'; import BlogPost from '../components/BlogPost.vue'; import Version from '../components/Version.vue'; import Adopters from '../components/Adopters.vue'; +import GuideRedirect from '../components/GuideRedirect.vue'; import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'; import { h } from 'vue'; import 'virtual:group-icons.css'; @@ -23,6 +24,7 @@ export default { app.component('BlogPost', BlogPost); app.component('Version', Version); app.component('Adopters', Adopters); + app.component('GuideRedirect', GuideRedirect); app.component('CopyOrDownloadAsMarkdownButtons', CopyOrDownloadAsMarkdownButtons); enhanceAppWithTabs(app); } diff --git a/website/Taskfile.yml b/website/Taskfile.yml index bbb7ca9324..bf8bad85eb 100644 --- a/website/Taskfile.yml +++ b/website/Taskfile.yml @@ -75,7 +75,7 @@ tasks: clean: desc: Clean temp directories cmds: - - rm -rf ./vitepress/dist + - rm -rf ./.vitepress/dist # --no-build is what makes the channel stick: the CLI builds by default, and # that build would come from netlify.toml, which knows nothing about the diff --git a/website/docsearch.config.js b/website/docsearch.config.js new file mode 100644 index 0000000000..294411c68b --- /dev/null +++ b/website/docsearch.config.js @@ -0,0 +1,226 @@ +// Algolia Crawler configuration for taskfile.dev. +// +// This file is the source of truth. The crawler itself runs on Algolia's side, +// configured through the dashboard at https://crawler.algolia.com, and until now +// nothing described it here: `git log --all -- '*algolia*' '*docsearch*'` +// returned nothing, so the only copy lived in a web form nobody could review. +// When you change the configuration in the dashboard, change it here too. +// +// The API key below is the crawler's *write* key and is deliberately not in +// this repository. Keep the existing key when pasting this file into the +// dashboard; never replace this placeholder in Git. +// +// Every selector here was checked against the generated HTML, not assumed. + +new Crawler({ + appId: '7IZIJ13AI7', + apiKey: '', + indexPrefix: '', + rateLimit: 8, + maxDepth: 10, + schedule: 'at 9:50 AM on Thursday', + ignoreCanonicalTo: true, + saveBackup: true, + + safetyChecks: { + beforeIndexPublishing: { + maxLostRecordsPercentage: 10 + }, + // The dashboard's current config.d.ts exposes this at the safetyChecks + // level, rather than inside beforeIndexPublishing. + maxFailedUrls: 5 + }, + + // Only the released site. next.taskfile.dev serves the same URLs from the + // upcoming release, and both sites share the single `taskfile` index, so + // crawling it as well would give every page a duplicate record. + // The root exists both before and after the documentation refactor. The old + // production site has no /docs/ landing page yet. + startUrls: ['https://taskfile.dev/'], + sitemaps: ['https://taskfile.dev/sitemap.xml'], + // Only /docs is indexed, so there is no reason to fetch the blog, the + // homepage or /adopters on every crawl. + discoveryPatterns: ['https://taskfile.dev/docs/**'], + + exclusionPatterns: [ + // Long, low-value for search, and it would outrank real pages on any + // version number or feature name it mentions. + 'https://taskfile.dev/docs/changelog**', + // Scaffolding for writing new pages; already out of the sitemap. + 'https://taskfile.dev/docs/*/template' + ], + + actions: [ + { + indexName: 'taskfile', + pathsToMatch: ['https://taskfile.dev/docs/**'], + recordExtractor: ({ $, helpers, url }) => { + // The banner the llms plugin injects sits inside .vp-doc, ahead of the + // h1. It is display:none for readers and must not become content. + $('[data-nosnippet]').remove(); + + // DocSearch expects content selectors to target paragraphs or list + // items. Copy code blocks into crawler-only paragraphs so experienced + // users can search for exact Taskfile keys and command syntax without + // changing the page rendered to readers. + $('.vp-doc pre code').each((_, element) => { + const code = $(element).text().trim(); + if (!code) return; + const paragraph = $('

').addClass('docsearch-code').text(code); + $(element).closest('pre').after(paragraph); + }); + + // Frontmatter metadata is available after the refactor. Infer the same + // values from the URL while the old monolithic guide is still live, so + // this configuration can be installed before the website PR merges. + // Remove this URL inference once the refactored documentation is live + // and every indexed page exposes the DocSearch metadata. + const pathname = url.pathname.replace(/\/+$/, '') || '/'; + const inferredSection = (() => { + if (pathname === '/docs') return 'Overview'; + if ( + /^\/docs\/(installation|getting-started|integrations)$/.test( + pathname + ) + ) { + return 'Getting Started'; + } + if (/^\/docs\/reference\//.test(pathname)) return 'Reference'; + if (/^\/docs\/(contributing|releasing|styleguide)$/.test(pathname)) { + return 'Contributing'; + } + if ( + /^\/docs\/(experiments|deprecations|security)(\/|$)/.test( + pathname + ) || + /^\/docs\/(changelog|faq|taskfile-versions|community)$/.test( + pathname + ) + ) { + return 'Project'; + } + return 'Guide'; + })(); + const section = + $('meta[name="docsearch:section"]').attr('content') || + inferredSection; + const docType = + $('meta[name="docsearch:doc_type"]').attr('content') || + ({ + Overview: 'overview', + Reference: 'reference', + Contributing: 'contributing', + Project: 'project', + Guide: 'guide', + 'Getting Started': 'guide' + }[section] ?? + 'guide'); + + return helpers.docsearch({ + recordProps: { + // Not a heading on the page: the section the page belongs to, + // stated in its own frontmatter and emitted by transformHead. The + // usual DocSearch recipe reads the active sidebar link out of the + // DOM instead, which ties the index to the theme's markup and + // breaks silently when that markup changes. + lvl0: { + // Algolia documents an empty selector as the way to provide a + // raw, dynamically computed lvl0 through defaultValue. + selectors: '', + defaultValue: section + }, + // Everything below is scoped to .vp-doc. VitePress renders the + // sidebar's section labels as

inside + //