astro-theme-popular 0.5.0 → 0.7.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.
Files changed (44) hide show
  1. package/README.md +10 -0
  2. package/index.mjs +53 -2
  3. package/package.json +3 -2
  4. package/schemas.ts +20 -0
  5. package/src/components/AuthorBox.astro +2 -2
  6. package/src/components/DemoBar.astro +2 -0
  7. package/src/components/EventRow.astro +8 -1
  8. package/src/components/EventsEmpty.astro +25 -0
  9. package/src/components/FAQ.astro +30 -0
  10. package/src/components/FAQItem.astro +10 -0
  11. package/src/components/Footer.astro +8 -2
  12. package/src/components/Gallery.astro +7 -0
  13. package/src/components/Notice.astro +19 -0
  14. package/src/components/OrganizerCard.astro +1 -1
  15. package/src/components/PageHero.astro +6 -1
  16. package/src/components/Photo.astro +12 -0
  17. package/src/components/PostCard.astro +1 -1
  18. package/src/components/Pullquote.astro +11 -0
  19. package/src/components/SpeakerInvite.astro +16 -0
  20. package/src/layouts/BaseLayout.astro +13 -3
  21. package/src/lib/eventld.ts +95 -0
  22. package/src/lib/ical.ts +93 -0
  23. package/src/lib/inline-md.ts +21 -0
  24. package/src/lib/jsonld.ts +48 -0
  25. package/src/lib/talks.ts +87 -0
  26. package/src/pages/[...slug].astro +1 -1
  27. package/src/pages/authors/[slug].astro +4 -4
  28. package/src/pages/blog/[...page].astro +2 -2
  29. package/src/pages/blog/[slug].astro +15 -1
  30. package/src/pages/events/[...page].astro +5 -5
  31. package/src/pages/events/[slug].astro +59 -2
  32. package/src/pages/events/calendar.ics.ts +34 -0
  33. package/src/pages/index.astro +27 -5
  34. package/src/pages/llms.txt.ts +46 -0
  35. package/src/pages/organizers/[...page].astro +2 -2
  36. package/src/pages/organizers/[slug].astro +4 -4
  37. package/src/pages/robots.txt.ts +10 -0
  38. package/src/pages/speakers/[slug].astro +4 -4
  39. package/src/pages/speakers/index.astro +26 -0
  40. package/src/pages/tags/[tag]/[...page].astro +4 -4
  41. package/src/pages/talks/[...page].astro +58 -0
  42. package/src/pages/venues/[slug].astro +13 -2
  43. package/src/pages/venues/index.astro +33 -0
  44. package/src/styles/components.css +41 -0
@@ -0,0 +1,10 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ /* Generated so the Sitemap URL respects `site` + base path. @astrojs/sitemap
4
+ emits sitemap-index.xml. */
5
+ export const GET: APIRoute = ({ site }) => {
6
+ const sitemap = new URL('sitemap-index.xml', site ?? 'https://example.com').href;
7
+ return new Response(`User-agent: *\nAllow: /\nSitemap: ${sitemap}\n`, {
8
+ headers: { 'Content-Type': 'text/plain' },
9
+ });
10
+ };
@@ -3,9 +3,9 @@ import { STRINGS } from 'popular:config';
3
3
  import { render } from 'astro:content';
4
4
  import { safeCollection } from '../../lib/collections';
5
5
  import BaseLayout from '../../layouts/BaseLayout.astro';
6
- import PageHero from '../../components/PageHero.astro';
7
- import AuthorBox from '../../components/AuthorBox.astro';
8
- import EventRow from '../../components/EventRow.astro';
6
+ import PageHero from 'popular:component/PageHero';
7
+ import AuthorBox from 'popular:component/AuthorBox';
8
+ import EventRow from 'popular:component/EventRow';
9
9
 
10
10
  export async function getStaticPaths() {
11
11
  const speakers = await safeCollection('speakers');
@@ -20,7 +20,7 @@ const events = (await safeCollection('events'))
20
20
  ---
21
21
 
22
22
  <BaseLayout title={entry.data.title} description={entry.data.bio}>
23
- <PageHero eyebrow={STRINGS.eyebrowSpeaker} title={entry.data.title} lead={entry.data.bio} />
23
+ <PageHero eyebrow={STRINGS.eyebrowSpeaker} backHref="/speakers/" backLabel={STRINGS.allSpeakers} title={entry.data.title} />
24
24
  <section class="g-bg-white g-section">
25
25
  <div class="g-container">
26
26
  <div class="g-article" style="margin-bottom:var(--space-5)">
@@ -0,0 +1,26 @@
1
+ ---
2
+ /** Speakers list, mirroring Hugo's layouts/speakers/list.html: every speaker
3
+ * rendered as an AuthorBox, sorted by name. Fixes the "All speakers" backlink
4
+ * on speaker detail pages, which previously had no list page to point to. */
5
+ import { safeCollection } from '../../lib/collections';
6
+ import BaseLayout from '../../layouts/BaseLayout.astro';
7
+ import PageHero from '../../components/PageHero.astro';
8
+ import AuthorBox from 'popular:component/AuthorBox';
9
+ import SpeakerInvite from '../../components/SpeakerInvite.astro';
10
+ import { SECTIONS } from 'popular:config';
11
+
12
+ const speakers = (await safeCollection('speakers')).sort((a, b) => a.data.title.localeCompare(b.data.title));
13
+ const sec = SECTIONS.speakers;
14
+ ---
15
+
16
+ <BaseLayout title={sec.title} description={sec.lead}>
17
+ <PageHero eyebrow={sec.eyebrow} title={sec.title} lead={sec.lead} />
18
+ <section class="g-bg-white g-section">
19
+ <div class="g-container">
20
+ <div class="g-stack" style="max-width:52rem;gap:var(--space-3)">
21
+ {speakers.map((s) => <AuthorBox entry={s} base="/speakers/" />)}
22
+ <SpeakerInvite />
23
+ </div>
24
+ </div>
25
+ </section>
26
+ </BaseLayout>
@@ -1,10 +1,10 @@
1
1
  ---
2
- import { STRINGS, PAGINATION } from 'popular:config';
2
+ import { SITE, STRINGS, PAGINATION } from 'popular:config';
3
3
  import { tagSlug } from '../../../lib/slugs';
4
4
  import { safeCollection } from '../../../lib/collections';
5
5
  import BaseLayout from '../../../layouts/BaseLayout.astro';
6
- import PageHero from '../../../components/PageHero.astro';
7
- import PostCard from '../../../components/PostCard.astro';
6
+ import PageHero from 'popular:component/PageHero';
7
+ import PostCard from 'popular:component/PostCard';
8
8
  import Pagination from '../../../components/Pagination.astro';
9
9
 
10
10
  /* One route serves /tags/<tag>/ and /tags/<tag>/page/N/, matching the Hugo
@@ -34,7 +34,7 @@ const { tag } = Astro.params;
34
34
  const { label, current, total, count, posts } = Astro.props;
35
35
  ---
36
36
 
37
- <BaseLayout title={`#${label}`}>
37
+ <BaseLayout title={`#${label}`} noindex={(SITE as Record<string, any>).seo?.noindexTaxonomies ?? false}>
38
38
  <PageHero eyebrow={STRINGS.eyebrowTag} title={`#${label}`} lead={STRINGS.taggedCount.replace('{n}', String(count)).replace('{tag}', label)} />
39
39
  <section class="g-bg-white g-section">
40
40
  <div class="g-container">
@@ -0,0 +1,58 @@
1
+ ---
2
+ /** §3 talk archive index. Opt-in: builds only when SITE.talks is true (the
3
+ * Astro counterpart of Hugo's "create content/talks/_index.md to enable"),
4
+ * so sites without recordings do not grow an empty page. Aggregates every
5
+ * talk across events, newest first, filterable by the parent event's tags
6
+ * via the shared blog-filter.js (same [data-filterbar] / [data-tags] markup
7
+ * as the blog list). */
8
+ import { SITE, STRINGS } from 'popular:config';
9
+ import { safeCollection } from '../../lib/collections';
10
+ import { collectTalks } from '../../lib/talks';
11
+ import BaseLayout from '../../layouts/BaseLayout.astro';
12
+ import PageHero from 'popular:component/PageHero';
13
+
14
+ export async function getStaticPaths() {
15
+ if ((SITE as Record<string, any>).talks !== true) return [];
16
+ const events = (await safeCollection('events')).filter((e) => !e.data.draft);
17
+ const speakers = await safeCollection('speakers');
18
+ return [{ params: { page: undefined }, props: { talks: collectTalks(events, speakers) } }];
19
+ }
20
+
21
+ const { talks } = Astro.props;
22
+ const tags = [...new Set(talks.flatMap((t) => t.tags))].sort();
23
+ const fmt = (d: Date) => d.toLocaleDateString(SITE.locale, { month: 'short', day: 'numeric', year: 'numeric' });
24
+ ---
25
+
26
+ <BaseLayout title={STRINGS.talks} description={STRINGS.talksLead}>
27
+ <PageHero eyebrow={STRINGS.talks} title={STRINGS.talks} lead={STRINGS.talksLead} />
28
+ <section class="g-bg-white g-section">
29
+ <div class="g-container">
30
+ <div class="g-row" data-filterbar style="margin-bottom:var(--space-5)">
31
+ {tags.length > 0 && (
32
+ <>
33
+ <span style="font-size:var(--text-sm);color:var(--text-muted)">{STRINGS.filterLabel}</span>
34
+ <button class="g-filter is-active" data-filter="*">{STRINGS.filterAll}</button>
35
+ {tags.map((t) => <button class="g-filter" data-filter={t}>#{t}</button>)}
36
+ </>
37
+ )}
38
+ </div>
39
+ <div class="g-stack" style="gap:var(--space-3)">
40
+ {talks.map((t) => (
41
+ <div class="g-talk" data-tags={t.tags.join(',')}>
42
+ <div class="g-talk__title">{t.title}</div>
43
+ {t.speakerName && (
44
+ <div class="g-talk__by"><i class="fa-solid fa-microphone-lines" aria-hidden="true"></i> {t.speakerSlug ? <a href={`/speakers/${t.speakerSlug}/`}>{t.speakerName}</a> : t.speakerName}</div>
45
+ )}
46
+ <div class="g-talk__event"><i class="fa-solid fa-calendar-day" aria-hidden="true"></i> <a href={`/events/${t.eventId}/`}>{t.eventTitle}</a> · {fmt(t.date)}</div>
47
+ {(t.recording || t.slides) && (
48
+ <div class="g-row" style="gap:var(--space-2)">
49
+ {t.recording && <a class="g-btn g-btn--sm g-btn--outline" href={t.recording} rel="noopener" target="_blank"><i class="fa-solid fa-circle-play" aria-hidden="true"></i> {STRINGS.watchRecording}</a>}
50
+ {t.slides && <a class="g-btn g-btn--sm g-btn--ghost" href={t.slides} rel="noopener" target="_blank"><i class="fa-solid fa-file-lines" aria-hidden="true"></i> {STRINGS.viewSlides}</a>}
51
+ </div>
52
+ )}
53
+ </div>
54
+ ))}
55
+ </div>
56
+ </div>
57
+ </section>
58
+ </BaseLayout>
@@ -20,11 +20,11 @@ const events = (await safeCollection('events'))
20
20
  ---
21
21
 
22
22
  <BaseLayout title={d.title} description={d.address}>
23
- <PageHero eyebrow={STRINGS.eyebrowVenue} title={d.title} lead={d.address} />
23
+ <PageHero eyebrow={STRINGS.eyebrowVenue} backHref="/venues/" backLabel={STRINGS.allVenues} title={d.title} lead={d.address} />
24
24
  <section class="g-bg-white g-section">
25
25
  <div class="g-container">
26
26
  <div class="g-article" style="margin-bottom:var(--space-5)">
27
- {d.photo && <img src={d.photo} alt={d.title} style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius-xl);box-shadow:var(--shadow-md);margin-bottom:var(--space-4)" />}
27
+ {d.photo && <img src={d.photo} alt={d.title} style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius-xl);box-shadow:var(--shadow-md);margin-bottom:var(--space-4)" loading="lazy" decoding="async" />}
28
28
  <div class="g-meta-line" style="margin-bottom:var(--space-3)">
29
29
  {d.address && <span><i class="fa-solid fa-location-dot" aria-hidden="true"></i> {d.address}</span>}
30
30
  {d.website && <span><i class="fa-solid fa-globe" aria-hidden="true"></i> <a href={d.website} rel="noopener" target="_blank">{STRINGS.website}</a></span>}
@@ -36,6 +36,17 @@ const events = (await safeCollection('events'))
36
36
  {d.accessibility && <div class="g-callout__body"><i class="fa-solid fa-universal-access" aria-hidden="true"></i> {d.accessibility}</div>}
37
37
  </div>
38
38
  )}
39
+ {(d.wheelchair || d.transit || d.parking || d.access) && (
40
+ <>
41
+ <div class="g-section-head" style="margin-top:var(--space-4)"><span class="g-eyebrow">{STRINGS.eyebrowVenue}</span><h2 style="font-size:var(--text-xl)">{STRINGS.gettingThere}</h2></div>
42
+ {d.wheelchair && <p><span class="g-badge g-badge--success g-badge--soft"><i class="fa-solid fa-wheelchair" aria-hidden="true"></i> {STRINGS.wheelchairAccessible}</span></p>}
43
+ <div class="g-meta-line" style="flex-direction:column;align-items:flex-start;gap:.4rem">
44
+ {d.transit && <span><i class="fa-solid fa-train-subway" aria-hidden="true"></i> <strong>{STRINGS.transit}:</strong> {d.transit}</span>}
45
+ {d.parking && <span><i class="fa-solid fa-square-parking" aria-hidden="true"></i> <strong>{STRINGS.parking}:</strong> {d.parking}</span>}
46
+ </div>
47
+ {d.access && <div class="g-prose" style="margin-top:var(--space-3)"><p>{d.access}</p></div>}
48
+ </>
49
+ )}
39
50
  <div class="g-prose" style="margin-top:var(--space-4)"><Content /></div>
40
51
  </div>
41
52
  {events.length > 0 && (
@@ -0,0 +1,33 @@
1
+ ---
2
+ /** Venues list, mirroring Hugo's layouts/venues/list.html: a card grid of every
3
+ * venue, sorted by name. Fixes the "All venues" backlink on venue detail pages,
4
+ * which previously had no list page to point to. */
5
+ import { safeCollection } from '../../lib/collections';
6
+ import BaseLayout from '../../layouts/BaseLayout.astro';
7
+ import PageHero from '../../components/PageHero.astro';
8
+ import { SECTIONS } from 'popular:config';
9
+
10
+ const venues = (await safeCollection('venues')).sort((a, b) => a.data.title.localeCompare(b.data.title));
11
+ const sec = SECTIONS.venues;
12
+ ---
13
+
14
+ <BaseLayout title={sec.title} description={sec.lead}>
15
+ <PageHero eyebrow={sec.eyebrow} title={sec.title} lead={sec.lead} />
16
+ <section class="g-bg-white g-section">
17
+ <div class="g-container">
18
+ <div class="g-grid-3">
19
+ {venues.map((v) => (
20
+ <article class="g-card">
21
+ {v.data.photo && (
22
+ <a href={`/venues/${v.id}/`} class="g-card__media"><img src={v.data.photo} alt={v.data.title} loading="lazy" decoding="async" /></a>
23
+ )}
24
+ <div class="g-card__body">
25
+ <h3 class="g-card__title"><a href={`/venues/${v.id}/`}>{v.data.title}</a></h3>
26
+ {v.data.address && <div class="g-card__text"><i class="fa-solid fa-location-dot" aria-hidden="true"></i> {v.data.address}</div>}
27
+ </div>
28
+ </article>
29
+ ))}
30
+ </div>
31
+ </div>
32
+ </section>
33
+ </BaseLayout>
@@ -49,6 +49,8 @@
49
49
  .g-badge--success.g-badge--soft { background: #e6f3ec; color: #0f6e44; }
50
50
  .g-badge--warning { background: var(--amber-500); color: #142a36; }
51
51
  .g-badge--warning.g-badge--soft { background: #fff7e0; color: #946a00; }
52
+ .g-badge--danger { background: #c8102e; color: #fff; }
53
+ .g-badge--danger.g-badge--soft { background: #fde7ea; color: #a01023; }
52
54
  .g-badge--ink { background: var(--ink-800); }
53
55
 
54
56
  .g-tag {
@@ -148,6 +150,16 @@ a.g-pager__num:hover, .g-pager__nav:hover { border-color: var(--color-primary);
148
150
  .g-org__role { font-size: var(--text-sm); color: var(--color-primary); font-weight: var(--weight-medium); }
149
151
  .g-org__bio { margin: 0.5rem 0 0; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-normal); }
150
152
 
153
+ /* ---------- FAQ (native <details>, zero JS) ---------- */
154
+ .g-faq { display: flex; flex-direction: column; gap: 0.6rem; margin: 1.2rem 0; }
155
+ .g-faq__item { border: var(--border-width) solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--surface-card); overflow: hidden; }
156
+ .g-faq__q { cursor: pointer; padding: var(--space-3) var(--space-4); font-weight: var(--weight-semibold); color: var(--text-strong); list-style: none; display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); }
157
+ .g-faq__q::-webkit-details-marker { display: none; }
158
+ .g-faq__q::after { content: "\f078"; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 0.75em; color: var(--text-muted); transition: transform 0.2s; }
159
+ .g-faq__item[open] .g-faq__q::after { transform: rotate(180deg); }
160
+ .g-faq__a { padding: 0 var(--space-4) var(--space-3); }
161
+ .g-faq__a > :first-child { margin-top: 0; }
162
+
151
163
  /* ---------- Post navigation (older/newer neighbors on blog singles) ---------- */
152
164
  .g-postnav { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); margin-top: var(--space-5); }
153
165
  .g-postnav__card { display: flex; flex-direction: column; gap: 0.35rem; padding: var(--space-3); border: var(--border-width) solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--surface-card); }
@@ -308,3 +320,32 @@ pre:hover .g-copy, .g-copy:focus-visible { opacity: 1; }
308
320
  .g-nav__toggle { display: block; }
309
321
  .g-nav__cta { display: none; }
310
322
  }
323
+
324
+ /* Notice banner (§5): site-wide static announcement above the header. */
325
+ .g-notice { background: var(--surface-wash); border-bottom: var(--border-width) solid var(--border-subtle); color: var(--text-body); font-size: var(--text-sm); }
326
+ .g-notice__inner { display: flex; gap: 0.4rem 0.7rem; align-items: center; justify-content: center; flex-wrap: wrap; padding: 0.6rem var(--space-4); text-align: center; }
327
+ .g-notice__inner > i { color: var(--text-link); }
328
+ .g-notice a { color: var(--text-link); font-weight: var(--weight-medium); text-decoration: underline; }
329
+
330
+ /* Designed empty state (§5): zero upcoming events on home + events list. */
331
+ .g-empty { display: flex; flex-direction: column; align-items: center; gap: var(--space-3); text-align: center; padding: var(--space-5) 0; }
332
+ .g-empty .g-lead { margin: 0; max-width: 32rem; }
333
+
334
+ /* Talk archive (§3): a talk entry on the event page and the /talks/ index. */
335
+ .g-talk { display: flex; flex-direction: column; gap: 0.4rem; padding: var(--space-3); border: var(--border-width) solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--surface-card); }
336
+ .g-talk__title { font-weight: var(--weight-medium); }
337
+ .g-talk__by { font-size: var(--text-sm); color: var(--text-muted); }
338
+ .g-talk__by a { color: var(--text-link); }
339
+ .g-talk__event { font-size: var(--text-sm); color: var(--text-muted); }
340
+ .g-talk__event a { color: var(--text-link); }
341
+
342
+ /* Photo gallery + pullquote (§6). No JS, no lightbox: each photo links to its
343
+ full-size image (native <a>, the deliberate non-feature). */
344
+ .g-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--space-3); margin: var(--space-4) 0; }
345
+ .g-gallery__item { margin: 0; }
346
+ .g-gallery__item img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius-lg); display: block; }
347
+ .g-gallery__item figcaption { font-size: var(--text-sm); color: var(--text-muted); margin-top: 0.35rem; }
348
+ .g-pullquote { margin: var(--space-4) 0; padding-left: var(--space-4); border-left: 3px solid var(--text-link); }
349
+ .g-pullquote blockquote { margin: 0; font-size: var(--text-lg); font-style: italic; }
350
+ .g-pullquote figcaption { margin-top: 0.5rem; font-size: var(--text-sm); color: var(--text-muted); font-style: normal; }
351
+ .g-pullquote figcaption::before { content: "\2014 \00a0"; }