thibaud frere
fix footer numbers | fix footer title | fix pdf
0268a3e
---
import HtmlEmbed from "./HtmlEmbed.astro";
interface Props {
title: string; // may contain HTML (e.g., <br/>)
titleRaw?: string; // plain title for slug/PDF (optional)
description?: string;
authors?: Array<string | { name: string; url?: string; affiliationIndices?: number[] }>;
affiliations?: Array<{ id: number; name: string; url?: string }>;
affiliation?: string; // legacy single affiliation
published?: string;
doi?: string;
}
const { title, titleRaw, description, authors = [], affiliations = [], affiliation, published, doi } = Astro.props as Props;
type Author = { name: string; url?: string; affiliationIndices?: number[] };
function normalizeAuthors(input: Array<string | { name?: string; url?: string; link?: string; affiliationIndices?: number[] }>): Author[] {
return (Array.isArray(input) ? input : [])
.map((a) => {
if (typeof a === 'string') {
return { name: a } as Author;
}
const name = (a?.name ?? '').toString();
const url = (a?.url ?? a?.link) as string | undefined;
const affiliationIndices = Array.isArray((a as any)?.affiliationIndices) ? (a as any).affiliationIndices : undefined;
return { name, url, affiliationIndices } as Author;
})
.filter((a) => a.name && a.name.trim().length > 0);
}
const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
// Determine if affiliation superscripts should be shown (only when there are multiple distinct affiliations referenced by authors)
const authorAffiliationIndexSet = new Set<number>();
for (const author of normalizedAuthors) {
const indices = Array.isArray(author.affiliationIndices) ? author.affiliationIndices : [];
for (const idx of indices) {
if (typeof idx === 'number') {
authorAffiliationIndexSet.add(idx);
}
}
}
const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
const hasMultipleAffiliations = Array.isArray(affiliations) && affiliations.length > 1;
function stripHtml(text: string): string {
return String(text || '').replace(/<[^>]*>/g, '');
}
function slugify(text: string): string {
return String(text || '')
.normalize('NFKD')
.replace(/\p{Diacritic}+/gu, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 120) || 'article';
}
const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
const pdfFilename = `${slugify(pdfBase)}.pdf`;
---
<section class="hero">
<h1 class="hero-title" set:html={title}></h1>
<div class="hero-banner">
<HtmlEmbed src="banner.html" frameless />
{description && <p class="hero-desc">{description}</p>}
</div>
</section>
<header class="meta" aria-label="Article meta information">
<div class="meta-container">
{normalizedAuthors.length > 0 && (
<div class="meta-container-cell">
<h3>Author{normalizedAuthors.length > 1 ? 's' : ''}</h3>
<ul class="authors">
{normalizedAuthors.map((a, i) => {
const supers = shouldShowAffiliationSupers && Array.isArray(a.affiliationIndices) && a.affiliationIndices.length
? <sup>{a.affiliationIndices.join(',')}</sup>
: null;
return (
<li>
{a.url ? <a href={a.url}>{a.name}</a> : a.name}{supers}
</li>
);
})}
</ul>
</div>
)}
{(Array.isArray(affiliations) && affiliations.length > 0) && (
<div class="meta-container-cell">
<h3>Affiliation{affiliations.length > 1 ? 's' : ''}</h3>
{hasMultipleAffiliations ? (
<ol class="affiliations">
{affiliations.map((af) => (
<li value={af.id}>{af.url ? <a href={af.url} target="_blank" rel="noopener noreferrer">{af.name}</a> : af.name}</li>
))}
</ol>
) : (
<p>
{affiliations[0]?.url
? <a href={affiliations[0].url} target="_blank" rel="noopener noreferrer">{affiliations[0].name}</a>
: affiliations[0]?.name}
</p>
)}
</div>
)}
{(!affiliations || affiliations.length === 0) && affiliation && (
<div class="meta-container-cell">
<h3>Affiliation</h3>
<p>{affiliation}</p>
</div>
)}
{published && (
<div class="meta-container-cell meta-container-cell--published">
<h3>Published</h3>
<p>{published}</p>
</div>
)}
<!-- {doi && (
<div class="meta-container-cell">
<h3>DOI</h3>
<p><a href={`https://doi.org/${doi}`} target="_blank" rel="noopener noreferrer">{doi}</a></p>
</div>
)} -->
<div class="meta-container-cell meta-container-cell--pdf">
<h3>PDF</h3>
<p>
<a class="button" href={`/${pdfFilename}`} download={pdfFilename} aria-label={`Download PDF ${pdfFilename}`}>
Download PDF
</a>
</p>
</div>
</div>
</header>
<style>
/* Hero (full-width) */
.hero {
width: 100%;
padding: 48px 16px 16px;
text-align: center;
}
.hero-title {
font-size: clamp(28px, 4vw, 48px);
font-weight: 800;
line-height: 1.1;
margin: 0 0 8px;
max-width: 100%;
margin: auto;
}
.hero-banner {
max-width: 980px;
margin: 0 auto;
}
.hero-desc {
color: var(--muted-color);
font-style: italic;
margin: 0 0 16px 0;
}
/* Meta (byline-like header) */
.meta {
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
padding: 1rem 0;
font-size: 0.9rem;
}
.meta-container {
max-width: 760px;
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0 auto;
padding: 0 var(--content-padding-x);
gap: 8px;
}
/* Subtle underline for links in meta; keep buttons without underline */
.meta-container a {
color: var(--primary-color);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 0.06em;
text-decoration-color: var(--link-underline);
transition: text-decoration-color .15s ease-in-out;
}
.meta-container a:hover {
text-decoration-color: var(--link-underline-hover);
}
.meta-container a.button,
.meta-container .button {
text-decoration: none;
}
.meta-container-cell {
display: flex;
flex-direction: column;
gap: 8px;
}
.meta-container-cell h3 {
margin: 0;
font-size: 12px;
font-weight: 400;
color: var(--muted-color);
text-transform: uppercase;
letter-spacing: .02em;
}
.meta-container-cell p {
margin: 0;
}
.authors {
margin: 0;
list-style-type: none;
padding-left: 0;
}
.affiliations {
margin: 0;
padding-left: 1.25em;
}
.affiliations li {
margin: 0;
}
header.meta .meta-container {
flex-wrap: wrap;
row-gap: 12px;
}
@media print {
.meta-container-cell--pdf {
display: none !important;
}
}
</style>