diff --git a/lib/utils/__tests__/seo.test.ts b/lib/utils/__tests__/seo.test.ts new file mode 100644 index 0000000..b03249d --- /dev/null +++ b/lib/utils/__tests__/seo.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest" +import { smartTruncate, buildBreadcrumbList, BreadcrumbSegment } from "@/lib/utils/seo" + +describe("smartTruncate", () => { + it("returns full text when shorter than maxLen", () => { + expect(smartTruncate("Short text", 100)).toBe("Short text") + }) + + it("truncates at word boundary and appends ellipsis", () => { + const result = smartTruncate("This is a longer sentence that should be truncated properly.", 30) + expect(result).toBe("This is a longer sentence...") + expect(result.length).toBeLessThanOrEqual(30 + 3) // + "..." + }) + + it("handles text with no spaces gracefully", () => { + const result = smartTruncate("SuperLongWordThatHasNoSpaces", 10) + expect(result).toBe("SuperLongW...") + }) + + it("does not append ellipsis when text fits exactly", () => { + expect(smartTruncate("abc", 3)).toBe("abc") + }) + + it("handles empty string", () => { + expect(smartTruncate("", 10)).toBe("") + }) +}) + +describe("buildBreadcrumbList", () => { + it("builds a valid BreadcrumbList from segments", () => { + const segments = [ + { name: "Home", url: "https://deckyvault.xyz" }, + { name: "Games", url: "https://deckyvault.xyz/games" }, + { name: "Elden Ring", url: "https://deckyvault.xyz/game/123" }, + ] + const result = buildBreadcrumbList(segments) + expect(result["@context"]).toBe("https://schema.org") + expect(result["@type"]).toBe("BreadcrumbList") + expect(result.itemListElement).toHaveLength(3) + expect(result.itemListElement[0]).toEqual({ + "@type": "ListItem", + position: 1, + name: "Home", + item: "https://deckyvault.xyz", + }) + }) + + it("handles a single segment", () => { + const segments = [{ name: "Home", url: "https://deckyvault.xyz" }] + const result = buildBreadcrumbList(segments) + expect(result.itemListElement).toHaveLength(1) + }) +}) \ No newline at end of file diff --git a/lib/utils/seo.ts b/lib/utils/seo.ts new file mode 100644 index 0000000..abb7f7b --- /dev/null +++ b/lib/utils/seo.ts @@ -0,0 +1,33 @@ +/** + * Truncate text at a word boundary, appending ellipsis if cut. + * Guarantees the result never breaks a word in half. + */ +export function smartTruncate(text: string, maxLen: number): string { + if (text.length <= maxLen) return text + const truncated = text.slice(0, maxLen) + const lastSpace = truncated.lastIndexOf(" ") + return lastSpace > 0 ? truncated.slice(0, lastSpace) + "..." : truncated + "..." +} + +/** Breadcrumb segment — name and absolute URL */ +export interface BreadcrumbSegment { + name: string + url: string +} + +/** + * Build a Schema.org BreadcrumbList from ordered segments. + * Position is 1-based per Schema.org spec. + */ +export function buildBreadcrumbList(segments: BreadcrumbSegment[]) { + return { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: segments.map((seg, i) => ({ + "@type": "ListItem", + position: i + 1, + name: seg.name, + item: seg.url, + })), + } +} \ No newline at end of file