Skip to content

Commit 0c31e19

Browse files
committed
Migrate driver pages and complete printer/driver lookup parity
Recreate the legacy OpenPrinting driver database (/driver/<name>) inside the static Next.js site and close the remaining content gaps against openprinting.org/printers. Everything is generated at build time from foomatic-db; no runtime backend. Driver dataset & pages: - split-drivers.ts derives 354 driver records (full + lightweight map + per-driver JSON) by reverse-indexing printers.json and merging driver XML metadata: supplier, license, free-software status, type, functionality ratings, colour/max-resolution, obsolete/replacement, support contacts, and the supported-printer list. - New routes: /foomatic/driver/<id> (detail) and /foomatic/driver (listing), with generateStaticParams + per-page SEO metadata (title/description/ canonical/OG). Printer pages: - Link driver names and the recommended driver to their driver pages. - Render previously-captured-but-unused data: connectivity, colour, duplex, support contacts; extract <lang> page-description-languages in combine-data. - Show driver type and obsolete/replacement inline; add the legacy "properties not yet entered" note for driver-only stub printers. Search, sitemap, redirects: - Index drivers in the Foomatic search index (7,011 docs total). - Add all printer and driver pages to the sitemap. - generate-legacy-redirects.ts emits static stubs preserving the legacy /printer/show/<id> and /driver/<name> URLs (meta-refresh + canonical).
1 parent 9026875 commit 0c31e19

16 files changed

Lines changed: 5726 additions & 19 deletions

File tree

app/foomatic/driver/[id]/page.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import fs from "fs/promises"
2+
import path from "path"
3+
import type { Metadata } from "next"
4+
import type { DriverRecord, DriverSummary } from "@/lib/foomatic/types"
5+
import DriverPageClient from "@/components/foomatic/DriverPageClient"
6+
import { getSiteUrl } from "@/lib/site"
7+
8+
async function getDriverSummaries(): Promise<DriverSummary[]> {
9+
const filePath = path.join(process.cwd(), "public", "foomatic-db", "driversMap.json")
10+
try {
11+
const data = await fs.readFile(filePath, "utf-8")
12+
const json = JSON.parse(data)
13+
return json.drivers
14+
} catch (error) {
15+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
16+
return []
17+
}
18+
19+
throw error
20+
}
21+
}
22+
23+
export async function generateStaticParams() {
24+
const drivers = await getDriverSummaries()
25+
return drivers.map((driver) => ({
26+
id: driver.id,
27+
}))
28+
}
29+
30+
interface DriverPageProps {
31+
params: Promise<{
32+
id: string
33+
}>
34+
}
35+
36+
async function getDriver(id: string): Promise<DriverRecord | null> {
37+
const filePath = path.join(process.cwd(), "public", "foomatic-db", "drivers", `${id}.json`)
38+
try {
39+
return JSON.parse(await fs.readFile(filePath, "utf-8")) as DriverRecord
40+
} catch {
41+
return null
42+
}
43+
}
44+
45+
export async function generateMetadata({ params }: DriverPageProps): Promise<Metadata> {
46+
const { id } = await params
47+
const driver = await getDriver(id)
48+
const name = driver?.name ?? id
49+
const printerCount = driver?.printerCount ?? 0
50+
const title = `${name} — Printer driver | OpenPrinting`
51+
const description =
52+
driver?.shortDescription ||
53+
`The ${name} printer driver in the OpenPrinting database` +
54+
(printerCount ? `, supporting ${printerCount} printer model${printerCount === 1 ? "" : "s"}.` : ".")
55+
const canonical = getSiteUrl(`/foomatic/driver/${id}/`)
56+
57+
return {
58+
title,
59+
description,
60+
alternates: { canonical },
61+
openGraph: { title, description, url: canonical, type: "website" },
62+
}
63+
}
64+
65+
export default async function DriverPage({ params }: DriverPageProps) {
66+
const { id } = await params
67+
68+
return <DriverPageClient driverId={id} />
69+
}

app/foomatic/driver/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Suspense } from "react"
2+
3+
import DriverListClient from "@/components/foomatic/DriverListClient"
4+
5+
export const metadata = {
6+
title: "Printer Drivers | OpenPrinting Foomatic",
7+
description:
8+
"Browse the OpenPrinting Foomatic driver database. Find printer drivers, their supported printers, licenses, and project home pages.",
9+
}
10+
11+
export default function DriverIndexPage() {
12+
return (
13+
<Suspense fallback={null}>
14+
<DriverListClient />
15+
</Suspense>
16+
)
17+
}

app/foomatic/printer/[id]/page.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import fs from "fs/promises"
22
import path from "path"
3-
import type { PrinterSummary } from "@/lib/foomatic/types"
3+
import type { Metadata } from "next"
4+
import type { Printer, PrinterSummary } from "@/lib/foomatic/types"
45
import PrinterPageClient from "@/components/foomatic/PrinterPageClient"
6+
import { getSiteUrl } from "@/lib/site"
57

68
async function getPrinterSummaries(): Promise<PrinterSummary[]> {
79
const filePath = path.join(process.cwd(), "public", "foomatic-db", "printersMap.json")
@@ -31,6 +33,34 @@ interface PrinterPageProps {
3133
}>
3234
}
3335

36+
async function getPrinter(id: string): Promise<Printer | null> {
37+
const filePath = path.join(process.cwd(), "public", "foomatic-db", "printers", `${id}.json`)
38+
try {
39+
return JSON.parse(await fs.readFile(filePath, "utf-8")) as Printer
40+
} catch {
41+
return null
42+
}
43+
}
44+
45+
export async function generateMetadata({ params }: PrinterPageProps): Promise<Metadata> {
46+
const { id } = await params
47+
const printer = await getPrinter(id)
48+
const name = printer ? `${printer.manufacturer} ${printer.model}` : id
49+
const driverCount = printer?.drivers?.length ?? 0
50+
const title = `${name} — Printer support & drivers | OpenPrinting`
51+
const description = printer
52+
? `Support information, recommended driver, and ${driverCount} available driver${driverCount === 1 ? "" : "s"} for the ${name} on Linux and Unix via OpenPrinting.`
53+
: `Printer support information for ${id} from the OpenPrinting database.`
54+
const canonical = getSiteUrl(`/foomatic/printer/${id}/`)
55+
56+
return {
57+
title,
58+
description,
59+
alternates: { canonical },
60+
openGraph: { title, description, url: canonical, type: "website" },
61+
}
62+
}
63+
3464
export default async function PrinterPage({ params }: PrinterPageProps) {
3565
const { id } = await params
3666

app/sitemap.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,36 @@ export default function sitemap(): MetadataRoute.Sitemap {
7676
entries.push({ url: getSiteUrl(`/upcoming-technologies/${slug}/`) });
7777
}
7878

79+
for (const route of foomaticRoutes()) {
80+
entries.push({ url: getSiteUrl(route) });
81+
}
82+
7983
return entries;
8084
}
85+
86+
function foomaticRoutes(): string[] {
87+
const routes: string[] = ["/foomatic/driver/"];
88+
const fdb = path.join(process.cwd(), "public", "foomatic-db");
89+
90+
const printersMap = path.join(fdb, "printersMap.json");
91+
if (fs.existsSync(printersMap)) {
92+
const data = JSON.parse(fs.readFileSync(printersMap, "utf8")) as {
93+
printers: { id: string }[];
94+
};
95+
for (const printer of data.printers) {
96+
routes.push(`/foomatic/printer/${printer.id.replace(/^printer\//, "")}/`);
97+
}
98+
}
99+
100+
const driversMap = path.join(fdb, "driversMap.json");
101+
if (fs.existsSync(driversMap)) {
102+
const data = JSON.parse(fs.readFileSync(driversMap, "utf8")) as {
103+
drivers: { id: string }[];
104+
};
105+
for (const driver of data.drivers) {
106+
routes.push(`/foomatic/driver/${driver.id}/`);
107+
}
108+
}
109+
110+
return routes;
111+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"use client"
2+
3+
import { useEffect, useMemo, useState } from "react"
4+
import Link from "next/link"
5+
import { ArrowRight, Search, Wrench } from "lucide-react"
6+
7+
import {
8+
FoomaticBadge,
9+
FoomaticCard,
10+
FoomaticHeroPill,
11+
FoomaticPageSection,
12+
} from "@/components/foomatic/shared"
13+
import { Button } from "@/components/ui/button"
14+
import { withBasePath } from "@/lib/foomatic/base-path"
15+
import type { DriverSummary } from "@/lib/foomatic/types"
16+
17+
const ITEMS_PER_PAGE = 60
18+
19+
export default function DriverListClient() {
20+
const [drivers, setDrivers] = useState<DriverSummary[]>([])
21+
const [loading, setLoading] = useState(true)
22+
const [error, setError] = useState<string | null>(null)
23+
const [query, setQuery] = useState("")
24+
const [hideObsolete, setHideObsolete] = useState(false)
25+
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE)
26+
27+
useEffect(() => {
28+
async function fetchData() {
29+
try {
30+
setLoading(true)
31+
setError(null)
32+
const response = await fetch(withBasePath("/foomatic-db/driversMap.json"))
33+
if (!response.ok) {
34+
throw new Error("The driver directory is temporarily unavailable.")
35+
}
36+
const data = await response.json()
37+
setDrivers(data.drivers)
38+
} catch (err) {
39+
console.error("Failed to load driver data:", err)
40+
setError(err instanceof Error ? err.message : "The driver directory could not be loaded.")
41+
} finally {
42+
setLoading(false)
43+
}
44+
}
45+
fetchData()
46+
}, [])
47+
48+
const filtered = useMemo(() => {
49+
const q = query.trim().toLowerCase()
50+
return drivers.filter((driver) => {
51+
if (hideObsolete && driver.obsolete) return false
52+
if (!q) return true
53+
return (
54+
driver.name.toLowerCase().includes(q) ||
55+
(driver.supplier || "").toLowerCase().includes(q) ||
56+
(driver.shortDescription || "").toLowerCase().includes(q)
57+
)
58+
})
59+
}, [drivers, query, hideObsolete])
60+
61+
const visible = filtered.slice(0, visibleCount)
62+
63+
return (
64+
<main className="min-h-screen bg-background pt-6 text-foreground">
65+
<section className="relative overflow-hidden border-b border-border/60">
66+
<div className="absolute inset-0 bg-white/10 dark:bg-black/80" />
67+
<div className="grid-pattern absolute inset-0" />
68+
<FoomaticPageSection className="relative py-14 sm:py-16">
69+
<FoomaticHeroPill>OpenPrinting Foomatic database</FoomaticHeroPill>
70+
<h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-5xl">Printer Drivers</h1>
71+
<p className="mt-4 max-w-2xl text-base leading-relaxed text-muted-foreground">
72+
Browse the drivers in the Foomatic database, see which printers each one supports, and
73+
open the matching printer pages.
74+
</p>
75+
<div className="mt-6">
76+
<Button asChild variant="outline" size="sm">
77+
<Link href="/foomatic">Browse printers instead</Link>
78+
</Button>
79+
</div>
80+
</FoomaticPageSection>
81+
</section>
82+
83+
<FoomaticPageSection className="space-y-8 py-10 sm:py-12">
84+
<FoomaticCard className="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:p-5">
85+
<div className="relative flex-1">
86+
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
87+
<input
88+
type="search"
89+
value={query}
90+
onChange={(event) => {
91+
setQuery(event.target.value)
92+
setVisibleCount(ITEMS_PER_PAGE)
93+
}}
94+
placeholder="Search drivers by name, supplier, or description…"
95+
className="w-full rounded-lg border border-border bg-background py-2 pl-9 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
96+
aria-label="Search drivers"
97+
/>
98+
</div>
99+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
100+
<input
101+
type="checkbox"
102+
checked={hideObsolete}
103+
onChange={(event) => setHideObsolete(event.target.checked)}
104+
className="h-4 w-4 rounded border-border"
105+
/>
106+
Hide obsolete drivers
107+
</label>
108+
</FoomaticCard>
109+
110+
{error ? (
111+
<FoomaticCard className="p-8 text-center sm:p-12">
112+
<h2 className="text-2xl font-semibold tracking-tight">Unable to load drivers</h2>
113+
<p className="mt-3 text-sm text-muted-foreground">{error}</p>
114+
</FoomaticCard>
115+
) : loading ? (
116+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
117+
{Array.from({ length: 6 }).map((_, index) => (
118+
<FoomaticCard key={index} className="h-32 animate-pulse" />
119+
))}
120+
</div>
121+
) : (
122+
<>
123+
<p className="text-sm text-muted-foreground" aria-live="polite">
124+
{filtered.length} driver{filtered.length === 1 ? "" : "s"}
125+
</p>
126+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
127+
{visible.map((driver) => (
128+
<Link key={driver.id} href={`/foomatic/driver/${driver.id}`} className="group block h-full">
129+
<FoomaticCard className="flex h-full flex-col p-5 transition-all duration-300 hover:border-border/80 hover:bg-accent/50 card-glow">
130+
<div className="flex items-start gap-3">
131+
<div className="rounded-lg border border-border bg-muted p-2.5 text-primary">
132+
<Wrench className="h-4 w-4" />
133+
</div>
134+
<div className="min-w-0 flex-1">
135+
<h3 className="truncate text-lg font-semibold tracking-tight text-foreground transition-colors group-hover:text-primary">
136+
{driver.name}
137+
</h3>
138+
{driver.supplier ? (
139+
<p className="truncate text-sm text-muted-foreground">{driver.supplier}</p>
140+
) : null}
141+
</div>
142+
</div>
143+
144+
{driver.shortDescription ? (
145+
<p className="mt-3 line-clamp-2 text-sm leading-6 text-muted-foreground">
146+
{driver.shortDescription}
147+
</p>
148+
) : null}
149+
150+
<div className="mt-4 flex flex-wrap items-center gap-2">
151+
{driver.type ? (
152+
<FoomaticBadge className="border-border bg-accent/50 text-muted-foreground">
153+
{driver.type}
154+
</FoomaticBadge>
155+
) : null}
156+
{driver.license ? (
157+
<FoomaticBadge className="border-border bg-accent/50 text-muted-foreground">
158+
{driver.license}
159+
</FoomaticBadge>
160+
) : null}
161+
{driver.obsolete ? (
162+
<FoomaticBadge className="border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300">
163+
Obsolete
164+
</FoomaticBadge>
165+
) : null}
166+
</div>
167+
168+
<div className="mt-auto flex items-center justify-between pt-4 text-sm">
169+
<FoomaticBadge className="border-border/70 bg-accent/60 text-muted-foreground">
170+
{driver.printerCount} printer{driver.printerCount === 1 ? "" : "s"}
171+
</FoomaticBadge>
172+
<span className="inline-flex items-center gap-1 font-medium text-primary">
173+
View driver
174+
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
175+
</span>
176+
</div>
177+
</FoomaticCard>
178+
</Link>
179+
))}
180+
</div>
181+
182+
{visibleCount < filtered.length ? (
183+
<div className="flex justify-center">
184+
<Button variant="outline" onClick={() => setVisibleCount((count) => count + ITEMS_PER_PAGE)}>
185+
Show more drivers
186+
</Button>
187+
</div>
188+
) : null}
189+
</>
190+
)}
191+
</FoomaticPageSection>
192+
</main>
193+
)
194+
}

0 commit comments

Comments
 (0)