Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@steno/plugin-seo",
"version": "0.7.3",
"version": "1.0.0",
"exports": "./mod.ts",
"imports": {
"steno": "jsr:@steno/steno@^0.12.0",
Expand Down
126 changes: 126 additions & 0 deletions test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,132 @@ Deno.test({
},
});

Deno.test({
name:
"seo: includes root/index pages in the sitemap but excludes them from RSS/Atom, and sorts by date descending",
fn: async () => {
const plugin = seoPlugin({ siteUrl: "https://myblog.com/" });

const writtenFiles: Record<string, string> = {};
const originalWriteTextFile = Deno.writeTextFile;

Deno.writeTextFile = (
path: string | URL,
data: string | ReadableStream<string>,
): Promise<void> => {
writtenFiles[String(path)] = typeof data === "string" ? data : "[Stream]";
return Promise.resolve();
};

try {
const mockConfig = {
output: "dist",
title: "Test Blog",
description: "Test description",
author: "Tester",
pages: [
{
slug: "",
title: "Home",
description: "Homepage",
date: "2026-01-01T00:00:00Z",
},
{
slug: "index.html",
title: "Index",
description: "Index page",
date: "2026-01-02T00:00:00Z",
},
{
slug: "posts/older-post",
title: "Older Post",
description: "An older post",
date: "2026-01-01T00:00:00Z",
},
{
slug: "posts/newer-post",
title: "Newer Post",
description: "A newer post",
date: "2026-02-01T00:00:00Z",
},
],
};

await plugin.afterBuild!(mockConfig);

const sitemap = writtenFiles["dist/sitemap.xml"];
const rss = writtenFiles["dist/feed.xml"];
const atom = writtenFiles["dist/atom.xml"];

// Root and index.html pages appear in the sitemap...
assertStringIncludes(sitemap, "<loc>https://myblog.com/</loc>");
assertStringIncludes(sitemap, "<loc>https://myblog.com/index.html</loc>");

// ...but are excluded from RSS/Atom item lists.
assert(
!rss.includes("<title>Home</title>") &&
!rss.includes("<title>Index</title>"),
"root/index pages should not appear as RSS items",
);
assert(
!atom.includes("<title>Home</title>") &&
!atom.includes("<title>Index</title>"),
"root/index pages should not appear as Atom items",
);

// Non-root pages are included, ordered newest-first.
const rssNewerIdx = rss.indexOf("Newer Post");
const rssOlderIdx = rss.indexOf("Older Post");
assert(rssNewerIdx !== -1 && rssOlderIdx !== -1);
assert(
rssNewerIdx < rssOlderIdx,
"expected newer post to appear before older post in RSS feed",
);
} finally {
Deno.writeTextFile = originalWriteTextFile;
}
},
});

Deno.test({
name: "seo: defaults pages to [] and output to 'dist' when omitted",
fn: async () => {
const plugin = seoPlugin({ siteUrl: "https://myblog.com/" });

const writtenFiles: Record<string, string> = {};
const originalWriteTextFile = Deno.writeTextFile;

Deno.writeTextFile = (
path: string | URL,
data: string | ReadableStream<string>,
): Promise<void> => {
writtenFiles[String(path)] = typeof data === "string" ? data : "[Stream]";
return Promise.resolve();
};

try {
// Neither `pages` nor `output` provided.
const mockConfig = {
title: "Test Blog",
description: "Test description",
author: "Tester",
};

await plugin.afterBuild!(mockConfig);

assert(writtenFiles["dist/sitemap.xml"] !== undefined);
assert(writtenFiles["dist/feed.xml"] !== undefined);
assert(writtenFiles["dist/atom.xml"] !== undefined);
assertStringIncludes(
writtenFiles["dist/sitemap.xml"],
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n</urlset>',
);
} finally {
Deno.writeTextFile = originalWriteTextFile;
}
},
});

Deno.test({
name: "seo: generates a default robots.txt allowing all crawling",
fn: async () => {
Expand Down
Loading