feat(library): add multilingual author archives and linked bylines

This commit is contained in:
DeCentN2Madness committed 2026-10-10 00:17:25 -07:00
1 parent 0957c06a18
commit 8646792d46
23 files changed
+2098 -115

No files matched your search

+7 -2
View File
@@ -1,5 +1,6 @@
---
import type { CollectionEntry } from 'astro:content';
import { getCollection, type CollectionEntry } from 'astro:content';
import { createAuthorResolver } from '../utils/authors';
import LikeViewCounter from './LikeViewCounter.astro';
interface Props {
@@ -8,13 +9,14 @@ interface Props {
}
const { post, prefix = '' } = Astro.props;
const author = createAuthorResolver(await getCollection('authors'))(post.data.lang, post.data.author);
const href = `${prefix}/library/${post.data.slug}`.replace('//', '/');
const readMore = { en: 'Read More', es: 'Leer Más', ar: 'اقرأ المزيد' }[post.data.lang] || 'Read More';
---
<article class="card blog-card">
{post.data.featuredImage && <a href={href}><img src={post.data.featuredImage} alt={post.data.alt || ''} loading="lazy" /></a>}
<div class="card-body">
<div class="blog-card-meta"><img src="/assets/images/rula-diab-avatar.jpg" alt="" /><p>Rula Diab, Clinical Director, M.Ed, BCBA, LBA<br /><time datetime={post.data.date.toISOString()}>{post.data.date.toLocaleDateString(post.data.lang, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' })}</time></p></div>
<div class="blog-card-meta">{author.avatar && <img src={author.avatar} alt="" loading="lazy" />}<p><a class="author-byline" rel="author" href={author.archiveRoute}><bdi>{author.displayName}</bdi></a><br /><time datetime={post.data.date.toISOString()}>{post.data.date.toLocaleDateString(post.data.lang, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' })}</time></p></div>
<h3><a href={href}>{post.data.title}</a></h3>
<p>{post.data.description}</p>
<div class="blog-card-footer" style="align-items:center;display:flex;justify-content:space-between;gap:var(--space-lg);margin-top:auto;">
@@ -25,6 +27,9 @@ const readMore = { en: 'Read More', es: 'Leer Más', ar: 'اقرأ المزيد'
</article>
<style>
.author-byline { color: inherit; text-decoration: none; }
.author-byline:hover { text-decoration: underline; }
.author-byline:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.blog-card-meta { align-items: center; display: flex; font-size: .78rem; gap: .75rem; }
.blog-card-meta img { aspect-ratio: 1; border-radius: 50%; height: 42px; width: 42px; }
.blog-card { background: white; border: 0; border-radius: 4px; box-shadow: rgba(54, 48, 48, 0.28) 0 0 12px 0; display: flex; margin-bottom: 40px; overflow: hidden; }
+7 -4
View File
@@ -4,13 +4,16 @@ import {
type LibraryPageSlice
} from '../utils/library-pagination';
import type { LibraryLocale } from '../utils/library-catalog';
import { authorPageRoute } from '../utils/author-archives';
interface Props {
locale: LibraryLocale;
page: LibraryPageSlice<unknown>;
authorRoot?: string;
}
const { locale, page } = Astro.props;
const { locale, page, authorRoot } = Astro.props;
const pageRoute = (number: number) => authorRoot ? authorPageRoute(authorRoot, number) : libraryPageRoute(locale, number);
const messages = {
en: { navigation: 'Library pagination', previous: 'Previous', next: 'Next', page: 'Page', current: 'current page' },
es: { navigation: 'Paginación de la biblioteca', previous: 'Anterior', next: 'Siguiente', page: 'Página', current: 'página actual' },
@@ -23,13 +26,13 @@ const pages = Array.from({ length: page.totalPages }, (_, index) => index + 1);
<ul>
{page.previousPage && (
<li class="library-pagination__previous">
<a rel="prev" href={libraryPageRoute(locale, page.previousPage)}>{messages.previous}</a>
<a rel="prev" href={pageRoute(page.previousPage)}>{messages.previous}</a>
</li>
)}
{pages.map((number) => (
<li>
<a
href={libraryPageRoute(locale, number)}
href={pageRoute(number)}
aria-current={number === page.currentPage ? 'page' : undefined}
aria-label={`${messages.page} ${number}${number === page.currentPage ? `, ${messages.current}` : ''}`}
>{number}</a>
@@ -37,7 +40,7 @@ const pages = Array.from({ length: page.totalPages }, (_, index) => index + 1);
))}
{page.nextPage && (
<li class="library-pagination__next">
<a rel="next" href={libraryPageRoute(locale, page.nextPage)}>{messages.next}</a>
<a rel="next" href={pageRoute(page.nextPage)}>{messages.next}</a>
</li>
)}
</ul>
@@ -0,0 +1,72 @@
---
import { render, type CollectionEntry } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import PageHero from '../PageHero.astro';
import BlogCard from '../BlogCard.astro';
import LibraryPagination from '../LibraryPagination.astro';
import { getLibraryCatalog } from '../../utils/library';
import { libraryRootRoute, paginateLibraryItems } from '../../utils/library-pagination';
import type { AuthorArchiveDescriptor } from '../../utils/author-archives';
interface Props { page: AuthorArchiveDescriptor<CollectionEntry<'authors'>>; }
const { page } = Astro.props;
const { author, locale, currentPage } = page;
const catalog = (await getLibraryCatalog()).byLocale[locale];
const slice = paginateLibraryItems(catalog.filter(({ entry }) => entry.data.author === author.slug), currentPage);
if (!slice?.items.length) throw new Error(`Empty author archive: ${author.archiveRoute}, page ${currentPage}`);
const { Content } = await render(author.entry);
const prefix = locale === 'en' ? '' : `/${locale}`;
const text = {
en: { heading: 'The Library at AZ Institute for Autism', title: 'Library & News Room | Arizona Institute for Autism', page: 'Page' },
es: { heading: 'La Biblioteca en AZ Institute for Autism', title: 'Biblioteca y Noticias | Instituto de Autismo de Arizona', page: 'Página' },
ar: { heading: 'مكتبة معهد أريزونا للتوحد', title: 'المكتبة وغرفة الأخبار | معهد أريزونا للتوحد', page: 'الصفحة' }
}[locale];
const title = `${text.title} | ${author.displayName}${currentPage > 1 ? ` | ${text.page} ${currentPage}` : ''}`;
// Use the existing catalog's latest-post treatment; no second article list.
const popular = catalog.slice(0, 5);
---
<BaseLayout title={title} description={author.description} image={author.avatar} lang={locale}>
<PageHero title={text.heading} image="/assets/images/hero-library-index.webp" eyebrow="Welcome to" constrain />
<section class="author-profile" aria-labelledby="author-heading">
{author.avatar && <img class="author-profile__avatar" src={author.avatar} alt={author.displayName} width="176" height="176" />}
<div class="author-profile__content">
<h2 id="author-heading"><bdi>{author.displayName}</bdi></h2>
<div class="author-biography"><Content /></div>
</div>
</section>
<div class="author-listing container">
<section aria-labelledby="author-posts-heading">
<h2 id="author-posts-heading" class="author-posts-heading"><a href={libraryRootRoute(locale)}>All Post</a></h2>
<div class="blog-list">{slice.items.map(({ entry }) => <BlogCard post={entry} prefix={prefix} />)}</div>
<LibraryPagination locale={locale} page={slice} authorRoot={author.archiveRoute} />
</section>
<aside class="author-sidebar" aria-labelledby="author-popular-heading">
<h2 id="author-popular-heading">Popular Posts</h2>
<ul>{popular.map(({ entry, route }) => <li><a href={route}>{entry.data.title}</a></li>)}</ul>
</aside>
</div>
</BaseLayout>
<style>
.author-profile { align-items: start; display: flex; gap: 4rem; margin: 100px auto 60px; max-width: 768px; width: calc(100% - 96px); }
.author-profile__avatar { flex: 0 0 176px; height: 176px; object-fit: cover; width: 176px; }
.author-profile__content { flex: 1; min-width: 0; }
.author-profile h2 { font-size: 2.4rem; line-height: 1.25; margin: 0 0 10px; }
.author-biography { font-size: 1rem; line-height: 26px; }
.author-biography :global(p) { margin: 0 0 20px; }
.author-listing { align-items: start; display: grid; gap: 30px; grid-template-columns: minmax(0, 1fr) 294px; padding-bottom: 60px; }
.author-posts-heading { font-family: var(--font-body); font-size: 1rem; margin: 0 0 24px; }
.author-posts-heading a { color: inherit; }
.author-sidebar h2 { font-size: 1.25rem; margin: 0 0 20px; }
.author-sidebar ul { list-style: none; margin: 0; padding: 0; }
.author-sidebar li { margin-bottom: 20px; }
.author-sidebar a { color: inherit; }
.author-listing a:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
@media (max-width: 760px) {
.author-profile { flex-direction: column; gap: 5px; }
.author-profile__avatar { flex-basis: auto; width: 100px; height: 100px; }
.author-profile h2 { font-size: 2rem; }
.author-biography { line-height: 20px; }
.author-listing { grid-template-columns: minmax(0, 1fr); }
}
</style>