@acms-subir/core 0.0.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/cli.mjs +75 -0
- package/integration.mjs +286 -0
- package/package.json +89 -0
- package/src/actions/index.ts +502 -0
- package/src/components/DesignManager.tsx +542 -0
- package/src/components/LinksManager.tsx +694 -0
- package/src/components/Modal.astro +65 -0
- package/src/components/NavManager.tsx +168 -0
- package/src/components/WidgetRenderer.astro +84 -0
- package/src/components/builder/PageBuilder.tsx +575 -0
- package/src/components/editor/ImagePicker.tsx +215 -0
- package/src/components/editor/PostEditor.tsx +1011 -0
- package/src/components/editor/PreviewOverlay.tsx +77 -0
- package/src/components/editor/extensions/calendar.tsx +230 -0
- package/src/components/editor/extensions/cards.tsx +488 -0
- package/src/components/editor/extensions/codeBlock.tsx +70 -0
- package/src/components/editor/extensions/infoCards.tsx +270 -0
- package/src/components/editor/extensions/mediaCards.tsx +376 -0
- package/src/components/editor/extensions/postList.tsx +130 -0
- package/src/components/editor/extensions/slash.tsx +250 -0
- package/src/components/editor/extensions/tableTyping.ts +63 -0
- package/src/components/editor/imaging.ts +146 -0
- package/src/components/media/CropModal.tsx +229 -0
- package/src/components/media/MediaLibrary.tsx +295 -0
- package/src/layouts/Admin.astro +119 -0
- package/src/layouts/Site.astro +218 -0
- package/src/lib/auth.ts +94 -0
- package/src/lib/backup/unzip.ts +38 -0
- package/src/lib/backup/zip.ts +105 -0
- package/src/lib/blocksToHtml.ts +68 -0
- package/src/lib/brand.ts +26 -0
- package/src/lib/calendar/ics.ts +402 -0
- package/src/lib/calendar/render.ts +275 -0
- package/src/lib/calendar/src.ts +68 -0
- package/src/lib/content.ts +53 -0
- package/src/lib/dates.ts +34 -0
- package/src/lib/dynamicBlocks.ts +115 -0
- package/src/lib/env.ts +6 -0
- package/src/lib/highlightCode.ts +44 -0
- package/src/lib/i18n.ts +40 -0
- package/src/lib/markdown.ts +7 -0
- package/src/lib/media/store.ts +95 -0
- package/src/lib/net.ts +51 -0
- package/src/lib/preview.ts +28 -0
- package/src/lib/repo/d1.ts +44 -0
- package/src/lib/repo/index.ts +23 -0
- package/src/lib/repo/migrations.ts +231 -0
- package/src/lib/repo/search.ts +14 -0
- package/src/lib/repo/sql.ts +721 -0
- package/src/lib/repo/sqlite.ts +43 -0
- package/src/lib/repo/types.ts +132 -0
- package/src/lib/schema.ts +286 -0
- package/src/lib/seo.ts +37 -0
- package/src/lib/theme.ts +206 -0
- package/src/lib/widgets.ts +108 -0
- package/src/middleware.ts +109 -0
- package/src/pages/[...slug].astro +43 -0
- package/src/pages/admin/c/[collection]/[id].astro +56 -0
- package/src/pages/admin/c/[collection]/index.astro +163 -0
- package/src/pages/admin/design.astro +42 -0
- package/src/pages/admin/index.astro +145 -0
- package/src/pages/admin/links.astro +21 -0
- package/src/pages/admin/login.astro +83 -0
- package/src/pages/admin/logout.ts +7 -0
- package/src/pages/admin/media.astro +13 -0
- package/src/pages/admin/navigation.astro +17 -0
- package/src/pages/admin/pages/[id].astro +49 -0
- package/src/pages/admin/pages/index.astro +100 -0
- package/src/pages/admin/posts/[id].astro +6 -0
- package/src/pages/admin/posts/index.astro +7 -0
- package/src/pages/admin/settings.astro +512 -0
- package/src/pages/admin/tags.astro +101 -0
- package/src/pages/admin/users.astro +149 -0
- package/src/pages/api/backup.ts +63 -0
- package/src/pages/api/drafts.ts +106 -0
- package/src/pages/api/media/upload.ts +89 -0
- package/src/pages/api/restore.ts +102 -0
- package/src/pages/author/[slug].astro +66 -0
- package/src/pages/blog/[slug].astro +240 -0
- package/src/pages/blog/index.astro +51 -0
- package/src/pages/blog/tag/[slug].astro +83 -0
- package/src/pages/embed/[pageId]/[blockId].ts +45 -0
- package/src/pages/go/[id].ts +10 -0
- package/src/pages/index.astro +52 -0
- package/src/pages/links.astro +110 -0
- package/src/pages/llms.txt.ts +47 -0
- package/src/pages/media/[...key].ts +18 -0
- package/src/pages/preview/[type]/[id].astro +52 -0
- package/src/pages/robots.txt.ts +28 -0
- package/src/pages/rss.xml.ts +58 -0
- package/src/pages/search.astro +83 -0
- package/src/pages/sitemap.xml.ts +55 -0
- package/src/styles/admin.css +652 -0
- package/src/styles/builder.css +121 -0
- package/src/styles/cards.css +176 -0
- package/src/styles/editor.css +596 -0
- package/src/styles/site.css +427 -0
- package/src/themes/editorial/templates/home.astro +127 -0
- package/src/themes/editorial/templates/post-minimal.astro +50 -0
- package/src/themes/editorial/theme.css +108 -0
- package/src/themes/editorial/theme.json +20 -0
- package/src/themes/field/theme.json +17 -0
- package/src/themes/ink/theme.json +17 -0
- package/src/themes/night/theme.css +7 -0
- package/src/themes/night/theme.json +17 -0
- package/src/virtual.d.ts +25 -0
- package/src/worker.ts +28 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Site from '../../../layouts/Site.astro';
|
|
3
|
+
import WidgetRenderer from '../../../components/WidgetRenderer.astro';
|
|
4
|
+
import { getRepo } from '../../../lib/repo';
|
|
5
|
+
import { renderMarkdown } from '../../../lib/markdown';
|
|
6
|
+
import { verifyPreviewToken } from '../../../lib/preview';
|
|
7
|
+
import { expandDynamicBlocks } from '../../../lib/dynamicBlocks';
|
|
8
|
+
import { highlightCodeHtml } from '../../../lib/highlightCode';
|
|
9
|
+
import { verifySessionToken, SESSION_COOKIE } from '../../../lib/auth';
|
|
10
|
+
|
|
11
|
+
// Draft preview, any status. Reachable by a logged-in admin (the editor's
|
|
12
|
+
// preview iframe) or by anyone holding a signed share token.
|
|
13
|
+
const { type, id } = Astro.params;
|
|
14
|
+
if (type !== 'post' && type !== 'page') return new Response(null, { status: 404 });
|
|
15
|
+
|
|
16
|
+
const isAdmin = verifySessionToken(Astro.cookies.get(SESSION_COOKIE)?.value);
|
|
17
|
+
const token = Astro.url.searchParams.get('token') ?? undefined;
|
|
18
|
+
if (!isAdmin && !verifyPreviewToken(type, id!, token)) {
|
|
19
|
+
return new Response(null, { status: 404 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const repo = await getRepo(Astro.locals);
|
|
23
|
+
const post = type === 'post' ? await repo.getPost(id!) : null;
|
|
24
|
+
const page = type === 'page' ? await repo.getPage(id!) : null;
|
|
25
|
+
if (!post && !page) return new Response(null, { status: 404 });
|
|
26
|
+
|
|
27
|
+
const html = post ? highlightCodeHtml(await expandDynamicBlocks(post.bodyHtml || renderMarkdown(post.body), repo)) : '';
|
|
28
|
+
const pageHtml = page?.bodyHtml ? highlightCodeHtml(await expandDynamicBlocks(page.bodyHtml, repo)) : '';
|
|
29
|
+
const fmt = (ts: number | null) =>
|
|
30
|
+
ts ? new Date(ts).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '';
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<Site title={post?.title ?? page?.seoTitle ?? page?.title}>
|
|
34
|
+
{post && (
|
|
35
|
+
<article>
|
|
36
|
+
<h1>{post.title}</h1>
|
|
37
|
+
<div class="post-meta">
|
|
38
|
+
{fmt(post.publishedAt ?? Date.now())}
|
|
39
|
+
{post.tags.map((t) => <span class="tag">{t}</span>)}
|
|
40
|
+
</div>
|
|
41
|
+
{post.cover && <img class="post-cover" src={post.cover} alt={post.coverAlt} />}
|
|
42
|
+
<div set:html={html} />
|
|
43
|
+
</article>
|
|
44
|
+
)}
|
|
45
|
+
{page && (
|
|
46
|
+
page.bodyHtml ? (
|
|
47
|
+
<article class="page-body" set:html={pageHtml} />
|
|
48
|
+
) : (
|
|
49
|
+
<WidgetRenderer pageId={page.id} blocks={page.blocks} />
|
|
50
|
+
)
|
|
51
|
+
)}
|
|
52
|
+
</Site>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../lib/repo';
|
|
3
|
+
import { siteOrigin } from '../lib/seo';
|
|
4
|
+
|
|
5
|
+
// Crawl policy: public content is open to all crawlers — including AI
|
|
6
|
+
// crawlers (discovery is the point of a public blog). Admin, previews, and
|
|
7
|
+
// machine endpoints are excluded for everyone.
|
|
8
|
+
|
|
9
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
10
|
+
const repo = await getRepo(locals);
|
|
11
|
+
const settings = await repo.getSettings();
|
|
12
|
+
const origin = siteOrigin(settings, url);
|
|
13
|
+
|
|
14
|
+
const body = `User-agent: *
|
|
15
|
+
Disallow: /admin
|
|
16
|
+
Disallow: /preview/
|
|
17
|
+
Disallow: /api/
|
|
18
|
+
Disallow: /embed/
|
|
19
|
+
Disallow: /go/
|
|
20
|
+
|
|
21
|
+
Sitemap: ${origin}/sitemap.xml
|
|
22
|
+
|
|
23
|
+
# AI crawlers welcome — a machine-readable site summary lives at /llms.txt
|
|
24
|
+
`;
|
|
25
|
+
return new Response(body, {
|
|
26
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=3600' },
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../lib/repo';
|
|
3
|
+
import { renderMarkdown } from '../lib/markdown';
|
|
4
|
+
import { publicTags } from '../lib/schema';
|
|
5
|
+
import { siteOrigin, xmlEscape } from '../lib/seo';
|
|
6
|
+
import { CONTENT, collectionUrl } from '../lib/content';
|
|
7
|
+
|
|
8
|
+
// Full-content RSS 2.0 feed of published posts. Body HTML ships in
|
|
9
|
+
// content:encoded; relative /media/ URLs are rewritten absolute so images
|
|
10
|
+
// render inside feed readers.
|
|
11
|
+
|
|
12
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
13
|
+
const repo = await getRepo(locals);
|
|
14
|
+
const [settings, posts, users] = await Promise.all([
|
|
15
|
+
repo.getSettings(),
|
|
16
|
+
repo.listPublishedPosts(),
|
|
17
|
+
repo.listUsers(),
|
|
18
|
+
]);
|
|
19
|
+
const origin = siteOrigin(settings, url);
|
|
20
|
+
const absolutize = (html: string) =>
|
|
21
|
+
html.replace(/(src|href)="\/(?!\/)/g, `$1="${origin}/`);
|
|
22
|
+
|
|
23
|
+
// the everything-feed: all collections in one channel; with several
|
|
24
|
+
// configured, each item carries its collection as an extra category
|
|
25
|
+
const multi = Object.keys(CONTENT.collections).length > 1;
|
|
26
|
+
const items = posts.slice(0, 20).map((p) => {
|
|
27
|
+
const link = `${origin}${collectionUrl(p.collection, p.slug)}`;
|
|
28
|
+
const author = users.find((u) => u.id === p.authorId);
|
|
29
|
+
const html = absolutize(p.bodyHtml || renderMarkdown(p.body));
|
|
30
|
+
return ` <item>
|
|
31
|
+
<title>${xmlEscape(p.title)}</title>
|
|
32
|
+
<link>${xmlEscape(link)}</link>
|
|
33
|
+
<guid isPermaLink="true">${xmlEscape(link)}</guid>
|
|
34
|
+
${p.publishedAt ? `<pubDate>${new Date(p.publishedAt).toUTCString()}</pubDate>` : ''}
|
|
35
|
+
${p.excerpt ? `<description>${xmlEscape(p.excerpt)}</description>` : ''}
|
|
36
|
+
${author ? `<dc:creator>${xmlEscape(author.name)}</dc:creator>` : ''}
|
|
37
|
+
${multi ? `<category>${xmlEscape(CONTENT.collections[p.collection]?.plural ?? p.collection)}</category>` : ''}${publicTags(p.tags).map((t) => `<category>${xmlEscape(t)}</category>`).join('')}
|
|
38
|
+
<content:encoded><![CDATA[${html.replace(/]]>/g, ']]]]><![CDATA[>')}]]></content:encoded>
|
|
39
|
+
</item>`;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
43
|
+
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
44
|
+
<channel>
|
|
45
|
+
<title>${xmlEscape(settings.siteTitle)}</title>
|
|
46
|
+
<link>${xmlEscape(origin)}/</link>
|
|
47
|
+
<description>${xmlEscape(settings.tagline || settings.siteTitle)}</description>
|
|
48
|
+
<language>en</language>
|
|
49
|
+
<atom:link href="${xmlEscape(origin)}/rss.xml" rel="self" type="application/rss+xml" />
|
|
50
|
+
${posts[0]?.publishedAt ? `<lastBuildDate>${new Date(posts[0].publishedAt).toUTCString()}</lastBuildDate>` : ''}
|
|
51
|
+
${items.join('\n')}
|
|
52
|
+
</channel>
|
|
53
|
+
</rss>
|
|
54
|
+
`;
|
|
55
|
+
return new Response(xml, {
|
|
56
|
+
headers: { 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=900' },
|
|
57
|
+
});
|
|
58
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Site from '../layouts/Site.astro';
|
|
3
|
+
import { getRepo } from '../lib/repo';
|
|
4
|
+
import { formatDate } from '../lib/dates';
|
|
5
|
+
import { CONTENT, collectionUrl } from '../lib/content';
|
|
6
|
+
|
|
7
|
+
const repo = await getRepo(Astro.locals);
|
|
8
|
+
const q = (Astro.url.searchParams.get('q') ?? '').trim();
|
|
9
|
+
const results = q ? await repo.searchPublished(q) : { posts: [], pages: [] };
|
|
10
|
+
const settings = await repo.getSettings();
|
|
11
|
+
const total = results.posts.length + results.pages.length;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<Site title={q ? `Search: ${q}` : 'Search'} description={`Search ${settings.siteTitle}`} noindex={Boolean(q)}>
|
|
15
|
+
<header class="search-hero">
|
|
16
|
+
<h1>Search</h1>
|
|
17
|
+
<form class="search-form" action="/search" method="get" role="search">
|
|
18
|
+
<input
|
|
19
|
+
type="search"
|
|
20
|
+
name="q"
|
|
21
|
+
value={q}
|
|
22
|
+
placeholder={`Search ${CONTENT.posts.plural.toLowerCase()} and pages…`}
|
|
23
|
+
aria-label="Search"
|
|
24
|
+
autofocus={!q}
|
|
25
|
+
/>
|
|
26
|
+
<button type="submit">Search</button>
|
|
27
|
+
</form>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
{q && (
|
|
31
|
+
<p class="search-count">
|
|
32
|
+
{total === 0 ? `Nothing found for “${q}”.` : `${total} ${total === 1 ? 'result' : 'results'} for “${q}”`}
|
|
33
|
+
</p>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
{results.posts.length > 0 && (
|
|
37
|
+
<ul class="post-list">
|
|
38
|
+
{results.posts.map((p) => (
|
|
39
|
+
<li class="post-item">
|
|
40
|
+
{p.cover && (
|
|
41
|
+
<a class="post-thumb" href={collectionUrl(p.collection, p.slug)}><img src={p.cover} alt={p.coverAlt} loading="lazy" /></a>
|
|
42
|
+
)}
|
|
43
|
+
<div>
|
|
44
|
+
<a href={collectionUrl(p.collection, p.slug)}>{p.title}</a>
|
|
45
|
+
<div class="post-meta">
|
|
46
|
+
{p.publishedAt && <time datetime={new Date(p.publishedAt).toISOString()}>{formatDate(p.publishedAt, settings)}</time>}
|
|
47
|
+
</div>
|
|
48
|
+
{p.excerpt && <p class="post-excerpt">{p.excerpt}</p>}
|
|
49
|
+
</div>
|
|
50
|
+
</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{results.pages.length > 0 && (
|
|
56
|
+
<section class="search-pages">
|
|
57
|
+
<h2>Pages</h2>
|
|
58
|
+
<ul class="post-list">
|
|
59
|
+
{results.pages.map((p) => (
|
|
60
|
+
<li class="post-item">
|
|
61
|
+
<div><a href={p.isHome ? '/' : `/${p.slug}`}>{p.title}</a></div>
|
|
62
|
+
</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
</section>
|
|
66
|
+
)}
|
|
67
|
+
</Site>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
.search-hero { margin: 24px 0 8px; }
|
|
71
|
+
.search-form { display: flex; gap: 10px; margin: 18px 0; }
|
|
72
|
+
.search-form input {
|
|
73
|
+
flex: 1; font: inherit; padding: 10px 14px; border: 1px solid var(--line);
|
|
74
|
+
border-radius: 9px; background: var(--panel); color: var(--ink);
|
|
75
|
+
}
|
|
76
|
+
.search-form input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
|
|
77
|
+
.search-form button {
|
|
78
|
+
font: inherit; font-weight: 600; padding: 10px 20px; border: 0; border-radius: 9px;
|
|
79
|
+
background: var(--accent); color: #fff; cursor: pointer;
|
|
80
|
+
}
|
|
81
|
+
.search-count { color: var(--muted); }
|
|
82
|
+
.search-pages h2 { font-size: 1.1rem; margin-top: 2rem; }
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../lib/repo';
|
|
3
|
+
import { isInternalTag } from '../lib/schema';
|
|
4
|
+
import { siteOrigin, xmlEscape } from '../lib/seo';
|
|
5
|
+
import { CONTENT, collectionUrl, tagUrl } from '../lib/content';
|
|
6
|
+
|
|
7
|
+
// DB-backed sitemap: content lives in D1/libSQL, so this is generated per
|
|
8
|
+
// request instead of at build time. Draft/scheduled content never appears.
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
11
|
+
const repo = await getRepo(locals);
|
|
12
|
+
const [settings, pages, posts, tags] = await Promise.all([
|
|
13
|
+
repo.getSettings(),
|
|
14
|
+
repo.listPages(),
|
|
15
|
+
repo.listPublishedPosts(),
|
|
16
|
+
repo.listTags(),
|
|
17
|
+
]);
|
|
18
|
+
const origin = siteOrigin(settings, url);
|
|
19
|
+
|
|
20
|
+
const usedTags = new Set(posts.flatMap((p) => p.tags.map((t) => t.toLowerCase())));
|
|
21
|
+
// one index entry per configured collection, each dated by its own content
|
|
22
|
+
const entries: { loc: string; lastmod?: number }[] = [
|
|
23
|
+
{ loc: '/', lastmod: Math.max(0, ...posts.map((p) => p.updatedAt)) || undefined },
|
|
24
|
+
// non-default-locale home pages live at /<locale> (acms({ i18n }))
|
|
25
|
+
...pages
|
|
26
|
+
.filter((p) => p.status === 'published' && p.isHome && p.locale)
|
|
27
|
+
.map((p) => ({ loc: `/${p.locale}`, lastmod: p.updatedAt })),
|
|
28
|
+
...Object.entries(CONTENT.collections).map(([key, c]) => ({
|
|
29
|
+
loc: c.base,
|
|
30
|
+
lastmod: Math.max(0, ...posts.filter((p) => p.collection === key).map((p) => p.updatedAt)) || undefined,
|
|
31
|
+
})),
|
|
32
|
+
...pages
|
|
33
|
+
.filter((p) => p.status === 'published' && !p.isHome)
|
|
34
|
+
.map((p) => ({ loc: `/${p.slug}`, lastmod: p.updatedAt })),
|
|
35
|
+
...posts.map((p) => ({ loc: collectionUrl(p.collection, p.slug), lastmod: p.updatedAt })),
|
|
36
|
+
...tags
|
|
37
|
+
.filter((t) => usedTags.has(t.name.toLowerCase()) && !isInternalTag(t.name))
|
|
38
|
+
.map((t) => ({ loc: tagUrl(t.slug) })),
|
|
39
|
+
{ loc: '/links' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
43
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
44
|
+
${entries
|
|
45
|
+
.map(
|
|
46
|
+
(e) =>
|
|
47
|
+
` <url><loc>${xmlEscape(origin + e.loc)}</loc>${e.lastmod ? `<lastmod>${new Date(e.lastmod).toISOString()}</lastmod>` : ''}</url>`
|
|
48
|
+
)
|
|
49
|
+
.join('\n')}
|
|
50
|
+
</urlset>
|
|
51
|
+
`;
|
|
52
|
+
return new Response(xml, {
|
|
53
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8', 'Cache-Control': 'public, max-age=3600' },
|
|
54
|
+
});
|
|
55
|
+
};
|