Files
task/website/.vitepress/config.ts

423 lines
14 KiB
TypeScript
Raw Normal View History

2025-12-18 08:52:32 +01:00
import { defineConfig, HeadConfig } from 'vitepress';
import githubLinksPlugin from './plugins/github-links';
import { readdirSync, readFileSync } from 'fs';
import { resolve } from 'path';
import matter from 'gray-matter';
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs';
import {
groupIconMdPlugin,
groupIconVitePlugin,
localIconLoader
} from 'vitepress-plugin-group-icons';
import { team } from './team.ts';
import { adopters } from './adopters.ts';
2025-12-18 08:52:32 +01:00
import { taskDescription, taskName, ogUrl, ogImage } from './meta.ts';
import { fileURLToPath, URL } from 'node:url';
import llmstxt from 'vitepress-plugin-llms';
import { sidebar as nextSidebar } from './sidebar/next.ts';
import { sidebar as latestSidebar } from './sidebar/latest.ts';
const version = readFileSync(
resolve(__dirname, '../../internal/version/version.txt'),
'utf8'
).trim();
// Which channel to build. `src/next` is written for the upcoming release and
// serves next.taskfile.dev; `src/latest` is its copy at the released version
// and serves taskfile.dev. Both mount at the same URLs, so taskfile.dev never
// documents or announces a feature that is not in the released binary.
// cmd/release owns the other half of this: it promotes one over the other.
const isLatest = process.env.DOCS_CHANNEL === 'latest';
const channel = isLatest ? 'latest' : 'next';
const other = isLatest ? 'next' : 'latest';
const docsSidebar = isLatest ? latestSidebar : nextSidebar;
// Builds the "/blog/" sidebar from each blog post's frontmatter.
function buildBlogSidebar() {
const blogDir = resolve(__dirname, `../src/${channel}/blog`);
const posts = readdirSync(blogDir)
.filter((file) => file.endsWith('.md') && file !== 'index.md')
.map((file) => {
const { data: frontmatter } = matter(
readFileSync(resolve(blogDir, file), 'utf8')
);
return {
slug: file.replace(/\.md$/, ''),
title: frontmatter.sidebarTitle ?? frontmatter.title,
date: new Date(frontmatter.date)
};
})
.sort((a, b) => b.date.getTime() - a.date.getTime());
const byYear = new Map<number, { text: string; link: string }[]>();
for (const post of posts) {
const year = post.date.getFullYear();
if (!byYear.has(year)) byYear.set(year, []);
byYear.get(year)!.push({ text: post.title, link: `/blog/${post.slug}` });
}
return [...byYear.entries()]
.sort((a, b) => b[0] - a[0])
.map(([year, items]) => ({
text: String(year),
collapsed: false,
items
}));
}
// Ports are the ones the dev tasks bind to; keep them in sync with
// website/Taskfile.yml. DOCS_LOCAL is set by those tasks alone, so a build can
// never end up shipping localhost URLs.
const localPorts = { latest: 3002, next: 3001 };
const urlVersion =
process.env.DOCS_LOCAL === '1'
? {
current: `http://localhost:${localPorts.latest}/`,
next: `http://localhost:${localPorts.next}/`
}
: {
current: 'https://taskfile.dev/',
next: 'https://next.taskfile.dev/'
};
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: taskName,
description: taskDescription,
lang: 'en-US',
head: [
// Favicon ICO for legacy browsers (auto-discovery)
['link', { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }],
// Favicon SVG for modern browsers (scalable)
['link', { rel: 'icon', href: '/img/logo.svg', type: 'image/svg+xml' }],
// Apple Touch Icon for iOS devices
['link', { rel: 'apple-touch-icon', href: '/img/logo.png' }],
[
'meta',
{ name: 'author', content: `${team.map((c) => c.name).join(', ')}` }
],
2025-12-18 08:52:32 +01:00
// Open Graph
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'Task' }],
2025-12-18 08:52:32 +01:00
['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 }],
[
'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'
}
],
2025-09-14 10:18:32 -03:00
[
"script",
{
defer: "",
2025-11-07 17:48:28 -03:00
src: "https://u.taskfile.dev/script.js",
2025-09-14 10:18:32 -03:00
"data-website-id": "084030b0-0e3f-4891-8d2a-0c12c40f5933"
}
],
[
"script",
{ type: "application/ld+json" },
JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Task",
"url": "https://taskfile.dev/"
})
]
],
2025-12-18 08:52:32 +01:00
transformHead({ pageData }) {
const head: HeadConfig[] = []
// Canonical URL dynamique
const canonicalUrl = `https://taskfile.dev/${pageData.relativePath
.replace(/\.md$/, '')
.replace(/index$/, '')}`
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;
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 }])
2025-12-18 08:52:32 +01:00
// Noindex pour 404
if (pageData.relativePath === '404.md') {
head.push(['meta', { name: 'robots', content: 'noindex, nofollow' }])
}
// Structured data for the adopters carousel on the homepage: an ItemList
// of Organization entities so search engines can surface Task's adopters
// directly in rich results.
if (isHome) {
head.push([
'script',
{ type: 'application/ld+json' },
JSON.stringify({
'@context': 'https://schema.org',
'@type': 'ItemList',
name: 'Organizations and projects using Task',
itemListOrder: 'https://schema.org/ItemListUnordered',
numberOfItems: adopters.length,
itemListElement: adopters.map((a, i) => ({
'@type': 'ListItem',
position: i + 1,
item: {
'@type': 'Organization',
name: a.name,
url: a.url,
logo: a.img,
sameAs: [a.url]
}
}))
})
])
}
// On the /adopters page, emit CollectionPage + ItemList (richer than the
// homepage snippet because it targets this specific URL) and FAQPage for
// the question block at the bottom of the page. Kept in sync by hand with
// components/Adopters.vue.
if (pageData.relativePath === 'adopters.md') {
head.push([
'script',
{ type: 'application/ld+json' },
JSON.stringify({
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'Who uses Task',
url: 'https://taskfile.dev/adopters',
description:
'Organizations and open source projects that use Task as their build and release runner.',
mainEntity: {
'@type': 'ItemList',
numberOfItems: adopters.length,
itemListElement: adopters.map((a, i) => ({
'@type': 'ListItem',
position: i + 1,
item: {
'@type': 'Organization',
name: a.name,
url: a.url,
logo: a.img,
description: a.description,
sameAs: [a.url]
}
}))
}
})
])
head.push([
'script',
{ type: 'application/ld+json' },
JSON.stringify({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'Is Task production-ready?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Yes. Task ships as a single static binary, has been in wide production use since 2018, and powers the release workflows of projects with millions of downloads including Arduino CLI, GoReleaser, FerretDB, and Gogs.'
}
},
{
'@type': 'Question',
name: 'Who uses Task in enterprise?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Docker, Vercel, HashiCorp, Microsoft (Azure Sentinel), Google Cloud, AWS, and Anthropic are among the organizations that ship code with a Taskfile.yml. Task is also embedded end-to-end in Arduinos developer tooling stack across more than 70 repositories.'
}
},
{
'@type': 'Question',
name: 'How is Task different from Make?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Task uses plain YAML instead of Makes tab-sensitive syntax, runs identically on Linux, macOS, and Windows, and provides built-in caching based on file fingerprints. It also comes with an ecosystem of editor and CI integrations that Make lacks by default.'
}
},
{
'@type': 'Question',
name: 'Where can I find real-world Taskfile examples?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Every adopter listed above links directly to a public repository containing a production Taskfile.yml. Browsing those is the fastest way to see Task used in real codebases at different scales.'
}
}
]
})
])
}
2025-12-18 08:52:32 +01:00
return head
},
srcDir: 'src',
cleanUrls: true,
srcExclude: [`${other}/**`],
rewrites: { [`${channel}/:path*`]: ':path*' },
markdown: {
config: (md) => {
md.use(githubLinksPlugin, {
baseUrl: 'https://github.com',
repo: 'go-task/task'
});
md.use(tabsMarkdownPlugin);
md.use(groupIconMdPlugin);
}
},
vite: {
plugins: [
llmstxt({
ignoreFiles: [
'index.md',
'team.md',
'donate.md',
// Matched against source paths, which `rewrites` does not touch.
`${channel}/docs/styleguide.md`,
`${channel}/docs/contributing.md`,
`${channel}/docs/releasing.md`,
`${channel}/docs/changelog.md`,
`${channel}/blog/*`
]
}),
groupIconVitePlugin({
customIcon: {
'.taskrc.yml': localIconLoader(
import.meta.url,
'./theme/icons/task.svg'
),
'Taskfile.yml': localIconLoader(
import.meta.url,
'./theme/icons/task.svg'
)
}
})
],
resolve: {
alias: [
{
find: /^.*\/VPTeamMembersItem\.vue$/,
replacement: fileURLToPath(
new URL('./components/VPTeamMembersItem.vue', import.meta.url)
)
},
{
find: /^.*\/VPSponsorsGrid\.vue$/,
replacement: fileURLToPath(
new URL('./components/VPSponsorsGrid.vue', import.meta.url)
)
}
]
}
},
themeConfig: {
logo: '/img/logo.svg',
carbonAds: {
code: 'CESI65QJ',
placement: 'taskfiledev'
},
search: {
provider: 'algolia',
options: {
appId: '7IZIJ13AI7',
apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc',
indexName: 'taskfile'
}
},
nav: [
{ text: 'Home', link: '/' },
{
text: 'Docs',
link: '/docs/guide',
activeMatch: '^/docs'
},
{ text: 'Blog', link: '/blog', activeMatch: '^/blog' },
{ text: 'Donate', link: '/donate' },
{ text: 'Team', link: '/team' },
{
text: isLatest ? `v${version}` : 'Next',
items: [
{
items: [
// Absolute links, so VitePress would treat them as external and
// open them in a new tab. Switching channels is navigation, not a
// detour off the site.
{
text: `v${version}`,
link: urlVersion.current,
target: '_self',
noIcon: true
},
{
text: 'Next',
link: urlVersion.next,
target: '_self',
noIcon: true
}
]
}
]
}
],
sidebar: {
'/blog/': buildBlogSidebar(),
'/': docsSidebar,
// Hacky to disable sidebar for these pages
'/donate': [],
'/team': [],
'/adopters': []
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/go-task/task' },
{ icon: 'discord', link: 'https://discord.gg/6TY36E39UK' },
{ icon: 'x', link: 'https://twitter.com/taskfiledev' },
{ icon: 'bluesky', link: 'https://bsky.app/profile/taskfile.dev' },
{ icon: 'mastodon', link: 'https://fosstodon.org/@task' }
],
editLink: {
text: 'Edit this page on GitHub',
// Docs are always edited in `src/next`, even when the latest channel
// serves them from `src/latest/docs`.
// Serialized with toString() and evaluated in the browser, so it must not
// reference anything from this module. Both channels are edited in
// src/next, so strip whichever prefix the page was built from.
pattern: ({ filePath }) =>
`https://github.com/go-task/task/edit/main/website/src/next/${filePath.replace(/^(next|latest)\//, '')}`
},
footer: {
message:
'Built with <a target="_blank" href="https://www.netlify.com">Netlify</a>'
}
},
sitemap: {
2025-12-18 08:52:32 +01:00
hostname: 'https://taskfile.dev',
transformItems: (items) => {
return items.map((item) => ({
...item,
lastmod: new Date().toISOString()
}));
}
}
});