Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
S
sumeiqiao
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
sumeiqiao
Commits
bbf00c36
Commit
bbf00c36
authored
Aug 04, 2026
by
xuchentao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: complete website SEO and GEO support
parent
aceb0832
Pipeline
#454
passed with stage
in 15 seconds
Changes
16
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
286 additions
and
25 deletions
+286
-25
astro.config.mjs
astro.config.mjs
+1
-1
app.js
public/admin/app.js
+12
-0
style.css
public/admin/style.css
+3
-0
favicon.svg
public/favicon.svg
+5
-0
site.webmanifest
public/site.webmanifest
+9
-1
ServiceCard.astro
src/components/ServiceCard.astro
+1
-1
site.ts
src/data/site.ts
+32
-4
Base.astro
src/layouts/Base.astro
+19
-2
article-store.ts
src/lib/article-store.ts
+37
-7
index.astro
src/pages/admin/index.astro
+7
-1
[slug].astro
src/pages/articles/[slug].astro
+12
-3
index.astro
src/pages/index.astro
+2
-3
llms-full.txt.ts
src/pages/llms-full.txt.ts
+65
-0
llms.txt.ts
src/pages/llms.txt.ts
+10
-1
index.astro
src/pages/services/[slug]/index.astro
+69
-0
index.astro
src/pages/services/index.astro
+2
-1
No files found.
astro.config.mjs
View file @
bbf00c36
...
@@ -7,7 +7,7 @@ const customOutDir = process.env.SUMEIQIAO_BUILD_OUT_DIR;
...
@@ -7,7 +7,7 @@ const customOutDir = process.env.SUMEIQIAO_BUILD_OUT_DIR;
export
default
defineConfig
({
export
default
defineConfig
({
site
:
"https://www.sumeiqiao.com"
,
site
:
"https://www.sumeiqiao.com"
,
integrations
:
[
sitemap
()],
integrations
:
[
sitemap
(
{
filter
:
(
page
)
=>
!
new
URL
(
page
).
pathname
.
startsWith
(
"/admin/"
)
}
)],
adapter
:
node
({
mode
:
"standalone"
}),
adapter
:
node
({
mode
:
"standalone"
}),
...(
customOutDir
?
{
outDir
:
path
.
resolve
(
customOutDir
)
}
:
{}),
...(
customOutDir
?
{
outDir
:
path
.
resolve
(
customOutDir
)
}
:
{}),
build
:
{
format
:
"directory"
},
build
:
{
format
:
"directory"
},
...
...
public/admin/app.js
View file @
bbf00c36
...
@@ -441,12 +441,18 @@ async function openEditor(slug) {
...
@@ -441,12 +441,18 @@ async function openEditor(slug) {
const
article
=
await
api
(
`/articles/
${
slug
}
`
);
const
article
=
await
api
(
`/articles/
${
slug
}
`
);
$
(
"#edit-heading"
).
textContent
=
"编辑文章"
;
$
(
"#edit-heading"
).
textContent
=
"编辑文章"
;
$
(
"#title"
).
value
=
article
.
title
;
$
(
"#title"
).
value
=
article
.
title
;
$
(
"#seo-title"
).
value
=
article
.
seoTitle
||
""
;
$
(
"#seo-description"
).
value
=
article
.
seoDescription
||
""
;
$
(
"#seo-keywords"
).
value
=
article
.
seoKeywords
||
""
;
fillCategories
(
article
.
category
);
fillCategories
(
article
.
category
);
$
(
"#body"
).
value
=
article
.
body
;
$
(
"#body"
).
value
=
article
.
body
;
setEditorStatus
(
article
.
publishStatus
);
setEditorStatus
(
article
.
publishStatus
);
}
else
{
}
else
{
$
(
"#edit-heading"
).
textContent
=
"新建文章"
;
$
(
"#edit-heading"
).
textContent
=
"新建文章"
;
$
(
"#title"
).
value
=
""
;
$
(
"#title"
).
value
=
""
;
$
(
"#seo-title"
).
value
=
""
;
$
(
"#seo-description"
).
value
=
""
;
$
(
"#seo-keywords"
).
value
=
""
;
$
(
"#body"
).
value
=
""
;
$
(
"#body"
).
value
=
""
;
setEditorStatus
(
"new-unsaved"
);
setEditorStatus
(
"new-unsaved"
);
}
}
...
@@ -466,6 +472,9 @@ function payload() {
...
@@ -466,6 +472,9 @@ function payload() {
return
{
return
{
title
:
$
(
"#title"
).
value
.
trim
(),
title
:
$
(
"#title"
).
value
.
trim
(),
category
:
$
(
"#category"
).
value
,
category
:
$
(
"#category"
).
value
,
seoTitle
:
$
(
"#seo-title"
).
value
.
trim
(),
seoDescription
:
$
(
"#seo-description"
).
value
.
trim
(),
seoKeywords
:
$
(
"#seo-keywords"
).
value
.
trim
(),
body
:
$
(
"#body"
).
value
,
body
:
$
(
"#body"
).
value
,
};
};
}
}
...
@@ -558,6 +567,9 @@ function schedulePreview(delay = 350) {
...
@@ -558,6 +567,9 @@ function schedulePreview(delay = 350) {
}
}
$
(
"#title"
).
addEventListener
(
"input"
,
()
=>
{
dirty
=
true
;
});
$
(
"#title"
).
addEventListener
(
"input"
,
()
=>
{
dirty
=
true
;
});
$
(
"#seo-title"
).
addEventListener
(
"input"
,
()
=>
{
dirty
=
true
;
});
$
(
"#seo-description"
).
addEventListener
(
"input"
,
()
=>
{
dirty
=
true
;
});
$
(
"#seo-keywords"
).
addEventListener
(
"input"
,
()
=>
{
dirty
=
true
;
});
$
(
"#body"
).
addEventListener
(
"input"
,
()
=>
{
dirty
=
true
;
schedulePreview
();
});
$
(
"#body"
).
addEventListener
(
"input"
,
()
=>
{
dirty
=
true
;
schedulePreview
();
});
$
(
"#upload-button"
).
addEventListener
(
"click"
,
()
=>
$
(
"#file"
).
click
());
$
(
"#upload-button"
).
addEventListener
(
"click"
,
()
=>
$
(
"#file"
).
click
());
...
...
public/admin/style.css
View file @
bbf00c36
...
@@ -64,6 +64,9 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
...
@@ -64,6 +64,9 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
.field-panel
,
.editor-panel
{
padding
:
24px
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
16px
;
}
.field-panel
,
.editor-panel
{
padding
:
24px
;
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
16px
;
}
.field-panel
{
display
:
grid
;
gap
:
15px
;
}
.field-panel
{
display
:
grid
;
gap
:
15px
;
}
.field-panel
small
{
font-weight
:
400
;
opacity
:
.8
;
}
.field-panel
small
{
font-weight
:
400
;
opacity
:
.8
;
}
.panel-heading
{
display
:
flex
;
align-items
:
baseline
;
justify-content
:
space-between
;
gap
:
14px
;
}
.panel-heading
small
{
color
:
var
(
--muted
);
}
.field-panel
textarea
{
resize
:
vertical
;
line-height
:
1.6
;
}
.field-row
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
15px
;
}
.field-row
{
display
:
grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
15px
;
}
.custom-select
{
position
:
relative
;
}
.custom-select
{
position
:
relative
;
}
.select-trigger
{
width
:
100%
;
padding
:
0
13px
;
justify-content
:
space-between
;
color
:
var
(
--text
);
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
text-align
:
left
;
}
.select-trigger
{
width
:
100%
;
padding
:
0
13px
;
justify-content
:
space-between
;
color
:
var
(
--text
);
background
:
white
;
border
:
1px
solid
var
(
--border
);
border-radius
:
9px
;
text-align
:
left
;
}
...
...
public/favicon.svg
0 → 100644
View file @
bbf00c36
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 64 64"
>
<rect
width=
"64"
height=
"64"
rx=
"16"
fill=
"#16ae9c"
/>
<path
d=
"M32 10c-2.8 7.4-7.8 12.2-12.1 17.7C16.7 31.8 15 36.1 15 41c0 9.4 7.6 17 17 17s17-7.6 17-17c0-4.9-1.7-9.2-4.9-13.3C39.8 22.2 34.8 17.4 32 10Z"
fill=
"#fff"
/>
<path
d=
"M23 42c4.7 0 8.1-2 10.5-6.2 1.8 4.2 4.3 6.2 7.5 6.2-1.6 6.3-5.1 9.4-10.4 9.4-4.5 0-7-3.1-7.6-9.4Z"
fill=
"#0e727c"
opacity=
".72"
/>
</svg>
public/site.webmanifest
View file @
bbf00c36
...
@@ -5,5 +5,13 @@
...
@@ -5,5 +5,13 @@
"display": "standalone",
"display": "standalone",
"background_color": "#FAF8F4",
"background_color": "#FAF8F4",
"theme_color": "#17AE9C",
"theme_color": "#17AE9C",
"lang": "zh-CN"
"lang": "zh-CN",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
]
}
}
src/components/ServiceCard.astro
View file @
bbf00c36
...
@@ -23,5 +23,5 @@ const { service, link = true } = Astro.props;
...
@@ -23,5 +23,5 @@ const { service, link = true } = Astro.props;
<div><dt>适合人群</dt><dd>{service.audience}</dd></div>
<div><dt>适合人群</dt><dd>{service.audience}</dd></div>
<div><dt>核心方向</dt><dd>{service.value}</dd></div>
<div><dt>核心方向</dt><dd>{service.value}</dd></div>
</dl>
</dl>
{link && <a class="text-link" href={`/services/
#${service.slug}
`}>了解项目 <span>↗</span></a>}
{link && <a class="text-link" href={`/services/
${service.slug}/
`}>了解项目 <span>↗</span></a>}
</article>
</article>
src/data/site.ts
View file @
bbf00c36
...
@@ -7,6 +7,14 @@ export const site = {
...
@@ -7,6 +7,14 @@ export const site = {
tagline
:
"让世界体验中式经络调理,改善全民健康"
,
tagline
:
"让世界体验中式经络调理,改善全民健康"
,
industry
:
"美业 · 大健康 · 体龄管理 · 减肥塑形 · 中式调理"
,
industry
:
"美业 · 大健康 · 体龄管理 · 减肥塑形 · 中式调理"
,
},
},
seo
:
{
title
:
"塑美俏体龄管理|中式经络调理与科学减重塑形"
,
description
:
"塑美俏以中式经络调理为核心,提供体龄管理减重、排湿排酸、体型塑形与亚健康调理服务,并为合作门店提供持续运营支持。"
,
keywords
:
[
"塑美俏"
,
"体龄管理"
,
"中式经络调理"
,
"科学减重"
,
"体型塑形"
,
"亚健康调理"
],
image
:
"/og-cover.svg"
,
favicon
:
"/favicon.svg"
,
},
home
:
{
home
:
{
eyebrow
:
"中式经络体龄管理专家"
,
eyebrow
:
"中式经络体龄管理专家"
,
headline
:
"让身体回到轻盈、舒展的自然状态"
,
headline
:
"让身体回到轻盈、舒展的自然状态"
,
...
@@ -50,6 +58,11 @@ export const site = {
...
@@ -50,6 +58,11 @@ export const site = {
audience
:
"有减重与体质管理需求的人群"
,
audience
:
"有减重与体质管理需求的人群"
,
value
:
"饮食管理更轻松,兼顾体质与体重"
,
value
:
"饮食管理更轻松,兼顾体质与体重"
,
note
:
"除孕期外,具体适用情况需由门店专业人员评估。"
,
note
:
"除孕期外,具体适用情况需由门店专业人员评估。"
,
seo
:
{
title
:
"体龄管理减重|中式经络调理减重|塑美俏"
,
description
:
"了解塑美俏体龄管理减重服务:从个人体质与日常状态出发,通过中式经络调理制定兼顾体质、体重与长期习惯的周期方案。"
,
keywords
:
[
"体龄管理减重"
,
"中式经络调理减重"
,
"科学减重"
,
"塑美俏"
],
},
},
},
{
{
slug
:
"dampness-conditioning"
,
slug
:
"dampness-conditioning"
,
...
@@ -59,6 +72,11 @@ export const site = {
...
@@ -59,6 +72,11 @@ export const site = {
audience
:
"关注湿气与亚健康状态的人群"
,
audience
:
"关注湿气与亚健康状态的人群"
,
value
:
"改善身体内环境,为后续管理打基础"
,
value
:
"改善身体内环境,为后续管理打基础"
,
note
:
"属于日常健康管理服务,不替代医学检查与治疗。"
,
note
:
"属于日常健康管理服务,不替代医学检查与治疗。"
,
seo
:
{
title
:
"排湿排酸调理|中式经络日常健康管理|塑美俏"
,
description
:
"了解塑美俏排湿排酸调理服务:面向关注湿气、疲惫与日常亚健康状态的人群,通过中式经络调理改善身体状态。"
,
keywords
:
[
"排湿排酸调理"
,
"祛湿调理"
,
"中式经络调理"
,
"塑美俏"
],
},
},
},
{
{
slug
:
"body-shaping"
,
slug
:
"body-shaping"
,
...
@@ -68,6 +86,11 @@ export const site = {
...
@@ -68,6 +86,11 @@ export const site = {
audience
:
"有局部塑形或体态管理需求的人群"
,
audience
:
"有局部塑形或体态管理需求的人群"
,
value
:
"减重与塑形协同规划"
,
value
:
"减重与塑形协同规划"
,
note
:
"实际方案与体验因个人基础、周期和配合度而异。"
,
note
:
"实际方案与体验因个人基础、周期和配合度而异。"
,
seo
:
{
title
:
"体型塑形管理|减重与局部塑形方案|塑美俏"
,
description
:
"了解塑美俏体型塑形管理服务:在整体体重管理基础上关注局部线条、体态与身形协调,按个人状态制定周期方案。"
,
keywords
:
[
"体型塑形"
,
"局部塑形"
,
"体态管理"
,
"减重塑形"
,
"塑美俏"
],
},
},
},
{
{
slug
:
"wellness-conditioning"
,
slug
:
"wellness-conditioning"
,
...
@@ -77,6 +100,11 @@ export const site = {
...
@@ -77,6 +100,11 @@ export const site = {
audience
:
"有慢性亚健康管理需求的人群"
,
audience
:
"有慢性亚健康管理需求的人群"
,
value
:
"在体重管理同时关注多维健康指标"
,
value
:
"在体重管理同时关注多维健康指标"
,
note
:
"不用于诊断、治疗疾病;异常指标请及时就医。"
,
note
:
"不用于诊断、治疗疾病;异常指标请及时就医。"
,
seo
:
{
title
:
"亚健康调理|日常健康与经络调理|塑美俏"
,
description
:
"了解塑美俏亚健康调理服务:面向关注循环及多维健康指标的人群,提供以中式经络调理为核心的日常健康管理支持。"
,
keywords
:
[
"亚健康调理"
,
"日常健康管理"
,
"中式经络调理"
,
"体龄管理"
,
"塑美俏"
],
},
},
},
],
],
about
:
{
about
:
{
...
@@ -158,10 +186,10 @@ export const site = {
...
@@ -158,10 +186,10 @@ export const site = {
label
:
"体龄管理"
,
label
:
"体龄管理"
,
children
:
[
children
:
[
{
href
:
"/services/"
,
label
:
"服务总览"
},
{
href
:
"/services/"
,
label
:
"服务总览"
},
{
href
:
"/services/
#weight-management
"
,
label
:
"体龄管理减重"
},
{
href
:
"/services/
weight-management/
"
,
label
:
"体龄管理减重"
},
{
href
:
"/services/
#dampness-conditioning
"
,
label
:
"排湿排酸调理"
},
{
href
:
"/services/
dampness-conditioning/
"
,
label
:
"排湿排酸调理"
},
{
href
:
"/services/
#body-shaping
"
,
label
:
"体型塑形管理"
},
{
href
:
"/services/
body-shaping/
"
,
label
:
"体型塑形管理"
},
{
href
:
"/services/
#wellness-conditioning
"
,
label
:
"亚健康调理"
},
{
href
:
"/services/
wellness-conditioning/
"
,
label
:
"亚健康调理"
},
],
],
},
},
{
{
...
...
src/layouts/Base.astro
View file @
bbf00c36
...
@@ -7,8 +7,11 @@ import { site } from "../data/site";
...
@@ -7,8 +7,11 @@ import { site } from "../data/site";
interface Props {
interface Props {
title: string;
title: string;
description: string;
description: string;
keywords?: string | readonly string[];
image?: string;
image?: string;
ogType?: "website" | "article";
ogType?: "website" | "article";
publishedTime?: string;
modifiedTime?: string;
noindex?: boolean;
noindex?: boolean;
jsonLd?: Record
<string
,
unknown
>
| Record
<string
,
unknown
>
[];
jsonLd?: Record
<string
,
unknown
>
| Record
<string
,
unknown
>
[];
}
}
...
@@ -16,14 +19,18 @@ interface Props {
...
@@ -16,14 +19,18 @@ interface Props {
const {
const {
title,
title,
description,
description,
image = "/og-cover.svg",
keywords = site.seo.keywords,
image = site.seo.image,
ogType = "website",
ogType = "website",
publishedTime,
modifiedTime,
noindex = false,
noindex = false,
jsonLd = [],
jsonLd = [],
} = Astro.props;
} = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site).href;
const canonical = new URL(Astro.url.pathname, Astro.site).href;
const ogImage = new URL(image, Astro.site).href;
const ogImage = new URL(image, Astro.site).href;
const keywordContent = typeof keywords === "string" ? keywords : [...keywords].join(",");
const organizationLd = {
const organizationLd = {
"@context": "https://schema.org",
"@context": "https://schema.org",
"@type": ["HealthAndBeautyBusiness", "Organization"],
"@type": ["HealthAndBeautyBusiness", "Organization"],
...
@@ -56,10 +63,14 @@ const allLd = [organizationLd, ...pageLd.filter((item) => Object.keys(item).leng
...
@@ -56,10 +63,14 @@ const allLd = [organizationLd, ...pageLd.filter((item) => Object.keys(item).leng
<meta
name=
"color-scheme"
content=
"light"
/>
<meta
name=
"color-scheme"
content=
"light"
/>
<title>
{title}
</title>
<title>
{title}
</title>
<meta
name=
"description"
content=
{description}
/>
<meta
name=
"description"
content=
{description}
/>
<meta
name=
"keywords"
content=
{keywordContent}
/>
<meta
name=
"robots"
content=
{noindex
?
"
noindex
,
nofollow
"
:
"
index
,
follow
,
max-image-preview:large
,
max-snippet:-1
,
max-video-preview:-1
"}
/>
<meta
name=
"robots"
content=
{noindex
?
"
noindex
,
nofollow
"
:
"
index
,
follow
,
max-image-preview:large
,
max-snippet:-1
,
max-video-preview:-1
"}
/>
<link
rel=
"canonical"
href=
{canonical}
/>
<link
rel=
"canonical"
href=
{canonical}
/>
<link
rel=
"icon"
href=
{logo.src}
type=
"image/svg+xml"
/>
<link
rel=
"icon"
href=
{site.seo.favicon}
type=
"image/svg+xml"
/>
<link
rel=
"shortcut icon"
href=
{site.seo.favicon}
/>
<link
rel=
"apple-touch-icon"
href=
{site.seo.favicon}
/>
<link
rel=
"manifest"
href=
"/site.webmanifest"
/>
<link
rel=
"manifest"
href=
"/site.webmanifest"
/>
<link
rel=
"alternate"
type=
"text/plain"
href=
"/llms.txt"
title=
{`${site.brand.name}
AI
内容索引`}
/>
<link
rel=
"alternate"
hreflang=
"zh-CN"
href=
{canonical}
/>
<link
rel=
"alternate"
hreflang=
"zh-CN"
href=
{canonical}
/>
<link
rel=
"alternate"
hreflang=
"x-default"
href=
{canonical}
/>
<link
rel=
"alternate"
hreflang=
"x-default"
href=
{canonical}
/>
...
@@ -68,9 +79,15 @@ const allLd = [organizationLd, ...pageLd.filter((item) => Object.keys(item).leng
...
@@ -68,9 +79,15 @@ const allLd = [organizationLd, ...pageLd.filter((item) => Object.keys(item).leng
<meta
property=
"og:description"
content=
{description}
/>
<meta
property=
"og:description"
content=
{description}
/>
<meta
property=
"og:url"
content=
{canonical}
/>
<meta
property=
"og:url"
content=
{canonical}
/>
<meta
property=
"og:image"
content=
{ogImage}
/>
<meta
property=
"og:image"
content=
{ogImage}
/>
<meta
property=
"og:image:alt"
content=
{`${title}
-
${
site
.
brand
.
name
}`}
/>
<meta
property=
"og:site_name"
content=
{site.brand.name}
/>
<meta
property=
"og:site_name"
content=
{site.brand.name}
/>
<meta
property=
"og:locale"
content=
"zh_CN"
/>
<meta
property=
"og:locale"
content=
"zh_CN"
/>
{ogType === "article"
&&
publishedTime
&&
<meta
property=
"article:published_time"
content=
{publishedTime}
/>
}
{ogType === "article"
&&
modifiedTime
&&
<meta
property=
"article:modified_time"
content=
{modifiedTime}
/>
}
<meta
name=
"twitter:card"
content=
"summary_large_image"
/>
<meta
name=
"twitter:card"
content=
"summary_large_image"
/>
<meta
name=
"twitter:title"
content=
{title}
/>
<meta
name=
"twitter:description"
content=
{description}
/>
<meta
name=
"twitter:image"
content=
{ogImage}
/>
<script
type=
"application/ld+json"
set:html=
{JSON.stringify(allLd)}
is:inline
></script>
<script
type=
"application/ld+json"
set:html=
{JSON.stringify(allLd)}
is:inline
></script>
</head>
</head>
...
...
src/lib/article-store.ts
View file @
bbf00c36
...
@@ -31,6 +31,9 @@ export interface StoredArticle {
...
@@ -31,6 +31,9 @@ export interface StoredArticle {
category
:
string
;
category
:
string
;
author
:
string
;
author
:
string
;
excerpt
:
string
;
excerpt
:
string
;
seoTitle
:
string
;
seoDescription
:
string
;
seoKeywords
:
string
;
status
:
"draft"
|
"published"
;
status
:
"draft"
|
"published"
;
body
:
string
;
body
:
string
;
}
}
...
@@ -45,6 +48,9 @@ export interface Article {
...
@@ -45,6 +48,9 @@ export interface Article {
category
:
string
;
category
:
string
;
author
:
string
;
author
:
string
;
excerpt
:
string
;
excerpt
:
string
;
seoTitle
:
string
;
seoDescription
:
string
;
seoKeywords
:
string
;
status
:
"published"
;
status
:
"published"
;
};
};
body
:
string
;
body
:
string
;
...
@@ -89,6 +95,9 @@ const autoExcerpt = (body: string, fallback: string): string => {
...
@@ -89,6 +95,9 @@ const autoExcerpt = (body: string, fallback: string): string => {
return
text
.
length
>
120
?
`
${
text
.
slice
(
0
,
120
).
trim
()}
…`
:
text
;
return
text
.
length
>
120
?
`
${
text
.
slice
(
0
,
120
).
trim
()}
…`
:
text
;
};
};
const
autoSeoKeywords
=
(
category
:
string
):
string
=>
[
category
,
"体龄管理"
,
"中式经络调理"
,
"塑美俏"
].
filter
((
item
,
index
,
items
)
=>
item
&&
items
.
indexOf
(
item
)
===
index
).
join
(
","
);
function
serialize
(
data
:
StoredArticle
,
body
:
string
):
string
{
function
serialize
(
data
:
StoredArticle
,
body
:
string
):
string
{
const
quote
=
(
value
:
unknown
)
=>
JSON
.
stringify
(
value
==
null
?
""
:
String
(
value
));
const
quote
=
(
value
:
unknown
)
=>
JSON
.
stringify
(
value
==
null
?
""
:
String
(
value
));
return
[
return
[
...
@@ -100,6 +109,9 @@ function serialize(data: StoredArticle, body: string): string {
...
@@ -100,6 +109,9 @@ function serialize(data: StoredArticle, body: string): string {
`category:
${
quote
(
data
.
category
)}
`
,
`category:
${
quote
(
data
.
category
)}
`
,
`author:
${
quote
(
data
.
author
||
DEFAULT_AUTHOR
)}
`
,
`author:
${
quote
(
data
.
author
||
DEFAULT_AUTHOR
)}
`
,
`excerpt:
${
quote
(
data
.
excerpt
)}
`
,
`excerpt:
${
quote
(
data
.
excerpt
)}
`
,
`seoTitle:
${
quote
(
data
.
seoTitle
)}
`
,
`seoDescription:
${
quote
(
data
.
seoDescription
)}
`
,
`seoKeywords:
${
quote
(
data
.
seoKeywords
)}
`
,
`status:
${
quote
(
data
.
status
||
"draft"
)}
`
,
`status:
${
quote
(
data
.
status
||
"draft"
)}
`
,
"---"
,
"---"
,
""
,
""
,
...
@@ -171,14 +183,20 @@ export async function addCategory(value: unknown): Promise<string[]> {
...
@@ -171,14 +183,20 @@ export async function addCategory(value: unknown): Promise<string[]> {
export
async
function
readArticleFrom
(
directory
:
string
,
slug
:
string
):
Promise
<
StoredArticle
>
{
export
async
function
readArticleFrom
(
directory
:
string
,
slug
:
string
):
Promise
<
StoredArticle
>
{
const
raw
=
await
fs
.
readFile
(
path
.
join
(
directory
,
`
${
slug
}
.md`
),
"utf8"
);
const
raw
=
await
fs
.
readFile
(
path
.
join
(
directory
,
`
${
slug
}
.md`
),
"utf8"
);
const
{
data
,
content
}
=
matter
(
raw
);
const
{
data
,
content
}
=
matter
(
raw
);
const
title
=
String
(
data
.
title
||
""
);
const
category
=
String
(
data
.
category
||
""
);
const
excerpt
=
String
(
data
.
excerpt
||
autoExcerpt
(
content
,
title
));
return
{
return
{
slug
,
slug
,
title
:
String
(
data
.
title
||
""
)
,
title
,
date
:
fmtDate
(
data
.
date
),
date
:
fmtDate
(
data
.
date
),
updated
:
fmtDate
(
data
.
updated
||
data
.
date
),
updated
:
fmtDate
(
data
.
updated
||
data
.
date
),
category
:
String
(
data
.
category
||
""
)
,
category
,
author
:
String
(
data
.
author
||
DEFAULT_AUTHOR
),
author
:
String
(
data
.
author
||
DEFAULT_AUTHOR
),
excerpt
:
String
(
data
.
excerpt
||
""
),
excerpt
,
seoTitle
:
String
(
data
.
seoTitle
||
title
),
seoDescription
:
String
(
data
.
seoDescription
||
excerpt
),
seoKeywords
:
String
(
data
.
seoKeywords
||
autoSeoKeywords
(
category
)),
status
:
data
.
status
===
"draft"
?
"draft"
:
"published"
,
status
:
data
.
status
===
"draft"
?
"draft"
:
"published"
,
body
:
content
.
trim
(),
body
:
content
.
trim
(),
};
};
...
@@ -221,7 +239,7 @@ export async function createContentSnapshot(): Promise<ContentSnapshot> {
...
@@ -221,7 +239,7 @@ export async function createContentSnapshot(): Promise<ContentSnapshot> {
}
}
const
sameArticleContent
=
(
left
:
StoredArticle
,
right
:
StoredArticle
):
boolean
=>
const
sameArticleContent
=
(
left
:
StoredArticle
,
right
:
StoredArticle
):
boolean
=>
[
"slug"
,
"title"
,
"date"
,
"category"
,
"author"
,
"excerpt"
,
"body"
]
[
"slug"
,
"title"
,
"date"
,
"category"
,
"author"
,
"excerpt"
,
"
seoTitle"
,
"seoDescription"
,
"seoKeywords"
,
"
body"
]
.
every
((
key
)
=>
String
(
left
[
key
as
keyof
StoredArticle
]
??
""
)
===
String
(
right
[
key
as
keyof
StoredArticle
]
??
""
));
.
every
((
key
)
=>
String
(
left
[
key
as
keyof
StoredArticle
]
??
""
)
===
String
(
right
[
key
as
keyof
StoredArticle
]
??
""
));
export
async
function
getPublishStatus
(
article
:
StoredArticle
):
Promise
<
PublishStatus
>
{
export
async
function
getPublishStatus
(
article
:
StoredArticle
):
Promise
<
PublishStatus
>
{
...
@@ -247,14 +265,20 @@ export async function writeArticle(slug: string, input: Record<string, unknown>,
...
@@ -247,14 +265,20 @@ export async function writeArticle(slug: string, input: Record<string, unknown>,
const
existing
=
!
isNew
&&
await
fileExists
(
file
)
?
await
readWorkingArticle
(
slug
)
:
null
;
const
existing
=
!
isNew
&&
await
fileExists
(
file
)
?
await
readWorkingArticle
(
slug
)
:
null
;
const
categories
=
await
getCategoryNames
();
const
categories
=
await
getCategoryNames
();
const
body
=
String
(
input
.
body
||
""
).
trim
();
const
body
=
String
(
input
.
body
||
""
).
trim
();
const
title
=
String
(
input
.
title
||
""
).
trim
();
const
category
=
categories
.
includes
(
String
(
input
.
category
))
?
String
(
input
.
category
)
:
categories
[
0
];
const
excerpt
=
autoExcerpt
(
body
,
title
);
const
normalized
:
StoredArticle
=
{
const
normalized
:
StoredArticle
=
{
title
:
String
(
input
.
title
||
""
).
trim
()
,
title
,
slug
,
slug
,
date
:
existing
?.
date
||
fmtDate
(),
date
:
existing
?.
date
||
fmtDate
(),
updated
:
fmtDate
(),
updated
:
fmtDate
(),
category
:
categories
.
includes
(
String
(
input
.
category
))
?
String
(
input
.
category
)
:
categories
[
0
]
,
category
,
author
:
DEFAULT_AUTHOR
,
author
:
DEFAULT_AUTHOR
,
excerpt
:
autoExcerpt
(
body
,
String
(
input
.
title
||
""
)),
excerpt
,
seoTitle
:
String
(
input
.
seoTitle
??
existing
?.
seoTitle
??
""
).
trim
()
||
title
,
seoDescription
:
String
(
input
.
seoDescription
??
existing
?.
seoDescription
??
""
).
trim
()
||
excerpt
,
seoKeywords
:
String
(
input
.
seoKeywords
??
existing
?.
seoKeywords
??
""
).
trim
()
||
autoSeoKeywords
(
category
),
status
:
"draft"
,
status
:
"draft"
,
body
,
body
,
};
};
...
@@ -431,6 +455,9 @@ export async function getPublishedArticles(): Promise<Article[]> {
...
@@ -431,6 +455,9 @@ export async function getPublishedArticles(): Promise<Article[]> {
category
:
article
.
category
,
category
:
article
.
category
,
author
:
article
.
author
,
author
:
article
.
author
,
excerpt
:
article
.
excerpt
,
excerpt
:
article
.
excerpt
,
seoTitle
:
article
.
seoTitle
,
seoDescription
:
article
.
seoDescription
,
seoKeywords
:
article
.
seoKeywords
,
status
:
"published"
as
const
,
status
:
"published"
as
const
,
},
},
body
:
article
.
body
,
body
:
article
.
body
,
...
@@ -453,6 +480,9 @@ export async function getPublishedArticle(slug: string): Promise<(Article & { ht
...
@@ -453,6 +480,9 @@ export async function getPublishedArticle(slug: string): Promise<(Article & { ht
category
:
article
.
category
,
category
:
article
.
category
,
author
:
article
.
author
,
author
:
article
.
author
,
excerpt
:
article
.
excerpt
,
excerpt
:
article
.
excerpt
,
seoTitle
:
article
.
seoTitle
,
seoDescription
:
article
.
seoDescription
,
seoKeywords
:
article
.
seoKeywords
,
status
:
"published"
,
status
:
"published"
,
},
},
body
:
article
.
body
,
body
:
article
.
body
,
...
...
src/pages/admin/index.astro
View file @
bbf00c36
...
@@ -65,7 +65,13 @@
...
@@ -65,7 +65,13 @@
</div>
</div>
</div>
</div>
</div>
</div>
<p
class=
"auto-meta"
>
文章网址、摘要、作者和 SEO 信息将由系统自动生成,无需填写。
</p>
<p
class=
"auto-meta"
>
文章网址、摘要与作者由系统自动生成;SEO 留空时会自动使用文章标题、摘要和分类关键词。
</p>
</section>
<section
class=
"field-panel"
>
<div
class=
"panel-heading"
><strong>
文章 SEO(TDK)
</strong><small>
可选;用于搜索结果和社交分享摘要
</small></div>
<label>
SEO 标题(Title)
<input
id=
"seo-title"
maxlength=
"70"
placeholder=
"留空则使用文章标题"
/></label>
<label>
SEO 描述(Description)
<textarea
id=
"seo-description"
maxlength=
"180"
rows=
"3"
placeholder=
"留空则自动提取文章摘要"
></textarea></label>
<label>
SEO 关键词(Keywords)
<input
id=
"seo-keywords"
maxlength=
"200"
placeholder=
"多个关键词用英文逗号分隔"
/></label>
</section>
</section>
<section
class=
"editor-panel"
>
<section
class=
"editor-panel"
>
<div
class=
"editor-head"
><div><strong>
文章正文
</strong><small>
右侧会自动显示预览
</small></div><div><span
id=
"upload-status"
></span><button
id=
"upload-button"
class=
"ghost small"
type=
"button"
>
插入图片
</button></div></div>
<div
class=
"editor-head"
><div><strong>
文章正文
</strong><small>
右侧会自动显示预览
</small></div><div><span
id=
"upload-status"
></span><button
id=
"upload-button"
class=
"ghost small"
type=
"button"
>
插入图片
</button></div></div>
...
...
src/pages/articles/[slug].astro
View file @
bbf00c36
...
@@ -19,14 +19,15 @@ export async function getStaticPaths() {
...
@@ -19,14 +19,15 @@ export async function getStaticPaths() {
const
{
entry
}
=
Astro
.
props
as
{
entry
:
ArticleDetail
};
const
{
entry
}
=
Astro
.
props
as
{
entry
:
ArticleDetail
};
const
article
=
entry
.
data
;
const
article
=
entry
.
data
;
const
date
=
fmtDate
(
article
.
date
);
const
date
=
fmtDate
(
article
.
date
);
const
title
=
`${article.title}|健康资讯|${site.brand.name}`
;
const
title
=
article
.
seoTitle
===
article
.
title
?
`${article.title}|健康资讯|${site.brand.name}`
:
article
.
seoTitle
;
const
description
=
article
.
seoDescription
||
article
.
excerpt
;
const
pageUrl
=
new
URL
(
`/articles/${entry.id}/`
,
Astro
.
site
)
.
href
;
const
pageUrl
=
new
URL
(
`/articles/${entry.id}/`
,
Astro
.
site
)
.
href
;
const
jsonLd
=
[
const
jsonLd
=
[
{
{
"@context"
:
"https://schema.org"
,
"@context"
:
"https://schema.org"
,
"@type"
:
"Article"
,
"@type"
:
"Article"
,
headline
:
article
.
title
,
headline
:
article
.
title
,
description
:
article
.
excerpt
,
description
,
datePublished
:
date
,
datePublished
:
date
,
dateModified
:
fmtDate
(
article
.
updated
||
article
.
date
),
dateModified
:
fmtDate
(
article
.
updated
||
article
.
date
),
articleSection
:
article
.
category
,
articleSection
:
article
.
category
,
...
@@ -47,7 +48,15 @@ const jsonLd = [
...
@@ -47,7 +48,15 @@ const jsonLd = [
},
},
];
];
---
---
<
Base
title
=
{
title
}
description
=
{
article
.
excerpt
}
ogType
=
"article"
jsonLd
=
{
jsonLd
}
>
<
Base
title
=
{
title
}
description
=
{
description
}
keywords
=
{
article
.
seoKeywords
}
ogType
=
"article"
publishedTime
=
{
article
.
date
.
toISOString
()}
modifiedTime
=
{
article
.
updated
.
toISOString
()}
jsonLd
=
{
jsonLd
}
>
<
Header
/>
<
Header
/>
<
main
id
=
"main-content"
class
="
article
-
detail
-
page
">
<
main
id
=
"main-content"
class
="
article
-
detail
-
page
">
<article>
<article>
...
...
src/pages/index.astro
View file @
bbf00c36
...
@@ -7,8 +7,7 @@ import ServiceCard from "../components/ServiceCard.astro";
...
@@ -7,8 +7,7 @@ import ServiceCard from "../components/ServiceCard.astro";
import CTA from "../components/CTA.astro";
import CTA from "../components/CTA.astro";
import { site } from "../data/site";
import { site } from "../data/site";
const title = `${site.brand.name}|中式经络调理与体龄管理`;
const { title, description, keywords } = site.seo;
const description = site.home.subhead;
const jsonLd = [
const jsonLd = [
{
{
"@context": "https://schema.org",
"@context": "https://schema.org",
...
@@ -32,7 +31,7 @@ const jsonLd = [
...
@@ -32,7 +31,7 @@ const jsonLd = [
},
},
];
];
---
---
<Base title={title} description={description} jsonLd={jsonLd}>
<Base title={title} description={description}
keywords={keywords}
jsonLd={jsonLd}>
<Header />
<Header />
<main id="main-content">
<main id="main-content">
<section class="hero">
<section class="hero">
...
...
src/pages/llms-full.txt.ts
0 → 100644
View file @
bbf00c36
import
type
{
APIRoute
}
from
"astro"
;
import
{
site
}
from
"../data/site"
;
import
{
getPublishedArticles
}
from
"../lib/article-store"
;
export
const
GET
:
APIRoute
=
async
({
site
:
siteUrl
})
=>
{
const
base
=
(
siteUrl
?.
href
||
"https://www.sumeiqiao.com/"
).
replace
(
/
\/
$/
,
""
);
const
articles
=
await
getPublishedArticles
();
const
serviceSections
=
site
.
services
.
map
((
item
)
=>
`##
${
item
.
name
}
- 官方页面:
${
base
}
/services/
${
item
.
slug
}
/
- 服务简介:
${
item
.
intro
}
- 适合人群:
${
item
.
audience
}
- 核心方向:
${
item
.
value
}
- 服务边界:
${
item
.
note
}
`
).
join
(
"
\n\n
"
);
const
articleSections
=
articles
.
map
((
article
)
=>
`##
${
article
.
data
.
title
}
- 官方页面:
${
base
}
/articles/
${
article
.
id
}
/
- 分类:
${
article
.
data
.
category
}
- 作者:
${
article
.
data
.
author
}
- 发布日期:
${
article
.
data
.
date
.
toISOString
().
slice
(
0
,
10
)}
- 更新日期:
${
article
.
data
.
updated
.
toISOString
().
slice
(
0
,
10
)}
- 摘要:
${
article
.
data
.
seoDescription
}
${
article
.
body
}
`
).
join
(
"
\n\n
---
\n\n
"
);
const
text
=
`#
${
site
.
brand
.
name
}
完整内容
> 本文件供搜索引擎与 AI 系统读取塑美俏官网的公开、可引用内容。最后生成时间:
${
new
Date
().
toISOString
()}
## 官方主体
- 品牌:
${
site
.
brand
.
name
}
(
${
site
.
brand
.
englishName
}
)
- 运营主体:
${
site
.
brand
.
legalName
}
- 品牌主张:
${
site
.
brand
.
tagline
}
- 成立时间:
${
site
.
about
.
founded
}
- 行业:
${
site
.
brand
.
industry
}
- 地址:
${
site
.
contact
.
address
}
- 邮箱:
${
site
.
contact
.
email
}
- 官网:
${
base
}
/
# 服务内容
${
serviceSections
}
# 品牌介绍
${
site
.
about
.
story
.
join
(
"
\n\n
"
)}
${
site
.
about
.
advantages
.
map
((
item
)
=>
`##
${
item
.
title
}
\n\n
${
item
.
desc
}
`
).
join
(
"
\n\n
"
)}
# 常见问题
${
site
.
faq
.
map
((
item
)
=>
`##
${
item
.
q
}
\n\n
${
item
.
a
}
`
).
join
(
"
\n\n
"
)}
# 健康资讯全文
${
articleSections
||
"暂无已发布文章。"
}
# 内容边界
- 官网健康内容用于日常健康信息参考,不替代医疗诊断与治疗。
- 个人体验与经营结果会因实际情况而异,官网不作绝对效果或收益承诺。
- 引用时请保留原始官方页面链接,并以页面最新内容为准。
`
;
return
new
Response
(
text
,
{
headers
:
{
"Content-Type"
:
"text/plain; charset=utf-8"
}
});
};
src/pages/llms.txt.ts
View file @
bbf00c36
import
type
{
APIRoute
}
from
"astro"
;
import
type
{
APIRoute
}
from
"astro"
;
import
{
site
}
from
"../data/site"
;
import
{
site
}
from
"../data/site"
;
import
{
getPublishedArticles
}
from
"../lib/article-store"
;
export
const
GET
:
APIRoute
=
({
site
:
siteUrl
})
=>
{
export
const
GET
:
APIRoute
=
async
({
site
:
siteUrl
})
=>
{
const
base
=
(
siteUrl
?.
href
||
"https://www.sumeiqiao.com/"
).
replace
(
/
\/
$/
,
""
);
const
base
=
(
siteUrl
?.
href
||
"https://www.sumeiqiao.com/"
).
replace
(
/
\/
$/
,
""
);
const
articles
=
await
getPublishedArticles
();
const
services
=
site
.
services
.
map
((
item
)
=>
`-
${
item
.
name
}
:
${
item
.
intro
}
`
).
join
(
"
\n
"
);
const
services
=
site
.
services
.
map
((
item
)
=>
`-
${
item
.
name
}
:
${
item
.
intro
}
`
).
join
(
"
\n
"
);
const
advantages
=
site
.
about
.
advantages
.
map
((
item
)
=>
`-
${
item
.
title
}
:
${
item
.
desc
}
`
).
join
(
"
\n
"
);
const
advantages
=
site
.
about
.
advantages
.
map
((
item
)
=>
`-
${
item
.
title
}
:
${
item
.
desc
}
`
).
join
(
"
\n
"
);
const
faq
=
site
.
faq
.
map
((
item
)
=>
`- 问:
${
item
.
q
}
\n 答:
${
item
.
a
}
`
).
join
(
"
\n
"
);
const
faq
=
site
.
faq
.
map
((
item
)
=>
`- 问:
${
item
.
q
}
\n 答:
${
item
.
a
}
`
).
join
(
"
\n
"
);
...
@@ -37,10 +39,17 @@ ${faq}
...
@@ -37,10 +39,17 @@ ${faq}
## 主要页面
## 主要页面
- 体龄管理:
${
base
}
/services/
- 体龄管理:
${
base
}
/services/
${
site
.
services
.
map
((
item
)
=>
`-
${
item
.
name
}
:
${
base
}
/services/
${
item
.
slug
}
/`
).
join
(
"
\n
"
)}
- 关于塑美俏:
${
base
}
/about/
- 关于塑美俏:
${
base
}
/about/
- 加盟支持:
${
base
}
/join/
- 加盟支持:
${
base
}
/join/
- 常见问题:
${
base
}
/faq/
- 常见问题:
${
base
}
/faq/
- 联系合作:
${
base
}
/contact/
- 联系合作:
${
base
}
/contact/
## 健康资讯
${
articles
.
map
((
article
)
=>
`-
${
article
.
data
.
title
}
:
${
base
}
/articles/
${
article
.
id
}
/ —
${
article
.
data
.
seoDescription
}
`
).
join
(
"
\n
"
)
||
"- 暂无已发布文章"
}
## 完整内容
- 完整站点与文章正文:
${
base
}
/llms-full.txt
`
;
`
;
return
new
Response
(
text
,
{
headers
:
{
"Content-Type"
:
"text/plain; charset=utf-8"
}
});
return
new
Response
(
text
,
{
headers
:
{
"Content-Type"
:
"text/plain; charset=utf-8"
}
});
};
};
src/pages/services/[slug]/index.astro
0 → 100644
View file @
bbf00c36
---
import Base from "../../../layouts/Base.astro";
import Header from "../../../components/Header.astro";
import Footer from "../../../components/Footer.astro";
import CTA from "../../../components/CTA.astro";
import { site } from "../../../data/site";
export function getStaticPaths() {
return site.services.map((service) => ({ params: { slug: service.slug }, props: { service } }));
}
const { service } = Astro.props as { service: (typeof site.services)[number] };
const pageUrl = new URL(`/services/${service.slug}/`, Astro.site).href;
const jsonLd = [
{
"@context": "https://schema.org",
"@type": "Service",
"@id": `${pageUrl}#service`,
name: service.name,
description: service.seo.description,
serviceType: service.name,
audience: { "@type": "Audience", audienceType: service.audience },
provider: { "@id": new URL("/#organization", Astro.site).href },
areaServed: { "@type": "Country", name: "中国" },
url: pageUrl,
},
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "首页", item: Astro.site?.href },
{ "@type": "ListItem", position: 2, name: "体龄管理", item: new URL("/services/", Astro.site).href },
{ "@type": "ListItem", position: 3, name: service.name, item: pageUrl },
],
},
];
---
<Base title={service.seo.title} description={service.seo.description} keywords={service.seo.keywords} jsonLd={jsonLd}>
<Header />
<main id="main-content">
<section class="page-hero" data-word="SERVICE">
<div class="container page-hero__inner" data-reveal>
<div class="breadcrumb"><a href="/">首页</a> / <a href="/services/">体龄管理</a> / {service.name}</div>
<div class="eyebrow"><span></span>BODY AGE SERVICE · {service.index}</div>
<h1>{service.name}</h1>
<p>{service.seo.description}</p>
</div>
</section>
<section class="section-pad">
<div class="container detail-grid">
<article class="detail-card" data-reveal>
<div class="detail-card__number">{service.index}</div>
<div>
<h2>服务方向</h2>
<p>{service.intro}</p>
<div class="detail-facts">
<div class="detail-fact"><span>适合人群</span><b>{service.audience}</b></div>
<div class="detail-fact"><span>核心价值</span><b>{service.value}</b></div>
</div>
<p class="detail-note">{service.note}</p>
</div>
</article>
</div>
</section>
<CTA title={`想进一步了解${service.name}?`} text="联系附近门店,说明你的目标与身体情况,由专业人员结合实际状态进行评估。" />
</main>
<Footer />
</Base>
src/pages/services/index.astro
View file @
bbf00c36
...
@@ -22,7 +22,7 @@ const jsonLd = [
...
@@ -22,7 +22,7 @@ const jsonLd = [
name: service.name,
name: service.name,
description: service.intro,
description: service.intro,
provider: { "@id": new URL("/#organization", Astro.site).href },
provider: { "@id": new URL("/#organization", Astro.site).href },
url:
`${pageUrl}#${service.slug}`
,
url:
new URL(`/services/${service.slug}/`, Astro.site).href
,
areaServed: "CN",
areaServed: "CN",
},
},
})),
})),
...
@@ -62,6 +62,7 @@ const jsonLd = [
...
@@ -62,6 +62,7 @@ const jsonLd = [
<div class="detail-fact"><span>核心方向</span><b>{service.value}</b></div>
<div class="detail-fact"><span>核心方向</span><b>{service.value}</b></div>
</div>
</div>
<p class="detail-note">{service.note}</p>
<p class="detail-note">{service.note}</p>
<a class="text-link" href={`/services/${service.slug}/`}>查看服务详情 <span>↗</span></a>
</div>
</div>
</article>
</article>
))}
))}
...
...
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