From 7eee5cc7e1f6f12b2a97a76eed67e26ec9d5cc05 Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sun, 30 Aug 2026 19:26:54 +0200 Subject: [PATCH] docs: isolate preview and production deploys (#3004) --- website/.vitepress/config.ts | 169 +++++++++++------- website/Taskfile.yml | 7 +- website/netlify.toml | 15 ++ .../next/docs/experiments/remote-taskfiles.md | 2 +- website/src/next/docs/reference/config.md | 14 +- .../src/next/docs/reference/environment.md | 8 +- website/src/next/docs/reference/schema.md | 2 +- website/src/public/_headers | 26 +++ 8 files changed, 167 insertions(+), 76 deletions(-) diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index 7bf78562..6437c69b 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -1,6 +1,6 @@ import { defineConfig, HeadConfig } from 'vitepress'; import githubLinksPlugin from './plugins/github-links'; -import { readdirSync, readFileSync } from 'fs'; +import { readdirSync, readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; import matter from 'gray-matter'; import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'; @@ -11,7 +11,7 @@ import { } from 'vitepress-plugin-group-icons'; import { team } from './team.ts'; import { adopters } from './adopters.ts'; -import { taskDescription, taskName, ogUrl, ogImage } from './meta.ts'; +import { taskDescription, taskName, ogImage } from './meta.ts'; import { fileURLToPath, URL } from 'node:url'; import llmstxt from 'vitepress-plugin-llms'; import { sidebar as nextSidebar } from './sidebar/next.ts'; @@ -30,6 +30,9 @@ const version = readFileSync( const isLatest = process.env.DOCS_CHANNEL === 'latest'; const channel = isLatest ? 'latest' : 'next'; const other = isLatest ? 'next' : 'latest'; +const isPublicDeploy = + process.env.DOCS_SITE === 'production' && process.env.DOCS_LOCAL !== '1'; +const isProduction = isLatest && isPublicDeploy; const docsSidebar = isLatest ? latestSidebar : nextSidebar; @@ -98,65 +101,89 @@ export default defineConfig({ { name: 'author', content: `${team.map((c) => c.name).join(', ')}` } ], // Open Graph - ['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:site_name', content: 'Task' }], ['meta', { property: 'og:image', content: ogImage }], // Twitter Card ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:site', content: '@taskfiledev' }], ['meta', { name: 'twitter:image', content: ogImage }], + ...(isPublicDeploy + ? ([ + [ + 'script', + { + defer: '', + src: 'https://u.taskfile.dev/script.js', + 'data-website-id': '084030b0-0e3f-4891-8d2a-0c12c40f5933' + } + ] + ] satisfies HeadConfig[]) + : []), [ - 'meta', - { - name: 'keywords', - content: - 'task runner, build tool, taskfile, yaml build tool, go task runner, make alternative, cross-platform build tool, makefile alternative, automation tool, ci cd pipeline, developer productivity, build automation, command line tool, go binary, yaml configuration' - } - ], - [ - "script", - { - defer: "", - src: "https://u.taskfile.dev/script.js", - "data-website-id": "084030b0-0e3f-4891-8d2a-0c12c40f5933" - } - ], - [ - "script", - { type: "application/ld+json" }, + 'script', + { type: 'application/ld+json' }, JSON.stringify({ - "@context": "https://schema.org", - "@type": "WebSite", - "name": "Task", - "url": "https://taskfile.dev/" + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'Task', + url: 'https://taskfile.dev/' }) ] ], transformHead({ pageData }) { - const head: HeadConfig[] = [] + const head: HeadConfig[] = []; - // Canonical URL dynamique - const canonicalUrl = `https://taskfile.dev/${pageData.relativePath + const canonicalPath = pageData.relativePath .replace(/\.md$/, '') - .replace(/index$/, '')}` - head.push(['link', { rel: 'canonical', href: canonicalUrl }]) + .replace(/index$/, ''); + const canonicalUrl = new URL( + typeof pageData.frontmatter.canonical === 'string' + ? pageData.frontmatter.canonical + : canonicalPath, + 'https://taskfile.dev/' + ).href; + head.push(['link', { rel: 'canonical', href: canonicalUrl }]); // Dynamic Open Graph and Twitter meta tags - const isHome = pageData.relativePath === 'index.md'; - var pageTitle = pageData.frontmatter.title || pageData.title || taskName; + const isHome = new URL(canonicalUrl).pathname === '/'; + let pageTitle = pageData.frontmatter.title || pageData.title || taskName; if (!isHome) { pageTitle = `${pageTitle} | ${taskName}`; } - const pageDescription = pageData.frontmatter.description || pageData.description || taskDescription - head.push(['meta', { property: 'og:title', content: pageTitle }]) - head.push(['meta', { property: 'og:description', content: pageDescription }]) - head.push(['meta', { property: 'og:url', content: canonicalUrl }]) - head.push(['meta', { name: 'twitter:title', content: pageTitle }]) - head.push(['meta', { name: 'twitter:description', content: pageDescription }]) + const pageDescription = + pageData.frontmatter.description || + pageData.description || + taskDescription; + head.push([ + 'meta', + { + property: 'og:type', + content: + canonicalUrl.includes('/blog/') && !canonicalUrl.endsWith('/blog/') + ? 'article' + : 'website' + } + ]); + head.push(['meta', { property: 'og:title', content: pageTitle }]); + head.push([ + 'meta', + { property: 'og:description', content: pageDescription } + ]); + head.push(['meta', { property: 'og:url', content: canonicalUrl }]); + head.push(['meta', { name: 'twitter:title', content: pageTitle }]); + head.push([ + 'meta', + { name: 'twitter:description', content: pageDescription } + ]); - // Noindex pour 404 - if (pageData.relativePath === '404.md') { - head.push(['meta', { name: 'robots', content: 'noindex, nofollow' }]) + // Only the released public site is indexable. The public next site and + // previews keep production canonicals but must never be indexed. + if ( + !isProduction || + pageData.relativePath === '404.md' || + pageData.frontmatter.noindex === true + ) { + head.push(['meta', { name: 'robots', content: 'noindex, nofollow' }]); } // Structured data for the adopters carousel on the homepage: an ItemList @@ -184,7 +211,7 @@ export default defineConfig({ } })) }) - ]) + ]); } // On the /adopters page, emit CollectionPage + ItemList (richer than the @@ -219,7 +246,7 @@ export default defineConfig({ })) } }) - ]) + ]); head.push([ 'script', @@ -262,14 +289,14 @@ export default defineConfig({ } ] }) - ]) + ]); } - return head + return head; }, srcDir: 'src', cleanUrls: true, - srcExclude: [`${other}/**`], + srcExclude: [`${other}/**`, `${channel}/docs/**/template.md`], rewrites: { [`${channel}/:path*`]: ':path*' }, markdown: { config: (md) => { @@ -333,14 +360,28 @@ export default defineConfig({ code: 'CESI65QJ', placement: 'taskfiledev' }, - search: { - provider: 'algolia', - options: { - appId: '7IZIJ13AI7', - apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc', - indexName: 'taskfile' - } - }, + search: isProduction + ? { + provider: 'algolia', + options: { + appId: '7IZIJ13AI7', + apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc', + indexName: 'taskfile' + } + } + : { + provider: 'local', + options: { + detailedView: true, + miniSearch: { + searchOptions: { + fuzzy: 0.2, + prefix: true, + boost: { title: 4, titles: 2, text: 1 } + } + } + } + }, nav: [ { text: 'Home', link: '/' }, { @@ -411,12 +452,18 @@ export default defineConfig({ } }, sitemap: { - hostname: 'https://taskfile.dev', - transformItems: (items) => { - return items.map((item) => ({ - ...item, - lastmod: new Date().toISOString() - })); - } + hostname: 'https://taskfile.dev' + }, + buildEnd({ outDir }) { + const robots = isProduction + ? [ + 'User-agent: *', + 'Allow: /', + '', + 'Sitemap: https://taskfile.dev/sitemap.xml', + '' + ] + : ['User-agent: *', 'Disallow: /', '']; + writeFileSync(resolve(outDir, 'robots.txt'), robots.join('\n')); } }); diff --git a/website/Taskfile.yml b/website/Taskfile.yml index cfaf794a..bbb7ca93 100644 --- a/website/Taskfile.yml +++ b/website/Taskfile.yml @@ -21,6 +21,7 @@ tasks: PORT: '{{default "3001" .PORT}}' env: DOCS_CHANNEL: '{{.CHANNEL | default "next"}}' + DOCS_SITE: '{{.SITE | default "preview"}}' # Only the dev server sets this: it is what keeps the localhost URLs of # the version selector out of a build. See .vitepress/config.ts. DOCS_LOCAL: '1' @@ -51,6 +52,7 @@ tasks: deps: [install] env: DOCS_CHANNEL: '{{.CHANNEL | default "next"}}' + DOCS_SITE: '{{.SITE | default "preview"}}' cmds: - pnpm build @@ -58,7 +60,7 @@ tasks: desc: Build website with the content of the released version cmds: - task: build - vars: { CHANNEL: latest } + vars: { CHANNEL: latest, SITE: production } preview: desc: Preview Website @@ -80,8 +82,9 @@ tasks: # channel these tasks just built. deploy:next: desc: Build and deploy next.taskfile.dev - deps: [build:next] cmds: + - task: build + vars: { CHANNEL: next, SITE: production } - pnpm netlify deploy --prod --no-build --site=4e13dfcf-fc0d-4bec-ad60-b918a8dc3942 deploy:prod: diff --git a/website/netlify.toml b/website/netlify.toml index c19ad159..3f50d6b5 100644 --- a/website/netlify.toml +++ b/website/netlify.toml @@ -1,3 +1,18 @@ [build] publish = ".vitepress/dist" command = "pnpm run build" + +[build.environment] + NODE_VERSION = "24" + +[context.production.environment] + DOCS_CHANNEL = "latest" + DOCS_SITE = "production" + +[context.deploy-preview.environment] + DOCS_CHANNEL = "next" + DOCS_SITE = "preview" + +[context.branch-deploy.environment] + DOCS_CHANNEL = "next" + DOCS_SITE = "preview" diff --git a/website/src/next/docs/experiments/remote-taskfiles.md b/website/src/next/docs/experiments/remote-taskfiles.md index c242620b..2e83b465 100644 --- a/website/src/next/docs/experiments/remote-taskfiles.md +++ b/website/src/next/docs/experiments/remote-taskfiles.md @@ -9,6 +9,6 @@ The Remote Taskfiles experiment has now [been released][changelog] :tada:. To learn more, you can read the [remote Taskfile docs][remote-taskfile-docs] or check out our [blog post][blog-post]. -[changelog]: ../changelog.md#v3511---2026-05-16 +[changelog]: ../changelog.md#v3-51-1-2026-05-16 [remote-taskfile-docs]: ../remote-taskfiles.md [blog-post]: ../../blog/remote-taskfiles diff --git a/website/src/next/docs/reference/config.md b/website/src/next/docs/reference/config.md index ff694117..be606b87 100644 --- a/website/src/next/docs/reference/config.md +++ b/website/src/next/docs/reference/config.md @@ -85,7 +85,7 @@ experiments: - **Type**: `boolean` - **Default**: `false` - **Description**: Enable verbose output for all tasks -- **CLI equivalent**: [`-v, --verbose`](./cli.md#-v---verbose) +- **CLI equivalent**: [`-v, --verbose`](./cli.md#v-verbose) - **Environment variable**: [`TASK_VERBOSE`](./environment.md#task-verbose) ```yaml @@ -97,7 +97,7 @@ verbose: true - **Type**: `boolean` - **Default**: `false` - **Description**: Disables echoing of commands -- **CLI equivalent**: [`-s, --silent`](./cli.md#-s---silent) +- **CLI equivalent**: [`-s, --silent`](./cli.md#s-silent) - **Environment variable**: [`TASK_SILENT`](./environment.md#task-silent) ```yaml @@ -110,7 +110,7 @@ silent: true - **Default**: `true` - **Description**: Enable colored output. Colors are automatically enabled in CI environments (`CI=true`). -- **CLI equivalent**: [`-c, --color`](./cli.md#-c---color) +- **CLI equivalent**: [`-c, --color`](./cli.md#c-color) - **Environment variable**: [`TASK_COLOR`](./environment.md#task-color) ```yaml @@ -123,7 +123,7 @@ color: false - **Default**: `false` - **Description**: Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name. -- **CLI equivalent**: [`--disable-fuzzy`](./cli.md#--disable-fuzzy) +- **CLI equivalent**: [`--disable-fuzzy`](./cli.md#disable-fuzzy) - **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy) @@ -136,7 +136,7 @@ disable-fuzzy: true - **Type**: `integer` - **Minimum**: `1` - **Description**: Number of concurrent tasks to run -- **CLI equivalent**: [`-C, --concurrency`](./cli.md#-c---concurrency-number) +- **CLI equivalent**: [`-C, --concurrency`](./cli.md#c-concurrency-number) - **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency) @@ -149,7 +149,7 @@ concurrency: 4 - **Type**: `boolean` - **Default**: `false` - **Description**: Stop executing dependencies as soon as one of them fail -- **CLI equivalent**: [`-F, --failfast`](./cli.md#-f---failfast) +- **CLI equivalent**: [`-F, --failfast`](./cli.md#f-failfast) - **Environment variable**: [`TASK_FAILFAST`](./environment.md#task-failfast) ```yaml @@ -164,7 +164,7 @@ failfast: true When enabled, Task will display an interactive prompt for any missing required variable. Requires a TTY. Task automatically detects non-TTY environments (CI pipelines, etc.) and skips prompts. -- **CLI equivalent**: [`--interactive`](./cli.md#--interactive) +- **CLI equivalent**: [`--interactive`](./cli.md#interactive) ```yaml interactive: true diff --git a/website/src/next/docs/reference/environment.md b/website/src/next/docs/reference/environment.md index 87bc29c7..fefe10ae 100644 --- a/website/src/next/docs/reference/environment.md +++ b/website/src/next/docs/reference/environment.md @@ -87,7 +87,7 @@ files > defaults. - **Type**: `string` (`interleaved`, `group`, `prefixed`) - **Description**: Sets the output style -- **CLI equivalent**: [`--output`](./cli.md#--output-string) +- **CLI equivalent**: [`--output`](./cli.md#o-output-mode) ### `TASK_OUTPUT_GROUP_BEGIN` @@ -95,7 +95,7 @@ files > defaults. - **Description**: Message template to print before a task's grouped output. Only applies when the output style is `group`. - **CLI equivalent**: - [`--output-group-begin`](./cli.md#--output-group-begin-template) + [`--output-group-begin`](./cli.md#output-group-begin-template) ### `TASK_OUTPUT_GROUP_END` @@ -103,7 +103,7 @@ files > defaults. - **Description**: Message template to print after a task's grouped output. Only applies when the output style is `group`. - **CLI equivalent**: - [`--output-group-end`](./cli.md#--output-group-end-template) + [`--output-group-end`](./cli.md#output-group-end-template) ### `TASK_OUTPUT_GROUP_ERROR_ONLY` @@ -112,7 +112,7 @@ files > defaults. - **Description**: Swallow output from successful tasks. Only applies when the output style is `group`. - **CLI equivalent**: - [`--output-group-error-only`](./cli.md#--output-group-error-only) + [`--output-group-error-only`](./cli.md#output-group-error-only) ### `TASK_TEMP_DIR` diff --git a/website/src/next/docs/reference/schema.md b/website/src/next/docs/reference/schema.md index 471d9816..737ef2e9 100644 --- a/website/src/next/docs/reference/schema.md +++ b/website/src/next/docs/reference/schema.md @@ -983,7 +983,7 @@ When a command exceeds its timeout, it is terminated and the task fails with an error, preventing commands from hanging indefinitely in a pipeline. The timeout bounds the whole step, so an [`if`](#command) condition that hangs is cut short too, and [`ignore_error`](#command) covers a timeout like any other failure. A -timed-out command reports [`EXIT_CODE`](/docs/reference/templating#exit_code) +timed-out command reports [`EXIT_CODE`](/docs/reference/templating#exit-code) `124`, following the convention of `timeout(1)`. A dependency takes the same key: diff --git a/website/src/public/_headers b/website/src/public/_headers index 95eac8eb..480f18ae 100644 --- a/website/src/public/_headers +++ b/website/src/public/_headers @@ -1,2 +1,28 @@ +/* + X-Content-Type-Options: nosniff + X-Frame-Options: DENY + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: camera=(), geolocation=(), microphone=() + Cache-Control: public, max-age=0, must-revalidate + +/assets/* + Cache-Control: public, max-age=31536000, immutable + +/img/* + Cache-Control: public, max-age=604800, stale-while-revalidate=86400 + +/*.md + Content-Type: text/markdown; charset=utf-8 + Access-Control-Allow-Origin: * + +/llms*.txt + Content-Type: text/plain; charset=utf-8 + Access-Control-Allow-Origin: * + +/*.json + Content-Type: application/json; charset=utf-8 + Access-Control-Allow-Origin: * + /Taskfile.yml Content-Type: text/plain; charset=utf-8 + Access-Control-Allow-Origin: *