elements
blockog-image-explorersource: ok · refreshed
OG Image Explorer
A dev tool to audit and preview Open Graph images from all routes in your app. Visual gallery with status indicators for broken images.
npx shadcn add "https://www.tryelements.dev/r/og-image-explorer.json"free
Where it goes
testimonialstatsgallery
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Files5 · source cached 10/8/2026, 3:42:46 AM
registry/default/blocks/media/og-image-explorer/components/elements/og-image-explorer.tsx
"use client";
import { useMemo } from "react";
import { ImageOff } from "lucide-react";
import { cn } from "@/lib/utils";
import { OgCategorySection } from "./og-category-section";
interface OgRoute {
path: string;
title?: string;
ogImageUrl: string;
category?: string;
}
interface OgImageExplorerProps {
routes: OgRoute[];
maxPerCategory?: number;
showPath?: boolean;
showTitle?: boolean;
className?: string;
}
function getCategoryFromPath(path: string): string {
const segments = path.split("/").filter(Boolean);
return segments[0] || "root";
}
export function OgImageExplorer({
routes,
maxPerCategory = 8,
showPath = true,
showTitle = true,
className,
}: OgImageExplorerProps) {
const groupedRoutes = useMemo(() => {
const groups = new Map<string, OgRoute[]>();
for (const route of routes) {
const category = route.category || getCategoryFromPath(route.path);
const existing = groups.get(category) || [];
groups.set(category, [...existing, route]);
}
const sortedCategories = Array.from(groups.entries()).sort(([a], [b]) => {
if (a === "root") return -1;
if (b === "root") return 1;
return a.localeCompare(b);
});
return sortedCategories;
}, [routes]);
if (routes.length === 0) {
return (
<div
data-slot="og-image-explorer"
className={cn(
"flex min-h-[200px] flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-8 text-center",
className,
)}
>
<ImageOff className="h-10 w-10 text-muted-foreground" />
<div className="flex flex-col gap-1">
<p className="text-sm font-medium text-foreground">
No routes configured
</p>
<p className="text-xs text-muted-foreground">
Add routes to explore your OG images
</p>
</div>
</div>
);
}
return (
<div
data-slot="og-image-explorer"
className={cn("flex flex-col gap-8", className)}
>
<div className="flex items-center justify-between border-b pb-4">
<div className="flex flex-col gap-1">
<h1 className="text-xl font-bold text-foreground">
OG Image Explorer
</h1>
<p className="text-sm text-muted-foreground">
{routes.length} route{routes.length !== 1 ? "s" : ""} across{" "}
{groupedRoutes.length} categor
{groupedRoutes.length !== 1 ? "ies" : "y"}
</p>
</div>
</div>
{groupedRoutes.map(([category, categoryRoutes]) => (
<OgCategorySection
key={category}
category={category}
routes={categoryRoutes}
maxItems={maxPerCategory}
showPath={showPath}
showTitle={showTitle}
/>
))}
</div>
);
}
registry/default/blocks/media/og-image-explorer/components/elements/og-category-section.tsx
"use client";
import { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
import { OgImageCard } from "./og-image-card";
interface OgRoute {
path: string;
title?: string;
ogImageUrl: string;
category?: string;
}
interface OgCategorySectionProps {
category: string;
routes: OgRoute[];
maxItems?: number;
showPath?: boolean;
showTitle?: boolean;
className?: string;
}
export function OgCategorySection({
category,
routes,
maxItems = 8,
showPath = true,
showTitle = true,
className,
}: OgCategorySectionProps) {
const [expanded, setExpanded] = useState(false);
const hasMore = routes.length > maxItems;
const visibleRoutes = expanded ? routes : routes.slice(0, maxItems);
const hiddenCount = routes.length - maxItems;
const formatCategoryName = (cat: string) => {
if (cat === "root") return "Root";
return cat.charAt(0).toUpperCase() + cat.slice(1);
};
return (
<section
data-slot="og-category-section"
className={cn("flex flex-col gap-4", className)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-foreground">
{formatCategoryName(category)}
</h2>
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
{routes.length}
</span>
</div>
{hasMore && (
<button
type="button"
onClick={() => setExpanded(!expanded)}
className={cn(
"flex items-center gap-1 text-sm text-muted-foreground transition-colors",
"hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-md px-2 py-1",
)}
>
{expanded ? (
<>
View less
<ChevronUp className="h-4 w-4" />
</>
) : (
<>
View {hiddenCount} more
<ChevronDown className="h-4 w-4" />
</>
)}
</button>
)}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{visibleRoutes.map((route) => (
<OgImageCard
key={route.path}
path={route.path}
title={route.title}
ogImageUrl={route.ogImageUrl}
showPath={showPath}
showTitle={showTitle}
/>
))}
</div>
</section>
);
}
registry/default/blocks/media/og-image-explorer/components/elements/og-image-card.tsx
"use client";
import { useState } from "react";
import { ExternalLink, ImageOff } from "lucide-react";
import { cn } from "@/lib/utils";
type ImageStatus = "loading" | "loaded" | "error";
interface OgImageCardProps {
path: string;
title?: string;
ogImageUrl: string;
showPath?: boolean;
showTitle?: boolean;
className?: string;
}
export function OgImageCard({
path,
title,
ogImageUrl,
showPath = true,
showTitle = true,
className,
}: OgImageCardProps) {
const [status, setStatus] = useState<ImageStatus>("loading");
const handleClick = () => {
window.open(ogImageUrl, "_blank", "noopener,noreferrer");
};
const statusBorderColor = {
loading: "border-muted-foreground/30",
loaded: "border-green-500/50",
error: "border-red-500/50",
};
return (
<div
data-slot="og-image-card"
className={cn("group flex flex-col gap-2", className)}
>
<button
type="button"
onClick={handleClick}
className={cn(
"relative aspect-[1.91/1] w-full overflow-hidden rounded-lg border-2 transition-all duration-200",
"hover:scale-[1.02] hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"bg-muted",
statusBorderColor[status],
)}
>
{status === "loading" && (
<div className="absolute inset-0 animate-pulse bg-muted" />
)}
{status === "error" ? (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 text-muted-foreground">
<ImageOff className="h-8 w-8" />
<span className="text-xs">Failed to load</span>
</div>
) : (
<img
src={ogImageUrl}
alt={title || path}
className={cn(
"h-full w-full object-cover transition-opacity duration-200",
status === "loading" ? "opacity-0" : "opacity-100",
)}
onLoad={() => setStatus("loaded")}
onError={() => setStatus("error")}
/>
)}
<div className="absolute bottom-2 right-2 rounded-md bg-background/80 p-1.5 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100">
<ExternalLink className="h-3.5 w-3.5 text-foreground" />
</div>
</button>
<div className="flex flex-col gap-0.5 px-1">
{showTitle && title && (
<span className="truncate text-sm font-medium text-foreground">
{title}
</span>
)}
{showPath && (
<span className="truncate text-xs text-muted-foreground font-mono">
{path}
</span>
)}
</div>
</div>
);
}
registry/default/blocks/media/og-image-explorer/routes/layout.tsx
export default function OgImageExplorerLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-background p-6 md:p-10">
<div className="mx-auto max-w-7xl">{children}</div>
</div>
);
}
registry/default/blocks/media/og-image-explorer/routes/page.tsx
// Auto-generated by scripts/generate-og-routes.ts
// Run: bun run scripts/generate-og-routes.ts
import { OgImageExplorer } from "../../components/elements/og-image-explorer";
const BASE_URL = "https://tryelements.dev";
const ROUTES = [
// Pages (root-level)
{ path: "/", title: "Home", category: "pages" },
{ path: "/contributors", title: "Contributors", category: "pages" },
{ path: "/sponsor", title: "Sponsor", category: "pages" },
// Providers
{ path: "/docs", title: "Documentation", category: "providers" },
{ path: "/docs/clerk", title: "Clerk", category: "providers" },
{ path: "/docs/devtools", title: "Dev Tools", category: "providers" },
{ path: "/docs/github", title: "GitHub", category: "providers" },
{ path: "/docs/logos", title: "Brand Logos", category: "providers" },
{ path: "/docs/polar", title: "Polar", category: "providers" },
{ path: "/docs/theme", title: "Theme", category: "providers" },
{ path: "/docs/tinte", title: "Tinte", category: "providers" },
{ path: "/docs/uploadthing", title: "UploadThing", category: "providers" },
// Components - Dev Tools
{
path: "/docs/devtools/og-image-explorer",
title: "OG Image Explorer",
category: "devtools",
},
// Components - GitHub
{
path: "/docs/github/github-contributions",
title: "GitHub Contributions",
category: "github",
},
{
path: "/docs/github/github-stars",
title: "GitHub Stars",
category: "github",
},
// Components - Polar
{
path: "/docs/polar/polar-sponsorship",
title: "Polar Sponsorship",
category: "polar",
},
// Components - Theme
{
path: "/docs/theme/theme-switcher-button",
title: "Theme Switcher Button",
category: "theme",
},
{
path: "/docs/theme/theme-switcher-classic",
title: "Theme Switcher Classic",
category: "theme",
},
{
path: "/docs/theme/theme-switcher-dropdown",
title: "Theme Switcher Dropdown",
category: "theme",
},
{
path: "/docs/theme/theme-switcher-multi-button",
title: "Theme Switcher Multi Button",
category: "theme",
},
{
path: "/docs/theme/theme-switcher-switch",
title: "Theme Switcher Switch",
category: "theme",
},
{
path: "/docs/theme/theme-switcher-toggle",
title: "Theme Switcher Toggle",
category: "theme",
},
// Components - Tinte
{
path: "/docs/tinte/tinte-editor",
title: "Tinte Theme Editor",
category: "tinte",
},
// Components - UploadThing
{
path: "/docs/uploadthing/uploadthing-button",
title: "UploadThing Button",
category: "uploadthing",
},
{
path: "/docs/uploadthing/uploadthing-dropzone",
title: "UploadThing Dropzone",
category: "uploadthing",
},
].map((route) => ({
...route,
ogImageUrl: `${BASE_URL}${route.path}/opengraph-image`,
}));
export default function OgImageExplorerPage() {
return <OgImageExplorer routes={ROUTES} maxPerCategory={8} />;
}