Files
task/website/.vitepress/config.ts

417 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
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(', ')}` }
],
// 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 }],
[
'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" },
JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Task",
"url": "https://taskfile.dev/"
})
]
],
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 }])
// 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.'
}
}
]
})
])
}
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)
)
}
]
}
},
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: {
hostname: 'https://taskfile.dev',
transformItems: (items) => {
return items.map((item) => ({
...item,
lastmod: new Date().toISOString()
}));
}
}
});