@acms-subir/core 0.2.0 → 0.3.1

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 (254) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/cli/common.mjs +35 -0
  3. package/cli/legacy.mjs +317 -0
  4. package/cli/main.mjs +66 -0
  5. package/cli/palette.mjs +4 -0
  6. package/cli/preview.mjs +50 -0
  7. package/cli/remote.mjs +55 -0
  8. package/cli/themes.mjs +127 -0
  9. package/cli.mjs +2 -369
  10. package/deploy/Caddyfile +8 -0
  11. package/deploy/Dockerfile +34 -0
  12. package/deploy/compose.https.yml +21 -0
  13. package/deploy/compose.yml +46 -0
  14. package/docs/live-video.md +91 -0
  15. package/docs/theme-authoring.md +103 -0
  16. package/email-runner.mjs +32 -0
  17. package/integration.mjs +189 -2
  18. package/ops-check.mjs +27 -0
  19. package/package.json +27 -5
  20. package/src/actions/index.ts +578 -49
  21. package/src/components/AiCleaner.tsx +291 -0
  22. package/src/components/AiSettings.tsx +575 -0
  23. package/src/components/CommandPalette.tsx +200 -0
  24. package/src/components/ContentList.astro +246 -0
  25. package/src/components/DesignAccess.tsx +182 -0
  26. package/src/components/DesignManager.tsx +418 -46
  27. package/src/components/EmailSettings.tsx +521 -0
  28. package/src/components/EventPage.astro +178 -0
  29. package/src/components/LegalPage.astro +27 -0
  30. package/src/components/LinksManager.tsx +119 -58
  31. package/src/components/LiveCard.astro +174 -0
  32. package/src/components/LiveManager.tsx +910 -0
  33. package/src/components/NavManager.tsx +42 -43
  34. package/src/components/ThemeScript.astro +50 -0
  35. package/src/components/ThemeSections.astro +58 -0
  36. package/src/components/ThemeSectionsEditor.tsx +513 -0
  37. package/src/components/WidgetRenderer.astro +3 -0
  38. package/src/components/booking/BookingFlow.astro +150 -0
  39. package/src/components/booking/HoursEditor.astro +63 -0
  40. package/src/components/builder/PageBuilder.tsx +10 -2
  41. package/src/components/editor/EventPanel.tsx +401 -0
  42. package/src/components/editor/FormatDock.tsx +81 -0
  43. package/src/components/editor/LivePane.tsx +122 -0
  44. package/src/components/editor/MobileToolbar.tsx +93 -0
  45. package/src/components/editor/PostEditor.tsx +605 -223
  46. package/src/components/editor/PreviewOverlay.tsx +242 -43
  47. package/src/components/editor/extensions/calendar.tsx +51 -7
  48. package/src/components/editor/extensions/events.tsx +153 -0
  49. package/src/components/editor/extensions/slash.tsx +7 -1
  50. package/src/components/site/Pager.astro +24 -0
  51. package/src/components/site/PostCard.astro +64 -0
  52. package/src/components/site/TagLabel.astro +11 -0
  53. package/src/layouts/Admin.astro +289 -49
  54. package/src/layouts/Site.astro +352 -59
  55. package/src/lib/agent-api.ts +13 -0
  56. package/src/lib/agent-discovery.ts +40 -0
  57. package/src/lib/agent.ts +114 -0
  58. package/src/lib/ai-config.ts +58 -0
  59. package/src/lib/ai-schema.ts +22 -0
  60. package/src/lib/ai.ts +90 -0
  61. package/src/lib/api-keys.ts +65 -0
  62. package/src/lib/api.ts +56 -0
  63. package/src/lib/auth.ts +25 -5
  64. package/src/lib/backup/content.ts +36 -0
  65. package/src/lib/backup/unzip.ts +12 -1
  66. package/src/lib/backup/zip.ts +1 -1
  67. package/src/lib/booking/agent.ts +108 -0
  68. package/src/lib/booking/availability.ts +221 -0
  69. package/src/lib/booking/emails.ts +115 -0
  70. package/src/lib/booking/forms.ts +77 -0
  71. package/src/lib/booking/service.ts +277 -0
  72. package/src/lib/booking/view.ts +54 -0
  73. package/src/lib/calendar/agent-schema.ts +48 -0
  74. package/src/lib/calendar/agent.ts +136 -0
  75. package/src/lib/calendar/api.ts +81 -0
  76. package/src/lib/calendar/cards.ts +102 -0
  77. package/src/lib/calendar/config.ts +52 -0
  78. package/src/lib/calendar/data.ts +130 -0
  79. package/src/lib/calendar/events.ts +261 -0
  80. package/src/lib/calendar/feeds.ts +43 -0
  81. package/src/lib/calendar/hooks.ts +38 -0
  82. package/src/lib/calendar/http.ts +30 -0
  83. package/src/lib/calendar/ics-write.ts +120 -0
  84. package/src/lib/calendar/ics.ts +15 -5
  85. package/src/lib/calendar/import.ts +203 -0
  86. package/src/lib/calendar/render.ts +120 -37
  87. package/src/lib/calendar/rrule.ts +149 -0
  88. package/src/lib/calendar/seo.ts +48 -0
  89. package/src/lib/design-service.ts +58 -0
  90. package/src/lib/designDiff.ts +145 -0
  91. package/src/lib/designPreview.ts +110 -12
  92. package/src/lib/documents/cleanup.ts +31 -0
  93. package/src/lib/documents/extract.ts +154 -0
  94. package/src/lib/documents/vendor.d.ts +4 -0
  95. package/src/lib/drafts.ts +60 -6
  96. package/src/lib/dynamicBlocks.ts +40 -8
  97. package/src/lib/email/accounts.ts +55 -0
  98. package/src/lib/email/config.ts +86 -0
  99. package/src/lib/email/migration.ts +24 -0
  100. package/src/lib/email/providers.ts +89 -0
  101. package/src/lib/email/service.ts +78 -0
  102. package/src/lib/email/store.ts +167 -0
  103. package/src/lib/email/templates.ts +18 -0
  104. package/src/lib/email/types.ts +32 -0
  105. package/src/lib/email/webhooks.ts +87 -0
  106. package/src/lib/email.ts +15 -0
  107. package/src/lib/env.ts +1 -1
  108. package/src/lib/legal/index.ts +25 -0
  109. package/src/lib/live/client.ts +88 -0
  110. package/src/lib/live/crypto.ts +28 -0
  111. package/src/lib/live/migration.ts +15 -0
  112. package/src/lib/live/providers.ts +152 -0
  113. package/src/lib/live/service.ts +170 -0
  114. package/src/lib/live/signals.ts +89 -0
  115. package/src/lib/live/store.ts +49 -0
  116. package/src/lib/live/types.ts +52 -0
  117. package/src/lib/live/view.ts +78 -0
  118. package/src/lib/mobile/bookings.ts +73 -0
  119. package/src/lib/mobile/calendar.ts +85 -0
  120. package/src/lib/mobile/content.ts +85 -0
  121. package/src/lib/mobile/http.ts +63 -0
  122. package/src/lib/mobile/route.ts +20 -0
  123. package/src/lib/ops.ts +8 -0
  124. package/src/lib/preview.ts +21 -1
  125. package/src/lib/repo/migrations.ts +221 -0
  126. package/src/lib/repo/sql.ts +907 -68
  127. package/src/lib/repo/types.ts +125 -5
  128. package/src/lib/scheduler.ts +27 -0
  129. package/src/lib/schema.ts +316 -1
  130. package/src/lib/search-i18n.ts +3 -0
  131. package/src/lib/socialIcons.ts +32 -0
  132. package/src/lib/tagColor.ts +11 -0
  133. package/src/lib/theme.ts +59 -40
  134. package/src/lib/themeTokens.ts +183 -0
  135. package/src/lib/umami.ts +108 -0
  136. package/src/lib/webhook-sql.ts +68 -0
  137. package/src/lib/webhooks.ts +147 -0
  138. package/src/lib/widgets.ts +1 -0
  139. package/src/middleware.ts +45 -3
  140. package/src/pages/404.astro +23 -0
  141. package/src/pages/[...slug].astro +14 -5
  142. package/src/pages/account/[action].astro +53 -0
  143. package/src/pages/admin/ai.astro +12 -0
  144. package/src/pages/admin/c/[collection]/[id].astro +4 -1
  145. package/src/pages/admin/c/[collection]/index.astro +127 -92
  146. package/src/pages/admin/calendar/bookings.astro +400 -0
  147. package/src/pages/admin/calendar/calendars.astro +265 -0
  148. package/src/pages/admin/calendar/events/[id].astro +56 -0
  149. package/src/pages/admin/calendar/events/index.astro +408 -0
  150. package/src/pages/admin/calendar/index.ts +4 -0
  151. package/src/pages/admin/calendar/integrations.astro +4 -0
  152. package/src/pages/admin/calendar/resources/[id].astro +180 -0
  153. package/src/pages/admin/calendar/resources/index.astro +106 -0
  154. package/src/pages/admin/calendar/resources/link.astro +196 -0
  155. package/src/pages/admin/design.astro +10 -3
  156. package/src/pages/admin/email.astro +13 -0
  157. package/src/pages/admin/index.astro +232 -85
  158. package/src/pages/admin/integrations.astro +237 -0
  159. package/src/pages/admin/links.astro +3 -3
  160. package/src/pages/admin/live.astro +12 -0
  161. package/src/pages/admin/login.astro +14 -10
  162. package/src/pages/admin/navigation.astro +8 -2
  163. package/src/pages/admin/pages/[id].astro +4 -1
  164. package/src/pages/admin/pages/index.astro +41 -48
  165. package/src/pages/admin/settings.astro +418 -367
  166. package/src/pages/admin/tags.astro +4 -4
  167. package/src/pages/admin/users.astro +38 -28
  168. package/src/pages/api/agent/docs.ts +33 -0
  169. package/src/pages/api/agent/index.ts +21 -0
  170. package/src/pages/api/ai/clean.ts +13 -0
  171. package/src/pages/api/ai/draft.ts +14 -0
  172. package/src/pages/api/ai/settings.ts +53 -0
  173. package/src/pages/api/ai/test.ts +12 -0
  174. package/src/pages/api/backup.ts +8 -21
  175. package/src/pages/api/calendar/availability.ts +35 -0
  176. package/src/pages/api/calendar/bookings/[id].ts +49 -0
  177. package/src/pages/api/calendar/bookings/index.ts +73 -0
  178. package/src/pages/api/calendar/calendars.ts +35 -0
  179. package/src/pages/api/calendar/docs.ts +66 -0
  180. package/src/pages/api/calendar/events/[id].ts +63 -0
  181. package/src/pages/api/calendar/events/index.ts +112 -0
  182. package/src/pages/api/calendar/resources.ts +34 -0
  183. package/src/pages/api/design/capabilities.ts +10 -0
  184. package/src/pages/api/design/index.ts +58 -0
  185. package/src/pages/api/design/keys.ts +24 -0
  186. package/src/pages/api/design/media.ts +32 -0
  187. package/src/pages/api/email/run.ts +11 -0
  188. package/src/pages/api/email/settings.ts +61 -0
  189. package/src/pages/api/email/webhooks/[provider].ts +4 -0
  190. package/src/pages/api/live/oauth/youtube/callback.ts +19 -0
  191. package/src/pages/api/live/settings.ts +45 -0
  192. package/src/pages/api/live/status.ts +7 -0
  193. package/src/pages/api/live/thumb.ts +48 -0
  194. package/src/pages/api/mcp.ts +59 -0
  195. package/src/pages/api/media/upload.ts +2 -2
  196. package/src/pages/api/ops/status.ts +39 -0
  197. package/src/pages/api/restore.ts +27 -37
  198. package/src/pages/api/search.ts +18 -4
  199. package/src/pages/api/v1/availability.ts +7 -0
  200. package/src/pages/api/v1/bookings.ts +17 -0
  201. package/src/pages/api/v1/calendars.ts +7 -0
  202. package/src/pages/api/v1/content/[id].ts +7 -0
  203. package/src/pages/api/v1/content/index.ts +7 -0
  204. package/src/pages/api/v1/events.ts +7 -0
  205. package/src/pages/api/v1/resources.ts +7 -0
  206. package/src/pages/api/webhooks/docs.ts +61 -0
  207. package/src/pages/author/[slug].astro +43 -35
  208. package/src/pages/blog/[slug].astro +137 -66
  209. package/src/pages/blog/index.astro +95 -30
  210. package/src/pages/blog/tag/[slug].astro +45 -40
  211. package/src/pages/book/[resource]/[link].astro +70 -0
  212. package/src/pages/book/[resource]/index.astro +63 -0
  213. package/src/pages/book/[resource].ics.ts +38 -0
  214. package/src/pages/book/index.astro +47 -0
  215. package/src/pages/book/manage/[token].astro +108 -0
  216. package/src/pages/calendar/[slug].ics.ts +27 -0
  217. package/src/pages/calendar/feed.ics.ts +15 -0
  218. package/src/pages/calendar/index.astro +191 -0
  219. package/src/pages/embed/book/[resource]/[link].astro +33 -0
  220. package/src/pages/embed/calendar/[slug].astro +43 -0
  221. package/src/pages/events/[slug].astro +74 -0
  222. package/src/pages/events/[slug].ics.ts +25 -0
  223. package/src/pages/index.astro +12 -3
  224. package/src/pages/legal/privacy.astro +4 -0
  225. package/src/pages/legal/terms.astro +4 -0
  226. package/src/pages/links.astro +29 -3
  227. package/src/pages/llms.txt.ts +4 -0
  228. package/src/pages/preview/[type]/[id].astro +27 -6
  229. package/src/pages/search.astro +40 -20
  230. package/src/pages/sitemap.xml.ts +16 -1
  231. package/src/styles/admin.css +924 -105
  232. package/src/styles/cards.css +92 -74
  233. package/src/styles/editor.css +492 -114
  234. package/src/styles/email-settings.css +117 -0
  235. package/src/styles/live-admin.css +126 -0
  236. package/src/styles/live.css +199 -0
  237. package/src/styles/sections.css +12 -0
  238. package/src/styles/site.css +938 -281
  239. package/src/styles/theme-editor.css +135 -0
  240. package/src/theme-sdk/contract.d.mts +619 -0
  241. package/src/theme-sdk/contract.mjs +81 -0
  242. package/src/theme-sdk/index.ts +15 -0
  243. package/src/theme-sdk/tokens.d.mts +148 -0
  244. package/src/theme-sdk/tokens.mjs +55 -0
  245. package/src/themes/editorial/templates/home.astro +4 -4
  246. package/src/themes/editorial/templates/post-minimal.astro +2 -2
  247. package/src/themes/editorial/theme.css +27 -37
  248. package/src/themes/editorial/theme.json +25 -3
  249. package/src/themes/field/theme.json +27 -9
  250. package/src/themes/ink/theme.json +30 -12
  251. package/src/themes/night/theme.css +4 -7
  252. package/src/themes/night/theme.json +28 -10
  253. package/src/virtual.d.ts +14 -0
  254. package/src/worker.ts +5 -1
@@ -0,0 +1,74 @@
1
+ ---
2
+ import Site from '../../layouts/Site.astro';
3
+ import EventPage from '../../components/EventPage.astro';
4
+ import { getRepo } from '../../lib/repo';
5
+ import { expandDynamicBlocks } from '../../lib/dynamicBlocks';
6
+ import { highlightCodeHtml } from '../../lib/highlightCode';
7
+ import { validateSession, SESSION_COOKIE } from '../../lib/auth';
8
+ import { effectiveSettings } from '../../lib/designPreview';
9
+ import { resolveDesign } from '../../lib/theme';
10
+ import { siteOrigin } from '../../lib/seo';
11
+ import { CALENDAR, eventUrl } from '../../lib/calendar/config';
12
+ import { eventIsPublic, featuredOccurrence } from '../../lib/calendar/events';
13
+ import { eventJsonLd } from '../../lib/calendar/seo';
14
+ import { upcomingEvents } from '../../lib/calendar/cards';
15
+ import { createRsvp, seatsTaken, BookingError } from '../../lib/booking/service';
16
+ import { manageUrl } from '../../lib/calendar/config';
17
+ import { waitUntilFrom } from '../../lib/webhooks';
18
+
19
+ // Public event page. Drafts and private-calendar events 404 for visitors
20
+ // (admins see them with a banner). ?on=YYYY-MM-DD shows one occurrence of a
21
+ // repeating event — the calendar grid links carry it.
22
+ if (!CALENDAR.enabled) return new Response(null, { status: 404, statusText: 'Not found' });
23
+ const repo = await getRepo(Astro.locals);
24
+ const event = await repo.getEventBySlug(Astro.params.slug ?? '');
25
+ const [rawSettings, allCalendars] = await Promise.all([repo.getSettings(), repo.listCalendars()]);
26
+ const calMap = new Map(allCalendars.map((c) => [c.id, c]));
27
+ const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
28
+ if (!event || (!eventIsPublic(event, calMap) && !isAdmin)) return new Response(null, { status: 404, statusText: 'Not found' });
29
+
30
+ const settings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
31
+ const now = Date.now();
32
+ const on = Astro.url.searchParams.get('on') ?? undefined;
33
+ const occurrence = featuredOccurrence(event, settings, now, on && /^\d{4}-\d{2}-\d{2}$/.test(on) ? on : undefined);
34
+ if (!occurrence) return new Response(null, { status: 404, statusText: 'Not found' });
35
+ const origin = siteOrigin(settings, Astro.url);
36
+ // RSVP: "Save my seat" posts back here and lands on the manage page
37
+ let rsvpError = '';
38
+ if (Astro.request.method === 'POST' && event.rsvp.enabled && event.status === 'published') {
39
+ const form = await Astro.request.formData();
40
+ try {
41
+ const b = await createRsvp(repo, {
42
+ event, start: occurrence.start, end: occurrence.end, seats: Number(form.get('seats')) || 1,
43
+ booker: { name: String(form.get('name') ?? ''), email: String(form.get('email') ?? '') },
44
+ }, { origin, locals: Astro.locals, waitUntil: waitUntilFrom(Astro.locals) });
45
+ return Astro.redirect(`${manageUrl(b.manageToken)}?new=1`);
46
+ } catch (err) {
47
+ rsvpError = err instanceof BookingError ? err.message : 'Couldn’t save your RSVP. Please try again.';
48
+ }
49
+ }
50
+ const taken = event.rsvp.enabled ? await seatsTaken(repo, event.id, occurrence.start) : 0;
51
+ const rsvp = event.rsvp.enabled
52
+ ? { going: taken, left: event.rsvp.capacity ? Math.max(0, event.rsvp.capacity - taken) : null, maxSeats: event.rsvp.maxSeats, error: rsvpError, open: occurrence.end > now && !event.cancelled }
53
+ : null;
54
+ const calendars = event.calendarIds.map((id) => calMap.get(id)).filter((c): c is NonNullable<typeof c> => Boolean(c) && c!.visibility !== 'private');
55
+ const html = highlightCodeHtml(await expandDynamicBlocks(event.bodyHtml, repo));
56
+ const upcoming = (await upcomingEvents(repo, settings, now, { limit: 4 })).filter((u) => u.event.id !== event.id).slice(0, 3);
57
+ const pageUrl = origin + eventUrl(event.slug);
58
+ const jsonLd = eventJsonLd(event, occurrence, settings, origin, pageUrl);
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ const Template = resolveDesign(settings).theme.EventTemplate as any;
61
+ const props = { event, occurrence, settings, calendars, html, upcoming, origin, preview: event.status !== 'published', rsvp };
62
+ ---
63
+
64
+ <Site
65
+ title={event.seoTitle || event.title}
66
+ exactTitle={Boolean(event.seoTitle)}
67
+ description={event.seoDesc || event.excerpt || undefined}
68
+ image={event.cover || undefined}
69
+ imageAlt={event.coverAlt || undefined}
70
+ jsonLd={jsonLd}
71
+ noindex={event.status !== 'published'}
72
+ >
73
+ {Template ? <Template {...props} /> : <EventPage {...props} />}
74
+ </Site>
@@ -0,0 +1,25 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getRepo } from '../../lib/repo';
3
+ import { siteOrigin } from '../../lib/seo';
4
+ import { CALENDAR, eventUrl } from '../../lib/calendar/config';
5
+ import { eventIsPublic } from '../../lib/calendar/events';
6
+ import { buildFeed } from '../../lib/calendar/ics-write';
7
+ import { icsResponse } from '../../lib/calendar/feeds';
8
+
9
+ // Single-event .ics behind "Add to calendar → Apple / Download". Served
10
+ // inline so iPhones open it straight in Calendar.
11
+ export const GET: APIRoute = async ({ locals, url, params }) => {
12
+ if (!CALENDAR.enabled) return new Response(null, { status: 404 });
13
+ const repo = await getRepo(locals);
14
+ const event = await repo.getEventBySlug(params.slug ?? '');
15
+ const calendars = await repo.listCalendars();
16
+ const map = new Map(calendars.map((c) => [c.id, c]));
17
+ if (!event || !eventIsPublic(event, map)) return new Response(null, { status: 404 });
18
+ const settings = await repo.getSettings();
19
+ const origin = siteOrigin(settings, url);
20
+ const body = buildFeed([event], settings.timezone, {
21
+ name: event.title, origin, host: new URL(origin).host, tz: settings.timezone || 'UTC',
22
+ eventUrl: (e) => eventUrl(e.slug), calendars: map, refreshMinutes: 1440,
23
+ });
24
+ return icsResponse(body, `${event.slug}.ics`);
25
+ };
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import ThemeSections from '../components/ThemeSections.astro';
2
3
  import Site from '../layouts/Site.astro';
3
4
  import WidgetRenderer from '../components/WidgetRenderer.astro';
4
5
  import { getRepo } from '../lib/repo';
@@ -17,16 +18,22 @@ let [home, rawSettings] = await Promise.all([
17
18
  repo.getSettings(),
18
19
  ]);
19
20
  // ?designPreview=1 (admin-only) renders the staged Design-screen changes
20
- const settings = effectiveSettings(rawSettings, Astro.url, Astro.cookies);
21
+ const settings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
21
22
  // a locale home that isn't authored yet falls back to the default home —
22
23
  // /es must never 404 mid-setup
23
24
  if (wantsLocaleHome && !home) home = await repo.getHomePage();
24
25
 
26
+ // Live state must not survive in a cached page after its snapshot expires.
27
+ if (settings.live.mode !== 'off' || home?.blocks.some(block => block.type === 'live') ||
28
+ (home && settings.sectionPages?.[home.id]?.sections.some(section => section.type === 'live'))) {
29
+ Astro.response.headers.set('Cache-Control', 'no-store');
30
+ }
31
+
25
32
  // A theme may ship templates/home.astro — a coded homepage that wins over the
26
33
  // isHome content page unless the Design screen forces 'content'. The template
27
34
  // receives the page (its body renders as the hero) plus published posts/tags.
28
35
  const design = resolveDesign(settings);
29
- const HomeTemplate = design.homeMode !== 'content' ? (design.HomeTemplate as any) : null;
36
+ const HomeTemplate = design.homeMode !== 'content' && (!home || settings.sectionPages?.[home.id]?.mode !== 'content') ? (design.HomeTemplate as any) : null;
30
37
  // theme homepages are blog-shaped — they receive the built-in collection only
31
38
  const [posts, tags] = HomeTemplate
32
39
  ? await Promise.all([repo.listPublishedPosts('posts'), repo.listTags()])
@@ -38,7 +45,9 @@ const homeForTemplate = home ? { ...home, bodyHtml: homeHtml } : null;
38
45
  ---
39
46
 
40
47
  <Site title={home?.seoTitle || undefined} description={home?.seoDesc || undefined}>
41
- {HomeTemplate ? (
48
+ {home && settings.sectionPages?.[home.id]?.mode === 'sections' ? (
49
+ <ThemeSections page={home} settings={settings} />
50
+ ) : HomeTemplate ? (
42
51
  <HomeTemplate settings={settings} page={homeForTemplate} posts={posts} tags={tags} />
43
52
  ) : home ? (
44
53
  home.bodyHtml ? (
@@ -0,0 +1,4 @@
1
+ ---
2
+ import LegalPage from '../../components/LegalPage.astro';
3
+ ---
4
+ <LegalPage policy="privacy" />
@@ -0,0 +1,4 @@
1
+ ---
2
+ import LegalPage from '../../components/LegalPage.astro';
3
+ ---
4
+ <LegalPage policy="terms" />
@@ -1,10 +1,20 @@
1
1
  ---
2
+ import LiveCard from '../components/LiveCard.astro';
2
3
  import Site from '../layouts/Site.astro';
3
4
  import { getRepo } from '../lib/repo';
4
5
  import { cleanEventName } from '../lib/analytics';
6
+ import { socialIconPath, socialIconSvg } from '../lib/socialIcons';
7
+ import { effectiveNav } from '../lib/designPreview';
5
8
 
6
9
  const repo = await getRepo(Astro.locals);
7
- const [links, settings] = await Promise.all([repo.listLinks(), repo.getSettings()]);
10
+ const [links, settings, liveNav] = await Promise.all([repo.listLinks(), repo.getSettings(), repo.listNavItems()]);
11
+ if (links.some(link => link.kind === 'live')) Astro.response.headers.set('Cache-Control', 'no-store');
12
+ // staged social links show up in design preview mode
13
+ const navItems = await effectiveNav(liveNav, Astro.url, Astro.cookies, Astro.locals);
14
+ // the site's social links (Navigation → Social) as an icon row under the bio
15
+ const socials = navItems
16
+ .filter((n) => n.area === 'social')
17
+ .map((n) => ({ ...n, path: socialIconPath(n.url) }));
8
18
 
9
19
  // links page has its own identity; falls back to the site's until it's set
10
20
  const profileName = settings.linksName || settings.siteTitle;
@@ -71,8 +81,10 @@ function vcardHref(l: (typeof links)[number]): string {
71
81
  return `data:text/vcard;charset=utf-8,${encodeURIComponent(lines.join('\r\n'))}`;
72
82
  }
73
83
 
74
- // incomplete buttons (no destination yet) stay hidden on the public page
84
+ // incomplete buttons (no destination yet) and blocks switched off in the
85
+ // admin (meta.hidden) stay hidden on the public page
75
86
  const visible = links.filter((l) => {
87
+ if (l.meta?.hidden) return false;
76
88
  if (l.kind === 'link' || l.kind === 'email' || l.kind === 'phone') return Boolean(l.url.trim());
77
89
  if (l.kind === 'heading' || l.kind === 'text') return Boolean((l.kind === 'heading' ? l.label : l.html).trim());
78
90
  if (l.kind === 'html') return Boolean(l.html.trim());
@@ -85,6 +97,17 @@ const visible = links.filter((l) => {
85
97
  {settings.linksAvatar && <img class="links-avatar" src={settings.linksAvatar} alt={profileName} />}
86
98
  <h1>{profileName}</h1>
87
99
  {profileBio && <p class="tagline">{profileBio}</p>}
100
+ {socials.length > 0 && (
101
+ <nav class="social-nav" aria-label="Social">
102
+ {socials.map((n) => (
103
+ <a href={n.url} class:list={[{ 'has-icon': n.path }]} aria-label={n.path ? n.label : undefined} title={n.path ? n.label : undefined} target="_blank" rel="noopener me">
104
+ {n.path ? <Fragment set:html={socialIconSvg(n.path)} /> : n.label}
105
+ </a>
106
+ ))}
107
+ </nav>
108
+ )}
109
+ {/* while live, the Live block moves here from wherever it was placed */}
110
+ <span data-live-slot hidden></span>
88
111
  {visible.map((l) => {
89
112
  const ico = iconInfo(l);
90
113
  const icon =
@@ -93,6 +116,8 @@ const visible = links.filter((l) => {
93
116
  ico.type === 'emoji' ? <span class="link-ico link-ico-emoji">{ico.value}</span> :
94
117
  null;
95
118
  switch (l.kind) {
119
+ case 'live':
120
+ return <LiveCard compact />;
96
121
  case 'heading':
97
122
  return <h2 class="link-heading">{l.label}</h2>;
98
123
  case 'text':
@@ -111,6 +136,7 @@ const visible = links.filter((l) => {
111
136
  return <a class="link-btn" style={buttonStyle(l)} href={`/go/${l.id}`} rel="noopener" data-umami-event={linkEvent(l.label)}>{icon}<span class="link-label">{l.label}</span></a>;
112
137
  }
113
138
  })}
114
- {visible.length === 0 && <p>No links yet.</p>}
139
+ {visible.length === 0 && <p class="muted-note">No links yet.</p>}
140
+ <p class="links-foot"><a href="/">{settings.siteTitle}</a></p>
115
141
  </div>
116
142
  </Site>
@@ -35,6 +35,10 @@ export const GET: APIRoute = async ({ locals, url }) => {
35
35
  .map((p) => `- [${p.title}](${origin}${collectionUrl(key, p.slug)})${p.excerpt ? `: ${p.excerpt}` : ''}`),
36
36
  ]),
37
37
  '',
38
+ '## Agent editing',
39
+ '',
40
+ `- [Agent API documentation](${origin}/api/agent/docs) — authenticated HTTP and MCP access; generate a key in admin settings.`,
41
+ '',
38
42
  '## Feeds',
39
43
  '',
40
44
  `- [RSS](${origin}/rss.xml)`,
@@ -6,17 +6,20 @@ import { renderMarkdown } from '../../../lib/markdown';
6
6
  import { verifyPreviewToken } from '../../../lib/preview';
7
7
  import { expandDynamicBlocks } from '../../../lib/dynamicBlocks';
8
8
  import { highlightCodeHtml } from '../../../lib/highlightCode';
9
- import { verifySessionToken, SESSION_COOKIE } from '../../../lib/auth';
10
- import { mergePageDraft, mergePostDraft } from '../../../lib/drafts';
9
+ import { validateSession, SESSION_COOKIE } from '../../../lib/auth';
10
+ import { mergeEventDraft, mergePageDraft, mergePostDraft } from '../../../lib/drafts';
11
+ import EventPage from '../../../components/EventPage.astro';
12
+ import { featuredOccurrence } from '../../../lib/calendar/events';
13
+ import { siteOrigin } from '../../../lib/seo';
11
14
 
12
15
  // Draft preview, any status. Reachable by a logged-in admin (the editor's
13
16
  // preview iframe) or by anyone holding a signed share token. Live documents
14
17
  // with pending edits render the PENDING version — exactly what Update will
15
18
  // publish — while the public URL keeps serving the live one.
16
19
  const { type, id } = Astro.params;
17
- if (type !== 'post' && type !== 'page') return new Response(null, { status: 404 });
20
+ if (type !== 'post' && type !== 'page' && type !== 'event') return new Response(null, { status: 404 });
18
21
 
19
- const isAdmin = verifySessionToken(Astro.cookies.get(SESSION_COOKIE)?.value);
22
+ const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
20
23
  const token = Astro.url.searchParams.get('token') ?? undefined;
21
24
  if (!isAdmin && !verifyPreviewToken(type, id!, token)) {
22
25
  return new Response(null, { status: 404 });
@@ -25,7 +28,22 @@ if (!isAdmin && !verifyPreviewToken(type, id!, token)) {
25
28
  const repo = await getRepo(Astro.locals);
26
29
  const livePost = type === 'post' ? await repo.getPost(id!) : null;
27
30
  const livePage = type === 'page' ? await repo.getPage(id!) : null;
28
- if (!livePost && !livePage) return new Response(null, { status: 404 });
31
+ const liveEvent = type === 'event' ? await repo.getEvent(id!) : null;
32
+ if (!livePost && !livePage && !liveEvent) return new Response(null, { status: 404 });
33
+ // events render through the real event page component, draft merged in
34
+ const event = liveEvent ? mergeEventDraft(liveEvent) : null;
35
+ const eventView = event
36
+ ? await (async () => {
37
+ const [settings, calendars] = await Promise.all([repo.getSettings(), repo.listCalendars()]);
38
+ const occurrence = featuredOccurrence(event, settings, Date.now());
39
+ return {
40
+ settings, occurrence,
41
+ calendars: event.calendarIds.map((cid) => calendars.find((c) => c.id === cid)).filter((c): c is NonNullable<typeof c> => Boolean(c)),
42
+ html: highlightCodeHtml(await expandDynamicBlocks(event.bodyHtml, repo)),
43
+ origin: siteOrigin(settings, Astro.url),
44
+ };
45
+ })()
46
+ : null;
29
47
  const post = livePost ? mergePostDraft(livePost) : null;
30
48
  const page = livePage ? mergePageDraft(livePage) : null;
31
49
 
@@ -35,7 +53,10 @@ const fmt = (ts: number | null) =>
35
53
  ts ? new Date(ts).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '';
36
54
  ---
37
55
 
38
- <Site title={post?.title ?? page?.seoTitle ?? page?.title}>
56
+ <Site title={post?.title ?? event?.title ?? page?.seoTitle ?? page?.title}>
57
+ {event && eventView?.occurrence && (
58
+ <EventPage event={event} occurrence={eventView.occurrence} settings={eventView.settings} calendars={eventView.calendars} html={eventView.html} upcoming={[]} origin={eventView.origin} preview />
59
+ )}
39
60
  {post && (
40
61
  <article>
41
62
  <h1>{post.title}</h1>
@@ -6,6 +6,9 @@ import { collectionUrl, tagUrl } from '../lib/content';
6
6
  import { authorSlug } from '../lib/schema';
7
7
  import { I18N, localeFromPath, localeHomeUrl, normalizePageLocale } from '../lib/i18n';
8
8
  import { searchStrings } from '../lib/search-i18n';
9
+ import { CALENDAR, eventUrl } from '../lib/calendar/config';
10
+ import { eventIsPublic, featuredOccurrence, formatOccurrence } from '../lib/calendar/events';
11
+ import { siteLocale } from '../lib/dates';
9
12
 
10
13
  // The no-JS search surface (the modal's form falls back here). Serves both
11
14
  // /search and the locale-prefixed /<locale>/search routes — the locale comes
@@ -20,8 +23,10 @@ const locale = normalizePageLocale(reqLocale);
20
23
  const t = searchStrings((reqLocale || siteLang(settings)).slice(0, 2));
21
24
  const results = q
22
25
  ? await repo.searchPublished(q, { locale })
23
- : { posts: [], pages: [], tags: [], authors: [] };
24
- const total = results.posts.length + results.pages.length + results.tags.length + results.authors.length;
26
+ : { posts: [], pages: [], events: [], tags: [], authors: [] };
27
+ const calMap = new Map((await repo.listCalendars()).map((c) => [c.id, c]));
28
+ const events = CALENDAR.enabled ? results.events.filter(({ doc }) => eventIsPublic(doc, calMap)).slice(0, 10) : [];
29
+ const total = results.posts.length + results.pages.length + events.length + results.tags.length + results.authors.length;
25
30
  ---
26
31
 
27
32
  <Site title={q ? `${t.label}: ${q}` : t.label} description={`${t.label} · ${settings.siteTitle}`} noindex={Boolean(q)}>
@@ -88,6 +93,26 @@ const total = results.posts.length + results.pages.length + results.tags.length
88
93
  </section>
89
94
  )}
90
95
 
96
+ {events.length > 0 && (
97
+ <section class="search-section">
98
+ <h2>{t.events}</h2>
99
+ <ul class="post-list">
100
+ {events.map(({ doc: e, snippet }) => {
101
+ const o = featuredOccurrence(e, settings, Date.now());
102
+ return (
103
+ <li class="post-item">
104
+ <div>
105
+ <a href={eventUrl(e.slug)}>{e.title}</a>
106
+ {o && <span class="search-loc">{formatOccurrence(o, settings.timezone || 'UTC', siteLocale(settings)).range}</span>}
107
+ {snippet && <p class="post-excerpt" set:html={snippet} />}
108
+ </div>
109
+ </li>
110
+ );
111
+ })}
112
+ </ul>
113
+ </section>
114
+ )}
115
+
91
116
  {results.tags.length > 0 && (
92
117
  <section class="search-section">
93
118
  <h2>{t.tags}</h2>
@@ -118,31 +143,26 @@ const total = results.posts.length + results.pages.length + results.tags.length
118
143
  </Site>
119
144
 
120
145
  <style>
121
- .search-hero { margin: 24px 0 8px; }
122
- .search-form { display: flex; gap: 10px; margin: 18px 0; }
123
- .search-form input {
124
- flex: 1; font: inherit; padding: 10px 14px; border: 1px solid var(--line);
125
- border-radius: 9px; background: var(--panel); color: var(--ink);
126
- }
127
- .search-form input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
146
+ .search-hero { padding: calc(20px * var(--space)) 0 0; }
147
+ .search-hero h1 { font-size: clamp(34px, 5vw, 46px); margin: 0 0 18px; }
148
+ .search-form { display: flex; gap: 6px; padding: 5px; margin: 0 0 18px; background: var(--panel); border: 1px solid var(--line-2); border-radius: 14px; box-shadow: var(--sh-1); }
149
+ .search-form:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
150
+ .search-form input { flex: 1; min-width: 0; font: inherit; padding: 0 12px; border: 0; background: none; color: var(--ink); outline: none; }
128
151
  .search-form button {
129
- font: inherit; font-weight: 600; padding: 10px 20px; border: 0; border-radius: 9px;
130
- background: var(--accent); color: #fff; cursor: pointer;
152
+ font: 550 15px/1 var(--font-body); height: 42px; padding: 0 18px; border: 0; border-radius: 10px;
153
+ background: var(--btn); color: var(--btn-ink); cursor: pointer;
131
154
  }
132
- .search-count { color: var(--muted); }
133
- .search-section h2 { font-size: 1.1rem; margin-top: 2rem; }
155
+ .search-count { color: var(--muted); font-size: 14.5px; margin: 0 0 8px; }
156
+ .search-section h2 { font-size: 13px; font-weight: 550; letter-spacing: 0; color: var(--muted); margin: 36px 0 4px; font-family: var(--font-body); }
157
+ .post-list :global(.post-excerpt) { color: var(--muted); font-size: 15px; margin: 4px 0 0; }
134
158
  /* snippets arrive pre-escaped with <mark> around matches (set:html), so
135
159
  the highlight style must reach into the injected fragment */
136
160
  .post-excerpt :global(mark) {
137
161
  background: color-mix(in srgb, var(--accent) 22%, transparent);
138
162
  color: inherit; border-radius: 3px; padding: 0 1px;
139
163
  }
140
- .search-loc {
141
- font-size: 11px; font-weight: 700; letter-spacing: 0.06em; color: var(--muted);
142
- border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; margin-left: 8px;
143
- vertical-align: 2px;
144
- }
145
- .search-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
146
- .search-tag-count { color: var(--muted); }
164
+ .search-loc { font-size: 12.5px; font-weight: 550; color: var(--muted); margin-left: 10px; }
165
+ .search-tags { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 12px; }
166
+ .search-tag-count { color: var(--muted); font-weight: 400; }
147
167
  .search-avatar { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; flex: none; }
148
168
  </style>
@@ -3,18 +3,24 @@ import { getRepo } from '../lib/repo';
3
3
  import { isInternalTag } from '../lib/schema';
4
4
  import { siteOrigin, xmlEscape } from '../lib/seo';
5
5
  import { CONTENT, collectionUrl, tagUrl } from '../lib/content';
6
+ import { CALENDAR, eventUrl, bookingsEnabled, resourceUrl } from '../lib/calendar/config';
7
+ import { eventIsPublic } from '../lib/calendar/events';
6
8
 
7
9
  // DB-backed sitemap: content lives in D1/libSQL, so this is generated per
8
10
  // request instead of at build time. Draft/scheduled content never appears.
9
11
 
10
12
  export const GET: APIRoute = async ({ locals, url }) => {
11
13
  const repo = await getRepo(locals);
12
- const [settings, pages, posts, tags] = await Promise.all([
14
+ const [settings, pages, posts, tags, events, calendars] = await Promise.all([
13
15
  repo.getSettings(),
14
16
  repo.listPages(),
15
17
  repo.listPublishedPosts(),
16
18
  repo.listTags(),
19
+ CALENDAR.enabled ? repo.listPublishedEvents() : Promise.resolve([]),
20
+ CALENDAR.enabled ? repo.listCalendars() : Promise.resolve([]),
17
21
  ]);
22
+ const calMap = new Map(calendars.map((c) => [c.id, c]));
23
+ const publicEvents = events.filter((e) => eventIsPublic(e, calMap));
18
24
  const origin = siteOrigin(settings, url);
19
25
 
20
26
  const usedTags = new Set(posts.flatMap((p) => p.tags.map((t) => t.toLowerCase())));
@@ -37,6 +43,15 @@ export const GET: APIRoute = async ({ locals, url }) => {
37
43
  .filter((t) => usedTags.has(t.name.toLowerCase()) && !isInternalTag(t.name))
38
44
  .map((t) => ({ loc: tagUrl(t.slug) })),
39
45
  { loc: '/links' },
46
+ ...(CALENDAR.enabled
47
+ ? [
48
+ { loc: CALENDAR.base, lastmod: Math.max(0, ...publicEvents.map((e) => e.updatedAt)) || undefined },
49
+ ...publicEvents.map((e) => ({ loc: eventUrl(e.slug), lastmod: e.updatedAt })),
50
+ ]
51
+ : []),
52
+ ...(bookingsEnabled()
53
+ ? [{ loc: CALENDAR.bookBase }, ...(await repo.listResources()).filter((r) => r.visibility === 'public').map((r) => ({ loc: resourceUrl(r.slug), lastmod: r.updatedAt }))]
54
+ : []),
40
55
  ];
41
56
 
42
57
  const xml = `<?xml version="1.0" encoding="UTF-8"?>