mirror of
https://github.com/go-task/task.git
synced 2026-08-29 10:08:27 +02:00
417 lines
14 KiB
TypeScript
417 lines
14 KiB
TypeScript
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 Arduino’s 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 Make’s 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()
|
||
}));
|
||
}
|
||
}
|
||
});
|