@acms-subir/core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -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 -315
- 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/theme-authoring.md +103 -0
- package/email-runner.mjs +32 -0
- package/integration.mjs +193 -2
- package/ops-check.mjs +27 -0
- package/package.json +35 -12
- package/src/actions/index.ts +744 -38
- 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 +455 -47
- 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 +216 -97
- package/src/components/NavManager.tsx +83 -38
- package/src/components/ThemeScript.astro +50 -0
- package/src/components/ThemeSections.astro +57 -0
- package/src/components/ThemeSectionsEditor.tsx +513 -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 +826 -271
- package/src/components/editor/PreviewOverlay.tsx +242 -43
- package/src/components/editor/extensions/calendar.tsx +51 -7
- package/src/components/editor/extensions/cards.tsx +17 -1
- package/src/components/editor/extensions/codeBlock.tsx +1 -1
- package/src/components/editor/extensions/events.tsx +153 -0
- package/src/components/editor/extensions/infoCards.tsx +16 -1
- package/src/components/editor/extensions/mediaCards.tsx +36 -2
- package/src/components/editor/extensions/slash.tsx +77 -9
- package/src/components/editor/extensions/toc.tsx +89 -0
- package/src/components/editor/extensions/toggle.tsx +70 -0
- 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 +331 -57
- 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/analytics.ts +121 -0
- package/src/lib/api-keys.ts +65 -0
- package/src/lib/api.ts +52 -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 +112 -0
- 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 +131 -0
- package/src/lib/dynamicBlocks.ts +79 -7
- 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/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 +254 -0
- package/src/lib/repo/sql.ts +956 -46
- package/src/lib/repo/types.ts +140 -3
- package/src/lib/scheduler.ts +25 -0
- package/src/lib/schema.ts +362 -0
- 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 +174 -0
- package/src/lib/umami.ts +108 -0
- package/src/lib/webhooks.ts +139 -0
- package/src/middleware.ts +36 -3
- package/src/pages/404.astro +23 -0
- package/src/pages/[...slug].astro +10 -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 +14 -4
- package/src/pages/admin/c/[collection]/index.astro +127 -91
- 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 +221 -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 +16 -4
- package/src/pages/admin/email.astro +13 -0
- package/src/pages/admin/index.astro +237 -87
- package/src/pages/admin/links.astro +3 -3
- package/src/pages/admin/login.astro +14 -10
- package/src/pages/admin/navigation.astro +8 -2
- package/src/pages/admin/pages/[id].astro +14 -3
- package/src/pages/admin/pages/index.astro +41 -47
- package/src/pages/admin/settings.astro +475 -276
- 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 +64 -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/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/author/[slug].astro +43 -35
- package/src/pages/blog/[slug].astro +140 -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 +9 -3
- package/src/pages/legal/privacy.astro +4 -0
- package/src/pages/legal/terms.astro +4 -0
- package/src/pages/links.astro +33 -7
- package/src/pages/llms.txt.ts +4 -0
- package/src/pages/preview/[type]/[id].astro +34 -8
- package/src/pages/search.astro +40 -20
- package/src/pages/sitemap.xml.ts +16 -1
- package/src/styles/admin.css +928 -105
- package/src/styles/cards.css +101 -60
- package/src/styles/editor.css +532 -102
- package/src/styles/email-settings.css +117 -0
- package/src/styles/sections.css +12 -0
- package/src/styles/site.css +938 -278
- package/src/styles/theme-editor.css +135 -0
- package/src/theme-sdk/contract.d.mts +619 -0
- package/src/theme-sdk/contract.mjs +80 -0
- package/src/theme-sdk/index.ts +15 -0
- package/src/theme-sdk/tokens.d.mts +148 -0
- package/src/theme-sdk/tokens.mjs +52 -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 +22 -37
- package/src/themes/editorial/theme.json +23 -3
- package/src/themes/field/theme.json +26 -9
- package/src/themes/ink/theme.json +30 -12
- package/src/themes/night/theme.css +4 -7
- package/src/themes/night/theme.json +27 -10
- package/src/virtual.d.ts +14 -0
- package/src/worker.ts +5 -1
|
@@ -3,65 +3,262 @@ import Admin from '../../layouts/Admin.astro';
|
|
|
3
3
|
import { getRepo } from '../../lib/repo';
|
|
4
4
|
import { userConfig } from 'virtual:acms-config';
|
|
5
5
|
import { CONTENT } from '../../lib/content';
|
|
6
|
+
import { validateSession, SESSION_COOKIE, OWNER_ENV } from '../../lib/auth';
|
|
7
|
+
import { getTraffic, umamiReadEnabled } from '../../lib/umami';
|
|
8
|
+
import { collectionUrl } from '../../lib/content';
|
|
6
9
|
|
|
7
10
|
const coreVersion = userConfig.coreVersion;
|
|
8
11
|
const repo = await getRepo(Astro.locals);
|
|
9
|
-
const
|
|
12
|
+
const sessionUserId = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
|
|
13
|
+
const [stats, pages, posts, links, settings, nav, users] = await Promise.all([
|
|
10
14
|
repo.getStats(),
|
|
11
15
|
repo.listPages(),
|
|
12
16
|
repo.listPosts(),
|
|
13
17
|
repo.listLinks(),
|
|
18
|
+
repo.getSettings(),
|
|
19
|
+
repo.listNavItems(),
|
|
20
|
+
repo.listUsers(),
|
|
14
21
|
]);
|
|
22
|
+
const traffic = await getTraffic(settings, 30);
|
|
23
|
+
const me = sessionUserId && sessionUserId !== OWNER_ENV ? users.find((u) => u.id === sessionUserId) : null;
|
|
24
|
+
const firstName = me?.name.split(/\s+/)[0] ?? '';
|
|
15
25
|
|
|
26
|
+
const tz = settings.timezone ? { timeZone: settings.timezone } : {};
|
|
27
|
+
const safe = <T,>(f: () => T, fb: T) => { try { return f(); } catch { return fb; } };
|
|
28
|
+
const hour = safe(() => Number(new Intl.DateTimeFormat('en-US', { hour: 'numeric', hourCycle: 'h23', ...tz }).format(new Date())), new Date().getHours());
|
|
29
|
+
const greeting = hour < 5 ? 'Working late' : hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
|
|
30
|
+
const when = (ts: number) => safe(() => new Date(ts).toLocaleString('en-US', { weekday: 'short', hour: 'numeric', minute: '2-digit', ...tz }), '');
|
|
31
|
+
function ago(ts: number) {
|
|
32
|
+
const s = Math.floor((Date.now() - ts) / 1000);
|
|
33
|
+
if (s < 60) return 'just now';
|
|
34
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
35
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
36
|
+
return `${Math.floor(s / 86400)}d ago`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const noun = (key: string) => (CONTENT.collections[key] ?? CONTENT.posts).singular;
|
|
40
|
+
const firstKey = Object.keys(CONTENT.collections)[0] ?? 'posts';
|
|
41
|
+
const drafts = posts.filter((p) => p.status === 'draft');
|
|
42
|
+
const scheduled = posts.filter((p) => p.status === 'scheduled' && p.scheduledAt).sort((a, b) => a.scheduledAt! - b.scheduledAt!);
|
|
43
|
+
const published = posts.filter((p) => p.status === 'published');
|
|
44
|
+
const summary = [
|
|
45
|
+
drafts.length ? `${drafts.length} ${drafts.length === 1 ? 'draft' : 'drafts'} waiting` : '',
|
|
46
|
+
scheduled.length ? `${scheduled.length === 1 ? `1 ${noun(scheduled[0].collection).toLowerCase()}` : `${scheduled.length} items`} going live ${when(scheduled[0].scheduledAt!)}` : '',
|
|
47
|
+
].filter(Boolean).join(' · ') || 'Everything is published. Nice.';
|
|
48
|
+
|
|
49
|
+
// continue writing: drafts + scheduled, most recently touched first
|
|
50
|
+
const continueRows = [...drafts, ...scheduled].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 4);
|
|
16
51
|
const recent = [
|
|
17
52
|
...pages.map((p) => ({ kind: 'Page', title: p.title, href: `/admin/pages/${p.id}`, at: p.updatedAt, status: p.status })),
|
|
18
|
-
...posts.map((p) => ({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
status: p.status,
|
|
24
|
-
})),
|
|
25
|
-
]
|
|
26
|
-
.sort((a, b) => b.at - a.at)
|
|
27
|
-
.slice(0, 8);
|
|
28
|
-
|
|
29
|
-
// per-collection counts for the stat row; content whose collection key was
|
|
30
|
-
// removed from config would otherwise be invisible, so it gets a notice
|
|
53
|
+
...posts.map((p) => ({ kind: noun(p.collection), title: p.title, href: `/admin/c/${p.collection}/${p.id}`, at: p.updatedAt, status: p.status })),
|
|
54
|
+
].sort((a, b) => b.at - a.at).slice(0, 5);
|
|
55
|
+
const topLinks = [...links].filter((l) => l.clicks > 0).sort((a, b) => b.clicks - a.clicks).slice(0, 5);
|
|
56
|
+
|
|
57
|
+
// content whose collection key was removed from config would otherwise be invisible
|
|
31
58
|
const countByCollection = new Map<string, number>();
|
|
32
59
|
for (const p of posts) countByCollection.set(p.collection, (countByCollection.get(p.collection) ?? 0) + 1);
|
|
33
60
|
const orphaned = [...countByCollection.entries()].filter(([key]) => !CONTENT.collections[key]);
|
|
34
61
|
|
|
35
|
-
|
|
62
|
+
// setup checklist — disappears once everything is done
|
|
63
|
+
const steps = [
|
|
64
|
+
{ label: 'Add your site URL', done: Boolean(settings.siteUrl), href: '/admin/settings#site' },
|
|
65
|
+
{ label: 'Connect analytics', done: Boolean(settings.analytics?.umami?.enabled), href: '/admin/settings#analytics' },
|
|
66
|
+
{ label: 'Set up navigation', done: nav.length > 0, href: '/admin/navigation' },
|
|
67
|
+
{ label: `Publish your first ${noun(firstKey).toLowerCase()}`, done: published.length > 0, href: `/admin/c/${firstKey}?new=1` },
|
|
68
|
+
{ label: 'Invite a teammate', done: users.length > 0, href: '/admin/users?new=1' },
|
|
69
|
+
];
|
|
70
|
+
const doneCount = steps.filter((s) => s.done).length;
|
|
36
71
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
72
|
+
// traffic → KPI tiles + chart
|
|
73
|
+
const fmtN = (n: number) => (n >= 10000 ? `${(n / 1000).toFixed(1)}k` : n.toLocaleString('en-US'));
|
|
74
|
+
const delta = (v: number, prev: number) => (prev > 0 ? Math.round(((v - prev) / prev) * 100) : null);
|
|
75
|
+
const dur = (s: number) => (s >= 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`);
|
|
76
|
+
function areaPath(vals: number[], w: number, h: number, pad = 3) {
|
|
77
|
+
if (vals.length < 2) return { line: '', fill: '' };
|
|
78
|
+
const max = Math.max(...vals, 1), step = (w - pad * 2) / (vals.length - 1);
|
|
79
|
+
const pts = vals.map((v, i) => [pad + i * step, h - pad - (v / max) * (h - pad * 2)]);
|
|
80
|
+
const line = pts.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' ');
|
|
81
|
+
return { line, fill: `${line} L${pts[pts.length - 1][0].toFixed(1)} ${h} L${pad} ${h}Z` };
|
|
43
82
|
}
|
|
83
|
+
const views = traffic?.series.map((s) => s.views) ?? [];
|
|
84
|
+
const chart = areaPath(views, 700, 150);
|
|
85
|
+
const spark = (vals: number[]) => areaPath(vals.slice(-14), 120, 26).line;
|
|
86
|
+
const titleByPath = new Map<string, string>([
|
|
87
|
+
...posts.map((p) => [collectionUrl(p.collection, p.slug), p.title] as [string, string]),
|
|
88
|
+
...pages.map((p) => [`/${p.slug}`, p.title] as [string, string]),
|
|
89
|
+
]);
|
|
90
|
+
const topContent = (traffic?.topPaths ?? []).filter((p) => titleByPath.has(p.path)).slice(0, 5);
|
|
91
|
+
const kpis = traffic
|
|
92
|
+
? [
|
|
93
|
+
{ label: 'Visitors · 30d', value: fmtN(traffic.visitors), d: delta(traffic.visitors, traffic.visitorsPrev), spark: spark(traffic.series.map((s) => s.visitors)) },
|
|
94
|
+
{ label: 'Pageviews', value: fmtN(traffic.pageviews), d: delta(traffic.pageviews, traffic.pageviewsPrev), spark: spark(views) },
|
|
95
|
+
{ label: 'Avg. visit', value: dur(traffic.avgSeconds), d: null, spark: '' },
|
|
96
|
+
{ label: 'Link clicks', value: fmtN(stats.linkClicks), d: null, spark: '' },
|
|
97
|
+
]
|
|
98
|
+
: [
|
|
99
|
+
{ label: 'Published', value: fmtN(published.length + stats.publishedPages), d: null, spark: '' },
|
|
100
|
+
{ label: 'Drafts', value: fmtN(stats.drafts), d: null, spark: '' },
|
|
101
|
+
{ label: 'Scheduled', value: fmtN(scheduled.length), d: null, spark: '' },
|
|
102
|
+
{ label: 'Link clicks', value: fmtN(stats.linkClicks), d: null, spark: '' },
|
|
103
|
+
];
|
|
104
|
+
const umamiOnNoKey = Boolean(settings.analytics?.umami?.enabled) && !umamiReadEnabled(settings);
|
|
105
|
+
const axis = traffic?.series.length
|
|
106
|
+
? [0, Math.floor(traffic.series.length / 3), Math.floor((traffic.series.length * 2) / 3)].map((i) =>
|
|
107
|
+
safe(() => new Date(traffic.series[i].t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', ...tz }), ''))
|
|
108
|
+
: [];
|
|
109
|
+
const I = {
|
|
110
|
+
write: 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4',
|
|
111
|
+
page: 'M7 3h8l4 4v14H7zM15 3v4h4M10 12h6M10 16h6',
|
|
112
|
+
upload: 'M12 16V4M7 9l5-5 5 5M4 17v3h16v-3',
|
|
113
|
+
links: 'M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6l1.5-1.5',
|
|
114
|
+
check: 'M5 12l5 5L20 7',
|
|
115
|
+
};
|
|
116
|
+
const tiles = [
|
|
117
|
+
{ icon: I.write, title: `Write a ${noun(firstKey).toLowerCase()}`, sub: 'Start a draft in the editor', href: `/admin/c/${firstKey}?new=1` },
|
|
118
|
+
{ icon: I.page, title: 'Add a page', sub: 'Same editor, lives at /your-slug', href: '/admin/pages?new=1' },
|
|
119
|
+
{ icon: I.upload, title: 'Upload media', sub: 'Drop images, get WebP variants', href: '/admin/media' },
|
|
120
|
+
{ icon: I.links, title: 'Edit links page', sub: `${links.length} blocks · ${fmtN(stats.linkClicks)} clicks`, href: '/admin/links' },
|
|
121
|
+
];
|
|
44
122
|
---
|
|
45
123
|
|
|
46
|
-
<Admin title="
|
|
47
|
-
<div class="
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
124
|
+
<Admin title="Overview" section="dashboard">
|
|
125
|
+
<div class="dash">
|
|
126
|
+
<div class="page-head">
|
|
127
|
+
<div>
|
|
128
|
+
<h1>{greeting}{firstName ? `, ${firstName}` : ''}</h1>
|
|
129
|
+
<p class="sub">{summary}</p>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="page-actions">
|
|
132
|
+
<a class="btn primary" href={`/admin/c/${firstKey}?new=1`}>New {noun(firstKey).toLowerCase()}</a>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div id="acms-update" hidden data-current={coreVersion} class="notice">
|
|
137
|
+
<div class="grow-text">
|
|
138
|
+
<b>ACMS <span id="acms-update-latest"></span> is available</b>
|
|
139
|
+
<span class="hint">you're on {coreVersion} — run <code>npx acms update</code>; it prints the deploy steps for your setup.</span>
|
|
140
|
+
</div>
|
|
141
|
+
<a id="acms-update-notes" class="btn sm ghost" href="https://www.npmjs.com/package/@acms-subir/core" target="_blank" rel="noopener">What's new</a>
|
|
142
|
+
<button id="acms-update-dismiss" class="icon-btn" type="button" aria-label="Dismiss until the next release" title="Dismiss until the next release">×</button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{orphaned.length > 0 && (
|
|
146
|
+
<div class="notice warn">
|
|
147
|
+
<div>
|
|
148
|
+
<b>Content in removed collections</b>
|
|
149
|
+
<p class="hint tight">
|
|
150
|
+
{orphaned.map(([key, n]) => `${n} ${n === 1 ? 'entry' : 'entries'} belong${n === 1 ? 's' : ''} to "${key}"`).join(' · ')}
|
|
151
|
+
— nothing was deleted; re-add the key under content.collections in astro.config to restore them.
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
<div class="kpis">
|
|
158
|
+
{kpis.map((k) => (
|
|
159
|
+
<div class="kpi">
|
|
160
|
+
<small>{k.label}</small>
|
|
161
|
+
<b>{k.value}{k.d !== null && <span class={`delta ${k.d < 0 ? 'down' : ''}`}>{k.d > 0 ? '+' : ''}{k.d}%</span>}</b>
|
|
162
|
+
{k.spark && <svg class="spark" viewBox="0 0 120 26" preserveAspectRatio="none" aria-hidden="true"><path d={k.spark} /></svg>}
|
|
163
|
+
</div>
|
|
52
164
|
))}
|
|
53
|
-
<a class="btn primary" href="/admin/pages?new=1">New page</a>
|
|
54
165
|
</div>
|
|
55
|
-
</div>
|
|
56
166
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
167
|
+
{traffic && views.length > 1 && (
|
|
168
|
+
<div class="card card-pad chart-card">
|
|
169
|
+
<div class="card-head"><b>Pageviews</b><span class="hint">Last 30 days · Umami</span></div>
|
|
170
|
+
<svg class="chart" viewBox="0 0 700 150" preserveAspectRatio="none" role="img" aria-label="Pageviews over the last 30 days">
|
|
171
|
+
<path class="fill" d={chart.fill} /><path class="line" d={chart.line} />
|
|
172
|
+
</svg>
|
|
173
|
+
<div class="chart-axis">{axis.map((a) => <span>{a}</span>)}<span>Today</span></div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
{umamiOnNoKey && (
|
|
177
|
+
<p class="hint dash-hint">Umami is tracking visitors. Add a <code>UMAMI_API_TOKEN</code> (self-hosted) or <code>UMAMI_API_KEY</code> (Cloud) environment variable to see traffic here.</p>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<div class="dash-two">
|
|
181
|
+
<div>
|
|
182
|
+
<div class="section-title">Start something</div>
|
|
183
|
+
<div class="tiles">
|
|
184
|
+
{tiles.map((t) => (
|
|
185
|
+
<a class="tile" href={t.href}>
|
|
186
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d={t.icon} /></svg>
|
|
187
|
+
<b>{t.title}</b><small>{t.sub}</small>
|
|
188
|
+
</a>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
{doneCount < steps.length ? (
|
|
193
|
+
<div class="card card-pad">
|
|
194
|
+
<div class="card-head"><b>Finish setting up</b><span class="hint">{doneCount} of {steps.length}</span></div>
|
|
195
|
+
<div class="progress"><i style={`width:${(doneCount / steps.length) * 100}%`} /></div>
|
|
196
|
+
{steps.map((s) => (
|
|
197
|
+
<a class={`step ${s.done ? 'done' : ''}`} href={s.href}>
|
|
198
|
+
<span class="step-dot">{s.done && <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d={I.check} /></svg>}</span>
|
|
199
|
+
{s.label}
|
|
200
|
+
{!s.done && <em>Set up →</em>}
|
|
201
|
+
</a>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
) : (
|
|
205
|
+
<div class="card card-pad">
|
|
206
|
+
<div class="card-head"><b>Top links</b><a class="hint" href="/admin/links">Links page →</a></div>
|
|
207
|
+
{topLinks.length ? topLinks.map((l) => (
|
|
208
|
+
<div class="kv-row"><span>{l.label}</span><b>{l.clicks.toLocaleString('en-US')}</b></div>
|
|
209
|
+
)) : <p class="hint tight">No clicks yet.</p>}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{continueRows.length > 0 && (
|
|
215
|
+
<>
|
|
216
|
+
<div class="section-title">Continue writing</div>
|
|
217
|
+
<div class="mini-rows">
|
|
218
|
+
{continueRows.map((p) => (
|
|
219
|
+
<a class="mini-row" href={`/admin/c/${p.collection}/${p.id}`}>
|
|
220
|
+
<span class="crow-thumb">{p.cover ? <img src={p.cover} alt="" /> : null}</span>
|
|
221
|
+
<span class="grow"><b>{p.title || 'Untitled'}</b><small>{noun(p.collection)} · edited {ago(p.updatedAt)}</small></span>
|
|
222
|
+
<span class={`status ${p.status}`}>{p.status === 'scheduled' ? `Scheduled · ${when(p.scheduledAt!)}` : 'Draft'}</span>
|
|
223
|
+
</a>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
<div class="dash-two">
|
|
230
|
+
<div>
|
|
231
|
+
<div class="section-title">{topContent.length ? 'Top content · 30d' : 'Recently edited'}</div>
|
|
232
|
+
<div class="mini-rows">
|
|
233
|
+
{topContent.length
|
|
234
|
+
? topContent.map((t) => (
|
|
235
|
+
<a class="mini-row bar-row" href={t.path} target="_blank" rel="noopener">
|
|
236
|
+
<i style={`width:${(t.views / topContent[0].views) * 100}%`} />
|
|
237
|
+
<span class="grow">{titleByPath.get(t.path)}</span><b>{t.views.toLocaleString('en-US')}</b>
|
|
238
|
+
</a>
|
|
239
|
+
))
|
|
240
|
+
: recent.map((r) => (
|
|
241
|
+
<a class="mini-row" href={r.href}>
|
|
242
|
+
<span class="grow"><b>{r.title || 'Untitled'}</b><small>{r.kind} · {ago(r.at)}</small></span>
|
|
243
|
+
<span class={`status ${r.status}`}>{r.status[0].toUpperCase() + r.status.slice(1)}</span>
|
|
244
|
+
</a>
|
|
245
|
+
))}
|
|
246
|
+
{!topContent.length && recent.length === 0 && <p class="hint">Nothing yet — create a page or {CONTENT.posts.singular.toLowerCase()}.</p>}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
{doneCount < steps.length && (
|
|
250
|
+
<div>
|
|
251
|
+
<div class="section-title">Top links</div>
|
|
252
|
+
<div class="mini-rows">
|
|
253
|
+
{topLinks.length ? topLinks.map((l) => (
|
|
254
|
+
<div class="mini-row"><span class="grow">{l.label}</span><b>{l.clicks.toLocaleString('en-US')}</b></div>
|
|
255
|
+
)) : <p class="hint">No link clicks yet.</p>}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
61
259
|
</div>
|
|
62
|
-
<a id="acms-update-notes" class="btn sm ghost" style="margin-left:auto" href="https://www.npmjs.com/package/@acms-subir/core" target="_blank" rel="noopener">What's new</a>
|
|
63
|
-
<button id="acms-update-dismiss" class="btn sm ghost" type="button" aria-label="Dismiss until the next release" title="Dismiss until the next release">×</button>
|
|
64
260
|
</div>
|
|
261
|
+
|
|
65
262
|
<script>
|
|
66
263
|
// Ghost-style update nudge: npm registry check, cached a day, silent on
|
|
67
264
|
// any failure (offline, unpublished package, CORS). Dismissal is stored
|
|
@@ -85,9 +282,12 @@ function ago(ts: number) {
|
|
|
85
282
|
}
|
|
86
283
|
} catch { return; }
|
|
87
284
|
const lt = (a: string, b: string) => {
|
|
88
|
-
|
|
285
|
+
// prerelease-blind on purpose except for the base-equal case: a site
|
|
286
|
+
// pinned to a canary (0.1.1-next.x) should be nudged once the real
|
|
287
|
+
// 0.1.1 lands on the latest tag, and never before
|
|
288
|
+
const pa = a.split('-')[0].split('.').map(Number), pb = b.split('-')[0].split('.').map(Number);
|
|
89
289
|
for (let i = 0; i < 3; i++) { if ((pa[i] ?? 0) < (pb[i] ?? 0)) return true; if ((pa[i] ?? 0) > (pb[i] ?? 0)) return false; }
|
|
90
|
-
return
|
|
290
|
+
return a.includes('-') && !b.includes('-');
|
|
91
291
|
};
|
|
92
292
|
if (!latest || !lt(current, latest)) return;
|
|
93
293
|
try { if (localStorage.getItem(DISMISSED) === latest) return; } catch {}
|
|
@@ -102,58 +302,8 @@ function ago(ts: number) {
|
|
|
102
302
|
document.getElementById('acms-update-dismiss')?.addEventListener('click', () => {
|
|
103
303
|
try { localStorage.setItem(DISMISSED, latest); } catch {}
|
|
104
304
|
el.hidden = true;
|
|
105
|
-
el.style.display = 'none';
|
|
106
305
|
});
|
|
107
306
|
el.hidden = false;
|
|
108
|
-
el.style.display = 'flex';
|
|
109
307
|
})();
|
|
110
308
|
</script>
|
|
111
|
-
|
|
112
|
-
{orphaned.length > 0 && (
|
|
113
|
-
<div class="card card-pad" style="margin-bottom:14px;border-left:3px solid var(--warn, #c77700)">
|
|
114
|
-
<b>Content in removed collections</b>
|
|
115
|
-
<p class="hint" style="margin:4px 0 0">
|
|
116
|
-
{orphaned.map(([key, n]) => `${n} ${n === 1 ? 'entry' : 'entries'} belong${n === 1 ? 's' : ''} to "${key}"`).join(' · ')}
|
|
117
|
-
— nothing was deleted; re-add the key under content.collections in astro.config to restore them.
|
|
118
|
-
</p>
|
|
119
|
-
</div>
|
|
120
|
-
)}
|
|
121
|
-
|
|
122
|
-
<div class="stats">
|
|
123
|
-
<div class="stat"><div class="label">Published pages</div><div class="value">{stats.publishedPages}</div></div>
|
|
124
|
-
{Object.entries(CONTENT.collections).map(([key, c]) => (
|
|
125
|
-
<div class="stat"><div class="label">{c.plural}</div><div class="value">{countByCollection.get(key) ?? 0}</div></div>
|
|
126
|
-
))}
|
|
127
|
-
<div class="stat"><div class="label">Drafts</div><div class="value">{stats.drafts}</div></div>
|
|
128
|
-
<div class="stat"><div class="label">Link clicks</div><div class="value">{stats.linkClicks}</div></div>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<div class="dash-grid">
|
|
132
|
-
<div class="card card-pad">
|
|
133
|
-
<p class="card-title">Recently edited</p>
|
|
134
|
-
<ul class="recent">
|
|
135
|
-
{recent.map((r) => (
|
|
136
|
-
<li>
|
|
137
|
-
<span class={`pill ${r.status}`}>{r.status}</span>
|
|
138
|
-
<a href={r.href}>{r.title}</a>
|
|
139
|
-
<span class="t-sub">{r.kind}</span>
|
|
140
|
-
<span class="when">{ago(r.at)}</span>
|
|
141
|
-
</li>
|
|
142
|
-
))}
|
|
143
|
-
{recent.length === 0 && <li class="hint">Nothing yet — create a page or {CONTENT.posts.singular.toLowerCase()}.</li>}
|
|
144
|
-
</ul>
|
|
145
|
-
</div>
|
|
146
|
-
<div class="card card-pad">
|
|
147
|
-
<p class="card-title">Top links</p>
|
|
148
|
-
<ul class="recent">
|
|
149
|
-
{topLinks.map((l) => (
|
|
150
|
-
<li>
|
|
151
|
-
<span>{l.label}</span>
|
|
152
|
-
<span class="when">{l.clicks} clicks</span>
|
|
153
|
-
</li>
|
|
154
|
-
))}
|
|
155
|
-
{topLinks.length === 0 && <li class="hint">No links yet.</li>}
|
|
156
|
-
</ul>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
309
|
</Admin>
|
|
@@ -11,11 +11,11 @@ const [links, settings] = await Promise.all([repo.listLinks(), repo.getSettings(
|
|
|
11
11
|
const unsplashEnabled = Boolean(env('UNSPLASH_ACCESS_KEY'));
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
<Admin title="Links" section="links">
|
|
14
|
+
<Admin title="Links page" section="links">
|
|
15
15
|
<div class="page-head">
|
|
16
|
-
<h1>Links</h1>
|
|
16
|
+
<div><h1>Links page</h1><p class="sub">Your link-in-bio page at /links</p></div>
|
|
17
17
|
<a class="btn sm" href="/links" target="_blank" rel="noopener">View links page ↗</a>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
|
-
<LinksManager client:only="react" links={links} settings={{ ...settings, draftsApiKey: '' }} unsplashEnabled={unsplashEnabled} />
|
|
20
|
+
<LinksManager client:only="react" links={links} settings={{ ...settings, draftsApiKey: '', ai: { ...settings.ai, encryptedKey: '' }, agent: { ...settings.agent, keyHash: '' } }} unsplashEnabled={unsplashEnabled} />
|
|
21
21
|
</Admin>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { checkPassword, createSessionToken, verifyPassword,
|
|
2
|
+
import { checkPassword, createSessionToken, verifyPassword, validateSession, SESSION_COOKIE } from '../../lib/auth';
|
|
3
3
|
import { getRepo } from '../../lib/repo';
|
|
4
4
|
import { BRAND } from '../../lib/brand';
|
|
5
|
+
import ThemeScript from '../../components/ThemeScript.astro';
|
|
5
6
|
import '../../styles/admin.css';
|
|
6
7
|
|
|
7
|
-
if (
|
|
8
|
+
if ((await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals))) {
|
|
8
9
|
return Astro.redirect('/admin');
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -17,12 +18,13 @@ if (Astro.request.method === 'POST') {
|
|
|
17
18
|
// Email empty → the env master password (bootstrap owner). Email present →
|
|
18
19
|
// a user account created in /admin/users.
|
|
19
20
|
let sessionUserId: string | null = null;
|
|
21
|
+
let sessionVersion = 0;
|
|
20
22
|
if (!email && checkPassword(password)) {
|
|
21
23
|
sessionUserId = null; // default token = env owner
|
|
22
24
|
} else if (email) {
|
|
23
25
|
const repo = await getRepo(Astro.locals);
|
|
24
26
|
const auth = await repo.getUserAuth(email);
|
|
25
|
-
if (auth && (await verifyPassword(password, auth.passHash))) sessionUserId = auth.user.id;
|
|
27
|
+
if (auth && (await verifyPassword(password, auth.passHash))) { sessionUserId = auth.user.id; sessionVersion = auth.sessionVersion; }
|
|
26
28
|
else {
|
|
27
29
|
error = 'Wrong email or password.';
|
|
28
30
|
}
|
|
@@ -31,7 +33,7 @@ if (Astro.request.method === 'POST') {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
if (!error) {
|
|
34
|
-
Astro.cookies.set(SESSION_COOKIE, sessionUserId ? createSessionToken(sessionUserId) : createSessionToken(), {
|
|
36
|
+
Astro.cookies.set(SESSION_COOKIE, sessionUserId ? createSessionToken(sessionUserId, sessionVersion) : createSessionToken(), {
|
|
35
37
|
httpOnly: true,
|
|
36
38
|
sameSite: 'lax',
|
|
37
39
|
path: '/',
|
|
@@ -47,7 +49,8 @@ if (Astro.request.method === 'POST') {
|
|
|
47
49
|
<html lang="en">
|
|
48
50
|
<head>
|
|
49
51
|
<meta charset="utf-8" />
|
|
50
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
53
|
+
<ThemeScript />
|
|
51
54
|
<meta name="robots" content="noindex" />
|
|
52
55
|
<title>Sign in · {BRAND.name} admin</title>
|
|
53
56
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
@@ -58,9 +61,9 @@ if (Astro.request.method === 'POST') {
|
|
|
58
61
|
<div class="card card-pad login-card">
|
|
59
62
|
<div class="brand">
|
|
60
63
|
<svg width="26" height="26" viewBox="0 0 24 24" role="img" aria-label={BRAND.name}>
|
|
61
|
-
<rect x="1" y="3" width="22" height="4.4" rx="1.9" fill="
|
|
62
|
-
<rect x="1" y="10" width="12.5" height="4.4" rx="1.9" fill="
|
|
63
|
-
<rect x="1" y="17" width="17.5" height="4.4" rx="1.9" fill="
|
|
64
|
+
<rect x="1" y="3" width="22" height="4.4" rx="1.9" fill="currentColor" opacity="0.85" />
|
|
65
|
+
<rect x="1" y="10" width="12.5" height="4.4" rx="1.9" fill="var(--accent)" />
|
|
66
|
+
<rect x="1" y="17" width="17.5" height="4.4" rx="1.9" fill="currentColor" opacity="0.35" />
|
|
64
67
|
</svg>
|
|
65
68
|
<div class="brand-name">{BRAND.name}</div>
|
|
66
69
|
</div>
|
|
@@ -74,9 +77,10 @@ if (Astro.request.method === 'POST') {
|
|
|
74
77
|
<span>Password</span>
|
|
75
78
|
<input type="password" name="password" autofocus autocomplete="current-password" />
|
|
76
79
|
</label>
|
|
77
|
-
<button class="btn primary
|
|
80
|
+
<button class="btn primary block" type="submit">Sign in</button>
|
|
78
81
|
</form>
|
|
79
|
-
<p class="hint"
|
|
82
|
+
<p class="hint"><a href="/account/forgot">Forgot your password?</a></p>
|
|
83
|
+
<p class="hint login-note">No account? The owner password from <code>ADMIN_PASSWORD</code> in <code>.env</code> always works with email left empty.</p>
|
|
80
84
|
</div>
|
|
81
85
|
</div>
|
|
82
86
|
</body>
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import Admin from '../../layouts/Admin.astro';
|
|
3
3
|
import { getRepo } from '../../lib/repo';
|
|
4
4
|
import NavManager from '../../components/NavManager';
|
|
5
|
+
import { draftNavItems, toNavItems } from '../../lib/drafts';
|
|
5
6
|
|
|
7
|
+
// Menus stage with the design: the editor shows the staged list when one
|
|
8
|
+
// exists, the live list otherwise.
|
|
6
9
|
const repo = await getRepo(Astro.locals);
|
|
7
|
-
const
|
|
10
|
+
const [live, settings] = await Promise.all([repo.listNavItems(), repo.getSettings()]);
|
|
11
|
+
const stagedList = draftNavItems(settings.draftDesign);
|
|
12
|
+
const items = stagedList ? toNavItems(stagedList) : live;
|
|
8
13
|
---
|
|
9
14
|
|
|
10
15
|
<Admin title="Navigation" section="navigation">
|
|
@@ -12,6 +17,7 @@ const items = await repo.listNavItems();
|
|
|
12
17
|
<h1>Navigation</h1>
|
|
13
18
|
<a class="btn sm" href="/" target="_blank" rel="noopener">View site ↗</a>
|
|
14
19
|
</div>
|
|
20
|
+
<p class="hint nm-intro">Menu changes are staged and go live with <b>Publish</b>, together with any staged design changes. Use Preview site to see them first.</p>
|
|
15
21
|
|
|
16
|
-
<NavManager client:only="react" items={items} />
|
|
22
|
+
<NavManager client:only="react" items={items} staged={Boolean(stagedList)} />
|
|
17
23
|
</Admin>
|
|
@@ -5,6 +5,9 @@ import { createPreviewToken } from '../../../lib/preview';
|
|
|
5
5
|
import { resolveDesign } from '../../../lib/theme';
|
|
6
6
|
import { env } from '../../../lib/env';
|
|
7
7
|
import { BRAND } from '../../../lib/brand';
|
|
8
|
+
import { mergePageDraft } from '../../../lib/drafts';
|
|
9
|
+
import { localeHomeUrl } from '../../../lib/i18n';
|
|
10
|
+
import ThemeScript from '../../../components/ThemeScript.astro';
|
|
8
11
|
import PostEditor from '../../../components/editor/PostEditor';
|
|
9
12
|
import '../../../styles/admin.css';
|
|
10
13
|
import '../../../styles/editor.css';
|
|
@@ -14,9 +17,14 @@ const id = Astro.params.id!;
|
|
|
14
17
|
const page = await repo.getPage(id);
|
|
15
18
|
if (!page) return new Response(null, { status: 404 });
|
|
16
19
|
|
|
20
|
+
// The editor hydrates from the pending draft when one exists (live pages
|
|
21
|
+
// stage their edits until Update); the live URL stays on the live slug.
|
|
22
|
+
const editable = mergePageDraft(page);
|
|
23
|
+
const livePath = page.isHome ? localeHomeUrl(page.locale) : `/${page.slug}`;
|
|
24
|
+
|
|
17
25
|
// Canonical content is the TipTap doc (as HTML). Legacy widget-block pages
|
|
18
26
|
// are converted on first open and become rich content on next save.
|
|
19
|
-
const initialHTML =
|
|
27
|
+
const initialHTML = editable.bodyHtml || blocksToHtml(editable.blocks);
|
|
20
28
|
const previewPath = `/preview/page/${page.id}`;
|
|
21
29
|
const sharePath = `${previewPath}?token=${createPreviewToken('page', page.id)}`;
|
|
22
30
|
const unsplashEnabled = Boolean(env('UNSPLASH_ACCESS_KEY'));
|
|
@@ -28,7 +36,8 @@ const templates = Object.keys(resolveDesign(settings).theme.pageTemplates);
|
|
|
28
36
|
<html lang="en">
|
|
29
37
|
<head>
|
|
30
38
|
<meta charset="utf-8" />
|
|
31
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-visual" />
|
|
40
|
+
<ThemeScript />
|
|
32
41
|
<meta name="robots" content="noindex" />
|
|
33
42
|
<title>{page.title || 'Untitled'} · {BRAND.name} editor</title>
|
|
34
43
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
@@ -38,12 +47,14 @@ const templates = Object.keys(resolveDesign(settings).theme.pageTemplates);
|
|
|
38
47
|
<PostEditor
|
|
39
48
|
client:only="react"
|
|
40
49
|
kind="page"
|
|
41
|
-
page={
|
|
50
|
+
page={editable}
|
|
42
51
|
initialHTML={initialHTML}
|
|
43
52
|
unsplashEnabled={unsplashEnabled}
|
|
44
53
|
previewPath={previewPath}
|
|
45
54
|
sharePath={sharePath}
|
|
55
|
+
livePath={livePath}
|
|
46
56
|
templates={templates}
|
|
57
|
+
site={{ title: settings.siteTitle, url: settings.siteUrl }}
|
|
47
58
|
/>
|
|
48
59
|
</body>
|
|
49
60
|
</html>
|