tfrere's picture
tfrere HF Staff
feat: agent-friendly project structure and paper template redesign
e4a2025
---
import HtmlEmbed from "../components/HtmlEmbed.astro";
import Image from "../components/Image.astro";
import Wide from "../components/Wide.astro";
import Stack from "../components/Stack.astro";
import Seo from "../components/Seo.astro";
import ThemeToggle from "../components/ThemeToggle.astro";
import { loadAllVisualsFromMDX } from "../utils/extract-embeds.mjs";
import * as ArticleMod from "../content/article.mdx";
import "katex/dist/katex.min.css";
import "../styles/global.css";
// Import all images from assets/image via glob
// @ts-ignore - Vite glob import
const imageModules = (import.meta as any).glob('../content/assets/image/**/*.{png,jpg,jpeg,gif,webp,svg}', { eager: true });
// Create a map from filename to image module
const imageMap = new Map();
for (const [path, module] of Object.entries(imageModules)) {
// Extract filename from path
const filename = path.split('/').pop();
if (filename) {
imageMap.set(filename, (module as any).default);
// Also map without extension for flexibility
const nameWithoutExt = filename.replace(/\.[^.]+$/, '');
imageMap.set(nameWithoutExt, (module as any).default);
}
}
// Helper to find image by filename or variable name
function findImage(nameOrFilename: string) {
if (!nameOrFilename) return null;
// 1. Try exact filename match (e.g. "placeholder.png")
if (imageMap.has(nameOrFilename)) {
return imageMap.get(nameOrFilename);
}
// 2. Try without extension (e.g. "placeholder")
const withoutExt = nameOrFilename.replace(/\.[^.]+$/, '');
if (imageMap.has(withoutExt)) {
return imageMap.get(withoutExt);
}
// 3. Normalized fuzzy match (for auto-generated names from Notion etc.)
const normalizedVar = nameOrFilename.toLowerCase().replace(/[-_]/g, '');
for (const [filename, img] of imageMap.entries()) {
const normalizedFilename = filename.toLowerCase().replace(/[-_]/g, '');
if (normalizedFilename.includes(normalizedVar) || normalizedVar.includes(normalizedFilename.replace(/\.[^.]+$/, ''))) {
return img;
}
}
// 4. Partial match fallback
const varParts = nameOrFilename.split(/[-_]/);
for (const [filename, img] of imageMap.entries()) {
const matchCount = varParts.filter(part =>
part.length > 3 && filename.toLowerCase().includes(part.toLowerCase())
).length;
if (matchCount >= 3) {
return img;
}
}
return null;
}
// Import article metadata
const articleFM = (ArticleMod as any).frontmatter ?? {};
// Keep <br> tags but remove other HTML
const articleTitle = String(articleFM?.title ?? "Article")
.replace(/<(?!br\s*\/?)[^>]+>/gi, "") // Remove all HTML except <br>
.replace(/\\n/g, "<br>")
.trim();
const articleDesc = articleFM?.description ?? "";
// Page metadata
const pageTitle = `Visualizations - ${articleTitle}`;
const pageDesc = articleDesc;
// Load all visual elements from the content
const allVisuals = loadAllVisualsFromMDX();
// Filter out skipGallery items
const visuals = allVisuals.filter((item: any) => !item.skipGallery);
const embedCount = visuals.filter((v: any) => v.type === 'embed').length + 1; // +1 for banner
const imageCount = visuals.filter((v: any) => v.type === 'image' || v.type === 'stack').length;
const tableCount = visuals.filter((v: any) => v.type === 'table').length;
// Generate a clean display name for each item (null if no meaningful name)
function getDisplayName(item: any): string | null {
if (item.type === 'embed') {
// Prefer title, else format filename
if (item.title) return item.title;
const filename = item.src?.replace(/\.html$/, '').replace(/^.*\//, '') || '';
// Convert kebab-case to Title Case
return filename
.split(/[-_]/)
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
} else if (item.type === 'image') {
// Prefer alt text
if (item.alt && item.alt !== 'Image') return item.alt;
if (item.caption) return item.caption;
return null; // No meaningful name
} else if (item.type === 'stack') {
// Use first image caption/alt or generic label
const imgs = item.images || [];
const firstCaption = imgs.find((i: any) => i.caption)?.caption;
const firstAlt = imgs.find((i: any) => i.alt && i.alt !== 'Image')?.alt;
return firstCaption || firstAlt || `Stack (${imgs.length} images)`;
} else if (item.type === 'table') {
// Tables don't have reliable names
return null;
}
return null;
}
// Get type label for display
function getTypeLabel(type: string): string {
switch (type) {
case 'embed': return 'Chart';
case 'image': return 'Image';
case 'stack': return 'Stack';
case 'table': return 'Table';
default: return 'Visual';
}
}
// Generate clean anchor slug - only for embeds with a real identifier
function generateSlug(item: any): string | null {
// Only embeds have reliable identifiers (filename)
if (item.type === 'embed' && item.src) {
const filename = item.src.replace(/\.html$/, '').replace(/^.*\//, '');
return filename
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 40);
}
// No anchor for tables, images without proper ID
return null;
}
// Add anchor IDs and display names to visuals
const usedSlugs = new Set<string>();
let displayIndex = 0; // Counter for displayed items (excludes banner)
const visualsWithMeta = visuals.map((item: any) => {
const displayName = getDisplayName(item);
let slug = generateSlug(item);
// Handle duplicates by adding a number suffix
if (slug && usedSlugs.has(slug)) {
let counter = 2;
while (usedSlugs.has(`${slug}-${counter}`)) {
counter++;
}
slug = `${slug}-${counter}`;
}
if (slug) usedSlugs.add(slug);
// Only increment index for non-banner items
if (item.type !== 'banner') {
displayIndex++;
}
return {
...item,
displayName,
anchorId: slug, // null if no identifier
index: displayIndex
};
});
---
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Seo title={pageTitle} description={pageDesc} />
<script is:inline>
(() => {
try {
const saved = localStorage.getItem("theme");
const prefersDark =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)")
.matches;
const theme = saved || (prefersDark ? "dark" : "light");
document.documentElement.setAttribute("data-theme", theme);
} catch {}
})();
</script>
<script type="module" src="/scripts/color-palettes.js"></script>
<!-- External libraries for embeds -->
<script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"></script>
<script src="/src/scripts/mermaid-zoom.js"></script>
</head>
<body class="dataviz-page">
<ThemeToggle />
<section class="content-grid dataviz-layout">
<main>
<header class="dataviz-header">
<a href="/" class="back-link">← Back to article</a>
<div class="header-content">
<p class="header-label">Visual assets from</p>
<h1 class="article-title" set:html={articleTitle} />
</div>
<div class="header-stats">
{embedCount > 0 && (
<div class="stat-card">
<span class="stat-number">{embedCount}</span>
<span class="stat-label">Charts</span>
</div>
)}
{imageCount > 0 && (
<div class="stat-card">
<span class="stat-number">{imageCount}</span>
<span class="stat-label">Images</span>
</div>
)}
{tableCount > 0 && (
<div class="stat-card">
<span class="stat-number">{tableCount}</span>
<span class="stat-label">Tables</span>
</div>
)}
</div>
</header>
<div class="dataviz-banner">
<Wide>
<HtmlEmbed src="banner.html" frameless />
</Wide>
</div>
<div class="dataviz-list">
{visualsWithMeta.filter((item: any) => item.type !== 'banner').map((item: any) => {
const isWide = item.wide || item.fullWidth;
return (
<article class={`dataviz-item ${isWide ? 'dataviz-item--wide' : ''}`} data-type={item.type} id={item.anchorId || undefined}>
{!item.hideCaption && (
<header class="item-header">
<span class="header-number">#{item.index}</span>
<span class="header-type">{getTypeLabel(item.type)}</span>
{item.displayName && (
<h2 class="header-title">{item.displayName}</h2>
)}
{(item.desc || item.caption) && (
<p class="header-desc" set:html={item.desc || item.caption} />
)}
{item.anchorId && (
<a href={`/#${item.anchorId}`} class="header-link">
View in article →
</a>
)}
</header>
)}
<div class="item-content">
{(item.type === 'embed' || item.type === 'banner') && !isWide && (
<HtmlEmbed
src={item.src}
frameless={item.frameless}
data={item.data}
config={item.config}
/>
)}
{(item.type === 'embed' || item.type === 'banner') && isWide && (
<Wide>
<HtmlEmbed
src={item.src}
frameless={item.frameless}
data={item.data}
config={item.config}
/>
</Wide>
)}
{item.type === 'image' && (() => {
// Prefer resolvedFilename (from MDX imports), fallback to variable name
const imgSrc = findImage(item.resolvedFilename || item.src);
return imgSrc ? (
<Image
src={imgSrc}
alt={item.alt || 'Image'}
caption={item.caption}
/>
) : (
<div class="image-not-found">
<span class="image-placeholder">Image</span>
<code class="image-ref">{item.src}</code>
</div>
);
})()}
{item.type === 'stack' && (
<Stack layout={item.layout || '2-column'} gap={item.gap || 'medium'}>
{(item.images || []).map((img: any) => {
const imgSrc = findImage(img.resolvedFilename || img.src);
return imgSrc ? (
<Image
src={imgSrc}
alt={img.alt || 'Image'}
caption={img.caption}
/>
) : (
<div class="image-not-found">
<span class="image-placeholder">Image</span>
<code class="image-ref">{img.src}</code>
</div>
);
})}
</Stack>
)}
{item.type === 'table' && (
<div class="table-scroll">
<table>
<thead>
<tr>
{item.headers.map((header: string) => (
<th set:html={header} />
))}
</tr>
</thead>
<tbody>
{item.rows.map((row: string[]) => (
<tr>
{row.map((cell: string) => (
<td set:html={cell} />
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</article>
);
})}
</div>
</main>
</section>
<script>
// Initialize image zoom
function initializeZoom() {
const zoomableImages = document.querySelectorAll('img[data-zoomable="true"]');
if ((window as any).mediumZoom && zoomableImages.length > 0) {
zoomableImages.forEach((img) => {
if (!img.classList.contains("medium-zoom-image")) {
try {
(window as any).mediumZoom(img, {
background: "rgba(0,0,0,.85)",
margin: 24,
scrollOffset: 0,
});
} catch (error) {
console.error("Error initializing zoom:", error);
}
}
});
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeZoom);
} else {
initializeZoom();
}
window.addEventListener("load", () => setTimeout(initializeZoom, 100));
</script>
<style is:global>
/* Layout */
.dataviz-layout {
padding-top: 40px;
padding-bottom: 80px;
}
.dataviz-layout main {
grid-column: 2;
}
.dataviz-banner {
margin-bottom: 0;
}
.dataviz-banner .wide {
width: min(1100px, 100vw - var(--content-padding-x) * 4);
margin-left: 50%;
transform: translateX(-50%);
padding: 0;
background: transparent;
}
.dataviz-banner .html-embed {
margin: 0;
}
/* Page Header */
.dataviz-header {
margin-bottom: 16px;
text-align: center;
}
.back-link {
color: var(--muted-color);
text-decoration: none;
font-size: 0.8125rem;
display: inline-block;
margin-bottom: 20px;
transition: color 0.15s;
}
.back-link:hover {
color: var(--text-color);
}
.header-content {
margin-bottom: 20px;
}
.header-label {
font-size: 0.6875rem;
color: var(--muted-color);
margin: 0 0 8px 0;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
}
.article-title {
font-size: clamp(28px, 4vw, 48px);
font-weight: 800;
margin: 0;
color: var(--text-color);
line-height: 1.1;
letter-spacing: -0.02em;
}
.header-stats {
display: flex;
justify-content: center;
gap: 32px;
flex-wrap: wrap;
}
.stat-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.stat-number {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-color);
font-variant-numeric: tabular-nums;
}
.stat-label {
font-size: 0.6875rem;
color: var(--muted-color);
text-transform: uppercase;
letter-spacing: 0.03em;
}
/* List */
.dataviz-list {
display: flex;
flex-direction: column;
gap: 56px;
}
/* Item */
.dataviz-item {
position: relative;
scroll-margin-top: 24px;
}
.dataviz-item .html-embed {
margin: 0 !important;
}
.dataviz-item .wide {
/* Keep the wide centering */
width: min(1100px, 100vw - var(--content-padding-x) * 4);
margin-left: 50%;
transform: translateX(-50%);
/* Remove visual effects */
padding: 0 !important;
background: transparent !important;
border-radius: 0 !important;
-webkit-mask: none !important;
mask: none !important;
}
.dataviz-item .wide .html-embed {
margin: 0 !important;
}
/* Header */
.item-header {
margin-bottom: 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
text-align: center;
}
.header-number {
font-size: 0.6875rem;
font-weight: 500;
color: var(--muted-color);
opacity: 0.5;
}
.header-type {
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-color);
}
.header-title {
font-size: 0.875rem;
font-weight: 500;
margin: 2px 0 0 0 !important;
padding: 0 !important;
color: var(--text-color);
line-height: 1.3;
border: none !important;
text-decoration: none !important;
}
.header-title::after {
display: none !important;
}
.header-desc {
font-size: 0.8125rem;
color: var(--muted-color);
margin: 4px 0 0 0;
max-width: 600px;
line-height: 1.4;
}
.header-link {
margin-top: 4px;
font-size: 0.6875rem;
color: var(--muted-color);
text-decoration: none;
opacity: 0.7;
transition: opacity 0.15s;
}
.header-link:hover {
opacity: 1;
color: var(--primary-color);
}
/* Image not found */
.image-not-found {
background: var(--neutral-50);
border: 1px dashed var(--border-color);
border-radius: 8px;
padding: 32px 24px;
text-align: center;
}
[data-theme="dark"] .image-not-found {
background: var(--neutral-900);
}
.image-placeholder {
font-size: 0.8125rem;
color: var(--muted-color);
display: block;
margin-bottom: 4px;
}
.image-ref {
font-size: 0.6875rem;
color: var(--muted-color);
opacity: 0.6;
}
/* Responsive */
@media (max-width: 768px) {
.dataviz-list {
gap: 40px;
}
}
</style>
</body>
</html>