diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d584e2d834..d9b2b89f71 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -97,6 +97,58 @@ jobs:
- name: π Validate JSON Schema
run: check-jsonschema --check-metaschema website/src/public/next-schema.json website/src/public/schema.json
+ website:
+ name: π Build documentation channels
+ runs-on: ubuntu-latest
+ steps:
+ - name: π₯ Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: β¬οΈ Setup Go
+ uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
+ with:
+ go-version: 1.27.x
+
+ - name: β¬οΈ Setup Node
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '24'
+
+ - name: β¬οΈ Setup pnpm
+ uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
+ with:
+ package_json_file: website/package.json
+
+ - name: β¬οΈ Install website dependencies
+ run: pnpm install --frozen-lockfile
+ working-directory: website
+
+ - name: π Check next documentation
+ run: pnpm build && pnpm check:links
+ working-directory: website
+ env:
+ DOCS_CHANNEL: next
+ DOCS_SITE: preview
+
+ - name: π Check released documentation
+ run: pnpm build && pnpm check:links
+ working-directory: website
+ env:
+ DOCS_CHANNEL: latest
+ DOCS_SITE: production
+
+ # Exercise the actual promotion in this disposable checkout. The current
+ # latest content alone cannot reveal links to files missing from promotion.
+ - name: π Simulate a release
+ run: go run ./cmd/release patch
+
+ - name: π Check promoted documentation
+ run: pnpm build && pnpm check:links
+ working-directory: website
+ env:
+ DOCS_CHANNEL: latest
+ DOCS_SITE: production
+
check-latest-content:
name: π Check latest content
# Pull requests only: the release commit is pushed straight to main and is
diff --git a/cmd/release/main.go b/cmd/release/main.go
index 362128a014..6b54d80536 100644
--- a/cmd/release/main.go
+++ b/cmd/release/main.go
@@ -32,6 +32,7 @@ var (
}
promotedFiles = []promotion{
+ {"website/src/next/agents.md", "website/src/latest/agents.md"},
{"website/.vitepress/sidebar/next.ts", "website/.vitepress/sidebar/latest.ts"},
{"website/src/public/next-schema.json", "website/src/public/schema.json"},
{"website/src/public/next-schema-taskrc.json", "website/src/public/schema-taskrc.json"},
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 159af5e2d2..b11bb5c438 100644
--- a/website/.vitepress/config.ts
+++ b/website/.vitepress/config.ts
@@ -1,7 +1,7 @@
import { defineConfig, HeadConfig } from 'vitepress';
import githubLinksPlugin from './plugins/github-links';
import { renderSearchContent } from './plugins/local-search';
-import { readdirSync, readFileSync, writeFileSync } from 'fs';
+import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import matter from 'gray-matter';
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs';
@@ -85,6 +85,10 @@ const urlVersion =
next: 'https://next.taskfile.dev/'
};
+const hasDocsOverview = existsSync(
+ resolve(__dirname, `../src/${channel}/docs/index.md`)
+);
+
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: taskName,
@@ -145,6 +149,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;
@@ -298,6 +319,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: {
@@ -352,6 +378,7 @@ export default defineConfig({
themeConfig: {
logo: '/img/logo.svg',
+ sidebarMenuLabel: 'Documentation',
carbonAds: {
code: 'CESI65QJ',
placement: 'taskfiledev'
@@ -368,8 +395,15 @@ export default defineConfig({
: {
provider: 'local',
options: {
- _render: renderSearchContent,
detailedView: true,
+ // Match the public DocSearch scope: current docs, without release
+ // notes or blog posts competing with feature documentation.
+ _render(src, env, md) {
+ const path = env.relativePath.replace(/^(next|latest)\//, '');
+ if (!path.startsWith('docs/') || path === 'docs/changelog.md')
+ return '';
+ return renderSearchContent(src, env, md);
+ },
miniSearch: {
searchOptions: {
fuzzy: 0.2,
@@ -383,7 +417,7 @@ export default defineConfig({
{ text: 'Home', link: '/' },
{
text: 'Docs',
- link: '/docs/guide',
+ link: hasDocsOverview ? '/docs/' : '/docs/guide',
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..10104e813b
--- /dev/null
+++ b/website/.vitepress/guideAnchors.ts
@@ -0,0 +1,103 @@
+// 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/dependencies#repeated-calls',
+ '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/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/errors-and-cleanup#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/conditional-execution#confirmation-prompts',
+ 'silent-mode': '/docs/guide/output#silent-mode',
+ 'dry-run-mode': '/docs/guide/running-tasks#dry-run-mode',
+ 'ignore-errors': '/docs/guide/errors-and-cleanup#ignoring-command-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..49d547bed3 100644
--- a/website/.vitepress/sidebar/next.ts
+++ b/website/.vitepress/sidebar/next.ts
@@ -3,39 +3,106 @@ 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: 'Writing and running tasks',
+ collapsed: false,
+ items: [
+ { text: 'Defining tasks', link: '/docs/guide/defining-tasks' },
+ { text: 'Running tasks', link: '/docs/guide/running-tasks' }
+ ]
+ },
+ {
+ text: 'Variables and arguments',
+ collapsed: false,
+ items: [
+ { text: 'Variables', link: '/docs/guide/variables' },
+ { text: 'Command-line arguments', link: '/docs/guide/arguments' },
+ {
+ text: 'Validation and prompts',
+ link: '/docs/guide/required-variables'
+ },
+ { text: 'Secret variables', link: '/docs/guide/secret-variables' }
+ ]
+ },
+ {
+ text: 'Task execution',
+ collapsed: false,
+ items: [
+ {
+ text: 'Dependencies and task calls',
+ link: '/docs/guide/dependencies'
+ },
+ { text: 'Loops', link: '/docs/guide/loops' },
+ {
+ text: 'Conditional execution',
+ link: '/docs/guide/conditional-execution'
+ },
+ {
+ text: 'Errors and cleanup',
+ link: '/docs/guide/errors-and-cleanup'
+ },
+ { text: 'Up-to-date checks', link: '/docs/guide/up-to-date' },
+ { text: 'Watch mode', link: '/docs/guide/watch' }
+ ]
+ },
+ {
+ text: 'Environment and output',
+ collapsed: false,
+ items: [
+ { text: 'Environment variables', link: '/docs/guide/environment' },
+ { text: 'Platforms and shells', link: '/docs/guide/platforms' },
+ { text: 'Output and logging', link: '/docs/guide/output' }
+ ]
+ },
+ {
+ text: 'Including Taskfiles',
+ collapsed: false,
+ items: [
+ { text: 'Including Taskfiles', link: '/docs/guide/includes' },
+ { text: 'Remote Taskfiles', link: '/docs/remote-taskfiles' }
+ ]
+ }
+ ]
},
{
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 +111,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 +126,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: 'Completion Scripts',
- link: '/docs/deprecations/completion-scripts'
+ text: 'Community',
+ link: '/docs/community'
},
{
- text: 'Template Functions',
- link: '/docs/deprecations/template-functions'
+ 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: 'Version 2 Schema (#1197)',
- link: '/docs/deprecations/version-2-schema'
+ 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: '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/custom.css b/website/.vitepress/theme/custom.css
index 795907739b..c7e1c40919 100644
--- a/website/.vitepress/theme/custom.css
+++ b/website/.vitepress/theme/custom.css
@@ -1,9 +1,9 @@
:root {
--ifm-color-primary: #43aba2;
--vp-home-hero-name-color: var(--ifm-color-primary);
- --vp-c-brand-1: var(--ifm-color-primary);
- --vp-c-brand-2: var(--ifm-color-primary);
- --vp-c-brand-3: var(--ifm-color-primary);
+ --vp-c-brand-1: #176e68;
+ --vp-c-brand-2: #1d7b74;
+ --vp-c-brand-3: #176e68;
--vp-icon-info: #3b82f6;
--vp-icon-tip: #10b981;
@@ -13,6 +13,11 @@
}
.dark {
+ --vp-c-brand-1: #63c7bd;
+ --vp-c-brand-2: #7dd8cf;
+ --vp-c-brand-3: #176e68;
+ --vp-button-brand-hover-bg: #1d7b74;
+ --vp-button-brand-active-bg: #135f59;
--vp-icon-info: #93c5fd;
--vp-icon-tip: #34d399;
--vp-icon-warning: #fbbf24;
@@ -20,6 +25,87 @@
--vp-icon-details: #9ca3af;
}
+/* Keep long headings inside the mobile/tablet outline, including nested lists. */
+.VPLocalNavOutlineDropdown .items {
+ grid-template-columns: minmax(0, 1fr);
+}
+
+.VPLocalNavOutlineDropdown .outline {
+ min-width: 0;
+}
+
+.VPLocalNavOutlineDropdown .outline-link {
+ padding-block: 10px;
+ min-height: 44px;
+ line-height: 24px;
+ white-space: normal;
+ overflow-wrap: anywhere;
+}
+
+.docs-overview .docs-start {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+ margin-block: 24px 40px;
+}
+
+.docs-overview .docs-card {
+ min-width: 0;
+ padding: 20px;
+ border: 1px solid var(--vp-c-divider);
+ border-radius: 12px;
+ background: var(--vp-c-bg-soft);
+}
+
+.docs-overview .docs-card h2 {
+ margin: 0 0 8px;
+ padding: 0;
+ border: 0;
+ font-size: 20px;
+ line-height: 28px;
+}
+
+.docs-overview .docs-card p {
+ margin: 8px 0;
+ font-size: 15px;
+ line-height: 24px;
+}
+
+.docs-overview .docs-card ul {
+ margin: 12px 0 0;
+ padding: 0;
+ list-style: none;
+}
+
+.docs-overview .docs-card li + li {
+ margin-top: 0;
+}
+
+.docs-overview .docs-card li a {
+ display: inline-flex;
+ align-items: center;
+ min-height: 44px;
+}
+
+@media (min-width: 1200px) {
+ .docs-overview .docs-card p {
+ min-height: 48px;
+ }
+}
+
+@media (max-width: 1199px) {
+ .docs-overview .docs-start {
+ grid-template-columns: 1fr;
+ }
+
+ .docs-overview .docs-card ul {
+ display: flex;
+ flex-wrap: wrap;
+ column-gap: 20px;
+ margin-top: 4px;
+ }
+}
+
img[src*='shields.io'] {
display: inline;
vertical-align: text-bottom;
diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts
index 91c9d08592..fc4caf8ec6 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
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
+ //