Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
W
WebAgent
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
xuchentao
WebAgent
Commits
467e65cb
Commit
467e65cb
authored
Jul 21, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 重构官网模板与文章发布流程
parent
56b080e4
Changes
26
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
523 additions
and
58 deletions
+523
-58
TEMPLATE.md
astro-template/TEMPLATE.md
+8
-9
astro.config.mjs
astro-template/astro.config.mjs
+17
-1
ai-content-growth.md
astro-template/examples/articles/ai-content-growth.md
+35
-0
product-story-draft.md
astro-template/examples/articles/product-story-draft.md
+23
-0
website-seo-checklist.md
astro-template/examples/articles/website-seo-checklist.md
+39
-0
ai-content-growth.svg
astro-template/examples/images/ai-content-growth.svg
+1
-0
seo-checklist.svg
astro-template/examples/images/seo-checklist.svg
+1
-0
package.json
astro-template/package.json
+1
-0
[...id].astro
astro-template/src/pages/articles/[...id].astro
+2
-0
llms-full.txt.ts
astro-template/src/pages/llms-full.txt.ts
+2
-2
content.css
astro-template/src/styles/content.css
+1
-0
orchestrator.ts
backend/src/agent/orchestrator.ts
+2
-0
article-service.test.ts
backend/src/articles/article-service.test.ts
+29
-2
article-service.ts
backend/src/articles/article-service.ts
+93
-28
build-manager.ts
backend/src/build/build-manager.ts
+8
-0
domain-deployment-service.ts
backend/src/domains/domain-deployment-service.ts
+1
-0
schemas.ts
backend/src/schemas.ts
+5
-0
server.ts
backend/src/server.ts
+30
-3
create-site.ts
backend/src/sites/create-site.ts
+9
-3
site-repository.ts
backend/src/sites/site-repository.ts
+18
-0
site-version-service.ts
backend/src/sites/site-version-service.ts
+16
-0
App.tsx
frontend/src/App.tsx
+140
-8
api.ts
frontend/src/api.ts
+9
-1
styles.css
frontend/src/styles.css
+8
-1
pnpm-lock.yaml
pnpm-lock.yaml
+3
-0
index.ts
shared/src/index.ts
+22
-0
No files found.
astro-template/TEMPLATE.md
View file @
467e65cb
# 网站
修改
规范
# 网站规范
修改前必须读完本文档
。
改代码前读完本文
。
1.
`src/_platform/`
和
`src/content.config.ts`
是平台能力内核,禁止修改。
2.
网站资料和默认 TDK 在
`src/data/site.json`
;首页内容在
`src/data/home.json`
。
3.
文章由平台外部存储,禁止在源码中创建或修改文章文件。产品或服务放
`src/content/products/*.md`
。仅已发布内容公开。
4.
页面、布局、组件和样式可自由修改,但公开页面必须通过
`BaseLayout`
保留 TDK、canonical、robots、OG 和 Schema。
5.
图片只能放
`public/images/`
,链接和资源路径必须兼容
`SITE_BASE_PATH`
。
6.
不得修改依赖、Astro 配置、构建产物或 Git 文件,不得访问站点外目录。
7.
完成后检查改动文件并执行允许的 Git diff 检查;不得部署或提交。
1.
禁改
`src/_platform/`
、
`src/content.config.ts`
、Astro 配置、依赖、产物及 Git 文件。
2.
资料与 TDK 在
`src/data/site.json`
;首页内容在
`src/data/home.json`
。
3.
文章及图片由平台外存,禁止写入源码;正文图片用
`/images/articles/文件名`
。
4.
样式自由;公开页须用
`BaseLayout`
保留 SEO/GEO。
5.
页面图片放
`public/images/`
,路径兼容
`SITE_BASE_PATH`
。
6.
禁止访问站点外目录、部署或提交;完成后检查 diff。
astro-template/astro.config.mjs
View file @
467e65cb
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
({
output
:
"static"
,
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"
},
});
astro-template/examples/articles/ai-content-growth.md
0 → 100644
View file @
467e65cb
---
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 系统理解内容。
## 保持稳定更新
每月复查旧文章中的数据、链接和产品描述。持续更新比盲目增加低质量页面更有价值。
astro-template/examples/articles/product-story-draft.md
0 → 100644
View file @
467e65cb
---
title
:
如何把产品能力写成客户愿意读的故事
summary
:
一篇用于演示草稿状态、Markdown 编辑和图片插入的示例文章。
status
:
draft
author
:
品牌团队
tags
:
-
产品内容
-
品牌表达
updatedAt
:
2026-07-20
seo
:
noindex
:
true
---
客户真正关心的不是功能列表,而是产品能否解决他的具体问题。先描述使用场景,再解释方案如何工作,最后补充结果与证据。
## 推荐结构
1.
客户处于什么场景;
2.
原有方法为什么不够;
3.
产品如何完成关键任务;
4.
可以获得什么可验证的结果。
这是一篇草稿,不会出现在公开文章列表中。你可以在文章中心继续编辑并发布它。
astro-template/examples/articles/website-seo-checklist.md
0 → 100644
View file @
467e65cb
---
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。

## 基础文件
-
网站地图包含所有正式页面,不包含草稿和测试地址。
-
Robots 文件允许抓取正式内容,并指向网站地图。
-
Favicon、Canonical 和分享图片可以正常访问。
## 内容结构
-
每页只有一个清晰的主标题。
-
标题层级连续,链接文字能表达目标内容。
-
图片包含有意义的替代文本。
## GEO 信息
为组织、网站和文章输出 Schema 结构,同时维护
`llms.txt`
与
`llms-full.txt`
,让 AI 系统更容易获取站点的权威摘要与完整内容索引。
astro-template/examples/images/ai-content-growth.svg
0 → 100644
View file @
467e65cb
<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>
astro-template/examples/images/seo-checklist.svg
0 → 100644
View file @
467e65cb
<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>
astro-template/package.json
View file @
467e65cb
...
...
@@ -12,6 +12,7 @@
},
"dependencies"
:
{
"@astrojs/check"
:
"^0.9.4"
,
"@types/node"
:
"^22.15.3"
,
"astro"
:
"^5.7.0"
,
"typescript"
:
"^5.8.3"
}
...
...
astro-template/src/pages/articles/[...id].astro
View file @
467e65cb
...
...
@@ -3,6 +3,7 @@ import { render, type CollectionEntry } from "astro:content";
import
ContentLayout
from
"../../layouts/ContentLayout.astro"
;
import
{
entryPath
,
publishedArticles
}
from
"../../_platform/content/public-content"
;
import
{
articleSchema
,
breadcrumbSchema
}
from
"../../_platform/seo/schema"
;
import
{
withBase
}
from
"../../_platform/seo/metadata"
;
export
async
function
getStaticPaths
()
{
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 |
]}
>
<
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
">
{entry.data.publishedAt && <time datetime=
{
entry.data.publishedAt.toISOString()}>{entry.data.publishedAt.toLocaleDateString("zh-CN")}</time>
}
{entry.data.author && <span>
{
entry.data.author}</span>
}
...
...
astro-template/src/pages/llms-full.txt.ts
View file @
467e65cb
import
type
{
APIRoute
}
from
"astro"
;
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
;
...
...
@@ -11,7 +11,7 @@ export const GET: APIRoute = async () => {
];
const
sections
=
[
`#
${
site
.
name
}
\n\n>
${
site
.
description
}
`
];
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
}
`
;
if
(
sections
.
join
(
"
\n\n
"
).
length
+
section
.
length
>
MAX_CHARACTERS
)
break
;
sections
.
push
(
section
);
...
...
astro-template/src/styles/content.css
View file @
467e65cb
...
...
@@ -13,6 +13,7 @@
.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-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
h2
,
.prose
h3
{
margin
:
2em
0
.7em
;
line-height
:
1.3
;
}
.prose
p
,
.prose
ul
,
.prose
ol
{
margin
:
0
0
1.35em
;
}
...
...
backend/src/agent/orchestrator.ts
View file @
467e65cb
...
...
@@ -92,6 +92,7 @@ export class AgentOrchestrator {
dist
=
await
this
.
builds
.
build
(
workspace
,
"agent_"
+
runId
,
{
basePath
:
getPublicPreviewUrl
(
run
.
tenantId
,
run
.
siteId
),
indexable
:
false
,
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
:
"网站构建通过"
};
this
.
runs
.
transition
(
runId
,
"validating_build"
,
{
validation
:
buildValidation
});
...
...
@@ -107,6 +108,7 @@ export class AgentOrchestrator {
const
latest
=
this
.
requireRun
(
runId
);
const
summary
=
(
latest
.
summary
||
"更新网站"
).
replace
(
/
\s
+/g
,
" "
).
slice
(
0
,
120
);
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
,
{
status
:
"ready"
,
previewCommit
:
baseCommit
,
draftBaseCommit
:
baseCommit
,
draftUpdatedAt
:
new
Date
().
toISOString
(),
draftSummary
:
summary
,
...
...
backend/src/articles/article-service.test.ts
View file @
467e65cb
...
...
@@ -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
project
=
path
.
join
(
root
,
"project"
);
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"
);
await
mkdir
(
path
.
join
(
project
,
"src"
),
{
recursive
:
true
});
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
createdAt
:
now
,
updatedAt
:
now
,
};
let
buildArticlesDirectory
=
""
;
let
buildCalls
=
0
;
const
sites
=
{
get
:
async
()
=>
site
,
update
:
async
(
_tenantId
:
string
,
_siteId
:
string
,
values
:
Partial
<
SiteInfo
>
)
=>
{
...
...
@@ -35,9 +38,18 @@ test("article mutations use external content storage without creating Git-manage
getProjectPath
:
()
=>
project
,
getDraftPath
:
()
=>
path
.
join
(
root
,
"draft"
),
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
;
const
builds
=
{
build
:
async
(
_workspace
:
string
,
_taskId
:
string
,
context
:
SiteBuildContext
)
=>
{
buildCalls
+=
1
;
buildArticlesDirectory
=
context
.
articlesDirectory
||
""
;
assert
.
match
(
await
readFile
(
path
.
join
(
buildArticlesDirectory
,
"hello.md"
),
"utf8"
),
/title: Hello/
);
await
mkdir
(
dist
,
{
recursive
:
true
});
...
...
@@ -56,8 +68,23 @@ test("article mutations use external content storage without creating Git-manage
assert
.
equal
(
result
.
article
?.
slug
,
"hello"
);
assert
.
equal
(
await
exists
(
path
.
join
(
articles
,
"hello.md"
)),
true
);
assert
.
equal
(
await
exists
(
path
.
join
(
project
,
"src"
,
"content"
,
"articles"
)),
false
);
assert
.
notEqual
(
buildArticlesDirectory
,
articles
);
assert
.
match
(
buildArticlesDirectory
,
/
\.
articles-next-/
);
assert
.
equal
(
buildCalls
,
0
);
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
{
await
rm
(
root
,
{
recursive
:
true
,
force
:
true
});
}
...
...
backend/src/articles/article-service.ts
View file @
467e65cb
This diff is collapsed.
Click to expand it.
backend/src/build/build-manager.ts
View file @
467e65cb
...
...
@@ -13,6 +13,7 @@ export interface SiteBuildContext {
publicOrigin
?:
string
;
indexable
?:
boolean
;
articlesDirectory
?:
string
;
articleImagesDirectory
?:
string
;
}
export
class
BuildManager
{
...
...
@@ -26,6 +27,7 @@ export class BuildManager {
const
options
=
typeof
context
===
"string"
?
{
basePath
:
context
}
:
context
;
const
publicOrigin
=
(
options
.
publicOrigin
||
config
.
frontendOrigin
).
replace
(
/
\/
$/
,
""
);
if
(
options
.
articlesDirectory
)
await
mkdir
(
options
.
articlesDirectory
,
{
recursive
:
true
});
if
(
options
.
articleImagesDirectory
)
await
mkdir
(
options
.
articleImagesDirectory
,
{
recursive
:
true
});
try
{
const
install
=
await
execa
(
"pnpm"
,
[
"install"
,
"--store-dir"
,
runtimePaths
.
pnpmStore
,
"--prefer-offline"
],
{
cwd
:
projectPath
,
...
...
@@ -44,6 +46,12 @@ export class BuildManager {
},
});
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"
);
return
path
.
join
(
projectPath
,
"dist"
);
}
catch
(
error
)
{
...
...
backend/src/domains/domain-deployment-service.ts
View file @
467e65cb
...
...
@@ -46,6 +46,7 @@ export class DomainDeploymentService {
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
{
basePath
:
"/"
,
publicOrigin
:
primary
?
`https://
${
primary
.
hostname
}
`
:
undefined
,
indexable
:
true
,
articlesDirectory
:
this
.
sites
.
getArticlesPath
(
tenantId
,
siteId
),
articleImagesDirectory
:
this
.
sites
.
getArticleImagesPath
(
tenantId
,
siteId
),
});
await
this
.
builds
.
publishCustomDomain
(
tenantId
,
siteId
,
dist
);
await
this
.
domains
.
updateSite
(
tenantId
,
siteId
,
(
domain
)
=>
domain
.
ownershipStatus
===
"verified"
...
...
backend/src/schemas.ts
View file @
467e65cb
...
...
@@ -60,6 +60,11 @@ export const articleImportSchema = z.object({
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
({
hostname
:
z
.
string
().
trim
().
min
(
4
,
"请输入完整域名"
).
max
(
253
),
});
...
...
backend/src/server.ts
View file @
467e65cb
...
...
@@ -12,7 +12,7 @@ import { config, runtimePaths } from "./config.js";
import
{
GitManager
}
from
"./git/git-manager.js"
;
import
{
PreviewProcessManager
}
from
"./preview/preview-process-manager.js"
;
import
{
addDomainSchema
,
agentSettingsSchema
,
chatSchema
,
createSiteSchema
,
createTenantSchema
,
loginSchema
,
resetPasswordSchema
,
saveDraftSchema
,
addDomainSchema
,
agentSettingsSchema
,
articleImageUploadSchema
,
articleImportSchema
,
articleInputSchema
,
chatSchema
,
createSiteSchema
,
createTenantSchema
,
loginSchema
,
resetPasswordSchema
,
saveDraftSchema
,
tenantStatusSchema
,
updateDomainSchema
,
versionCommitSchema
,
}
from
"./schemas.js"
;
import
{
CreateSiteService
}
from
"./sites/create-site.js"
;
...
...
@@ -24,12 +24,13 @@ import { DomainRepository } from "./domains/domain-repository.js";
import
{
DomainRoutingConfig
}
from
"./domains/domain-routing-config.js"
;
import
{
DomainService
}
from
"./domains/domain-service.js"
;
import
{
createDomainProvider
}
from
"./domains/domain-provider.js"
;
import
{
ArticleService
}
from
"./articles/article-service.js"
;
declare
module
"fastify"
{
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"
]
});
const
auth
=
new
AuthRepository
();
...
...
@@ -48,6 +49,7 @@ const domainProvider = createDomainProvider();
const
domainDeployments
=
new
DomainDeploymentService
(
sites
,
domainRepository
,
git
,
builds
,
domainRouting
);
const
domainService
=
new
DomainService
(
sites
,
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
)
=>
{
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
void
domainDeployments
.
schedule
(
tenantId
,
request
.
params
.
siteId
,
result
.
commit
).
catch
((
error
)
=>
app
.
log
.
error
(
error
));
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
)
=>
{
const
tenantId
=
requireTenant
(
request
.
auth
);
await
sites
.
get
(
tenantId
,
request
.
params
.
siteId
);
...
...
@@ -252,7 +279,7 @@ await mkdir(runtimePaths.builds, { recursive: true });
for
(
let
site
of
await
sites
.
listAll
())
{
await
git
.
pruneWorktrees
(
sites
.
getProjectPath
(
site
.
tenantId
,
site
.
siteId
));
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
));
site
=
await
sites
.
get
(
site
.
tenantId
,
site
.
siteId
);
}
...
...
backend/src/sites/create-site.ts
View file @
467e65cb
...
...
@@ -28,10 +28,15 @@ export class CreateSiteService {
createdAt
:
now
,
updatedAt
:
now
,
};
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
);
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
});
const
siteDataPath
=
path
.
join
(
projectPath
,
"src/data/site.json"
);
const
siteData
=
JSON
.
parse
(
await
readFile
(
siteDataPath
,
"utf8"
))
as
{
...
...
@@ -57,12 +62,13 @@ export class CreateSiteService {
const
theme
=
(
await
readFile
(
themePath
,
"utf8"
)).
replaceAll
(
"#7028ff"
,
input
.
brandColor
.
toLowerCase
());
await
writeFile
(
themePath
,
theme
,
"utf8"
);
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
);
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
currentCommit
:
commit
});
const
published
=
await
this
.
builds
.
publishPreview
(
tenantId
,
siteId
,
dist
);
await
this
.
previews
.
start
(
tenantId
,
siteId
,
published
,
previewPort
);
await
this
.
sites
.
markArticlesCompiled
(
tenantId
,
siteId
);
await
this
.
builds
.
ensureProductionPlaceholder
(
tenantId
,
siteId
,
input
.
name
);
return
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
lastError
:
undefined
});
}
catch
(
error
)
{
...
...
backend/src/sites/site-repository.ts
View file @
467e65cb
...
...
@@ -40,6 +40,24 @@ export class SiteRepository {
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
{
return
path
.
join
(
this
.
getSiteRoot
(
tenantId
,
siteId
),
"metadata"
,
"site.json"
);
}
...
...
backend/src/sites/site-version-service.ts
View file @
467e65cb
import
crypto
from
"node:crypto"
;
import
path
from
"node:path"
;
import
{
readFile
}
from
"node:fs/promises"
;
import
type
{
DraftPreviewResult
,
PreviewVersionResult
,
PublishResult
,
SiteInfo
}
from
"@webagent/shared"
;
import
{
getPublicPreviewUrl
,
getPublicProductionUrl
,
runtimePaths
}
from
"../config.js"
;
import
{
BuildError
,
BuildManager
}
from
"../build/build-manager.js"
;
...
...
@@ -53,9 +54,11 @@ export class SiteVersionService {
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
{
basePath
:
getPublicPreviewUrl
(
tenantId
,
siteId
),
indexable
:
false
,
articlesDirectory
:
this
.
sites
.
getArticlesPath
(
tenantId
,
siteId
),
articleImagesDirectory
:
this
.
sites
.
getArticleImagesPath
(
tenantId
,
siteId
),
});
const
published
=
await
this
.
builds
.
publishPreview
(
tenantId
,
siteId
,
dist
);
await
this
.
previews
.
start
(
tenantId
,
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
markArticlesCompiled
(
tenantId
,
siteId
);
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
status
:
"ready"
,
previewCommit
:
site
.
draftBaseCommit
,
draftUpdatedAt
:
new
Date
().
toISOString
(),
...
...
@@ -85,6 +88,7 @@ export class SiteVersionService {
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
{
basePath
:
getPublicPreviewUrl
(
tenantId
,
siteId
),
indexable
:
false
,
articlesDirectory
:
this
.
sites
.
getArticlesPath
(
tenantId
,
siteId
),
articleImagesDirectory
:
this
.
sites
.
getArticleImagesPath
(
tenantId
,
siteId
),
});
const
draftCommit
=
await
this
.
git
.
hasChanges
(
workspace
)
?
await
this
.
git
.
commit
(
workspace
,
message
)
...
...
@@ -92,6 +96,7 @@ export class SiteVersionService {
const
commit
=
await
this
.
git
.
fastForward
(
project
,
draftCommit
,
site
.
draftBaseCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
tenantId
,
siteId
,
dist
);
await
this
.
previews
.
start
(
tenantId
,
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
markArticlesCompiled
(
tenantId
,
siteId
);
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
draftBaseCommit
:
undefined
,
draftUpdatedAt
:
undefined
,
draftSummary
:
undefined
,
...
...
@@ -133,10 +138,12 @@ export class SiteVersionService {
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
{
basePath
:
getPublicPreviewUrl
(
tenantId
,
siteId
),
indexable
:
false
,
articlesDirectory
:
this
.
sites
.
getArticlesPath
(
tenantId
,
siteId
),
articleImagesDirectory
:
this
.
sites
.
getArticleImagesPath
(
tenantId
,
siteId
),
});
const
commit
=
await
this
.
git
.
restoreAsCommit
(
project
,
targetCommit
);
const
published
=
await
this
.
builds
.
publishPreview
(
tenantId
,
siteId
,
dist
);
await
this
.
previews
.
start
(
tenantId
,
siteId
,
published
,
site
.
previewPort
);
await
this
.
sites
.
markArticlesCompiled
(
tenantId
,
siteId
);
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
status
:
"ready"
,
currentCommit
:
commit
,
previewCommit
:
commit
,
environmentVersion
:
4
,
lastError
:
undefined
,
...
...
@@ -153,6 +160,12 @@ export class SiteVersionService {
const
site
=
await
this
.
sites
.
get
(
tenantId
,
siteId
);
if
(
site
.
status
!==
"ready"
)
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
workspace
=
path
.
join
(
runtimePaths
.
builds
,
taskId
,
"workspace"
);
await
this
.
sites
.
update
(
tenantId
,
siteId
,
{
publishStatus
:
"publishing"
,
lastPublishError
:
undefined
});
...
...
@@ -164,6 +177,7 @@ export class SiteVersionService {
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
{
basePath
:
getPublicProductionUrl
(
tenantId
,
siteId
),
indexable
:
true
,
articlesDirectory
:
this
.
sites
.
getArticlesPath
(
tenantId
,
siteId
),
articleImagesDirectory
:
this
.
sites
.
getArticleImagesPath
(
tenantId
,
siteId
),
});
await
this
.
builds
.
publishProduction
(
tenantId
,
siteId
,
dist
);
const
publishedAt
=
new
Date
().
toISOString
();
...
...
@@ -199,9 +213,11 @@ export class SiteVersionService {
const
dist
=
await
this
.
builds
.
build
(
workspace
,
taskId
,
{
basePath
:
getPublicPreviewUrl
(
site
.
tenantId
,
site
.
siteId
),
indexable
:
false
,
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
);
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
,
{
...
metadata
,
status
:
"ready"
,
previewCommit
:
commit
,
environmentVersion
:
4
,
lastError
:
undefined
,
...
...
frontend/src/App.tsx
View file @
467e65cb
This diff is collapsed.
Click to expand it.
frontend/src/api.ts
View file @
467e65cb
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
>
{
const
headers
=
new
Headers
(
options
?.
headers
);
...
...
@@ -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
})
}),
rebuild
:
(
siteId
:
string
)
=>
request
<
{
previewUrl
:
string
}
>
(
"/api/sites/"
+
siteId
+
"/build"
,
{
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"
),
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"
}),
...
...
frontend/src/styles.css
View file @
467e65cb
This diff is collapsed.
Click to expand it.
pnpm-lock.yaml
View file @
467e65cb
...
...
@@ -17,6 +17,9 @@ importers:
'
@astrojs/check'
:
specifier
:
^0.9.4
version
:
0.9.9(prettier@3.9.5)(typescript@5.8.3)
'
@types/node'
:
specifier
:
^22.15.3
version
:
22.20.1
astro
:
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)
...
...
shared/src/index.ts
View file @
467e65cb
...
...
@@ -222,6 +222,7 @@ export interface ArticleInput {
export
interface
ArticleSummary
extends
Omit
<
ArticleInput
,
"body"
>
{
wordCount
:
number
;
pendingBuild
:
boolean
;
}
export
interface
ArticleDocument
extends
ArticleInput
{
...
...
@@ -231,12 +232,33 @@ export interface ArticleDocument extends ArticleInput {
export
interface
ArticleListResult
{
supported
:
boolean
;
articles
:
ArticleSummary
[];
pendingBuild
:
boolean
;
pendingCount
:
number
;
lastCompiledAt
?:
string
;
}
export
interface
ArticleMutationResult
{
article
?:
ArticleDocument
;
deletedSlug
?:
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"
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment