astro-theme-popular 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +5 -2
- package/package.json +1 -1
- package/schemas.ts +1 -0
- package/src/components/AuthorBox.astro +4 -3
- package/src/components/OrganizerCard.astro +2 -2
- package/src/components/PostCard.astro +9 -4
- package/src/lib/stats.ts +36 -0
- package/src/pages/{[slug].astro → [...slug].astro} +18 -5
- package/src/pages/blog/[slug].astro +3 -3
- package/src/pages/index.astro +12 -7
- package/src/pages/organizers/[slug].astro +29 -0
- package/src/pages/rss.xml.js +2 -2
package/index.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
folder-organized content ids (2019-pycon-us/cooper-lees) resolve. */
|
|
22
22
|
const ROUTES = {
|
|
23
23
|
home: [['/', 'index.astro']],
|
|
24
|
-
pages: [['/[...slug]', '[slug].astro']],
|
|
24
|
+
pages: [['/[...slug]', '[...slug].astro']],
|
|
25
25
|
authors: [['/authors/[...slug]', 'authors/[slug].astro']],
|
|
26
26
|
blog: [
|
|
27
27
|
['/blog/[...page]', 'blog/[...page].astro'],
|
|
@@ -31,7 +31,10 @@ const ROUTES = {
|
|
|
31
31
|
['/events/[...page]', 'events/[...page].astro'],
|
|
32
32
|
['/events/[...slug]', 'events/[slug].astro'],
|
|
33
33
|
],
|
|
34
|
-
organizers: [
|
|
34
|
+
organizers: [
|
|
35
|
+
['/organizers/[...page]', 'organizers/[...page].astro'],
|
|
36
|
+
['/organizers/[...slug]', 'organizers/[slug].astro'],
|
|
37
|
+
],
|
|
35
38
|
speakers: [['/speakers/[...slug]', 'speakers/[slug].astro']],
|
|
36
39
|
venues: [['/venues/[...slug]', 'venues/[slug].astro']],
|
|
37
40
|
tags: [['/tags/[tag]/[...page]', 'tags/[tag]/[...page].astro']],
|
package/package.json
CHANGED
package/schemas.ts
CHANGED
|
@@ -10,13 +10,14 @@ interface Guest {
|
|
|
10
10
|
social?: { label: string; icon: string; url: string }[];
|
|
11
11
|
}
|
|
12
12
|
interface Props {
|
|
13
|
-
entry?: CollectionEntry<'authors'> | CollectionEntry<'speakers'>;
|
|
13
|
+
entry?: CollectionEntry<'authors'> | CollectionEntry<'speakers'> | CollectionEntry<'organizers'>;
|
|
14
14
|
guest?: Guest;
|
|
15
15
|
/** Profile URL prefix; speakers pass '/speakers/'. */
|
|
16
16
|
base?: string;
|
|
17
17
|
}
|
|
18
18
|
const { entry, guest, base = '/authors/' } = Astro.props;
|
|
19
19
|
const d = entry?.data;
|
|
20
|
+
const bio = d ? ((d as any).bio ?? (d as any).description) : undefined;
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
<div class="g-persona g-persona--primary">
|
|
@@ -26,9 +27,9 @@ const d = entry?.data;
|
|
|
26
27
|
<div>
|
|
27
28
|
<div class="g-persona__name"><a href={`${base}${entry.id}/`} style="color:inherit">{d.title}</a></div>
|
|
28
29
|
{d.role && <div class="g-persona__role">{d.role}</div>}
|
|
29
|
-
{
|
|
30
|
+
{bio && <p class="g-persona__bio">{bio}</p>}
|
|
30
31
|
<div class="g-persona__socials">
|
|
31
|
-
{d.website && <a href={d.website} aria-label={STRINGS.website} rel="noopener" target="_blank"><i class="fa-solid fa-globe" aria-hidden="true"></i></a>}
|
|
32
|
+
{(d as any).website && <a href={(d as any).website} aria-label={STRINGS.website} rel="noopener" target="_blank"><i class="fa-solid fa-globe" aria-hidden="true"></i></a>}
|
|
32
33
|
{d.social.map((s) => <a href={s.url} aria-label={s.label} rel="noopener" target="_blank"><i class={s.icon} aria-hidden="true"></i></a>)}
|
|
33
34
|
</div>
|
|
34
35
|
</div>
|
|
@@ -7,8 +7,8 @@ const d = organizer.data;
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
<div class="g-org">
|
|
10
|
-
{d.photo && <img class="g-org__img" src={d.photo} alt={d.title} loading="lazy"
|
|
11
|
-
<div class="g-org__name">{d.title}</div>
|
|
10
|
+
{d.photo && <a href={`/organizers/${organizer.id}/`}><img class="g-org__img" src={d.photo} alt={d.title} loading="lazy" /></a>}
|
|
11
|
+
<div class="g-org__name"><a href={`/organizers/${organizer.id}/`} style="color:inherit">{d.title}</a></div>
|
|
12
12
|
{d.role && <div class="g-org__role">{d.role}</div>}
|
|
13
13
|
{d.description && <p class="g-org__bio">{d.description}</p>}
|
|
14
14
|
{d.social.length > 0 && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { SITE } from 'popular:config';
|
|
2
|
+
import { SITE, SECTIONS_MAP } from 'popular:config';
|
|
3
3
|
import { tagSlug } from '../lib/slugs';
|
|
4
4
|
import type { CollectionEntry } from 'astro:content';
|
|
5
5
|
import { safeCollection } from '../lib/collections';
|
|
@@ -8,8 +8,11 @@ interface Props { post: CollectionEntry<'blog'> }
|
|
|
8
8
|
const { post } = Astro.props;
|
|
9
9
|
const d = post.data;
|
|
10
10
|
const href = `/blog/${post.id}/`;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Mirror of the Hugo card's date guard: this component can render date-less
|
|
12
|
+
// content, so omit the date (and, below, the whole eyebrow) when absent.
|
|
13
|
+
const hasDate = d.date instanceof Date && !Number.isNaN(+d.date) && d.date.getFullYear() > 1;
|
|
14
|
+
const month = hasDate ? d.date.toLocaleDateString(SITE.locale, { month: 'short', year: 'numeric' }) : '';
|
|
15
|
+
const allAuthors = await safeCollection(SECTIONS_MAP.authors);
|
|
13
16
|
const names = [
|
|
14
17
|
...d.authors.map((slug) => allAuthors.find((a) => a.id === slug)?.data.title ?? slug),
|
|
15
18
|
...d.guestAuthors.map((g) => g.name),
|
|
@@ -22,7 +25,9 @@ if (!names.length && d.author) names.push(d.author);
|
|
|
22
25
|
<a href={href} class="g-card__media"><img src={d.image} alt={d.title} loading="lazy" /></a>
|
|
23
26
|
)}
|
|
24
27
|
<div class="g-card__body">
|
|
25
|
-
|
|
28
|
+
{(month || names.length > 0) && (
|
|
29
|
+
<span class="g-card__eyebrow">{month}{month && names.length > 0 && ' · '}{names.length > 0 && names.join(', ')}</span>
|
|
30
|
+
)}
|
|
26
31
|
<h3 class="g-card__title"><a href={href}>{d.title}</a></h3>
|
|
27
32
|
{d.description && <div class="g-card__text">{d.description}</div>}
|
|
28
33
|
{d.tags.length > 0 && (
|
package/src/lib/stats.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Resolve a home-page stat value, mirroring partials/stat-value.html.
|
|
2
|
+
* Reusable from any page that shows a stats strip.
|
|
3
|
+
* - @pastEventCount past-event count, rounded ("70+")
|
|
4
|
+
* - @count:<section> live entry count of a collection
|
|
5
|
+
* - @count:<section>:rounded same, rounded down to "70+" at ten or more
|
|
6
|
+
* Any other value passes through unchanged. */
|
|
7
|
+
const round = (n: number) => (n >= 10 ? `${Math.floor(n / 10) * 10}+` : String(n));
|
|
8
|
+
|
|
9
|
+
export function formatStat(
|
|
10
|
+
value: string,
|
|
11
|
+
ctx: { pastCount: number; counts: Record<string, number> },
|
|
12
|
+
): string {
|
|
13
|
+
if (value === '@pastEventCount') return round(ctx.pastCount);
|
|
14
|
+
if (value.startsWith('@count:')) {
|
|
15
|
+
let rest = value.slice('@count:'.length);
|
|
16
|
+
let rounded = false;
|
|
17
|
+
if (rest.endsWith(':rounded')) {
|
|
18
|
+
rounded = true;
|
|
19
|
+
rest = rest.slice(0, -':rounded'.length);
|
|
20
|
+
}
|
|
21
|
+
const n = ctx.counts[rest] ?? 0;
|
|
22
|
+
return rounded ? round(n) : String(n);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Collection names referenced by @count: values in a stats array. */
|
|
28
|
+
export function countSectionsIn(stats: { value: string }[]): string[] {
|
|
29
|
+
const names = new Set<string>();
|
|
30
|
+
for (const s of stats ?? []) {
|
|
31
|
+
if (s?.value?.startsWith('@count:')) {
|
|
32
|
+
names.add(s.value.slice('@count:'.length).replace(/:rounded$/, ''));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...names];
|
|
36
|
+
}
|
|
@@ -10,13 +10,23 @@ import PageHero from '../components/PageHero.astro';
|
|
|
10
10
|
export async function getStaticPaths() {
|
|
11
11
|
const pages = await safeCollection('pages');
|
|
12
12
|
const docs = await safeCollection('docs');
|
|
13
|
+
const byId = new Map(pages.map((p) => [p.id, p]));
|
|
13
14
|
return [
|
|
14
|
-
...pages.map((entry) =>
|
|
15
|
-
|
|
15
|
+
...pages.map((entry) => {
|
|
16
|
+
// Nested page (policies/conduct) links back to its parent segment;
|
|
17
|
+
// root pages have no parent. Label prefers the parent's shortTitle.
|
|
18
|
+
const cut = entry.id.lastIndexOf('/');
|
|
19
|
+
const parentEntry = cut > 0 ? byId.get(entry.id.slice(0, cut)) : undefined;
|
|
20
|
+
const parent = parentEntry
|
|
21
|
+
? { href: `/${parentEntry.id}/`, label: parentEntry.data.shortTitle ?? parentEntry.data.title }
|
|
22
|
+
: null;
|
|
23
|
+
return { params: { slug: entry.id }, props: { entry, kind: 'page', parent } };
|
|
24
|
+
}),
|
|
25
|
+
...docs.map((entry) => ({ params: { slug: entry.id }, props: { entry, kind: 'doc', parent: null } })),
|
|
16
26
|
];
|
|
17
27
|
}
|
|
18
28
|
|
|
19
|
-
const { entry, kind } = Astro.props;
|
|
29
|
+
const { entry, kind, parent } = Astro.props;
|
|
20
30
|
const d = entry.data;
|
|
21
31
|
const { Content, headings } = await render(entry);
|
|
22
32
|
/* Current page's h2/h3 links nest under its sidebar entry, in document
|
|
@@ -54,8 +64,11 @@ const allDocs = (await safeCollection('docs')).sort(
|
|
|
54
64
|
) : (
|
|
55
65
|
<section class="g-bg-white g-section">
|
|
56
66
|
<div class="g-container">
|
|
57
|
-
<div class="g-article
|
|
58
|
-
|
|
67
|
+
<div class="g-article">
|
|
68
|
+
{parent && (
|
|
69
|
+
<a href={parent.href} style="display:inline-flex;gap:.4rem;align-items:center;font-size:var(--text-sm);font-weight:var(--weight-medium);margin-bottom:1rem"><i class="fa-solid fa-arrow-left" aria-hidden="true"></i> {parent.label}</a>
|
|
70
|
+
)}
|
|
71
|
+
<div class="g-prose"><Content /></div>
|
|
59
72
|
</div>
|
|
60
73
|
</div>
|
|
61
74
|
</section>
|
|
@@ -3,7 +3,7 @@ import { render } from 'astro:content';
|
|
|
3
3
|
import { safeCollection } from '../../lib/collections';
|
|
4
4
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
5
5
|
import AuthorBox from '../../components/AuthorBox.astro';
|
|
6
|
-
import { SUPPORT, SITE, STRINGS } from 'popular:config';
|
|
6
|
+
import { SUPPORT, SITE, STRINGS, SECTIONS_MAP } from 'popular:config';
|
|
7
7
|
import { tagSlug } from '../../lib/slugs';
|
|
8
8
|
|
|
9
9
|
export async function getStaticPaths() {
|
|
@@ -25,7 +25,7 @@ const d = post.data;
|
|
|
25
25
|
const { Content } = await render(post);
|
|
26
26
|
const dateStr = d.date.toLocaleDateString(SITE.locale, { month: 'short', day: 'numeric', year: 'numeric' });
|
|
27
27
|
|
|
28
|
-
const allAuthors = await safeCollection(
|
|
28
|
+
const allAuthors = await safeCollection(SECTIONS_MAP.authors);
|
|
29
29
|
const resolved = d.authors.map((slug) => allAuthors.find((a) => a.id === slug)).filter(Boolean);
|
|
30
30
|
const bylineNames = [
|
|
31
31
|
...d.authors.map((slug) => allAuthors.find((a) => a.id === slug)?.data.title ?? slug),
|
|
@@ -84,7 +84,7 @@ const byline = bylineNames.length > 1
|
|
|
84
84
|
<>
|
|
85
85
|
<h2 style="font-size:var(--text-xl);margin:var(--space-5) 0 1rem">About the author{resolved.length + d.guestAuthors.length > 1 ? 's' : ''}</h2>
|
|
86
86
|
<div class="g-stack" style="gap:var(--space-3)">
|
|
87
|
-
{resolved.map((a) => <AuthorBox entry={a} />)}
|
|
87
|
+
{resolved.map((a) => <AuthorBox entry={a} base={`/${SECTIONS_MAP.authors}/`} />)}
|
|
88
88
|
{d.guestAuthors.map((g) => <AuthorBox guest={g} />)}
|
|
89
89
|
</div>
|
|
90
90
|
</>
|
package/src/pages/index.astro
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { safeCollection } from '../lib/collections';
|
|
3
|
+
import { formatStat, countSectionsIn } from '../lib/stats';
|
|
3
4
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
4
5
|
import PostCard from '../components/PostCard.astro';
|
|
5
6
|
import OrganizerCard from '../components/OrganizerCard.astro';
|
|
6
|
-
import { HOME, SITE, STRINGS } from 'popular:config';
|
|
7
|
+
import { HOME, SITE, STRINGS, SECTIONS_MAP } from 'popular:config';
|
|
7
8
|
|
|
8
9
|
const now = new Date();
|
|
9
10
|
const events = (await safeCollection('events')).filter((e) => !e.data.draft);
|
|
@@ -11,13 +12,17 @@ const upcoming = events.filter((e) => e.data.date >= now).sort((a, b) => +a.data
|
|
|
11
12
|
const next = upcoming[0];
|
|
12
13
|
|
|
13
14
|
const posts = (await safeCollection('blog')).filter((p) => !p.data.draft).sort((a, b) => +b.data.date - +a.data.date).slice(0, 3);
|
|
14
|
-
/*
|
|
15
|
-
|
|
15
|
+
/* Stat values: @pastEventCount and @count:<section>[:rounded] resolve to
|
|
16
|
+
live numbers (Hugo parity, see lib/stats). Draft entries are excluded to
|
|
17
|
+
match Hugo's RegularPages. */
|
|
16
18
|
const pastCount = events.filter((e) => e.data.date < now).length;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
+
const counts: Record<string, number> = {};
|
|
20
|
+
for (const name of countSectionsIn(HOME.stats ?? [])) {
|
|
21
|
+
counts[name] = (await safeCollection(name)).filter((e) => !(e.data as any)?.draft).length;
|
|
22
|
+
}
|
|
23
|
+
const statValue = (v: string) => formatStat(v, { pastCount, counts });
|
|
19
24
|
const nextVenue = next?.data.venueRef ? (await safeCollection('venues')).find((v) => v.id === next.data.venueRef)?.data.title : undefined;
|
|
20
|
-
const organizers = (await safeCollection(
|
|
25
|
+
const organizers = (await safeCollection(SECTIONS_MAP.team)).sort((a, b) => a.data.weight - b.data.weight);
|
|
21
26
|
|
|
22
27
|
const h = HOME;
|
|
23
28
|
---
|
|
@@ -130,7 +135,7 @@ const h = HOME;
|
|
|
130
135
|
{organizers.slice(0, 3).map((o) => <OrganizerCard organizer={o} />)}
|
|
131
136
|
</div>
|
|
132
137
|
<div class="g-row" style="justify-content:center;margin-top:var(--space-4)">
|
|
133
|
-
<a class="g-btn g-btn--outline" href=
|
|
138
|
+
<a class="g-btn g-btn--outline" href={`/${SECTIONS_MAP.team}/`}>All organizers</a>
|
|
134
139
|
</div>
|
|
135
140
|
</div>
|
|
136
141
|
</section>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { STRINGS } from 'popular:config';
|
|
3
|
+
import { render } from 'astro:content';
|
|
4
|
+
import { safeCollection } from '../../lib/collections';
|
|
5
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
6
|
+
import PageHero from '../../components/PageHero.astro';
|
|
7
|
+
import AuthorBox from '../../components/AuthorBox.astro';
|
|
8
|
+
|
|
9
|
+
export async function getStaticPaths() {
|
|
10
|
+
const organizers = await safeCollection('organizers');
|
|
11
|
+
return organizers.map((entry) => ({ params: { slug: entry.id }, props: { entry } }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { entry } = Astro.props;
|
|
15
|
+
const d = entry.data;
|
|
16
|
+
const { Content } = await render(entry);
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<BaseLayout title={d.title} description={d.description}>
|
|
20
|
+
<PageHero eyebrow={d.role ?? STRINGS.eyebrowOrganizer} title={d.title} lead={d.description} />
|
|
21
|
+
<section class="g-bg-white g-section">
|
|
22
|
+
<div class="g-container">
|
|
23
|
+
<div class="g-article">
|
|
24
|
+
<AuthorBox entry={entry} base="/organizers/" />
|
|
25
|
+
<Content />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
</BaseLayout>
|
package/src/pages/rss.xml.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import rss from '@astrojs/rss';
|
|
2
2
|
import { safeCollection } from '../lib/collections';
|
|
3
|
-
import { SITE } from 'popular:config';
|
|
3
|
+
import { SITE, SECTIONS_MAP } from 'popular:config';
|
|
4
4
|
|
|
5
5
|
const escapeXml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ export async function GET(context) {
|
|
|
8
8
|
const posts = (await safeCollection('blog'))
|
|
9
9
|
.filter((p) => !p.data.draft)
|
|
10
10
|
.sort((a, b) => +b.data.date - +a.data.date);
|
|
11
|
-
const allAuthors = await safeCollection(
|
|
11
|
+
const allAuthors = await safeCollection(SECTIONS_MAP.authors);
|
|
12
12
|
return rss({
|
|
13
13
|
title: SITE.title,
|
|
14
14
|
description: SITE.description,
|