refactor: convert to bun workspaces monorepo
- Move web app into apps/web/ - Create packages/shared/ with shared types - Create plugins/decky-vault/ scaffold - Root package.json manages workspaces only
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { generateSlug } from "@/lib/utils/slug"
|
||||
|
||||
describe("generateSlug", () => {
|
||||
it("converts a simple title to lowercase hyphenated slug", () => {
|
||||
expect(generateSlug("The Witcher 3")).toBe("the-witcher-3")
|
||||
})
|
||||
it("replaces special characters with hyphens", () => {
|
||||
expect(generateSlug("Hades II: The Sequel")).toBe("hades-ii-the-sequel")
|
||||
})
|
||||
it("collapses multiple consecutive hyphens", () => {
|
||||
expect(generateSlug("Game!!! -- Test")).toBe("game-test")
|
||||
})
|
||||
it("trims leading and trailing hyphens", () => {
|
||||
expect(generateSlug(" -- My Game -- ")).toBe("my-game")
|
||||
})
|
||||
it("truncates to 80 characters", () => {
|
||||
const longTitle = "A".repeat(100)
|
||||
expect(generateSlug(longTitle).length).toBeLessThanOrEqual(80)
|
||||
})
|
||||
it("does not leave trailing hyphen after truncation", () => {
|
||||
const title = "A".repeat(79) + " -"
|
||||
expect(generateSlug(title)).not.toMatch(/-$/)
|
||||
})
|
||||
it("handles empty string", () => {
|
||||
expect(generateSlug("")).toBe("")
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function generateSlug(title: string): string {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 80)
|
||||
.replace(/-$/g, "")
|
||||
}
|
||||
Reference in New Issue
Block a user