Skip to content

Commit 2b3e190

Browse files
Website audit
1 parent 6e8d109 commit 2b3e190

8 files changed

Lines changed: 173 additions & 29 deletions

File tree

website/eleventy.config.js

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,20 @@ export default function(eleventyConfig) {
3636
{% endif %}
3737
</article>`;
3838

39-
const blogIndexOverride = `---
39+
const blogIndexOverride = `---
4040
layout: layouts/base.njk
41-
title: Blog
41+
title: CommandTree Blog - VS Code Command Runner Guide Updates
42+
description: CommandTree release notes and practical VS Code task runner guides for command discovery, AI summaries, mise tasks, monorepo workflows, and workspace automation.
4243
permalink: /blog/
4344
---
4445
<div class="blog-container">
4546
<header class="blog-header">
4647
<h1>Blog</h1>
47-
<p class="blog-subtitle">Latest posts and updates</p>
48+
<p class="blog-subtitle">Release notes and practical guides for VS Code task discovery.</p>
4849
</header>
4950
<nav class="blog-nav">
5051
<a href="/blog/tags/" class="blog-nav-link">Tags</a>
51-
<a href="/blog/categories/" class="blog-nav-link">Categories</a>
52+
{% if collections.categoryList | length > 0 %}<a href="/blog/categories/" class="blog-nav-link">Categories</a>{% endif %}
5253
</nav>
5354
<div class="post-list">
5455
{% for post in collections.posts | sortByDateDesc %}
@@ -62,19 +63,20 @@ permalink: /blog/
6263
</div>
6364
</div>`;
6465

65-
const tagsIndexOverride = `---
66+
const tagsIndexOverride = `---
6667
layout: layouts/base.njk
67-
title: Tags
68+
title: CommandTree Blog Tags - VS Code Task Runner Topics
69+
description: Browse CommandTree blog tags for VS Code command runner topics including AI summaries, task discovery, mise tasks, monorepos, and workspace automation.
6870
permalink: /blog/tags/
6971
---
7072
<div class="blog-container">
7173
<header class="blog-header">
7274
<h1>Tags</h1>
73-
<p class="blog-subtitle">Browse blog posts by tag</p>
75+
<p class="blog-subtitle">Browse CommandTree posts by VS Code task runner topic.</p>
7476
</header>
7577
<nav class="blog-nav">
7678
<a href="/blog/" class="blog-nav-link">All posts</a>
77-
<a href="/blog/categories/" class="blog-nav-link">Categories</a>
79+
{% if collections.categoryList | length > 0 %}<a href="/blog/categories/" class="blog-nav-link">Categories</a>{% endif %}
7880
</nav>
7981
<ul class="taxonomy-grid">
8082
{% for tag in collections.tagList %}
@@ -87,15 +89,16 @@ permalink: /blog/tags/
8789
</ul>
8890
</div>`;
8991

90-
const categoriesIndexOverride = `---
92+
const categoriesIndexOverride = `---
9193
layout: layouts/base.njk
92-
title: Categories
94+
title: CommandTree Blog Categories - VS Code Task Runner Guides
95+
description: Browse CommandTree blog categories for VS Code command runner guides covering task discovery, AI summaries, mise tasks, and workspace automation.
9396
permalink: /blog/categories/
9497
---
9598
<div class="blog-container">
9699
<header class="blog-header">
97100
<h1>Categories</h1>
98-
<p class="blog-subtitle">Browse blog posts by category</p>
101+
<p class="blog-subtitle">Browse CommandTree posts by guide category.</p>
99102
</header>
100103
<nav class="blog-nav">
101104
<a href="/blog/" class="blog-nav-link">All posts</a>
@@ -123,8 +126,8 @@ pagination:
123126
permalink: /blog/tags/{{ tag | slugify }}/
124127
layout: layouts/base.njk
125128
eleventyComputed:
126-
title: "Posts tagged '{{ tag | capitalize }}'"
127-
description: "All blog posts tagged with {{ tag | capitalize }}."
129+
title: "{{ tag | capitalize }} Articles - CommandTree VS Code Task Runner Blog"
130+
description: "CommandTree articles tagged with {{ tag | capitalize }} for VS Code developers who need task discovery, command running, AI summaries, and workspace automation tips."
128131
---
129132
<div class="blog-container">
130133
<header class="blog-header">
@@ -151,8 +154,8 @@ pagination:
151154
permalink: /blog/categories/{{ category | slugify }}/
152155
layout: layouts/base.njk
153156
eleventyComputed:
154-
title: "{{ category | capitalize }}"
155-
description: "All blog posts in the {{ category }} category."
157+
title: "{{ category | capitalize }} Guides - CommandTree VS Code Task Runner Blog"
158+
description: "CommandTree posts in the {{ category }} category for VS Code developers covering command runners, task discovery, AI summaries, and workspace automation."
156159
---
157160
<div class="blog-container">
158161
<header class="blog-header">
@@ -171,12 +174,46 @@ eleventyComputed:
171174
</div>
172175
</div>`;
173176

177+
const sitemapOverride = `---json
178+
{
179+
"permalink": "sitemap.xml",
180+
"eleventyExcludeFromCollections": true
181+
}
182+
---
183+
<?xml version="1.0" encoding="utf-8"?>
184+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
185+
{%- for page in collections.all %}
186+
{%- set isTagPage = page.url.startsWith('/blog/tags/') %}
187+
{%- set isCategoryPage = page.url.startsWith('/blog/categories/') %}
188+
{%- if not page.data.eleventyExcludeFromCollections and not isTagPage and not isCategoryPage %}
189+
<url>
190+
<loc>{{ site.url }}{{ page.url }}</loc>
191+
<lastmod>{{ page.date | isoDate }}</lastmod>
192+
{%- if page.url == "/" or page.url == "/index.html" %}
193+
<priority>1.0</priority>
194+
<changefreq>weekly</changefreq>
195+
{%- elif "/docs/" in page.url %}
196+
<priority>0.8</priority>
197+
<changefreq>monthly</changefreq>
198+
{%- elif "/blog/" in page.url %}
199+
<priority>0.7</priority>
200+
<changefreq>monthly</changefreq>
201+
{%- else %}
202+
<priority>0.5</priority>
203+
<changefreq>monthly</changefreq>
204+
{%- endif %}
205+
</url>
206+
{%- endif %}
207+
{%- endfor %}
208+
</urlset>`;
209+
174210
const blogOverrides = {
175211
"blog/index.njk": blogIndexOverride,
176212
"blog/tags.njk": tagsIndexOverride,
177213
"blog/categories.njk": categoriesIndexOverride,
178214
"blog/tags-pages.njk": tagsPagesOverride,
179215
"blog/categories-pages.njk": categoriesPagesOverride,
216+
"sitemap.njk": sitemapOverride,
180217
};
181218
// Register as an inline plugin so it runs AFTER the techdoc plugin
182219
// (plugins are processed in addPlugin order, after the user config callback).
@@ -296,6 +333,50 @@ eleventyComputed:
296333
return false;
297334
};
298335

336+
const isTaxonomyUrl = (url) => {
337+
if (!url) { return false; }
338+
return url.startsWith("/blog/tags/") || url.startsWith("/blog/categories/");
339+
};
340+
341+
const findJsonLdBlock = (content) => {
342+
const open = '<script type="application/ld+json">';
343+
const close = "</script>";
344+
const openStart = content.indexOf(open);
345+
if (openStart < 0) { return null; }
346+
const jsonStart = openStart + open.length;
347+
const closeStart = content.indexOf(close, jsonStart);
348+
if (closeStart < 0) { return null; }
349+
return { jsonStart, closeStart };
350+
};
351+
352+
const asCollectionPage = (item) => {
353+
if (item["@type"] !== "BlogPosting") { return item; }
354+
const collectionPage = { ...item, "@type": "CollectionPage" };
355+
delete collectionPage.author;
356+
delete collectionPage.datePublished;
357+
return collectionPage;
358+
};
359+
360+
const renderJsonLd = (data) => JSON.stringify(data, null, 2).split("\n").join("\n ");
361+
362+
const rewriteTaxonomyJsonLd = (content) => {
363+
const block = findJsonLdBlock(content);
364+
if (!block) { return content; }
365+
try {
366+
const data = JSON.parse(content.slice(block.jsonStart, block.closeStart).trim());
367+
if (Array.isArray(data["@graph"])) {
368+
data["@graph"] = data["@graph"].map(asCollectionPage);
369+
}
370+
return content.slice(0, block.jsonStart) + "\n " + renderJsonLd(data) + "\n " + content.slice(block.closeStart);
371+
} catch {
372+
return content;
373+
}
374+
};
375+
376+
const updateTaxonomySeo = (content) => rewriteTaxonomyJsonLd(content
377+
.replace('<meta name="robots" content="index, follow">', '<meta name="robots" content="noindex, follow">')
378+
.replace('<meta property="og:type" content="article">', '<meta property="og:type" content="website">'));
379+
299380
eleventyConfig.addTransform("blogHero", function(content) {
300381
if (!this.page.outputPath?.endsWith(".html")) {
301382
return content;
@@ -315,6 +396,13 @@ eleventyComputed:
315396
);
316397
});
317398

399+
eleventyConfig.addTransform("taxonomySeo", function(content) {
400+
if (!this.page.outputPath?.endsWith(".html") || !isTaxonomyUrl(this.page.url)) {
401+
return content;
402+
}
403+
return updateTaxonomySeo(content);
404+
});
405+
318406
eleventyConfig.addTransform("llmsTxt", function(content) {
319407
if (!this.page.outputPath?.endsWith("llms.txt")) {
320408
return content;

website/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"main": "index.js",
55
"scripts": {
66
"test": "npx playwright test",
7-
"dev": "npx @11ty/eleventy --serve",
8-
"build": "npx @11ty/eleventy"
7+
"clean": "node -e \"require('node:fs').rmSync('_site',{recursive:true,force:true})\"",
8+
"dev": "npm run clean && npx @11ty/eleventy --serve",
9+
"build": "npm run clean && npx @11ty/eleventy"
910
},
1011
"keywords": [],
1112
"author": "",

website/playwright.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default defineConfig({
1111
trace: "on-first-retry",
1212
},
1313
webServer: {
14-
command: "npx @11ty/eleventy --serve --port=8080",
14+
command: "npm run dev -- --port=8080",
1515
port: 8080,
1616
reuseExistingServer: !process.env.CI,
1717
timeout: 30000,

website/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default defineConfig({
1818
},
1919
],
2020
webServer: {
21-
command: 'npx @11ty/eleventy --serve --port=8080',
21+
command: 'npm run dev -- --port=8080',
2222
url: 'http://localhost:8080',
2323
reuseExistingServer: !process.env['CI'],
2424
timeout: 30000,

website/src/blog/ai-summaries-hover.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Copilot also flags dangerous operations. If a script runs `rm -rf`, force-pushes
3636

3737
## Stored Locally, Updated Automatically
3838

39-
Summaries are cached in a local SQLite database at `.commandtree/commandtree.sqlite3` in your workspace. They persist across sessions and only regenerate when the underlying script content changes, so there is no repeated API overhead.
39+
Summaries are cached in a local SQLite database at `.commandtree/commandtree.sqlite3` in your workspace. They persist across sessions and only regenerate when the underlying script content changes, reducing repeated summary generation.
4040

4141
## Works Without Copilot
4242

website/src/blog/mise-tasks-vscode.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
layout: layouts/blog.njk
33
title: Run Mise Tasks From the VS Code Sidebar - CommandTree 0.9.0
4-
description: CommandTree 0.9.0 auto-discovers every mise task in your workspace - mise.toml, .mise.toml, mise.yaml - and runs them from the VS Code sidebar alongside npm, Make, Just, and 18 other command types.
4+
description: CommandTree 0.9.0 discovers mise tasks from mise.toml, .mise.toml, and mise.yaml, then runs them from the VS Code sidebar beside npm, Make, and Just.
55
date: 2026-04-06
66
author: Christian Findlay
77
tags:
@@ -36,17 +36,17 @@ Both TOML tasks (`[tasks.build]` sections) and YAML task maps work. Descriptions
3636
3737
## One Click to Run
3838
39-
Click any mise task and CommandTree opens a new terminal in the same directory as the `mise.toml` file and runs `mise run <task>`. Tool versions, environment variables, and dependencies all resolve normally — *it is exactly the same command you would type yourself*. Tasks with parameters get prompted for input before they run.
39+
Click any mise task and CommandTree opens a new terminal in the same directory as the `mise.toml` file and runs `mise run <task>`, matching the command format in the [mise task runner documentation](https://mise.jdx.dev/tasks/). Tasks with parameters get prompted for input before they run.
4040
4141
## Mise *And* Everything Else
4242
43-
This is the part the mise-only extensions can't do. Most real projects are not pure mise. There is a `Makefile` from before the migration, an `npm run lint` script in `package.json`, a couple of shell scripts in `scripts/`, maybe a `Justfile` for the deploy step.
43+
Projects often keep more than one task system around: a `Makefile` from before the migration, an `npm run lint` script in `package.json`, shell scripts in `scripts/`, or a `Justfile` for the deploy step.
4444
4545
CommandTree discovers **22 command types** and shows them in one tree:
4646
4747
mise tasks, npm scripts, Makefile targets, Just recipes, Taskfile, shell scripts, Python scripts, PowerShell, Cargo, Gradle, Maven, Ant, Deno, Rake, Composer, Docker Compose services, .NET projects, C# scripts, F# scripts, VS Code tasks, launch configs, and Markdown files.
4848
49-
**One extension instead of three.** Filter by tag, pin favourites, search by text — it all works across every command type at once.
49+
Filter by tag, pin favourites, and search by text across every command type at once.
5050
5151
## Hover to See What a Task Does
5252

website/src/docs/ai-summaries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Each summary is a one-to-two sentence plain-language description of what the com
4141

4242
### Are summaries stored locally?
4343

44-
Yes. All summaries are stored in a SQLite database at `.commandtree/commandtree.sqlite3` in your workspace root. No data is sent to external servers beyond the GitHub Copilot API that runs locally in VS Code.
44+
Yes. Summaries are stored in a SQLite database at `.commandtree/commandtree.sqlite3` in your workspace root. When AI summaries are enabled, CommandTree uses the installed [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) extension to generate the text.
4545

4646
### How are security warnings triggered?
4747

website/tests/seo.spec.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,33 @@ const ALL_PAGES = [
88
'/docs/execution/',
99
'/docs/configuration/',
1010
'/blog/',
11+
'/blog/introducing-commandtree/',
12+
'/blog/ai-summaries-hover/',
13+
'/blog/mise-tasks-vscode/',
1114
];
1215

16+
const TAXONOMY_PAGES = [
17+
'/blog/tags/',
18+
'/blog/tags/mise/',
19+
'/blog/categories/',
20+
];
21+
22+
function isRecord(value: unknown): value is Record<string, unknown> {
23+
return typeof value === 'object' && value !== null;
24+
}
25+
26+
function getGraph(value: unknown): Record<string, unknown>[] {
27+
if (!isRecord(value)) {
28+
return [];
29+
}
30+
const graph = value['@graph'];
31+
return Array.isArray(graph) ? graph.filter(isRecord) : [];
32+
}
33+
34+
function getString(value: unknown): string {
35+
return typeof value === 'string' ? value : '';
36+
}
37+
1338
test.describe('SEO and Meta', () => {
1439
test('homepage has meta description', async ({ page }) => {
1540
await page.goto('/');
@@ -36,8 +61,12 @@ test.describe('SEO and Meta', () => {
3661
await page.goto(url);
3762
const content = await page.locator('meta[name="description"]').getAttribute('content');
3863
expect(content, `${url} should have a meta description`).toBeTruthy();
39-
expect(content!.length, `${url} description should be at least 50 chars`).toBeGreaterThanOrEqual(50);
40-
descriptions.push(content!);
64+
if (!content) {
65+
continue;
66+
}
67+
expect(content.length, `${url} description should be at least 120 chars`).toBeGreaterThanOrEqual(120);
68+
expect(content.length, `${url} description should be at most 170 chars`).toBeLessThanOrEqual(170);
69+
descriptions.push(content);
4170
}
4271
const unique = new Set(descriptions);
4372
expect(unique.size, 'All pages should have unique meta descriptions').toBe(descriptions.length);
@@ -49,6 +78,8 @@ test.describe('SEO and Meta', () => {
4978
await page.goto(url);
5079
const title = await page.title();
5180
expect(title, `${url} should have a title`).toBeTruthy();
81+
expect(title.length, `${url} title should be at least 30 chars`).toBeGreaterThanOrEqual(30);
82+
expect(title.length, `${url} title should be at most 70 chars`).toBeLessThanOrEqual(70);
5283
titles.push(title);
5384
}
5485
const unique = new Set(titles);
@@ -84,11 +115,32 @@ test.describe('SEO and Meta', () => {
84115
expect(count, `${url} should have JSON-LD`).toBeGreaterThanOrEqual(1);
85116
for (let i = 0; i < count; i++) {
86117
const text = await scripts.nth(i).textContent();
87-
expect(() => JSON.parse(text!), `${url} JSON-LD should be valid JSON`).not.toThrow();
118+
expect(text, `${url} JSON-LD should not be empty`).toBeTruthy();
119+
if (!text) {
120+
continue;
121+
}
122+
expect(() => JSON.parse(text), `${url} JSON-LD should be valid JSON`).not.toThrow();
88123
}
89124
}
90125
});
91126

127+
test('taxonomy pages are noindex collection pages', async ({ page }) => {
128+
for (const url of TAXONOMY_PAGES) {
129+
await page.goto(url);
130+
await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', 'noindex, follow');
131+
await expect(page.locator('meta[property="og:type"]')).toHaveAttribute('content', 'website');
132+
const text = await page.locator('script[type="application/ld+json"]').first().textContent();
133+
expect(text, `${url} should have JSON-LD`).toBeTruthy();
134+
if (!text) {
135+
continue;
136+
}
137+
const graph = getGraph(JSON.parse(text));
138+
const pageNode = graph.find((item) => getString(item['url']).endsWith(url));
139+
expect(getString(pageNode?.['@type']), `${url} should use CollectionPage schema`).toBe('CollectionPage');
140+
expect(pageNode?.['datePublished'], `${url} should not use article dates`).toBeUndefined();
141+
}
142+
});
143+
92144
test('homepage has og:image', async ({ page }) => {
93145
await page.goto('/');
94146
const ogImage = await page.locator('meta[property="og:image"]').getAttribute('content');
@@ -111,7 +163,10 @@ test.describe('SEO and Meta', () => {
111163
for (let i = 0; i < count; i++) {
112164
const alt = await images.nth(i).getAttribute('alt');
113165
expect(alt, `Image ${i} should have alt text`).toBeTruthy();
114-
expect(alt!.length, `Image ${i} alt text should be descriptive`).toBeGreaterThan(3);
166+
if (!alt) {
167+
continue;
168+
}
169+
expect(alt.length, `Image ${i} alt text should be descriptive`).toBeGreaterThan(3);
115170
}
116171
});
117172

0 commit comments

Comments
 (0)