Initial commit
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { CaptureResult } from "../src/capture/capture.js";
|
||||
import type { AssetGraph } from "../src/infer/assets.js";
|
||||
import type { IR } from "../src/normalize/ir.js";
|
||||
import {
|
||||
buildSeoInventory,
|
||||
emitSeoAssetFiles,
|
||||
emitSeoRoutes,
|
||||
jsonLdHeadMarkup,
|
||||
metadataExport,
|
||||
routeSummaryFromIr,
|
||||
seoRouteFiles,
|
||||
} from "../src/generate/seo.js";
|
||||
import { agentsMd, architectureMd } from "../src/generate/docs.js";
|
||||
|
||||
function fixtureIr(): IR {
|
||||
return {
|
||||
doc: {
|
||||
sourceUrl: "https://example.test/seo",
|
||||
title: "SEO Fixture",
|
||||
lang: "en",
|
||||
charset: "UTF-8",
|
||||
metaViewport: "width=device-width, initial-scale=1",
|
||||
viewports: [375, 768, 1280, 1920],
|
||||
sampleViewports: [375, 768, 1280, 1920],
|
||||
canonicalViewport: 1280,
|
||||
perViewport: {
|
||||
1280: { scrollHeight: 800, scrollWidth: 1280, htmlBg: "rgb(255, 255, 255)", bodyBg: "rgb(255, 255, 255)", bodyColor: "rgb(0, 0, 0)", bodyFont: "Arial" },
|
||||
},
|
||||
nodeCount: 2,
|
||||
keyframes: [],
|
||||
head: {
|
||||
description: "Source description",
|
||||
canonical: "https://example.test/seo",
|
||||
themeColor: "#123456",
|
||||
colorScheme: "light dark",
|
||||
meta: [
|
||||
{ name: "description", content: "Source description" },
|
||||
{ name: "keywords", content: "clone, seo" },
|
||||
{ name: "robots", content: "index,follow" },
|
||||
{ name: "referrer", content: "strict-origin" },
|
||||
{ name: "theme-color", content: "#123456" },
|
||||
{ name: "color-scheme", content: "light dark" },
|
||||
{ property: "og:title", content: "OG Title" },
|
||||
{ property: "og:description", content: "OG Description" },
|
||||
{ property: "og:image", content: "https://example.test/og.png" },
|
||||
{ name: "twitter:card", content: "summary_large_image" },
|
||||
{ name: "twitter:title", content: "Twitter Title" },
|
||||
],
|
||||
links: [
|
||||
{ rel: "canonical", href: "https://example.test/seo" },
|
||||
{ rel: "icon", href: "https://example.test/icon.png", type: "image/png", sizes: "32x32" },
|
||||
{ rel: "shortcut icon", href: "https://example.test/favicon.ico" },
|
||||
{ rel: "apple-touch-icon", href: "https://example.test/apple.png", sizes: "180x180" },
|
||||
{ rel: "mask-icon", href: "https://example.test/mask.svg", color: "#123456" },
|
||||
{ rel: "manifest", href: "https://example.test/site.webmanifest" },
|
||||
{ rel: "alternate", hrefLang: "es", href: "https://example.test/es/seo" },
|
||||
],
|
||||
jsonLd: [{ id: "schema", text: '{"@context":"https://schema.org","@type":"WebSite","name":"SEO Fixture"}' }],
|
||||
},
|
||||
},
|
||||
root: {
|
||||
id: "n0",
|
||||
tag: "body",
|
||||
attrs: {},
|
||||
visibleByVp: { 1280: true },
|
||||
bboxByVp: { 1280: { x: 0, y: 0, width: 1280, height: 800 } },
|
||||
computedByVp: { 1280: {} },
|
||||
children: [
|
||||
{ id: "n1", tag: "main", attrs: {}, visibleByVp: { 1280: true }, bboxByVp: { 1280: { x: 0, y: 0, width: 1280, height: 800 } }, computedByVp: { 1280: {} }, children: [{ text: "Useful page text for llms generation." }] },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixtureAssets(): AssetGraph {
|
||||
const entries = [
|
||||
["https://example.test/icon.png", "image", "/assets/cloned/images/icon.png", "icon.png", ["head link"]],
|
||||
["https://example.test/favicon.ico", "image", "/assets/cloned/images/favicon.ico", "favicon.ico", ["head link"]],
|
||||
["https://example.test/apple.png", "image", "/assets/cloned/images/apple.png", "apple.png", ["head link"]],
|
||||
["https://example.test/site.webmanifest", "manifest", "/assets/cloned/manifest/site.webmanifest", "site.webmanifest", ["head link"]],
|
||||
["https://example.test/manifest-icon.png", "image", "/assets/cloned/images/manifest-icon.png", "manifest-icon.png", ["manifest:icons"]],
|
||||
].map(([sourceUrl, type, localPath, storedFile, via]) => ({
|
||||
sourceUrl: sourceUrl as string,
|
||||
type: type as string,
|
||||
classification: "downloaded" as const,
|
||||
localPath: localPath as string,
|
||||
storedFile: storedFile as string,
|
||||
bytes: 10,
|
||||
reason: null,
|
||||
impact: null,
|
||||
via: via as string[],
|
||||
}));
|
||||
return { entries, byUrl: new Map(entries.map((entry) => [entry.sourceUrl, entry])) };
|
||||
}
|
||||
|
||||
function fixtureCapture(): CaptureResult {
|
||||
return {
|
||||
sourceUrl: "https://example.test/seo",
|
||||
capturedAt: "2026-01-01T00:00:00.000Z",
|
||||
viewports: [375, 768, 1280, 1920],
|
||||
perViewport: [],
|
||||
assets: [],
|
||||
fontFaces: [],
|
||||
cssTexts: [],
|
||||
seoResources: [
|
||||
{ kind: "llms", url: "https://example.test/llms.txt", status: 200, contentType: "text/plain", text: "# Source LLMS\n" },
|
||||
{ kind: "llms-full", url: "https://example.test/llms-full.txt", status: 200, contentType: "text/plain", text: "# Source LLMS Full\n" },
|
||||
{ kind: "sitemap", url: "https://example.test/sitemap.xml", status: 200, contentType: "application/xml" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("SEO inventory and emission", () => {
|
||||
it("captures rich SEO inventory and emits metadata, JSON-LD, llms, icons, and docs", () => {
|
||||
const ir = fixtureIr();
|
||||
const assets = fixtureAssets();
|
||||
const report = buildSeoInventory(ir, assets, fixtureCapture());
|
||||
assert.equal(report.metrics.iconLinks, 4);
|
||||
assert.equal(report.metrics.openGraphTags, 3);
|
||||
assert.equal(report.metrics.twitterTags, 2);
|
||||
assert.equal(report.metrics.jsonLdBlocks, 1);
|
||||
assert.equal(report.metrics.llmsTxt, true);
|
||||
assert.equal(report.metrics.llmsFullTxt, true);
|
||||
assert.equal(report.manifest?.localPath, "/assets/cloned/manifest/site.webmanifest");
|
||||
assert.equal(report.manifest?.assets.length, 1);
|
||||
assert.ok(metadataExport(report).includes("summary_large_image"));
|
||||
assert.ok(jsonLdHeadMarkup(report).includes("application/ld+json"));
|
||||
|
||||
const files = seoRouteFiles(report, [routeSummaryFromIr(ir, "/", "/", ir.doc.sourceUrl)]);
|
||||
assert.ok(files.find(([path]) => path === join("llms.txt", "route.ts"))?.[1].includes("Source LLMS"));
|
||||
assert.ok(files.find(([path]) => path === join("llms-full.txt", "route.ts"))?.[1].includes("Source LLMS Full"));
|
||||
|
||||
const temp = mkdtempSync(join(tmpdir(), "seo-emission-"));
|
||||
try {
|
||||
const sourceDir = join(temp, "source");
|
||||
const appDir = join(temp, "app");
|
||||
mkdirSync(join(sourceDir, "assets-store"), { recursive: true });
|
||||
for (const name of ["icon.png", "favicon.ico", "apple.png", "site.webmanifest", "manifest-icon.png"]) {
|
||||
writeFileSync(join(sourceDir, "assets-store", name), "asset");
|
||||
}
|
||||
emitSeoRoutes(appDir, report, [routeSummaryFromIr(ir, "/", "/", ir.doc.sourceUrl)]);
|
||||
emitSeoAssetFiles(appDir, sourceDir, assets, report);
|
||||
assert.ok(existsSync(join(appDir, "src", "app", "favicon.ico")));
|
||||
assert.ok(existsSync(join(appDir, "src", "app", "icon.png")));
|
||||
assert.ok(existsSync(join(appDir, "src", "app", "apple-icon.png")));
|
||||
assert.ok(existsSync(join(appDir, "src", "app", "llms.txt", "route.ts")));
|
||||
} finally {
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const docsInput = {
|
||||
sourceUrl: ir.doc.sourceUrl,
|
||||
routes: [routeSummaryFromIr(ir, "/", "/", ir.doc.sourceUrl)],
|
||||
styling: "tailwind" as const,
|
||||
framework: "next" as const,
|
||||
multiRoute: false,
|
||||
components: true,
|
||||
sectionCount: 1,
|
||||
componentCount: 2,
|
||||
svgCount: 1,
|
||||
hasContentModule: true,
|
||||
runtimeUtilities: ["DittoWire"],
|
||||
};
|
||||
assert.ok(agentsMd(docsInput).includes("src/app/ditto"));
|
||||
assert.ok(architectureMd(docsInput).includes("data-ditto-id"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user