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
+ //