Commit 467e65cb authored by xuchentao's avatar xuchentao

feat: 重构官网模板与文章发布流程

parent 56b080e4
# 网站修改规范 # 网站规范
修改前必须读完本文档 改代码前读完本文
1. `src/_platform/``src/content.config.ts` 是平台能力内核,禁止修改。 1. 禁改 `src/_platform/``src/content.config.ts`、Astro 配置、依赖、产物及 Git 文件。
2. 网站资料和默认 TDK 在 `src/data/site.json`;首页内容在 `src/data/home.json` 2. 资料与 TDK 在 `src/data/site.json`;首页内容在 `src/data/home.json`
3. 文章由平台外部存储,禁止在源码中创建或修改文章文件。产品或服务放 `src/content/products/*.md`。仅已发布内容公开。 3. 文章及图片由平台外存,禁止写入源码;正文图片用 `/images/articles/文件名`
4. 页面、布局、组件和样式可自由修改,但公开页面必须通过 `BaseLayout` 保留 TDK、canonical、robots、OG 和 Schema。 4. 样式自由;公开页须用 `BaseLayout` 保留 SEO/GEO。
5. 图片只能放 `public/images/`,链接和资源路径必须兼容 `SITE_BASE_PATH` 5. 页面图片放 `public/images/`,路径兼容 `SITE_BASE_PATH`
6. 不得修改依赖、Astro 配置、构建产物或 Git 文件,不得访问站点外目录。 6. 禁止访问站点外目录、部署或提交;完成后检查 diff。
7. 完成后检查改动文件并执行允许的 Git diff 检查;不得部署或提交。
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
const configuredBase = process.env.SITE_BASE_PATH || "/";
const basePath = configuredBase === "/" ? "/" : `/${configuredBase.replace(/^\/+|\/+$/g, "")}/`;
function remarkArticleImages() {
return (tree) => {
const visit = (node) => {
if (node?.type === "image" && typeof node.url === "string" && node.url.startsWith("/images/articles/")) {
node.url = `${basePath}${node.url.replace(/^\/+/, "")}`.replace(/\/+/g, "/");
}
if (Array.isArray(node?.children)) node.children.forEach(visit);
};
visit(tree);
};
}
export default defineConfig({ export default defineConfig({
output: "static", output: "static",
site: process.env.SITE_PUBLIC_ORIGIN || "https://example.com", site: process.env.SITE_PUBLIC_ORIGIN || "https://example.com",
base: process.env.SITE_BASE_PATH || "/", base: configuredBase,
markdown: { remarkPlugins: [remarkArticleImages] },
server: { host: "127.0.0.1" }, server: { host: "127.0.0.1" },
}); });
---
title: 用 AI 建立可持续的官网内容增长机制
summary: 从选题、撰写到发布,建立一套兼顾品牌表达、SEO 与 GEO 的内容工作流。
status: published
author: 内容团队
cover: /images/articles/ai-content-growth.svg
tags:
- AI
- 内容增长
- GEO
publishedAt: 2026-07-18
updatedAt: 2026-07-18
seo:
title: AI 官网内容增长指南
description: 了解如何用 AI 建立稳定、可复用的官网内容生产与优化流程。
ogDescription: 一套兼顾品牌、SEO 与 GEO 的官网内容增长方法。
ogImage: /images/articles/ai-content-growth.svg
noindex: false
---
官网内容不应是一次性交付物,而应该成为持续积累的品牌资产。AI 最适合承担资料整理、结构生成和初稿优化,人仍然负责事实、判断与品牌语气。
![AI 内容增长工作流](/images/articles/ai-content-growth.svg)
## 从真实问题开始
优先整理销售、客服和项目交付中反复出现的问题。每篇文章解决一个明确问题,并给出可验证的结论、过程和下一步行动。
## 同时服务搜索与阅读
标题要说明主题,摘要要直接回答核心问题,正文使用清晰的层级。补充作者、发布日期、结构化数据和开放图谱信息,可以帮助搜索引擎与 AI 系统理解内容。
## 保持稳定更新
每月复查旧文章中的数据、链接和产品描述。持续更新比盲目增加低质量页面更有价值。
---
title: 如何把产品能力写成客户愿意读的故事
summary: 一篇用于演示草稿状态、Markdown 编辑和图片插入的示例文章。
status: draft
author: 品牌团队
tags:
- 产品内容
- 品牌表达
updatedAt: 2026-07-20
seo:
noindex: true
---
客户真正关心的不是功能列表,而是产品能否解决他的具体问题。先描述使用场景,再解释方案如何工作,最后补充结果与证据。
## 推荐结构
1. 客户处于什么场景;
2. 原有方法为什么不够;
3. 产品如何完成关键任务;
4. 可以获得什么可验证的结果。
这是一篇草稿,不会出现在公开文章列表中。你可以在文章中心继续编辑并发布它。
---
title: 企业官网上线前的 SEO 检查清单
summary: 用一份简洁清单检查 TDK、网站地图、Robots、结构化数据和分享摘要。
status: published
author: 官网运营团队
cover: /images/articles/seo-checklist.svg
tags:
- SEO
- 官网
- 上线检查
publishedAt: 2026-07-12
updatedAt: 2026-07-16
seo:
title: 企业官网 SEO 上线检查清单
description: 企业官网发布前必须检查的 SEO 与 GEO 基础配置。
ogDescription: 发布官网前,用这份清单完成 SEO 与 GEO 基础检查。
ogImage: /images/articles/seo-checklist.svg
noindex: false
---
网站上线前,先确认每个可索引页面都有独立、准确的标题和摘要。首页、产品页和文章页不应复用同一组 TDK。
![官网 SEO 检查清单](/images/articles/seo-checklist.svg)
## 基础文件
- 网站地图包含所有正式页面,不包含草稿和测试地址。
- Robots 文件允许抓取正式内容,并指向网站地图。
- Favicon、Canonical 和分享图片可以正常访问。
## 内容结构
- 每页只有一个清晰的主标题。
- 标题层级连续,链接文字能表达目标内容。
- 图片包含有意义的替代文本。
## GEO 信息
为组织、网站和文章输出 Schema 结构,同时维护 `llms.txt``llms-full.txt`,让 AI 系统更容易获取站点的权威摘要与完整内容索引。
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="t d"><title id="t">AI 内容增长</title><desc id="d">紫色渐变背景上的内容增长流程示意</desc><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#30108c"/><stop offset="1" stop-color="#7755ff"/></linearGradient></defs><rect width="1200" height="630" rx="36" fill="url(#g)"/><g fill="none" stroke="#fff" stroke-width="10" opacity=".9"><circle cx="260" cy="315" r="82"/><circle cx="600" cy="315" r="82"/><circle cx="940" cy="315" r="82"/><path d="M342 315h176m164 0h176"/></g><g fill="#fff" font-family="system-ui,sans-serif" text-anchor="middle"><text x="260" y="330" font-size="38">选题</text><text x="600" y="330" font-size="38">创作</text><text x="940" y="330" font-size="38">增长</text><text x="600" y="115" font-size="54" font-weight="700">AI 内容增长工作流</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="t d"><title id="t">SEO 上线检查</title><desc id="d">带有勾选项的搜索优化检查清单</desc><rect width="1200" height="630" rx="36" fill="#f2efff"/><rect x="255" y="70" width="690" height="490" rx="32" fill="#fff" stroke="#6f45df" stroke-width="8"/><g fill="none" stroke="#6f45df" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"><path d="m335 190 24 26 48-62m-72 151 24 26 48-62m-72 151 24 26 48-62"/><path d="M470 188h380M470 303h310M470 418h350"/></g><text x="600" y="125" fill="#2b175f" font-family="system-ui,sans-serif" font-size="45" font-weight="700" text-anchor="middle">官网 SEO 上线检查</text></svg>
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@types/node": "^22.15.3",
"astro": "^5.7.0", "astro": "^5.7.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
......
...@@ -3,6 +3,7 @@ import { render, type CollectionEntry } from "astro:content"; ...@@ -3,6 +3,7 @@ import { render, type CollectionEntry } from "astro:content";
import ContentLayout from "../../layouts/ContentLayout.astro"; import ContentLayout from "../../layouts/ContentLayout.astro";
import { entryPath, publishedArticles } from "../../_platform/content/public-content"; import { entryPath, publishedArticles } from "../../_platform/content/public-content";
import { articleSchema, breadcrumbSchema } from "../../_platform/seo/schema"; import { articleSchema, breadcrumbSchema } from "../../_platform/seo/schema";
import { withBase } from "../../_platform/seo/metadata";
export async function getStaticPaths() { export async function getStaticPaths() {
return (await publishedArticles()).map((entry) => ({ params: { id: entry.id.replace(/\.(md|mdx)$/i, "") }, props: { entry } })); return (await publishedArticles()).map((entry) => ({ params: { id: entry.id.replace(/\.(md|mdx)$/i, "") }, props: { entry } }));
...@@ -29,6 +30,7 @@ const description = entry.data.seo.ogDescription || entry.data.seo.description | ...@@ -29,6 +30,7 @@ const description = entry.data.seo.ogDescription || entry.data.seo.description |
]} ]}
> >
<h1>{entry.data.title}</h1> <h1>{entry.data.title}</h1>
{entry.data.cover && <img class="article-cover" src={withBase(entry.data.cover)} alt={entry.data.title} />}
<div class="content-meta"> <div class="content-meta">
{entry.data.publishedAt && <time datetime={entry.data.publishedAt.toISOString()}>{entry.data.publishedAt.toLocaleDateString("zh-CN")}</time>} {entry.data.publishedAt && <time datetime={entry.data.publishedAt.toISOString()}>{entry.data.publishedAt.toLocaleDateString("zh-CN")}</time>}
{entry.data.author && <span>{entry.data.author}</span>} {entry.data.author && <span>{entry.data.author}</span>}
......
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import { publishedArticles, publishedProducts } from "../_platform/content/public-content"; import { publishedArticles, publishedProducts } from "../_platform/content/public-content";
import { site } from "../_platform/seo/metadata"; import { absoluteUrl, site } from "../_platform/seo/metadata";
const MAX_CHARACTERS = 200_000; const MAX_CHARACTERS = 200_000;
...@@ -11,7 +11,7 @@ export const GET: APIRoute = async () => { ...@@ -11,7 +11,7 @@ export const GET: APIRoute = async () => {
]; ];
const sections = [`# ${site.name}\n\n> ${site.description}`]; const sections = [`# ${site.name}\n\n> ${site.description}`];
for (const entry of entries) { for (const entry of entries) {
const body = entry.body || entry.data.summary; const body = (entry.body || entry.data.summary).replace(/\]\((\/images\/articles\/[^)]+)\)/g, (_match, image: string) => `](${absoluteUrl(image)})`);
const section = `## ${entry.data.title}\n\n${entry.data.summary}\n\n${body}`; const section = `## ${entry.data.title}\n\n${entry.data.summary}\n\n${body}`;
if (sections.join("\n\n").length + section.length > MAX_CHARACTERS) break; if (sections.join("\n\n").length + section.length > MAX_CHARACTERS) break;
sections.push(section); sections.push(section);
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
.content-list p { margin: 0 0 16px; color: var(--color-text-secondary); line-height: 1.75; } .content-list p { margin: 0 0 16px; color: var(--color-text-secondary); line-height: 1.75; }
.content-list a { color: var(--color-primary); font-size: 14px; font-weight: 750; } .content-list a { color: var(--color-primary); font-size: 14px; font-weight: 750; }
.content-meta { display: flex; flex-wrap: wrap; gap: 10px 20px; margin: 0 0 42px; color: var(--color-text-muted); font-size: 13px; } .content-meta { display: flex; flex-wrap: wrap; gap: 10px 20px; margin: 0 0 42px; color: var(--color-text-muted); font-size: 13px; }
.article-cover { display: block; width: 100%; height: auto; margin: 0 0 28px; border-radius: 22px; }
.prose { font-size: 17px; line-height: 1.9; } .prose { font-size: 17px; line-height: 1.9; }
.prose h2, .prose h3 { margin: 2em 0 .7em; line-height: 1.3; } .prose h2, .prose h3 { margin: 2em 0 .7em; line-height: 1.3; }
.prose p, .prose ul, .prose ol { margin: 0 0 1.35em; } .prose p, .prose ul, .prose ol { margin: 0 0 1.35em; }
......
...@@ -92,6 +92,7 @@ export class AgentOrchestrator { ...@@ -92,6 +92,7 @@ export class AgentOrchestrator {
dist = await this.builds.build(workspace, "agent_" + runId, { dist = await this.builds.build(workspace, "agent_" + runId, {
basePath: getPublicPreviewUrl(run.tenantId, run.siteId), indexable: false, basePath: getPublicPreviewUrl(run.tenantId, run.siteId), indexable: false,
articlesDirectory: this.sites.getArticlesPath(run.tenantId, run.siteId), articlesDirectory: this.sites.getArticlesPath(run.tenantId, run.siteId),
articleImagesDirectory: this.sites.getArticleImagesPath(run.tenantId, run.siteId),
}); });
buildValidation = { passed: true, buildExitCode: 0, durationMs: Date.now() - buildStarted, output: "网站构建通过" }; buildValidation = { passed: true, buildExitCode: 0, durationMs: Date.now() - buildStarted, output: "网站构建通过" };
this.runs.transition(runId, "validating_build", { validation: buildValidation }); this.runs.transition(runId, "validating_build", { validation: buildValidation });
...@@ -107,6 +108,7 @@ export class AgentOrchestrator { ...@@ -107,6 +108,7 @@ export class AgentOrchestrator {
const latest = this.requireRun(runId); const latest = this.requireRun(runId);
const summary = (latest.summary || "更新网站").replace(/\s+/g, " ").slice(0, 120); const summary = (latest.summary || "更新网站").replace(/\s+/g, " ").slice(0, 120);
await this.builds.publishPreview(run.tenantId, run.siteId, dist); await this.builds.publishPreview(run.tenantId, run.siteId, dist);
await this.sites.markArticlesCompiled(run.tenantId, run.siteId);
await this.sites.update(run.tenantId, run.siteId, { await this.sites.update(run.tenantId, run.siteId, {
status: "ready", previewCommit: baseCommit, status: "ready", previewCommit: baseCommit,
draftBaseCommit: baseCommit, draftUpdatedAt: new Date().toISOString(), draftSummary: summary, draftBaseCommit: baseCommit, draftUpdatedAt: new Date().toISOString(), draftSummary: summary,
......
...@@ -15,6 +15,8 @@ test("article mutations use external content storage without creating Git-manage ...@@ -15,6 +15,8 @@ test("article mutations use external content storage without creating Git-manage
const root = await mkdtemp(path.join(os.tmpdir(), "webagent-articles-")); const root = await mkdtemp(path.join(os.tmpdir(), "webagent-articles-"));
const project = path.join(root, "project"); const project = path.join(root, "project");
const articles = path.join(root, "content", "articles"); const articles = path.join(root, "content", "articles");
const images = path.join(root, "content", "images");
const articleState = path.join(root, "metadata", "articles.json");
const dist = path.join(root, "dist"); const dist = path.join(root, "dist");
await mkdir(path.join(project, "src"), { recursive: true }); await mkdir(path.join(project, "src"), { recursive: true });
await writeFile(path.join(project, "src", "content.config.ts"), "export const collections = {};\n", "utf8"); await writeFile(path.join(project, "src", "content.config.ts"), "export const collections = {};\n", "utf8");
...@@ -26,6 +28,7 @@ test("article mutations use external content storage without creating Git-manage ...@@ -26,6 +28,7 @@ test("article mutations use external content storage without creating Git-manage
createdAt: now, updatedAt: now, createdAt: now, updatedAt: now,
}; };
let buildArticlesDirectory = ""; let buildArticlesDirectory = "";
let buildCalls = 0;
const sites = { const sites = {
get: async () => site, get: async () => site,
update: async (_tenantId: string, _siteId: string, values: Partial<SiteInfo>) => { update: async (_tenantId: string, _siteId: string, values: Partial<SiteInfo>) => {
...@@ -35,9 +38,18 @@ test("article mutations use external content storage without creating Git-manage ...@@ -35,9 +38,18 @@ test("article mutations use external content storage without creating Git-manage
getProjectPath: () => project, getProjectPath: () => project,
getDraftPath: () => path.join(root, "draft"), getDraftPath: () => path.join(root, "draft"),
getArticlesPath: () => articles, getArticlesPath: () => articles,
getArticleImagesPath: () => images,
getArticleStatePath: () => articleState,
markArticlesCompiled: async () => {
const compiledAt = new Date().toISOString();
await mkdir(path.dirname(articleState), { recursive: true });
await writeFile(articleState, JSON.stringify({ pendingSlugs: [], lastCompiledAt: compiledAt }), "utf8");
return compiledAt;
},
} as unknown as SiteRepository; } as unknown as SiteRepository;
const builds = { const builds = {
build: async (_workspace: string, _taskId: string, context: SiteBuildContext) => { build: async (_workspace: string, _taskId: string, context: SiteBuildContext) => {
buildCalls += 1;
buildArticlesDirectory = context.articlesDirectory || ""; buildArticlesDirectory = context.articlesDirectory || "";
assert.match(await readFile(path.join(buildArticlesDirectory, "hello.md"), "utf8"), /title: Hello/); assert.match(await readFile(path.join(buildArticlesDirectory, "hello.md"), "utf8"), /title: Hello/);
await mkdir(dist, { recursive: true }); await mkdir(dist, { recursive: true });
...@@ -56,8 +68,23 @@ test("article mutations use external content storage without creating Git-manage ...@@ -56,8 +68,23 @@ test("article mutations use external content storage without creating Git-manage
assert.equal(result.article?.slug, "hello"); assert.equal(result.article?.slug, "hello");
assert.equal(await exists(path.join(articles, "hello.md")), true); assert.equal(await exists(path.join(articles, "hello.md")), true);
assert.equal(await exists(path.join(project, "src", "content", "articles")), false); assert.equal(await exists(path.join(project, "src", "content", "articles")), false);
assert.notEqual(buildArticlesDirectory, articles); assert.equal(buildCalls, 0);
assert.match(buildArticlesDirectory, /\.articles-next-/); assert.equal((await new ArticleService(sites, builds, previews).list("tenant_test", "site_test")).pendingBuild, true);
const published = await new ArticleService(sites, builds, previews).update("tenant_test", "site_test", "hello", { ...article, status: "published" });
assert.equal(published.article?.status, "published");
assert.match(await readFile(path.join(articles, "hello.md"), "utf8"), /status: published/);
assert.equal(buildCalls, 0);
await new ArticleService(sites, builds, previews).compilePreview("tenant_test", "site_test");
assert.equal(buildCalls, 1);
assert.equal(buildArticlesDirectory, articles);
assert.equal((await new ArticleService(sites, builds, previews).list("tenant_test", "site_test")).pendingBuild, false);
const uploaded = await new ArticleService(sites, builds, previews).uploadImage("tenant_test", "site_test", {
filename: "pixel.png",
dataUrl: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
});
assert.match(uploaded.path, /^\/images\/articles\/pixel-[a-f0-9]{10}\.png$/);
assert.equal(await exists(path.join(images, uploaded.filename)), true);
assert.equal(await exists(path.join(project, "public", "images", "articles", uploaded.filename)), false);
} finally { } finally {
await rm(root, { recursive: true, force: true }); await rm(root, { recursive: true, force: true });
} }
......
This diff is collapsed.
...@@ -13,6 +13,7 @@ export interface SiteBuildContext { ...@@ -13,6 +13,7 @@ export interface SiteBuildContext {
publicOrigin?: string; publicOrigin?: string;
indexable?: boolean; indexable?: boolean;
articlesDirectory?: string; articlesDirectory?: string;
articleImagesDirectory?: string;
} }
export class BuildManager { export class BuildManager {
...@@ -26,6 +27,7 @@ export class BuildManager { ...@@ -26,6 +27,7 @@ export class BuildManager {
const options = typeof context === "string" ? { basePath: context } : context; const options = typeof context === "string" ? { basePath: context } : context;
const publicOrigin = (options.publicOrigin || config.frontendOrigin).replace(/\/$/, ""); const publicOrigin = (options.publicOrigin || config.frontendOrigin).replace(/\/$/, "");
if (options.articlesDirectory) await mkdir(options.articlesDirectory, { recursive: true }); if (options.articlesDirectory) await mkdir(options.articlesDirectory, { recursive: true });
if (options.articleImagesDirectory) await mkdir(options.articleImagesDirectory, { recursive: true });
try { try {
const install = await execa("pnpm", ["install", "--store-dir", runtimePaths.pnpmStore, "--prefer-offline"], { const install = await execa("pnpm", ["install", "--store-dir", runtimePaths.pnpmStore, "--prefer-offline"], {
cwd: projectPath, cwd: projectPath,
...@@ -44,6 +46,12 @@ export class BuildManager { ...@@ -44,6 +46,12 @@ export class BuildManager {
}, },
}); });
output += build.stdout + "\n" + build.stderr; output += build.stdout + "\n" + build.stderr;
if (options.articleImagesDirectory) {
const target = path.join(projectPath, "dist", "images", "articles");
await rm(target, { recursive: true, force: true });
await mkdir(path.dirname(target), { recursive: true });
await cp(options.articleImagesDirectory, target, { recursive: true });
}
await writeFile(path.join(runtimePaths.logs, taskId + ".log"), output, "utf8"); await writeFile(path.join(runtimePaths.logs, taskId + ".log"), output, "utf8");
return path.join(projectPath, "dist"); return path.join(projectPath, "dist");
} catch (error) { } catch (error) {
......
...@@ -46,6 +46,7 @@ export class DomainDeploymentService { ...@@ -46,6 +46,7 @@ export class DomainDeploymentService {
const dist = await this.builds.build(workspace, taskId, { const dist = await this.builds.build(workspace, taskId, {
basePath: "/", publicOrigin: primary ? `https://${primary.hostname}` : undefined, indexable: true, basePath: "/", publicOrigin: primary ? `https://${primary.hostname}` : undefined, indexable: true,
articlesDirectory: this.sites.getArticlesPath(tenantId, siteId), articlesDirectory: this.sites.getArticlesPath(tenantId, siteId),
articleImagesDirectory: this.sites.getArticleImagesPath(tenantId, siteId),
}); });
await this.builds.publishCustomDomain(tenantId, siteId, dist); await this.builds.publishCustomDomain(tenantId, siteId, dist);
await this.domains.updateSite(tenantId, siteId, (domain) => domain.ownershipStatus === "verified" await this.domains.updateSite(tenantId, siteId, (domain) => domain.ownershipStatus === "verified"
......
...@@ -60,6 +60,11 @@ export const articleImportSchema = z.object({ ...@@ -60,6 +60,11 @@ export const articleImportSchema = z.object({
markdown: z.string().min(1).max(256 * 1024), markdown: z.string().min(1).max(256 * 1024),
}); });
export const articleImageUploadSchema = z.object({
filename: z.string().trim().min(1).max(180),
dataUrl: z.string().max(7 * 1024 * 1024).regex(/^data:image\/(?:png|jpeg|webp|gif);base64,/i, "仅支持 PNG、JPEG、WebP 或 GIF 图片"),
});
export const addDomainSchema = z.object({ export const addDomainSchema = z.object({
hostname: z.string().trim().min(4, "请输入完整域名").max(253), hostname: z.string().trim().min(4, "请输入完整域名").max(253),
}); });
......
...@@ -12,7 +12,7 @@ import { config, runtimePaths } from "./config.js"; ...@@ -12,7 +12,7 @@ import { config, runtimePaths } from "./config.js";
import { GitManager } from "./git/git-manager.js"; import { GitManager } from "./git/git-manager.js";
import { PreviewProcessManager } from "./preview/preview-process-manager.js"; import { PreviewProcessManager } from "./preview/preview-process-manager.js";
import { import {
addDomainSchema, agentSettingsSchema, chatSchema, createSiteSchema, createTenantSchema, loginSchema, resetPasswordSchema, saveDraftSchema, addDomainSchema, agentSettingsSchema, articleImageUploadSchema, articleImportSchema, articleInputSchema, chatSchema, createSiteSchema, createTenantSchema, loginSchema, resetPasswordSchema, saveDraftSchema,
tenantStatusSchema, updateDomainSchema, versionCommitSchema, tenantStatusSchema, updateDomainSchema, versionCommitSchema,
} from "./schemas.js"; } from "./schemas.js";
import { CreateSiteService } from "./sites/create-site.js"; import { CreateSiteService } from "./sites/create-site.js";
...@@ -24,12 +24,13 @@ import { DomainRepository } from "./domains/domain-repository.js"; ...@@ -24,12 +24,13 @@ import { DomainRepository } from "./domains/domain-repository.js";
import { DomainRoutingConfig } from "./domains/domain-routing-config.js"; import { DomainRoutingConfig } from "./domains/domain-routing-config.js";
import { DomainService } from "./domains/domain-service.js"; import { DomainService } from "./domains/domain-service.js";
import { createDomainProvider } from "./domains/domain-provider.js"; import { createDomainProvider } from "./domains/domain-provider.js";
import { ArticleService } from "./articles/article-service.js";
declare module "fastify" { declare module "fastify" {
interface FastifyRequest { auth?: SessionInfo } interface FastifyRequest { auth?: SessionInfo }
} }
const app = Fastify({ logger: { level: process.env.LOG_LEVEL || "info" }, bodyLimit: 1024 * 1024 }); const app = Fastify({ logger: { level: process.env.LOG_LEVEL || "info" }, bodyLimit: 8 * 1024 * 1024 });
await app.register(cors, { origin: [config.frontendOrigin, "http://127.0.0.1:5173"] }); await app.register(cors, { origin: [config.frontendOrigin, "http://127.0.0.1:5173"] });
const auth = new AuthRepository(); const auth = new AuthRepository();
...@@ -48,6 +49,7 @@ const domainProvider = createDomainProvider(); ...@@ -48,6 +49,7 @@ const domainProvider = createDomainProvider();
const domainDeployments = new DomainDeploymentService(sites, domainRepository, git, builds, domainRouting); const domainDeployments = new DomainDeploymentService(sites, domainRepository, git, builds, domainRouting);
const domainService = new DomainService(sites, domainRepository, domainRouting, domainProvider); const domainService = new DomainService(sites, domainRepository, domainRouting, domainProvider);
const siteLifecycle = new SiteLifecycleService(sites, previews, domainRepository, domainRouting, domainProvider); const siteLifecycle = new SiteLifecycleService(sites, previews, domainRepository, domainRouting, domainProvider);
const articles = new ArticleService(sites, builds, previews);
app.addHook("onRequest", async (request, reply) => { app.addHook("onRequest", async (request, reply) => {
if (!request.url.startsWith("/api/") || request.url === "/api/health" || request.url === "/api/login") return; if (!request.url.startsWith("/api/") || request.url === "/api/health" || request.url === "/api/login") return;
...@@ -164,6 +166,31 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/publish", async (re ...@@ -164,6 +166,31 @@ app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/publish", async (re
void domainDeployments.schedule(tenantId, request.params.siteId, result.commit).catch((error) => app.log.error(error)); void domainDeployments.schedule(tenantId, request.params.siteId, result.commit).catch((error) => app.log.error(error));
return result; return result;
}); });
app.get<{ Params: { siteId: string } }>("/api/sites/:siteId/articles", async (request) => (
articles.list(requireTenant(request.auth), request.params.siteId)
));
app.get<{ Params: { siteId: string; slug: string } }>("/api/sites/:siteId/articles/:slug", async (request) => (
articles.get(requireTenant(request.auth), request.params.siteId, request.params.slug)
));
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/articles", async (request, reply) => (
reply.code(201).send(await articles.create(requireTenant(request.auth), request.params.siteId, articleInputSchema.parse(request.body)))
));
app.put<{ Params: { siteId: string; slug: string } }>("/api/sites/:siteId/articles/:slug", async (request) => (
articles.update(requireTenant(request.auth), request.params.siteId, request.params.slug, articleInputSchema.parse(request.body))
));
app.delete<{ Params: { siteId: string; slug: string } }>("/api/sites/:siteId/articles/:slug", async (request) => (
articles.remove(requireTenant(request.auth), request.params.siteId, request.params.slug)
));
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/articles/import", async (request, reply) => {
const body = articleImportSchema.parse(request.body);
return reply.code(201).send(await articles.importMarkdown(requireTenant(request.auth), request.params.siteId, body.filename, body.markdown));
});
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/articles/compile", async (request) => (
articles.compilePreview(requireTenant(request.auth), request.params.siteId)
));
app.post<{ Params: { siteId: string } }>("/api/sites/:siteId/article-images", async (request, reply) => (
reply.code(201).send(await articles.uploadImage(requireTenant(request.auth), request.params.siteId, articleImageUploadSchema.parse(request.body)))
));
app.get<{ Params: { siteId: string } }>("/api/sites/:siteId/history", async (request) => { app.get<{ Params: { siteId: string } }>("/api/sites/:siteId/history", async (request) => {
const tenantId = requireTenant(request.auth); const tenantId = requireTenant(request.auth);
await sites.get(tenantId, request.params.siteId); await sites.get(tenantId, request.params.siteId);
...@@ -252,7 +279,7 @@ await mkdir(runtimePaths.builds, { recursive: true }); ...@@ -252,7 +279,7 @@ await mkdir(runtimePaths.builds, { recursive: true });
for (let site of await sites.listAll()) { for (let site of await sites.listAll()) {
await git.pruneWorktrees(sites.getProjectPath(site.tenantId, site.siteId)); await git.pruneWorktrees(sites.getProjectPath(site.tenantId, site.siteId));
await builds.ensureProductionPlaceholder(site.tenantId, site.siteId, site.name); await builds.ensureProductionPlaceholder(site.tenantId, site.siteId, site.name);
if (site.environmentVersion !== 3) { if (site.environmentVersion !== 4) {
await versions.rebuild(site.tenantId, site.siteId).catch((error) => app.log.warn(error)); await versions.rebuild(site.tenantId, site.siteId).catch((error) => app.log.warn(error));
site = await sites.get(site.tenantId, site.siteId); site = await sites.get(site.tenantId, site.siteId);
} }
......
...@@ -28,10 +28,15 @@ export class CreateSiteService { ...@@ -28,10 +28,15 @@ export class CreateSiteService {
createdAt: now, updatedAt: now, createdAt: now, updatedAt: now,
}; };
const articlesDirectory = this.sites.getArticlesPath(tenantId, siteId); const articlesDirectory = this.sites.getArticlesPath(tenantId, siteId);
await Promise.all([mkdir(projectPath, { recursive: true }), mkdir(articlesDirectory, { recursive: true })]); const articleImagesDirectory = this.sites.getArticleImagesPath(tenantId, siteId);
await Promise.all([mkdir(projectPath, { recursive: true }), mkdir(articlesDirectory, { recursive: true }), mkdir(articleImagesDirectory, { recursive: true })]);
await this.sites.save(site); await this.sites.save(site);
try { try {
await cp(config.templateDir, projectPath, { recursive: true, filter: (source) => !["node_modules", "dist", ".astro", ".git", "pnpm-lock.yaml"].includes(path.basename(source)) }); await cp(config.templateDir, projectPath, { recursive: true, filter: (source) => !["node_modules", "dist", ".astro", ".git", "pnpm-lock.yaml", "examples"].includes(path.basename(source)) });
await Promise.all([
cp(path.join(config.templateDir, "examples", "articles"), articlesDirectory, { recursive: true }),
cp(path.join(config.templateDir, "examples", "images"), articleImagesDirectory, { recursive: true }),
]);
await rm(path.join(projectPath, "src/data/company.json"), { force: true }); await rm(path.join(projectPath, "src/data/company.json"), { force: true });
const siteDataPath = path.join(projectPath, "src/data/site.json"); const siteDataPath = path.join(projectPath, "src/data/site.json");
const siteData = JSON.parse(await readFile(siteDataPath, "utf8")) as { const siteData = JSON.parse(await readFile(siteDataPath, "utf8")) as {
...@@ -57,12 +62,13 @@ export class CreateSiteService { ...@@ -57,12 +62,13 @@ export class CreateSiteService {
const theme = (await readFile(themePath, "utf8")).replaceAll("#7028ff", input.brandColor.toLowerCase()); const theme = (await readFile(themePath, "utf8")).replaceAll("#7028ff", input.brandColor.toLowerCase());
await writeFile(themePath, theme, "utf8"); await writeFile(themePath, theme, "utf8");
const dist = await this.builds.build(projectPath, "create_" + siteId, { const dist = await this.builds.build(projectPath, "create_" + siteId, {
basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false, articlesDirectory, basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false, articlesDirectory, articleImagesDirectory,
}); });
const commit = await this.git.init(projectPath); const commit = await this.git.init(projectPath);
await this.sites.update(tenantId, siteId, { currentCommit: commit }); await this.sites.update(tenantId, siteId, { currentCommit: commit });
const published = await this.builds.publishPreview(tenantId, siteId, dist); const published = await this.builds.publishPreview(tenantId, siteId, dist);
await this.previews.start(tenantId, siteId, published, previewPort); await this.previews.start(tenantId, siteId, published, previewPort);
await this.sites.markArticlesCompiled(tenantId, siteId);
await this.builds.ensureProductionPlaceholder(tenantId, siteId, input.name); await this.builds.ensureProductionPlaceholder(tenantId, siteId, input.name);
return await this.sites.update(tenantId, siteId, { status: "ready", currentCommit: commit, previewCommit: commit, lastError: undefined }); return await this.sites.update(tenantId, siteId, { status: "ready", currentCommit: commit, previewCommit: commit, lastError: undefined });
} catch (error) { } catch (error) {
......
...@@ -40,6 +40,24 @@ export class SiteRepository { ...@@ -40,6 +40,24 @@ export class SiteRepository {
return path.join(this.getSiteRoot(tenantId, siteId), "content", "articles"); return path.join(this.getSiteRoot(tenantId, siteId), "content", "articles");
} }
getArticleImagesPath(tenantId: string, siteId: string): string {
return path.join(this.getSiteRoot(tenantId, siteId), "content", "images");
}
getArticleStatePath(tenantId: string, siteId: string): string {
return path.join(this.getSiteRoot(tenantId, siteId), "metadata", "articles.json");
}
async markArticlesCompiled(tenantId: string, siteId: string): Promise<string> {
const destination = this.getArticleStatePath(tenantId, siteId);
const compiledAt = new Date().toISOString();
await mkdir(path.dirname(destination), { recursive: true });
const temporary = `${destination}.tmp`;
await writeFile(temporary, JSON.stringify({ pendingSlugs: [], lastCompiledAt: compiledAt }, null, 2) + "\n", "utf8");
await rename(temporary, destination);
return compiledAt;
}
getMetadataPath(tenantId: string, siteId: string): string { getMetadataPath(tenantId: string, siteId: string): string {
return path.join(this.getSiteRoot(tenantId, siteId), "metadata", "site.json"); return path.join(this.getSiteRoot(tenantId, siteId), "metadata", "site.json");
} }
......
import crypto from "node:crypto"; import crypto from "node:crypto";
import path from "node:path"; import path from "node:path";
import { readFile } from "node:fs/promises";
import type { DraftPreviewResult, PreviewVersionResult, PublishResult, SiteInfo } from "@webagent/shared"; import type { DraftPreviewResult, PreviewVersionResult, PublishResult, SiteInfo } from "@webagent/shared";
import { getPublicPreviewUrl, getPublicProductionUrl, runtimePaths } from "../config.js"; import { getPublicPreviewUrl, getPublicProductionUrl, runtimePaths } from "../config.js";
import { BuildError, BuildManager } from "../build/build-manager.js"; import { BuildError, BuildManager } from "../build/build-manager.js";
...@@ -53,9 +54,11 @@ export class SiteVersionService { ...@@ -53,9 +54,11 @@ export class SiteVersionService {
const dist = await this.builds.build(workspace, taskId, { const dist = await this.builds.build(workspace, taskId, {
basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false, basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false,
articlesDirectory: this.sites.getArticlesPath(tenantId, siteId), articlesDirectory: this.sites.getArticlesPath(tenantId, siteId),
articleImagesDirectory: this.sites.getArticleImagesPath(tenantId, siteId),
}); });
const published = await this.builds.publishPreview(tenantId, siteId, dist); const published = await this.builds.publishPreview(tenantId, siteId, dist);
await this.previews.start(tenantId, siteId, published, site.previewPort); await this.previews.start(tenantId, siteId, published, site.previewPort);
await this.sites.markArticlesCompiled(tenantId, siteId);
await this.sites.update(tenantId, siteId, { await this.sites.update(tenantId, siteId, {
status: "ready", previewCommit: site.draftBaseCommit, status: "ready", previewCommit: site.draftBaseCommit,
draftUpdatedAt: new Date().toISOString(), draftUpdatedAt: new Date().toISOString(),
...@@ -85,6 +88,7 @@ export class SiteVersionService { ...@@ -85,6 +88,7 @@ export class SiteVersionService {
const dist = await this.builds.build(workspace, taskId, { const dist = await this.builds.build(workspace, taskId, {
basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false, basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false,
articlesDirectory: this.sites.getArticlesPath(tenantId, siteId), articlesDirectory: this.sites.getArticlesPath(tenantId, siteId),
articleImagesDirectory: this.sites.getArticleImagesPath(tenantId, siteId),
}); });
const draftCommit = await this.git.hasChanges(workspace) const draftCommit = await this.git.hasChanges(workspace)
? await this.git.commit(workspace, message) ? await this.git.commit(workspace, message)
...@@ -92,6 +96,7 @@ export class SiteVersionService { ...@@ -92,6 +96,7 @@ export class SiteVersionService {
const commit = await this.git.fastForward(project, draftCommit, site.draftBaseCommit); const commit = await this.git.fastForward(project, draftCommit, site.draftBaseCommit);
const published = await this.builds.publishPreview(tenantId, siteId, dist); const published = await this.builds.publishPreview(tenantId, siteId, dist);
await this.previews.start(tenantId, siteId, published, site.previewPort); await this.previews.start(tenantId, siteId, published, site.previewPort);
await this.sites.markArticlesCompiled(tenantId, siteId);
await this.sites.update(tenantId, siteId, { await this.sites.update(tenantId, siteId, {
status: "ready", currentCommit: commit, previewCommit: commit, status: "ready", currentCommit: commit, previewCommit: commit,
draftBaseCommit: undefined, draftUpdatedAt: undefined, draftSummary: undefined, draftBaseCommit: undefined, draftUpdatedAt: undefined, draftSummary: undefined,
...@@ -133,10 +138,12 @@ export class SiteVersionService { ...@@ -133,10 +138,12 @@ export class SiteVersionService {
const dist = await this.builds.build(workspace, taskId, { const dist = await this.builds.build(workspace, taskId, {
basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false, basePath: getPublicPreviewUrl(tenantId, siteId), indexable: false,
articlesDirectory: this.sites.getArticlesPath(tenantId, siteId), articlesDirectory: this.sites.getArticlesPath(tenantId, siteId),
articleImagesDirectory: this.sites.getArticleImagesPath(tenantId, siteId),
}); });
const commit = await this.git.restoreAsCommit(project, targetCommit); const commit = await this.git.restoreAsCommit(project, targetCommit);
const published = await this.builds.publishPreview(tenantId, siteId, dist); const published = await this.builds.publishPreview(tenantId, siteId, dist);
await this.previews.start(tenantId, siteId, published, site.previewPort); await this.previews.start(tenantId, siteId, published, site.previewPort);
await this.sites.markArticlesCompiled(tenantId, siteId);
await this.sites.update(tenantId, siteId, { await this.sites.update(tenantId, siteId, {
status: "ready", currentCommit: commit, previewCommit: commit, status: "ready", currentCommit: commit, previewCommit: commit,
environmentVersion: 4, lastError: undefined, environmentVersion: 4, lastError: undefined,
...@@ -153,6 +160,12 @@ export class SiteVersionService { ...@@ -153,6 +160,12 @@ export class SiteVersionService {
const site = await this.sites.get(tenantId, siteId); const site = await this.sites.get(tenantId, siteId);
if (site.status !== "ready") throw new Error("测试环境尚未构建成功,不能发布到生产环境"); if (site.status !== "ready") throw new Error("测试环境尚未构建成功,不能发布到生产环境");
if (site.draftBaseCommit) throw new Error("存在未保存修改,请先在“版本”中保存或放弃草稿后再发布"); if (site.draftBaseCommit) throw new Error("存在未保存修改,请先在“版本”中保存或放弃草稿后再发布");
const articleState = await readFile(this.sites.getArticleStatePath(tenantId, siteId), "utf8")
.then((raw) => JSON.parse(raw) as { pendingSlugs?: unknown[] })
.catch(() => ({ pendingSlugs: [] }));
if (articleState.pendingSlugs?.length) {
throw Object.assign(new Error("存在尚未编译的文章修改,请先在“文章”中统一编译测试预览"), { statusCode: 409 });
}
const taskId = "publish_" + crypto.randomBytes(4).toString("hex"); const taskId = "publish_" + crypto.randomBytes(4).toString("hex");
const workspace = path.join(runtimePaths.builds, taskId, "workspace"); const workspace = path.join(runtimePaths.builds, taskId, "workspace");
await this.sites.update(tenantId, siteId, { publishStatus: "publishing", lastPublishError: undefined }); await this.sites.update(tenantId, siteId, { publishStatus: "publishing", lastPublishError: undefined });
...@@ -164,6 +177,7 @@ export class SiteVersionService { ...@@ -164,6 +177,7 @@ export class SiteVersionService {
const dist = await this.builds.build(workspace, taskId, { const dist = await this.builds.build(workspace, taskId, {
basePath: getPublicProductionUrl(tenantId, siteId), indexable: true, basePath: getPublicProductionUrl(tenantId, siteId), indexable: true,
articlesDirectory: this.sites.getArticlesPath(tenantId, siteId), articlesDirectory: this.sites.getArticlesPath(tenantId, siteId),
articleImagesDirectory: this.sites.getArticleImagesPath(tenantId, siteId),
}); });
await this.builds.publishProduction(tenantId, siteId, dist); await this.builds.publishProduction(tenantId, siteId, dist);
const publishedAt = new Date().toISOString(); const publishedAt = new Date().toISOString();
...@@ -199,9 +213,11 @@ export class SiteVersionService { ...@@ -199,9 +213,11 @@ export class SiteVersionService {
const dist = await this.builds.build(workspace, taskId, { const dist = await this.builds.build(workspace, taskId, {
basePath: getPublicPreviewUrl(site.tenantId, site.siteId), indexable: false, basePath: getPublicPreviewUrl(site.tenantId, site.siteId), indexable: false,
articlesDirectory: this.sites.getArticlesPath(site.tenantId, site.siteId), articlesDirectory: this.sites.getArticlesPath(site.tenantId, site.siteId),
articleImagesDirectory: this.sites.getArticleImagesPath(site.tenantId, site.siteId),
}); });
const published = await this.builds.publishPreview(site.tenantId, site.siteId, dist); const published = await this.builds.publishPreview(site.tenantId, site.siteId, dist);
await this.previews.start(site.tenantId, site.siteId, published, site.previewPort); await this.previews.start(site.tenantId, site.siteId, published, site.previewPort);
await this.sites.markArticlesCompiled(site.tenantId, site.siteId);
await this.sites.update(site.tenantId, site.siteId, { await this.sites.update(site.tenantId, site.siteId, {
...metadata, ...metadata,
status: "ready", previewCommit: commit, environmentVersion: 4, lastError: undefined, status: "ready", previewCommit: commit, environmentVersion: 4, lastError: undefined,
......
This diff is collapsed.
import type { AgentRunEvent, AgentRunInfo, AgentSettings, CreateSiteInput, CreateTenantInput, DomainBinding, DomainListResult, DraftPreviewResult, GitHistoryItem, PreviewVersionResult, PublishResult, SessionInfo, SiteInfo, TenantAdminInfo, TenantAdminSiteInfo } from "@webagent/shared"; import type { AgentRunEvent, AgentRunInfo, AgentSettings, ArticleCompileResult, ArticleDocument, ArticleImageUploadResult, ArticleInput, ArticleListResult, ArticleMutationResult, CreateSiteInput, CreateTenantInput, DomainBinding, DomainListResult, DraftPreviewResult, GitHistoryItem, PreviewVersionResult, PublishResult, SessionInfo, SiteInfo, TenantAdminInfo, TenantAdminSiteInfo } from "@webagent/shared";
async function request<T>(url: string, options?: RequestInit): Promise<T> { async function request<T>(url: string, options?: RequestInit): Promise<T> {
const headers = new Headers(options?.headers); const headers = new Headers(options?.headers);
...@@ -52,6 +52,14 @@ export const api = { ...@@ -52,6 +52,14 @@ export const api = {
restoreVersion: (siteId: string, commit: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/restore-version", { method: "POST", body: JSON.stringify({ commit }) }), restoreVersion: (siteId: string, commit: string) => request<PreviewVersionResult>("/api/sites/" + siteId + "/restore-version", { method: "POST", body: JSON.stringify({ commit }) }),
rebuild: (siteId: string) => request<{ previewUrl: string }>("/api/sites/" + siteId + "/build", { method: "POST" }), rebuild: (siteId: string) => request<{ previewUrl: string }>("/api/sites/" + siteId + "/build", { method: "POST" }),
publish: (siteId: string) => request<PublishResult>("/api/sites/" + siteId + "/publish", { method: "POST" }), publish: (siteId: string) => request<PublishResult>("/api/sites/" + siteId + "/publish", { method: "POST" }),
articles: (siteId: string) => request<ArticleListResult>(`/api/sites/${siteId}/articles`),
article: (siteId: string, slug: string) => request<ArticleDocument>(`/api/sites/${siteId}/articles/${slug}`),
createArticle: (siteId: string, input: ArticleInput) => request<ArticleMutationResult>(`/api/sites/${siteId}/articles`, { method: "POST", body: JSON.stringify(input) }),
updateArticle: (siteId: string, slug: string, input: ArticleInput) => request<ArticleMutationResult>(`/api/sites/${siteId}/articles/${slug}`, { method: "PUT", body: JSON.stringify(input) }),
deleteArticle: (siteId: string, slug: string) => request<ArticleMutationResult>(`/api/sites/${siteId}/articles/${slug}`, { method: "DELETE" }),
importArticle: (siteId: string, filename: string, markdown: string) => request<ArticleMutationResult>(`/api/sites/${siteId}/articles/import`, { method: "POST", body: JSON.stringify({ filename, markdown }) }),
compileArticles: (siteId: string) => request<ArticleCompileResult>(`/api/sites/${siteId}/articles/compile`, { method: "POST" }),
uploadArticleImage: (siteId: string, filename: string, dataUrl: string) => request<ArticleImageUploadResult>(`/api/sites/${siteId}/article-images`, { method: "POST", body: JSON.stringify({ filename, dataUrl }) }),
domains: (siteId: string) => request<DomainListResult>("/api/sites/" + siteId + "/domains"), domains: (siteId: string) => request<DomainListResult>("/api/sites/" + siteId + "/domains"),
addDomain: (siteId: string, hostname: string) => request<DomainBinding>("/api/sites/" + siteId + "/domains", { method: "POST", body: JSON.stringify({ hostname }) }), addDomain: (siteId: string, hostname: string) => request<DomainBinding>("/api/sites/" + siteId + "/domains", { method: "POST", body: JSON.stringify({ hostname }) }),
verifyDomain: (siteId: string, domainId: string) => request<DomainBinding>("/api/sites/" + siteId + "/domains/" + domainId + "/verify", { method: "POST" }), verifyDomain: (siteId: string, domainId: string) => request<DomainBinding>("/api/sites/" + siteId + "/domains/" + domainId + "/verify", { method: "POST" }),
......
This diff is collapsed.
...@@ -17,6 +17,9 @@ importers: ...@@ -17,6 +17,9 @@ importers:
'@astrojs/check': '@astrojs/check':
specifier: ^0.9.4 specifier: ^0.9.4
version: 0.9.9(prettier@3.9.5)(typescript@5.8.3) version: 0.9.9(prettier@3.9.5)(typescript@5.8.3)
'@types/node':
specifier: ^22.15.3
version: 22.20.1
astro: astro:
specifier: ^5.7.0 specifier: ^5.7.0
version: 5.18.2(@types/node@22.20.1)(rollup@4.62.2)(tsx@4.23.1)(typescript@5.8.3)(yaml@2.9.0) version: 5.18.2(@types/node@22.20.1)(rollup@4.62.2)(tsx@4.23.1)(typescript@5.8.3)(yaml@2.9.0)
......
...@@ -222,6 +222,7 @@ export interface ArticleInput { ...@@ -222,6 +222,7 @@ export interface ArticleInput {
export interface ArticleSummary extends Omit<ArticleInput, "body"> { export interface ArticleSummary extends Omit<ArticleInput, "body"> {
wordCount: number; wordCount: number;
pendingBuild: boolean;
} }
export interface ArticleDocument extends ArticleInput { export interface ArticleDocument extends ArticleInput {
...@@ -231,12 +232,33 @@ export interface ArticleDocument extends ArticleInput { ...@@ -231,12 +232,33 @@ export interface ArticleDocument extends ArticleInput {
export interface ArticleListResult { export interface ArticleListResult {
supported: boolean; supported: boolean;
articles: ArticleSummary[]; articles: ArticleSummary[];
pendingBuild: boolean;
pendingCount: number;
lastCompiledAt?: string;
} }
export interface ArticleMutationResult { export interface ArticleMutationResult {
article?: ArticleDocument; article?: ArticleDocument;
deletedSlug?: string; deletedSlug?: string;
previewUrl: string; previewUrl: string;
pendingBuild: boolean;
}
export interface ArticleCompileResult {
previewUrl: string;
compiledAt: string;
articleCount: number;
}
export interface ArticleImageUploadInput {
filename: string;
dataUrl: string;
}
export interface ArticleImageUploadResult {
path: string;
filename: string;
size: number;
} }
export type DomainOwnershipStatus = "pending" | "verified" | "failed"; export type DomainOwnershipStatus = "pending" | "verified" | "failed";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment