@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.
- package/CHANGELOG.md +87 -0
- package/cli/common.mjs +35 -0
- package/cli/legacy.mjs +317 -0
- package/cli/main.mjs +66 -0
- package/cli/palette.mjs +4 -0
- package/cli/preview.mjs +50 -0
- package/cli/remote.mjs +55 -0
- package/cli/themes.mjs +127 -0
- package/cli.mjs +2 -369
- package/deploy/Caddyfile +8 -0
- package/deploy/Dockerfile +34 -0
- package/deploy/compose.https.yml +21 -0
- package/deploy/compose.yml +46 -0
- package/docs/live-video.md +91 -0
- package/docs/theme-authoring.md +103 -0
- package/email-runner.mjs +32 -0
- package/integration.mjs +189 -2
- package/ops-check.mjs +27 -0
- package/package.json +27 -5
- package/src/actions/index.ts +578 -49
- package/src/components/AiCleaner.tsx +291 -0
- package/src/components/AiSettings.tsx +575 -0
- package/src/components/CommandPalette.tsx +200 -0
- package/src/components/ContentList.astro +246 -0
- package/src/components/DesignAccess.tsx +182 -0
- package/src/components/DesignManager.tsx +418 -46
- package/src/components/EmailSettings.tsx +521 -0
- package/src/components/EventPage.astro +178 -0
- package/src/components/LegalPage.astro +27 -0
- package/src/components/LinksManager.tsx +119 -58
- package/src/components/LiveCard.astro +174 -0
- package/src/components/LiveManager.tsx +910 -0
- package/src/components/NavManager.tsx +42 -43
- package/src/components/ThemeScript.astro +50 -0
- package/src/components/ThemeSections.astro +58 -0
- package/src/components/ThemeSectionsEditor.tsx +513 -0
- package/src/components/WidgetRenderer.astro +3 -0
- package/src/components/booking/BookingFlow.astro +150 -0
- package/src/components/booking/HoursEditor.astro +63 -0
- package/src/components/builder/PageBuilder.tsx +10 -2
- package/src/components/editor/EventPanel.tsx +401 -0
- package/src/components/editor/FormatDock.tsx +81 -0
- package/src/components/editor/LivePane.tsx +122 -0
- package/src/components/editor/MobileToolbar.tsx +93 -0
- package/src/components/editor/PostEditor.tsx +605 -223
- package/src/components/editor/PreviewOverlay.tsx +242 -43
- package/src/components/editor/extensions/calendar.tsx +51 -7
- package/src/components/editor/extensions/events.tsx +153 -0
- package/src/components/editor/extensions/slash.tsx +7 -1
- package/src/components/site/Pager.astro +24 -0
- package/src/components/site/PostCard.astro +64 -0
- package/src/components/site/TagLabel.astro +11 -0
- package/src/layouts/Admin.astro +289 -49
- package/src/layouts/Site.astro +352 -59
- package/src/lib/agent-api.ts +13 -0
- package/src/lib/agent-discovery.ts +40 -0
- package/src/lib/agent.ts +114 -0
- package/src/lib/ai-config.ts +58 -0
- package/src/lib/ai-schema.ts +22 -0
- package/src/lib/ai.ts +90 -0
- package/src/lib/api-keys.ts +65 -0
- package/src/lib/api.ts +56 -0
- package/src/lib/auth.ts +25 -5
- package/src/lib/backup/content.ts +36 -0
- package/src/lib/backup/unzip.ts +12 -1
- package/src/lib/backup/zip.ts +1 -1
- package/src/lib/booking/agent.ts +108 -0
- package/src/lib/booking/availability.ts +221 -0
- package/src/lib/booking/emails.ts +115 -0
- package/src/lib/booking/forms.ts +77 -0
- package/src/lib/booking/service.ts +277 -0
- package/src/lib/booking/view.ts +54 -0
- package/src/lib/calendar/agent-schema.ts +48 -0
- package/src/lib/calendar/agent.ts +136 -0
- package/src/lib/calendar/api.ts +81 -0
- package/src/lib/calendar/cards.ts +102 -0
- package/src/lib/calendar/config.ts +52 -0
- package/src/lib/calendar/data.ts +130 -0
- package/src/lib/calendar/events.ts +261 -0
- package/src/lib/calendar/feeds.ts +43 -0
- package/src/lib/calendar/hooks.ts +38 -0
- package/src/lib/calendar/http.ts +30 -0
- package/src/lib/calendar/ics-write.ts +120 -0
- package/src/lib/calendar/ics.ts +15 -5
- package/src/lib/calendar/import.ts +203 -0
- package/src/lib/calendar/render.ts +120 -37
- package/src/lib/calendar/rrule.ts +149 -0
- package/src/lib/calendar/seo.ts +48 -0
- package/src/lib/design-service.ts +58 -0
- package/src/lib/designDiff.ts +145 -0
- package/src/lib/designPreview.ts +110 -12
- package/src/lib/documents/cleanup.ts +31 -0
- package/src/lib/documents/extract.ts +154 -0
- package/src/lib/documents/vendor.d.ts +4 -0
- package/src/lib/drafts.ts +60 -6
- package/src/lib/dynamicBlocks.ts +40 -8
- package/src/lib/email/accounts.ts +55 -0
- package/src/lib/email/config.ts +86 -0
- package/src/lib/email/migration.ts +24 -0
- package/src/lib/email/providers.ts +89 -0
- package/src/lib/email/service.ts +78 -0
- package/src/lib/email/store.ts +167 -0
- package/src/lib/email/templates.ts +18 -0
- package/src/lib/email/types.ts +32 -0
- package/src/lib/email/webhooks.ts +87 -0
- package/src/lib/email.ts +15 -0
- package/src/lib/env.ts +1 -1
- package/src/lib/legal/index.ts +25 -0
- package/src/lib/live/client.ts +88 -0
- package/src/lib/live/crypto.ts +28 -0
- package/src/lib/live/migration.ts +15 -0
- package/src/lib/live/providers.ts +152 -0
- package/src/lib/live/service.ts +170 -0
- package/src/lib/live/signals.ts +89 -0
- package/src/lib/live/store.ts +49 -0
- package/src/lib/live/types.ts +52 -0
- package/src/lib/live/view.ts +78 -0
- package/src/lib/mobile/bookings.ts +73 -0
- package/src/lib/mobile/calendar.ts +85 -0
- package/src/lib/mobile/content.ts +85 -0
- package/src/lib/mobile/http.ts +63 -0
- package/src/lib/mobile/route.ts +20 -0
- package/src/lib/ops.ts +8 -0
- package/src/lib/preview.ts +21 -1
- package/src/lib/repo/migrations.ts +221 -0
- package/src/lib/repo/sql.ts +907 -68
- package/src/lib/repo/types.ts +125 -5
- package/src/lib/scheduler.ts +27 -0
- package/src/lib/schema.ts +316 -1
- package/src/lib/search-i18n.ts +3 -0
- package/src/lib/socialIcons.ts +32 -0
- package/src/lib/tagColor.ts +11 -0
- package/src/lib/theme.ts +59 -40
- package/src/lib/themeTokens.ts +183 -0
- package/src/lib/umami.ts +108 -0
- package/src/lib/webhook-sql.ts +68 -0
- package/src/lib/webhooks.ts +147 -0
- package/src/lib/widgets.ts +1 -0
- package/src/middleware.ts +45 -3
- package/src/pages/404.astro +23 -0
- package/src/pages/[...slug].astro +14 -5
- package/src/pages/account/[action].astro +53 -0
- package/src/pages/admin/ai.astro +12 -0
- package/src/pages/admin/c/[collection]/[id].astro +4 -1
- package/src/pages/admin/c/[collection]/index.astro +127 -92
- package/src/pages/admin/calendar/bookings.astro +400 -0
- package/src/pages/admin/calendar/calendars.astro +265 -0
- package/src/pages/admin/calendar/events/[id].astro +56 -0
- package/src/pages/admin/calendar/events/index.astro +408 -0
- package/src/pages/admin/calendar/index.ts +4 -0
- package/src/pages/admin/calendar/integrations.astro +4 -0
- package/src/pages/admin/calendar/resources/[id].astro +180 -0
- package/src/pages/admin/calendar/resources/index.astro +106 -0
- package/src/pages/admin/calendar/resources/link.astro +196 -0
- package/src/pages/admin/design.astro +10 -3
- package/src/pages/admin/email.astro +13 -0
- package/src/pages/admin/index.astro +232 -85
- package/src/pages/admin/integrations.astro +237 -0
- package/src/pages/admin/links.astro +3 -3
- package/src/pages/admin/live.astro +12 -0
- package/src/pages/admin/login.astro +14 -10
- package/src/pages/admin/navigation.astro +8 -2
- package/src/pages/admin/pages/[id].astro +4 -1
- package/src/pages/admin/pages/index.astro +41 -48
- package/src/pages/admin/settings.astro +418 -367
- package/src/pages/admin/tags.astro +4 -4
- package/src/pages/admin/users.astro +38 -28
- package/src/pages/api/agent/docs.ts +33 -0
- package/src/pages/api/agent/index.ts +21 -0
- package/src/pages/api/ai/clean.ts +13 -0
- package/src/pages/api/ai/draft.ts +14 -0
- package/src/pages/api/ai/settings.ts +53 -0
- package/src/pages/api/ai/test.ts +12 -0
- package/src/pages/api/backup.ts +8 -21
- package/src/pages/api/calendar/availability.ts +35 -0
- package/src/pages/api/calendar/bookings/[id].ts +49 -0
- package/src/pages/api/calendar/bookings/index.ts +73 -0
- package/src/pages/api/calendar/calendars.ts +35 -0
- package/src/pages/api/calendar/docs.ts +66 -0
- package/src/pages/api/calendar/events/[id].ts +63 -0
- package/src/pages/api/calendar/events/index.ts +112 -0
- package/src/pages/api/calendar/resources.ts +34 -0
- package/src/pages/api/design/capabilities.ts +10 -0
- package/src/pages/api/design/index.ts +58 -0
- package/src/pages/api/design/keys.ts +24 -0
- package/src/pages/api/design/media.ts +32 -0
- package/src/pages/api/email/run.ts +11 -0
- package/src/pages/api/email/settings.ts +61 -0
- package/src/pages/api/email/webhooks/[provider].ts +4 -0
- package/src/pages/api/live/oauth/youtube/callback.ts +19 -0
- package/src/pages/api/live/settings.ts +45 -0
- package/src/pages/api/live/status.ts +7 -0
- package/src/pages/api/live/thumb.ts +48 -0
- package/src/pages/api/mcp.ts +59 -0
- package/src/pages/api/media/upload.ts +2 -2
- package/src/pages/api/ops/status.ts +39 -0
- package/src/pages/api/restore.ts +27 -37
- package/src/pages/api/search.ts +18 -4
- package/src/pages/api/v1/availability.ts +7 -0
- package/src/pages/api/v1/bookings.ts +17 -0
- package/src/pages/api/v1/calendars.ts +7 -0
- package/src/pages/api/v1/content/[id].ts +7 -0
- package/src/pages/api/v1/content/index.ts +7 -0
- package/src/pages/api/v1/events.ts +7 -0
- package/src/pages/api/v1/resources.ts +7 -0
- package/src/pages/api/webhooks/docs.ts +61 -0
- package/src/pages/author/[slug].astro +43 -35
- package/src/pages/blog/[slug].astro +137 -66
- package/src/pages/blog/index.astro +95 -30
- package/src/pages/blog/tag/[slug].astro +45 -40
- package/src/pages/book/[resource]/[link].astro +70 -0
- package/src/pages/book/[resource]/index.astro +63 -0
- package/src/pages/book/[resource].ics.ts +38 -0
- package/src/pages/book/index.astro +47 -0
- package/src/pages/book/manage/[token].astro +108 -0
- package/src/pages/calendar/[slug].ics.ts +27 -0
- package/src/pages/calendar/feed.ics.ts +15 -0
- package/src/pages/calendar/index.astro +191 -0
- package/src/pages/embed/book/[resource]/[link].astro +33 -0
- package/src/pages/embed/calendar/[slug].astro +43 -0
- package/src/pages/events/[slug].astro +74 -0
- package/src/pages/events/[slug].ics.ts +25 -0
- package/src/pages/index.astro +12 -3
- package/src/pages/legal/privacy.astro +4 -0
- package/src/pages/legal/terms.astro +4 -0
- package/src/pages/links.astro +29 -3
- package/src/pages/llms.txt.ts +4 -0
- package/src/pages/preview/[type]/[id].astro +27 -6
- package/src/pages/search.astro +40 -20
- package/src/pages/sitemap.xml.ts +16 -1
- package/src/styles/admin.css +924 -105
- package/src/styles/cards.css +92 -74
- package/src/styles/editor.css +492 -114
- package/src/styles/email-settings.css +117 -0
- package/src/styles/live-admin.css +126 -0
- package/src/styles/live.css +199 -0
- package/src/styles/sections.css +12 -0
- package/src/styles/site.css +938 -281
- package/src/styles/theme-editor.css +135 -0
- package/src/theme-sdk/contract.d.mts +619 -0
- package/src/theme-sdk/contract.mjs +81 -0
- package/src/theme-sdk/index.ts +15 -0
- package/src/theme-sdk/tokens.d.mts +148 -0
- package/src/theme-sdk/tokens.mjs +55 -0
- package/src/themes/editorial/templates/home.astro +4 -4
- package/src/themes/editorial/templates/post-minimal.astro +2 -2
- package/src/themes/editorial/theme.css +27 -37
- package/src/themes/editorial/theme.json +25 -3
- package/src/themes/field/theme.json +27 -9
- package/src/themes/ink/theme.json +30 -12
- package/src/themes/night/theme.css +4 -7
- package/src/themes/night/theme.json +28 -10
- package/src/virtual.d.ts +14 -0
- 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
|
+
};
|
package/src/pages/index.astro
CHANGED
|
@@ -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
|
-
{
|
|
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 ? (
|
package/src/pages/links.astro
CHANGED
|
@@ -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)
|
|
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>
|
package/src/pages/llms.txt.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
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>
|
package/src/pages/search.astro
CHANGED
|
@@ -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
|
|
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 {
|
|
122
|
-
.search-
|
|
123
|
-
.search-form
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
130
|
-
background: var(--
|
|
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:
|
|
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
|
-
|
|
142
|
-
|
|
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>
|
package/src/pages/sitemap.xml.ts
CHANGED
|
@@ -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"?>
|