@roottale/cms-mcp 0.46.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +5 -0
- package/dist/index.js +156 -10
- package/dist/index.js.map +1 -1
- package/docs/api-reference.md +2 -1
- package/docs/blog.md +50 -0
- package/docs/inquiries.md +27 -3
- package/docs/seo.md +110 -5
- package/examples/nextjs/app/blog/[slug]/page.tsx +37 -2
- package/examples/nextjs/app/blog/categories/[slug]/page.tsx +60 -0
- package/examples/nextjs/lib/blog.ts +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @roottale/cms-mcp
|
|
2
2
|
|
|
3
|
+
## 0.49.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b618458: Rank Math 1.0.274의 MCP 기능을 참고해 Google Search Console 상위 검색어 조회, 글 SEO·가독성 분석, 발행 글 SEO 점수 조회 도구를 추가했습니다.
|
|
8
|
+
|
|
9
|
+
## 0.48.0
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a4c7070: 문서 갱신 — `blog.md`/`seo.md`에 `author_slug` 가산, 저자 링크·브레드크럼
|
|
14
|
+
(avcd 구조), 카테고리 허브 라우트(`/blog/categories/{slug}`) 필수화 안내,
|
|
15
|
+
avcd 확장 메타데이터(RSS alternate·Twitter Card·og article 확장)를 반영.
|
|
16
|
+
예시 코드에 브레드크럼·BlogPosting·카테고리 허브 라우트 사용법 추가.
|
|
17
|
+
|
|
3
18
|
## 0.46.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ RootTale CMS 연동 MCP 서버 + CLI — AI 코딩 에이전트(Claude Code, Cur
|
|
|
12
12
|
라우트, RSS/사이트맵, 상담문의 서버 액션 등 완전한 연동 1세트
|
|
13
13
|
- **CMS 자동화 tool** — 초안 작성·수정·예약·발행, 카테고리/태그 연결,
|
|
14
14
|
썸네일·본문 이미지 업로드/수정/삭제
|
|
15
|
+
- **SEO 분석 tool** — Google Search Console 상위 검색어, 글별 SEO·가독성
|
|
16
|
+
분석, 발행 글 점수 필터
|
|
15
17
|
- **공개 CLI** — 같은 기능을 `npx @roottale/cms-mcp cli ...`로 실행
|
|
16
18
|
|
|
17
19
|
## 설정
|
|
@@ -85,6 +87,9 @@ npx -y @roottale/cms-mcp cli posts publish "<post-id>"
|
|
|
85
87
|
| `uploadCmsMedia` | 로컬 이미지/PDF 업로드 및 media id 발급 |
|
|
86
88
|
| `updateCmsMedia` | 대체 텍스트·캡션 수정 |
|
|
87
89
|
| `deleteCmsMedia` | 미디어 삭제 |
|
|
90
|
+
| `getTopSeoKeywords` | Google Search Console 상위 검색어 조회 |
|
|
91
|
+
| `analyzeCmsPostSeo` | ROOT-ADMIN과 같은 규칙으로 글 SEO·가독성 분석 |
|
|
92
|
+
| `getCmsPostSeoScores` | 발행 글의 최신 SEO 점수·등급 조회 |
|
|
88
93
|
|
|
89
94
|
## 사람용 문서 (브라우저 열람)
|
|
90
95
|
|
package/dist/index.js
CHANGED
|
@@ -542,7 +542,7 @@ import {
|
|
|
542
542
|
ListToolsRequestSchema,
|
|
543
543
|
McpError
|
|
544
544
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
545
|
-
import { z as
|
|
545
|
+
import { z as z4 } from "zod";
|
|
546
546
|
|
|
547
547
|
// src/docs.ts
|
|
548
548
|
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
@@ -624,16 +624,155 @@ ${content}`;
|
|
|
624
624
|
return sections.join("\n\n");
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
-
// src/tools.ts
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
627
|
+
// src/seo-tools.ts
|
|
628
|
+
import { z as z3 } from "zod";
|
|
629
|
+
|
|
630
|
+
// src/seo-definitions.ts
|
|
631
|
+
var DATE_RANGES = [
|
|
632
|
+
"last_7_days",
|
|
633
|
+
"last_30_days",
|
|
634
|
+
"last_3_months",
|
|
635
|
+
"last_6_months",
|
|
636
|
+
"last_12_months"
|
|
637
|
+
];
|
|
638
|
+
var SEO_TOOL_DEFINITIONS = [
|
|
639
|
+
tool2(
|
|
640
|
+
"getTopSeoKeywords",
|
|
641
|
+
"Google Search Console\uC5D0\uC11C \uC0AC\uC774\uD2B8\uC758 \uC0C1\uC704 \uAC80\uC0C9\uC5B4\uC640 \uD074\uB9AD\xB7\uB178\uCD9C\xB7CTR\xB7\uD3C9\uADE0 \uC21C\uC704\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
|
|
642
|
+
{
|
|
643
|
+
siteId: stringProperty2("\uC0AC\uC774\uD2B8 ID"),
|
|
644
|
+
dateRange: enumProperty2(DATE_RANGES, "\uC870\uD68C \uAE30\uAC04, \uAE30\uBCF8 last_30_days"),
|
|
645
|
+
limit: numberProperty2("1-100, \uAE30\uBCF8 25")
|
|
646
|
+
}
|
|
647
|
+
),
|
|
648
|
+
tool2(
|
|
649
|
+
"analyzeCmsPostSeo",
|
|
650
|
+
"ROOT-ADMIN\uACFC \uAC19\uC740 \uADDC\uCE59\uC73C\uB85C \uAE00\uC758 SEO\uC640 \uD55C\uAD6D\uC5B4 \uAC00\uB3C5\uC131\uC744 \uB2E4\uC2DC \uAC80\uC0AC\uD569\uB2C8\uB2E4. \uAE00\uC744 \uC218\uC815\uD558\uAC70\uB098 \uBC1C\uD589\uD558\uAE30 \uC804\uC5D0 \uC0AC\uC6A9\uD558\uC138\uC694.",
|
|
651
|
+
{
|
|
652
|
+
postId: stringProperty2("\uBD84\uC11D\uD560 \uAE00 ID"),
|
|
653
|
+
siteId: stringProperty2("\uC0AC\uC774\uD2B8 ID"),
|
|
654
|
+
focusKeyword: stringProperty2("\uC774\uBC88 \uBD84\uC11D\uC5D0 \uC0AC\uC6A9\uD560 \uC9D1\uC911 \uD0A4\uC6CC\uB4DC")
|
|
655
|
+
},
|
|
656
|
+
["postId"]
|
|
657
|
+
),
|
|
658
|
+
tool2(
|
|
659
|
+
"getCmsPostSeoScores",
|
|
660
|
+
"\uBC1C\uD589\uB41C \uAE00\uC758 \uCD5C\uC2E0 SEO \uC810\uC218\uC640 \uB4F1\uAE09\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. \uAC1C\uC120\uC774 \uD544\uC694\uD55C \uAE00\uC774\uB098 \uC9D1\uC911 \uD0A4\uC6CC\uB4DC\uAC00 \uC5C6\uB294 \uAE00\uC744 \uCC3E\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
661
|
+
{
|
|
662
|
+
siteId: stringProperty2("\uC0AC\uC774\uD2B8 ID"),
|
|
663
|
+
limit: numberProperty2("1-100, \uAE30\uBCF8 10"),
|
|
664
|
+
type: enumProperty2(["post", "page"], "\uCF58\uD150\uCE20 \uC720\uD615"),
|
|
665
|
+
seoFilter: enumProperty2(
|
|
666
|
+
["bad", "ok", "good", "empty_keyword"],
|
|
667
|
+
"SEO \uC0C1\uD0DC \uD544\uD130"
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
];
|
|
672
|
+
function isSeoToolName(name) {
|
|
673
|
+
return SEO_TOOL_DEFINITIONS.some((definition) => definition.name === name);
|
|
674
|
+
}
|
|
675
|
+
function tool2(name, description, properties, required = []) {
|
|
676
|
+
return {
|
|
677
|
+
name,
|
|
678
|
+
description,
|
|
679
|
+
inputSchema: {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties,
|
|
682
|
+
...required.length > 0 ? { required } : {},
|
|
683
|
+
additionalProperties: false
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function stringProperty2(description) {
|
|
688
|
+
return { type: "string", description };
|
|
689
|
+
}
|
|
690
|
+
function numberProperty2(description) {
|
|
691
|
+
return { type: "number", description };
|
|
692
|
+
}
|
|
693
|
+
function enumProperty2(values, description) {
|
|
694
|
+
return { type: "string", enum: values, description };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/seo-tools.ts
|
|
698
|
+
var SiteIdInput = z3.object({ siteId: z3.string().optional() });
|
|
699
|
+
var TopKeywordsInput = SiteIdInput.extend({
|
|
700
|
+
dateRange: z3.enum([
|
|
701
|
+
"last_7_days",
|
|
702
|
+
"last_30_days",
|
|
703
|
+
"last_3_months",
|
|
704
|
+
"last_6_months",
|
|
705
|
+
"last_12_months"
|
|
706
|
+
]).optional(),
|
|
707
|
+
limit: z3.number().int().min(1).max(100).optional()
|
|
708
|
+
}).strict();
|
|
709
|
+
var AnalyzePostInput = SiteIdInput.extend({
|
|
710
|
+
postId: z3.string().min(1),
|
|
711
|
+
focusKeyword: z3.string().max(200).optional()
|
|
712
|
+
}).strict();
|
|
713
|
+
var ScoresInput = SiteIdInput.extend({
|
|
631
714
|
limit: z3.number().int().min(1).max(100).optional(),
|
|
632
715
|
type: z3.enum(["post", "page"]).optional(),
|
|
633
|
-
|
|
716
|
+
seoFilter: z3.enum(["bad", "ok", "good", "empty_keyword"]).optional()
|
|
717
|
+
}).strict();
|
|
718
|
+
async function callSeoTool(name, args) {
|
|
719
|
+
switch (name) {
|
|
720
|
+
case "getTopSeoKeywords": {
|
|
721
|
+
const input = TopKeywordsInput.parse(args);
|
|
722
|
+
return requestRootTaleApi({
|
|
723
|
+
method: "GET",
|
|
724
|
+
pathname: "/v1/cms/seo/keywords",
|
|
725
|
+
query: optionalQuery2({
|
|
726
|
+
site_id: input.siteId,
|
|
727
|
+
date_range: input.dateRange,
|
|
728
|
+
limit: input.limit
|
|
729
|
+
})
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
case "analyzeCmsPostSeo": {
|
|
733
|
+
const input = AnalyzePostInput.parse(args);
|
|
734
|
+
return requestRootTaleApi({
|
|
735
|
+
method: "GET",
|
|
736
|
+
pathname: `/v1/cms/seo/posts/${encodeURIComponent(input.postId)}/analysis`,
|
|
737
|
+
query: optionalQuery2({
|
|
738
|
+
site_id: input.siteId,
|
|
739
|
+
focus_keyword: input.focusKeyword
|
|
740
|
+
})
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
case "getCmsPostSeoScores": {
|
|
744
|
+
const input = ScoresInput.parse(args);
|
|
745
|
+
return requestRootTaleApi({
|
|
746
|
+
method: "GET",
|
|
747
|
+
pathname: "/v1/cms/seo/scores",
|
|
748
|
+
query: optionalQuery2({
|
|
749
|
+
site_id: input.siteId,
|
|
750
|
+
limit: input.limit,
|
|
751
|
+
type: input.type,
|
|
752
|
+
seo_filter: input.seoFilter
|
|
753
|
+
})
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function optionalQuery2(values) {
|
|
759
|
+
return Object.fromEntries(
|
|
760
|
+
Object.entries(values).filter(
|
|
761
|
+
(entry) => entry[1] !== void 0
|
|
762
|
+
).map(([key, value]) => [key, String(value)])
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/tools.ts
|
|
767
|
+
var ReadDocInput = z4.object({ path: z4.string().min(1) }).strict();
|
|
768
|
+
var SearchDocsInput = z4.object({ query: z4.string().min(1) }).strict();
|
|
769
|
+
var ListPostsInput = z4.object({
|
|
770
|
+
limit: z4.number().int().min(1).max(100).optional(),
|
|
771
|
+
type: z4.enum(["post", "page"]).optional(),
|
|
772
|
+
cursor: z4.string().optional()
|
|
634
773
|
}).strict();
|
|
635
|
-
var GetPostInput =
|
|
636
|
-
var GetSiteKnowledgeInput =
|
|
774
|
+
var GetPostInput = z4.object({ slugOrId: z4.string().min(1) }).strict();
|
|
775
|
+
var GetSiteKnowledgeInput = z4.object({ siteId: z4.string().optional() }).strict();
|
|
637
776
|
var DOCUMENTATION_TOOL_DEFINITIONS = [
|
|
638
777
|
{
|
|
639
778
|
name: "listRootTaleDocs",
|
|
@@ -711,7 +850,8 @@ var DOCUMENTATION_TOOL_DEFINITIONS = [
|
|
|
711
850
|
];
|
|
712
851
|
var TOOL_DEFINITIONS = [
|
|
713
852
|
...DOCUMENTATION_TOOL_DEFINITIONS,
|
|
714
|
-
...AUTOMATION_TOOL_DEFINITIONS
|
|
853
|
+
...AUTOMATION_TOOL_DEFINITIONS,
|
|
854
|
+
...SEO_TOOL_DEFINITIONS
|
|
715
855
|
];
|
|
716
856
|
function textResponse(text) {
|
|
717
857
|
return { content: [{ type: "text", text }] };
|
|
@@ -731,6 +871,9 @@ function registerTools(server) {
|
|
|
731
871
|
await callAutomationTool(request.params.name, args)
|
|
732
872
|
);
|
|
733
873
|
}
|
|
874
|
+
if (isSeoToolName(request.params.name)) {
|
|
875
|
+
return jsonResponse(await callSeoTool(request.params.name, args));
|
|
876
|
+
}
|
|
734
877
|
switch (request.params.name) {
|
|
735
878
|
case "listRootTaleDocs":
|
|
736
879
|
return jsonResponse(await listDocs());
|
|
@@ -801,7 +944,7 @@ function registerTools(server) {
|
|
|
801
944
|
}
|
|
802
945
|
|
|
803
946
|
// src/server.ts
|
|
804
|
-
var VERSION = true ? "0.
|
|
947
|
+
var VERSION = true ? "0.49.0" : "dev";
|
|
805
948
|
var SERVER_INSTRUCTIONS = `
|
|
806
949
|
roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC\uB85C Next.js)\uC5D0 \uC5F0\uB3D9\uD558\uACE0
|
|
807
950
|
\uAE00\xB7\uC378\uB124\uC77C\xB7\uBCF8\uBB38 \uC774\uBBF8\uC9C0\uB97C \uC790\uB3D9\uD654\uD558\uAE30 \uC704\uD55C \uBB38\uC11C\xB7\uC608\uC2DC \uCF54\uB4DC\xB7API tool\uC744 \uC81C\uACF5\uD569\uB2C8\uB2E4.
|
|
@@ -817,6 +960,9 @@ roottale-cms-mcp\uB294 RootTale CMS\uB97C \uC678\uBD80 \uC0AC\uC774\uD2B8(\uC8FC
|
|
|
817
960
|
publish=true \uB610\uB294 publishCmsPost\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
|
|
818
961
|
- \uC774\uBBF8\uC9C0\uB294 uploadCmsMedia\uB85C \uBA3C\uC800 \uB4F1\uB85D\uD55C \uB4A4 \uBC18\uD658\uB41C id\uB97C featuredMediaId\uC5D0,
|
|
819
962
|
\uBC18\uD658\uB41C URL\uC744 Tiptap image \uB178\uB4DC\uC758 attrs.src\uC5D0 \uC0AC\uC6A9\uD558\uC138\uC694.
|
|
963
|
+
- \uAE00\uC744 \uC218\uC815\uD558\uAC70\uB098 \uBC1C\uD589\uD558\uAE30 \uC804 analyzeCmsPostSeo\uB85C SEO\uC640 \uAC00\uB3C5\uC131\uC744 \uD655\uC778\uD558\uC138\uC694.
|
|
964
|
+
- \uAC1C\uC120\uD560 \uAE00\uC744 \uCC3E\uC744 \uB54C getCmsPostSeoScores\uB97C, \uC2E4\uC81C \uC720\uC785 \uAC80\uC0C9\uC5B4\uB97C \uACE0\uB97C \uB54C
|
|
965
|
+
getTopSeoKeywords\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
|
|
820
966
|
- \uC774\uBBF8 \uD559\uC2B5\uB41C \uB0B4\uC6A9\uC774\uB354\uB77C\uB3C4 \uBCF8 \uC11C\uBC84\uC758 \uBB38\uC11C\uB85C \uB354\uBE14\uCCB4\uD06C \uD6C4 \uC791\uC5C5\uD558\uC138\uC694.
|
|
821
967
|
`.trim();
|
|
822
968
|
function createServer() {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/api.ts","../src/automation-definitions.ts","../src/automation-media.ts","../src/automation-schemas.ts","../src/automation-tools.ts","../src/server.ts","../src/tools.ts","../src/docs.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { runCmsCli } from \"./cli.js\";\nimport { createServer } from \"./server.js\";\n\nif (process.argv[2] === \"cli\") {\n await runCmsCli(process.argv);\n} else {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","import { readFile } from \"node:fs/promises\";\n\nimport { Command, InvalidArgumentError } from \"commander\";\n\nimport { callAutomationTool } from \"./automation-tools.js\";\n\ntype CommonOptions = { readonly siteId?: string };\n\nexport function createCmsCli(): Command {\n const cli = new Command()\n .name(\"roottale-cms\")\n .description(\"RootTale CMS 글·썸네일·본문 이미지 자동화 CLI\");\n\n registerPostCommands(cli);\n registerMediaCommands(cli);\n return cli;\n}\n\nexport async function runCmsCli(argv: readonly string[]): Promise<void> {\n try {\n await createCmsCli().parseAsync([\n argv[0] ?? \"node\",\n \"roottale-cms\",\n ...argv.slice(3),\n ]);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 1;\n }\n}\n\nfunction registerPostCommands(cli: Command): void {\n const posts = cli.command(\"posts\").description(\"CMS 글 자동화\");\n\n posts\n .command(\"list\")\n .description(\"초안·예약·발행 글 목록 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--cursor <cursor>\", \"다음 페이지 커서\")\n .option(\"--type <type>\", \"post 또는 page\")\n .option(\"--status <status>\", \"draft, published, scheduled, private\")\n .action(async (options) => {\n await run(\"listManagedCmsPosts\", options);\n });\n\n posts\n .command(\"create\")\n .description(\"Tiptap JSON 본문으로 새 글 생성\")\n .requiredOption(\"--title <title>\", \"글 제목\")\n .requiredOption(\"--slug <slug>\", \"URL 슬러그\")\n .requiredOption(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--type <type>\", \"post 또는 page\", \"post\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--publish\", \"즉시 발행\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--category-ids <ids>\", \"쉼표로 구분한 카테고리 ID\")\n .option(\"--tag-ids <ids>\", \"쉼표로 구분한 태그 ID\")\n .action(async (options) => {\n await run(\"createCmsPost\", {\n ...options,\n bodyJson: await readJsonObject(options.bodyFile),\n categoryIds: commaSeparated(options.categoryIds),\n tagIds: commaSeparated(options.tagIds),\n bodyFile: undefined,\n });\n });\n\n posts\n .command(\"update\")\n .description(\"기존 글의 일부 필드 수정\")\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--title <title>\", \"글 제목\")\n .option(\"--slug <slug>\", \"URL 슬러그\")\n .option(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--clear-featured-media\", \"썸네일 연결 해제\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--clear-schedule\", \"예약 해제\")\n .action(async (postId: string, options) => {\n await run(\"updateCmsPost\", {\n postId,\n siteId: options.siteId,\n title: options.title,\n slug: options.slug,\n bodyJson: options.bodyFile\n ? await readJsonObject(options.bodyFile)\n : undefined,\n excerpt: options.excerpt,\n featuredMediaId: options.clearFeaturedMedia\n ? null\n : options.featuredMediaId,\n scheduledAt: options.clearSchedule ? null : options.scheduledAt,\n });\n });\n\n lifecycle(posts, \"publish\", \"글을 즉시 발행\", \"publishCmsPost\");\n lifecycle(posts, \"unpublish\", \"발행 글을 초안으로 전환\", \"unpublishCmsPost\");\n\n posts\n .command(\"set-terms\")\n .description(\"글의 카테고리 또는 태그 전체 교체\")\n .argument(\"<post-id>\", \"글 ID\")\n .requiredOption(\"--taxonomy <type>\", \"category 또는 tag\")\n .requiredOption(\"--term-ids <ids>\", \"쉼표로 구분한 term ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options) => {\n await run(\"setCmsPostTerms\", {\n postId,\n siteId: options.siteId,\n taxonomy: options.taxonomy,\n termIds: commaSeparated(options.termIds) ?? [],\n });\n });\n}\n\nfunction registerMediaCommands(cli: Command): void {\n const media = cli.command(\"media\").description(\"CMS 미디어 자동화\");\n\n media\n .command(\"list\")\n .description(\"미디어 라이브러리 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--offset <number>\", \"조회 시작 위치\", integer, 0)\n .action(async (options) => {\n await run(\"listCmsMedia\", options);\n });\n\n media\n .command(\"upload\")\n .description(\"로컬 파일을 업로드하고 CMS 미디어로 등록\")\n .argument(\"<file>\", \"jpg, png, webp, gif 또는 pdf 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--content-type <type>\", \"MIME type 직접 지정\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (filePath: string, options) => {\n await run(\"uploadCmsMedia\", { filePath, ...options });\n });\n\n media\n .command(\"update\")\n .description(\"미디어 대체 텍스트 또는 캡션 수정\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (mediaId: string, options) => {\n await run(\"updateCmsMedia\", { mediaId, ...options });\n });\n\n media\n .command(\"delete\")\n .description(\"미디어 파일과 라이브러리 항목 삭제\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (mediaId: string, options: CommonOptions) => {\n await run(\"deleteCmsMedia\", { mediaId, ...options });\n });\n}\n\nfunction lifecycle(\n posts: Command,\n commandName: string,\n description: string,\n toolName: \"publishCmsPost\" | \"unpublishCmsPost\",\n): void {\n posts\n .command(commandName)\n .description(description)\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options: CommonOptions) => {\n await run(toolName, { postId, ...options });\n });\n}\n\nasync function run(\n name: Parameters<typeof callAutomationTool>[0],\n args: Record<string, unknown>,\n): Promise<void> {\n const definedArgs = Object.fromEntries(\n Object.entries(args).filter(([, value]) => value !== undefined),\n );\n const result = await callAutomationTool(name, definedArgs);\n process.stdout.write(`${JSON.stringify(result, null, 2)}\\n`);\n}\n\nasync function readJsonObject(filePath: string): Promise<Record<string, unknown>> {\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf8\"));\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new InvalidArgumentError(\"본문 파일은 Tiptap JSON 객체여야 합니다.\");\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction commaSeparated(value: string | undefined): string[] | undefined {\n if (value === undefined) return undefined;\n return value.split(\",\").map((item) => item.trim()).filter(Boolean);\n}\n\nfunction integer(value: string): number {\n const parsed = Number.parseInt(value, 10);\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError(\"0 이상의 정수를 입력하세요.\");\n }\n return parsed;\n}\n","import ky from \"ky\";\n\nconst DEFAULT_API_BASE = \"https://api.roottale.com\";\n\nexport class CmsAutomationError extends Error {\n readonly name = \"CmsAutomationError\";\n}\n\nexport type ApiRequest = {\n readonly method: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n readonly pathname: string;\n readonly query?: Readonly<Record<string, string | undefined>>;\n readonly body?: unknown;\n};\n\nfunction apiConfig(): { readonly apiKey: string; readonly baseUrl: string } {\n const apiKey = process.env.ROOTTALE_API_KEY;\n if (!apiKey) {\n throw new CmsAutomationError(\n \"ROOTTALE_API_KEY 환경변수가 없습니다. 어드민 설정 > 사이트 연결 키에서 read_write 키를 발급해 MCP env에 추가하세요.\",\n );\n }\n return {\n apiKey,\n baseUrl: (process.env.ROOTTALE_API_BASE ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n ),\n };\n}\n\nexport async function requestRootTaleApi(\n request: ApiRequest,\n): Promise<unknown> {\n const { apiKey, baseUrl } = apiConfig();\n const url = new URL(`${baseUrl}${request.pathname}`);\n for (const [key, value] of Object.entries(request.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n return ky(url, {\n method: request.method,\n headers: { authorization: `Bearer ${apiKey}` },\n json: request.body,\n retry: 0,\n timeout: 30_000,\n }).json();\n}\n\nexport async function putUpload(\n uploadUrl: string,\n bytes: Uint8Array,\n contentType: string,\n): Promise<void> {\n const body = new Uint8Array(bytes.byteLength);\n body.set(bytes);\n await ky.put(uploadUrl, {\n body: body.buffer,\n headers: { \"content-type\": contentType },\n retry: 0,\n timeout: 120_000,\n });\n}\n","type JsonSchemaProperty = Readonly<Record<string, unknown>>;\n\nexport const AUTOMATION_TOOL_DEFINITIONS = [\n tool(\"listManagedCmsPosts\", \"초안·예약·발행 글을 포함한 관리용 글 목록을 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n type: enumProperty([\"post\", \"page\"]),\n status: enumProperty([\"draft\", \"published\", \"scheduled\", \"private\"]),\n cursor: stringProperty(\"이전 응답의 next_cursor\"),\n }),\n tool(\n \"createCmsPost\",\n \"Tiptap bodyJson으로 글 또는 페이지를 생성합니다. 기본은 초안이며 publish=true면 즉시 발행합니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n type: enumProperty([\"post\", \"page\"]),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n publish: { type: \"boolean\" },\n scheduledAt: stringProperty(\"미래 ISO 8601 발행 시각\"),\n categoryIds: arrayProperty(\"카테고리 term id\"),\n tagIds: arrayProperty(\"태그 term id\"),\n },\n [\"title\", \"slug\", \"bodyJson\"],\n ),\n tool(\"updateCmsPost\", \"기존 CMS 글을 부분 수정합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n scheduledAt: nullableStringProperty(\"예약 시각\"),\n }, [\"postId\"]),\n tool(\"publishCmsPost\", \"초안 또는 예약 글을 즉시 발행하고 웹훅을 전송합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"unpublishCmsPost\", \"발행 글을 초안으로 되돌립니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"setCmsPostTerms\", \"글의 카테고리 또는 태그 연결을 교체합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n taxonomy: enumProperty([\"category\", \"tag\"]),\n termIds: arrayProperty(\"term id\"),\n }, [\"postId\", \"taxonomy\", \"termIds\"]),\n tool(\"listCmsMedia\", \"CMS 미디어 라이브러리를 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n offset: numberProperty(\"0 이상\"),\n }),\n tool(\n \"uploadCmsMedia\",\n \"로컬 이미지/PDF 파일을 업로드하고 CMS media id와 공개 URL을 반환합니다.\",\n {\n filePath: stringProperty(\"로컬 파일 절대/상대 경로\"),\n siteId: stringProperty(\"사이트 ID\"),\n contentType: enumProperty([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ]),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n },\n [\"filePath\"],\n ),\n tool(\"updateCmsMedia\", \"CMS 미디어의 대체 텍스트 또는 캡션을 수정합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n }, [\"mediaId\"]),\n tool(\"deleteCmsMedia\", \"CMS 미디어를 R2와 라이브러리에서 삭제합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"mediaId\"]),\n] as const;\n\nexport type AutomationToolName =\n (typeof AUTOMATION_TOOL_DEFINITIONS)[number][\"name\"];\n\nexport function isAutomationToolName(name: string): name is AutomationToolName {\n return AUTOMATION_TOOL_DEFINITIONS.some(\n (toolDefinition) => toolDefinition.name === name,\n );\n}\n\nfunction tool<const TName extends string>(\n name: TName,\n description: string,\n properties: Readonly<Record<string, JsonSchemaProperty>>,\n required: readonly string[] = [],\n) {\n return {\n name,\n description,\n inputSchema: {\n type: \"object\",\n properties,\n ...(required.length > 0 ? { required } : {}),\n additionalProperties: false,\n },\n } as const;\n}\n\nfunction stringProperty(description: string): JsonSchemaProperty {\n return { type: \"string\", description };\n}\n\nfunction nullableStringProperty(description: string): JsonSchemaProperty {\n return { type: [\"string\", \"null\"], description };\n}\n\nfunction numberProperty(description: string): JsonSchemaProperty {\n return { type: \"number\", description };\n}\n\nfunction enumProperty(values: readonly string[]): JsonSchemaProperty {\n return { type: \"string\", enum: values };\n}\n\nfunction arrayProperty(description: string): JsonSchemaProperty {\n return { type: \"array\", items: { type: \"string\" }, description };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { basename, extname, resolve } from \"node:path\";\n\nimport { z } from \"zod\";\n\nimport { CmsAutomationError, putUpload, requestRootTaleApi } from \"./api.js\";\nimport { UploadMediaInput } from \"./automation-schemas.js\";\n\nexport async function uploadCmsMedia(args: Record<string, unknown>): Promise<unknown> {\n const input = UploadMediaInput.parse(args);\n const filePath = resolve(input.filePath);\n const bytes = await readFile(filePath);\n const contentType = input.contentType ?? inferContentType(filePath);\n if (!contentType) {\n throw new CmsAutomationError(\n \"contentType을 추론할 수 없습니다. jpg/png/webp/gif/pdf 파일이 아니면 contentType을 지정하세요.\",\n );\n }\n const requested = z\n .object({\n upload_url: z.string().url(),\n r2_key: z.string().min(1),\n })\n .parse(\n await requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads\",\n body: {\n site_id: input.siteId,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n },\n }),\n );\n await putUpload(requested.upload_url, bytes, contentType);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads/complete\",\n body: {\n site_id: input.siteId,\n r2_key: requested.r2_key,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n alt: input.alt,\n caption: input.caption,\n },\n });\n}\n\nfunction inferContentType(filePath: string): string | undefined {\n switch (extname(filePath).toLowerCase()) {\n case \".jpg\":\n case \".jpeg\":\n return \"image/jpeg\";\n case \".png\":\n return \"image/png\";\n case \".webp\":\n return \"image/webp\";\n case \".gif\":\n return \"image/gif\";\n case \".pdf\":\n return \"application/pdf\";\n default:\n return undefined;\n }\n}\n","import { z } from \"zod\";\n\nconst SiteId = z.string().min(1).optional();\nconst PostId = z.string().min(1);\nconst NullableText = z.string().nullable().optional();\n\nexport const CreatePostInput = z\n .object({\n siteId: SiteId,\n type: z.enum([\"post\", \"page\"]).optional(),\n title: z.string().min(1).max(500),\n slug: z.string().min(1).max(120),\n bodyJson: z.record(z.string(), z.unknown()),\n excerpt: z.string().max(1000).nullable().optional(),\n featuredMediaId: z.string().nullable().optional(),\n publish: z.boolean().optional(),\n scheduledAt: z.string().datetime({ offset: true }).optional(),\n categoryIds: z.array(z.string()).optional(),\n tagIds: z.array(z.string()).optional(),\n })\n .strict();\n\nexport const UpdatePostInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n title: z.string().min(1).max(500).optional(),\n slug: z.string().min(1).max(120).optional(),\n bodyJson: z.record(z.string(), z.unknown()).optional(),\n excerpt: NullableText,\n featuredMediaId: z.string().nullable().optional(),\n scheduledAt: z.string().datetime({ offset: true }).nullable().optional(),\n })\n .strict();\n\nexport const PostLifecycleInput = z\n .object({ postId: PostId, siteId: SiteId })\n .strict();\n\nexport const ListManagedPostsInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n status: z\n .enum([\"draft\", \"published\", \"scheduled\", \"private\"])\n .optional(),\n cursor: z.string().optional(),\n })\n .strict();\n\nexport const SetTermsInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n taxonomy: z.enum([\"category\", \"tag\"]),\n termIds: z.array(z.string()),\n })\n .strict();\n\nexport const UploadMediaInput = z\n .object({\n filePath: z.string().min(1),\n siteId: SiteId,\n contentType: z\n .enum([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ])\n .optional(),\n alt: NullableText,\n caption: NullableText,\n })\n .strict();\n\nexport const ListMediaInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n offset: z.number().int().nonnegative().optional(),\n })\n .strict();\n\nexport const UpdateMediaInput = z\n .object({\n mediaId: z.string().min(1),\n siteId: SiteId,\n alt: NullableText,\n caption: NullableText,\n })\n .strict()\n .refine((input) => input.alt !== undefined || input.caption !== undefined, {\n message: \"alt 또는 caption 중 하나 이상이 필요합니다.\",\n });\n\nexport const DeleteMediaInput = z\n .object({ mediaId: z.string().min(1), siteId: SiteId })\n .strict();\n","import { CmsAutomationError, requestRootTaleApi } from \"./api.js\";\nimport {\n type AutomationToolName,\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n} from \"./automation-definitions.js\";\nimport { uploadCmsMedia } from \"./automation-media.js\";\nimport {\n CreatePostInput,\n DeleteMediaInput,\n ListManagedPostsInput,\n ListMediaInput,\n PostLifecycleInput,\n SetTermsInput,\n UpdateMediaInput,\n UpdatePostInput,\n} from \"./automation-schemas.js\";\n\nexport {\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n};\n\nexport async function callAutomationTool(\n name: AutomationToolName,\n args: Record<string, unknown>,\n): Promise<unknown> {\n switch (name) {\n case \"listManagedCmsPosts\": {\n const input = ListManagedPostsInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/posts\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n type: input.type,\n status: input.status,\n cursor: input.cursor,\n }),\n });\n }\n case \"createCmsPost\": {\n const input = CreatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/posts\",\n body: {\n site_id: input.siteId,\n type: input.type,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n publish: input.publish,\n scheduled_at: input.scheduledAt,\n category_ids: input.categoryIds,\n tag_ids: input.tagIds,\n },\n });\n }\n case \"updateCmsPost\": {\n const input = UpdatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}`,\n body: {\n site_id: input.siteId,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n scheduled_at: input.scheduledAt,\n },\n });\n }\n case \"publishCmsPost\":\n case \"unpublishCmsPost\": {\n const input = PostLifecycleInput.parse(args);\n const action = name === \"publishCmsPost\" ? \"publish\" : \"unpublish\";\n return requestRootTaleApi({\n method: \"POST\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/${action}`,\n body: { site_id: input.siteId },\n });\n }\n case \"setCmsPostTerms\": {\n const input = SetTermsInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/terms`,\n body: {\n site_id: input.siteId,\n taxonomy: input.taxonomy,\n term_ids: input.termIds,\n },\n });\n }\n case \"listCmsMedia\": {\n const input = ListMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/media\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n offset: input.offset,\n }),\n });\n }\n case \"uploadCmsMedia\":\n return uploadCmsMedia(args);\n case \"updateCmsMedia\": {\n const input = UpdateMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n body: {\n site_id: input.siteId,\n alt: input.alt,\n caption: input.caption,\n },\n });\n }\n case \"deleteCmsMedia\": {\n const input = DeleteMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"DELETE\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n query: optionalQuery({ site_id: input.siteId }),\n });\n }\n default:\n return assertNever(name);\n }\n}\n\nfunction optionalQuery(\n values: Readonly<Record<string, string | number | undefined>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(values)\n .filter((entry): entry is [string, string | number] => entry[1] !== undefined)\n .map(([key, value]) => [key, String(value)]),\n );\n}\n\nfunction assertNever(value: never): never {\n throw new CmsAutomationError(`Unknown automation tool: ${String(value)}`);\n}\n","import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\n\nimport { registerTools } from \"./tools.js\";\n\n// tsup define 으로 빌드타임 주입 — 비번들 실행(vitest 등)에서는 \"dev\".\ndeclare const __CMS_MCP_VERSION__: string | undefined;\nconst VERSION =\n typeof __CMS_MCP_VERSION__ === \"string\" ? __CMS_MCP_VERSION__ : \"dev\";\n\nexport const SERVER_INSTRUCTIONS = `\nroottale-cms-mcp는 RootTale CMS를 외부 사이트(주로 Next.js)에 연동하고\n글·썸네일·본문 이미지를 자동화하기 위한 문서·예시 코드·API tool을 제공합니다.\n\n지켜야 할 규칙:\n- RootTale 연동 관련 작업은 가장 먼저 listRootTaleDocs를 호출해 문서 목록을 파악하세요.\n- 연동 코드를 작성하기 전 반드시 readRootTaleNextjsExampleCode를 호출해 예시 코드를\n 참고한 후 그 패턴을 따라 작성하세요.\n- API 키(rtlk_cust_*)는 서버 전용입니다. NEXT_PUBLIC_* 등 브라우저로 노출되는\n 환경변수에 절대 넣지 마세요.\n- 글쓰기 전에 getSiteKnowledge로 브랜드 보이스와 용어 규칙을 확인하세요.\n- 새 글은 기본적으로 draft로 만들고, 사용자가 즉시 발행을 명시했을 때만\n publish=true 또는 publishCmsPost를 사용하세요.\n- 이미지는 uploadCmsMedia로 먼저 등록한 뒤 반환된 id를 featuredMediaId에,\n 반환된 URL을 Tiptap image 노드의 attrs.src에 사용하세요.\n- 이미 학습된 내용이더라도 본 서버의 문서로 더블체크 후 작업하세요.\n`.trim();\n\nexport function createServer(): Server {\n const server = new Server(\n { name: \"roottale-cms\", version: VERSION },\n { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },\n );\n registerTools(server);\n return server;\n}\n","import type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n ErrorCode,\n ListToolsRequestSchema,\n McpError,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport {\n AUTOMATION_TOOL_DEFINITIONS,\n callAutomationTool,\n isAutomationToolName,\n} from \"./automation-tools.js\";\nimport { requestRootTaleApi } from \"./api.js\";\nimport { listDocs, readDoc, readNextjsExampleCode, searchDocs } from \"./docs.js\";\n\nconst ReadDocInput = z.object({ path: z.string().min(1) }).strict();\nconst SearchDocsInput = z.object({ query: z.string().min(1) }).strict();\nconst ListPostsInput = z\n .object({\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n cursor: z.string().optional(),\n })\n .strict();\nconst GetPostInput = z.object({ slugOrId: z.string().min(1) }).strict();\nconst GetSiteKnowledgeInput = z\n .object({ siteId: z.string().optional() })\n .strict();\n\nconst DOCUMENTATION_TOOL_DEFINITIONS = [\n {\n name: \"listRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체 목록(경로·제목·설명)을 반환합니다. \" +\n \"RootTale 연동 작업을 시작하기 전 가장 먼저 호출하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"readRootTaleDoc\",\n description:\n \"RootTale CMS 통합 문서 1개를 경로로 읽습니다. \" +\n \"경로는 listRootTaleDocs 가 반환한 path 값을 사용하세요.\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"문서 경로 (예: blog.md)\" },\n },\n required: [\"path\"],\n additionalProperties: false,\n },\n },\n {\n name: \"searchRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체에서 정규식으로 검색합니다 (대소문자 무시). \" +\n \"매치된 파일 경로·라인 번호·라인 내용을 반환합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"정규식 패턴 (예: revalidate|webhook)\" },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: \"readRootTaleNextjsExampleCode\",\n description:\n \"RootTale CMS를 외부 Next.js(App Router) 사이트에 연동하는 완전한 예시 코드 세트를 반환합니다 \" +\n \"(블로그 목록/상세, 웹훅 revalidate 라우트, RSS/사이트맵, 상담문의 서버 액션 등). \" +\n \"연동 코드를 작성하기 전 반드시 호출해 패턴을 따라 작성하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"listPublishedPosts\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts)로 발행된 글 목록을 조회합니다. \" +\n \"환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n limit: { type: \"number\", description: \"1-100, 기본 20\" },\n type: { type: \"string\", enum: [\"post\", \"page\"] },\n cursor: { type: \"string\", description: \"이전 응답의 next_cursor\" },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"getPublishedPost\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts/:identifier)로 발행된 글 1개를 \" +\n \"slug 또는 UUID로 조회합니다. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n slugOrId: { type: \"string\", description: \"글 slug 또는 UUID\" },\n },\n required: [\"slugOrId\"],\n additionalProperties: false,\n },\n },\n {\n name: \"getSiteKnowledge\",\n description:\n \"이 RootTale 사이트의 브랜드 보이스(어조·톤·화자)와 용어 규칙(금지어·교정어)을 조회합니다 \" +\n \"(GET /v1/cms/public/site-knowledge). 이 사이트에 맞는 글을 쓰기 전에 호출해 톤·표현을 맞추세요. \" +\n \"금지어는 쓰지 말고, 교정어는 권장 표현으로 바꿔 쓰세요. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n siteId: {\n type: \"string\",\n description: \"사이트 ID (선택, 미설정 시 기본 사이트)\",\n },\n },\n additionalProperties: false,\n },\n },\n] as const;\n\nexport const TOOL_DEFINITIONS = [\n ...DOCUMENTATION_TOOL_DEFINITIONS,\n ...AUTOMATION_TOOL_DEFINITIONS,\n] as const;\n\nfunction textResponse(text: string) {\n return { content: [{ type: \"text\" as const, text }] };\n}\n\nfunction jsonResponse(value: unknown) {\n return textResponse(JSON.stringify(value, null, 2));\n}\n\nexport function registerTools(server: Server): void {\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: TOOL_DEFINITIONS.map((t) => ({ ...t })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const args = (request.params.arguments ?? {}) as Record<string, unknown>;\n try {\n if (isAutomationToolName(request.params.name)) {\n return jsonResponse(\n await callAutomationTool(request.params.name, args),\n );\n }\n switch (request.params.name) {\n case \"listRootTaleDocs\":\n return jsonResponse(await listDocs());\n case \"readRootTaleDoc\": {\n const input = ReadDocInput.parse(args);\n const content = await readDoc(input.path);\n if (content === null) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `문서를 찾을 수 없습니다: ${input.path} — listRootTaleDocs로 경로를 확인하세요.`,\n );\n }\n return textResponse(content);\n }\n case \"searchRootTaleDocs\": {\n const input = SearchDocsInput.parse(args);\n return jsonResponse(await searchDocs(input.query));\n }\n case \"readRootTaleNextjsExampleCode\":\n return textResponse(await readNextjsExampleCode());\n case \"listPublishedPosts\": {\n const input = ListPostsInput.parse(args);\n const params: Record<string, string> = {};\n if (input.limit) params.limit = String(input.limit);\n if (input.type) params.type = input.type;\n if (input.cursor) params.cursor = input.cursor;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/posts\",\n query: params,\n }),\n );\n }\n case \"getPublishedPost\": {\n const input = GetPostInput.parse(args);\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: `/v1/cms/public/posts/${encodeURIComponent(input.slugOrId)}`,\n }),\n );\n }\n case \"getSiteKnowledge\": {\n const input = GetSiteKnowledgeInput.parse(args);\n const params: Record<string, string> = {};\n if (input.siteId) params.site_id = input.siteId;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/site-knowledge\",\n query: params,\n }),\n );\n }\n default:\n throw new McpError(\n ErrorCode.InvalidParams,\n `Unknown RootTale tool: ${request.params.name}`,\n );\n }\n } catch (error) {\n if (error instanceof McpError) throw error;\n const message = error instanceof Error ? error.message : String(error);\n return { isError: true, content: [{ type: \"text\" as const, text: message }] };\n }\n });\n}\n","/**\n * 패키지에 번들된 통합 문서(`docs/`)·예시 코드(`examples/`) 로더.\n *\n * dist/index.js 와 src/docs.ts 모두 패키지 루트에서 한 단계 아래이므로\n * `..` 상대 경로 해석이 빌드 전후 동일하다. npm publish 시 `files` 에\n * docs/, examples/ 가 포함되어 npx 환경에서도 같은 구조로 설치된다.\n */\nimport { readdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst PKG_ROOT = fileURLToPath(new URL(\"..\", import.meta.url));\nconst DOCS_ROOT = path.join(PKG_ROOT, \"docs\");\nconst EXAMPLES_ROOT = path.join(PKG_ROOT, \"examples\");\n\nexport interface DocEntry {\n /** docs/ 기준 상대 경로 (예: \"blog.md\"). */\n path: string;\n title: string;\n description: string;\n}\n\ninterface Frontmatter {\n title: string;\n description: string;\n}\n\n/** `---\\ntitle: ...\\ndescription: ...\\n---` 형태의 단순 frontmatter 파서. */\nexport function parseFrontmatter(markdown: string): Frontmatter {\n const result: Frontmatter = { title: \"\", description: \"\" };\n const match = markdown.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return result;\n for (const line of match[1].split(\"\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx === -1) continue;\n const key = line.slice(0, idx).trim();\n const value = line.slice(idx + 1).trim();\n if (key === \"title\") result.title = value;\n if (key === \"description\") result.description = value;\n }\n return result;\n}\n\nasync function walkFiles(root: string, dir = root): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await walkFiles(root, full)));\n } else {\n files.push(path.relative(root, full));\n }\n }\n return files.sort();\n}\n\nexport async function listDocs(): Promise<DocEntry[]> {\n const files = (await walkFiles(DOCS_ROOT)).filter((f) => f.endsWith(\".md\"));\n return Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(DOCS_ROOT, rel), \"utf8\");\n const fm = parseFrontmatter(content);\n return { path: rel, title: fm.title || rel, description: fm.description };\n }),\n );\n}\n\n/** 경로 탈출(../) 방지 후 단일 문서 읽기. 없으면 null. */\nexport async function readDoc(relPath: string): Promise<string | null> {\n const resolved = path.resolve(DOCS_ROOT, relPath);\n if (!resolved.startsWith(DOCS_ROOT + path.sep)) return null;\n try {\n return await readFile(resolved, \"utf8\");\n } catch {\n return null;\n }\n}\n\nexport interface SearchMatch {\n path: string;\n line: number;\n text: string;\n}\n\n/** 모든 문서에 대해 정규식 검색 (대소문자 무시). 매치 라인을 반환. */\nexport async function searchDocs(query: string): Promise<SearchMatch[]> {\n const regex = new RegExp(query, \"i\");\n const matches: SearchMatch[] = [];\n for (const doc of await listDocs()) {\n const content = await readDoc(doc.path);\n if (!content) continue;\n content.split(\"\\n\").forEach((text, idx) => {\n if (regex.test(text)) {\n matches.push({ path: doc.path, line: idx + 1, text: text.trim() });\n }\n });\n }\n return matches;\n}\n\n/**\n * Next.js 통합 예시 코드 전체를 `--- FILE: <path> ---` 헤더로 이어붙여 반환.\n * 파일 수가 적어(통합 1세트) 한 번에 주는 쪽이 에이전트 왕복을 줄인다.\n */\nexport async function readNextjsExampleCode(): Promise<string> {\n const root = path.join(EXAMPLES_ROOT, \"nextjs\");\n const files = await walkFiles(root);\n const sections = await Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(root, rel), \"utf8\");\n return `--- FILE: ${rel} ---\\n${content}`;\n }),\n );\n return sections.join(\"\\n\\n\");\n}\n"],"mappings":"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","names":["readFile","z","z","readFile","z","readFile","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/api.ts","../src/automation-definitions.ts","../src/automation-media.ts","../src/automation-schemas.ts","../src/automation-tools.ts","../src/server.ts","../src/tools.ts","../src/docs.ts","../src/seo-tools.ts","../src/seo-definitions.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { runCmsCli } from \"./cli.js\";\nimport { createServer } from \"./server.js\";\n\nif (process.argv[2] === \"cli\") {\n await runCmsCli(process.argv);\n} else {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n","import { readFile } from \"node:fs/promises\";\n\nimport { Command, InvalidArgumentError } from \"commander\";\n\nimport { callAutomationTool } from \"./automation-tools.js\";\n\ntype CommonOptions = { readonly siteId?: string };\n\nexport function createCmsCli(): Command {\n const cli = new Command()\n .name(\"roottale-cms\")\n .description(\"RootTale CMS 글·썸네일·본문 이미지 자동화 CLI\");\n\n registerPostCommands(cli);\n registerMediaCommands(cli);\n return cli;\n}\n\nexport async function runCmsCli(argv: readonly string[]): Promise<void> {\n try {\n await createCmsCli().parseAsync([\n argv[0] ?? \"node\",\n \"roottale-cms\",\n ...argv.slice(3),\n ]);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 1;\n }\n}\n\nfunction registerPostCommands(cli: Command): void {\n const posts = cli.command(\"posts\").description(\"CMS 글 자동화\");\n\n posts\n .command(\"list\")\n .description(\"초안·예약·발행 글 목록 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--cursor <cursor>\", \"다음 페이지 커서\")\n .option(\"--type <type>\", \"post 또는 page\")\n .option(\"--status <status>\", \"draft, published, scheduled, private\")\n .action(async (options) => {\n await run(\"listManagedCmsPosts\", options);\n });\n\n posts\n .command(\"create\")\n .description(\"Tiptap JSON 본문으로 새 글 생성\")\n .requiredOption(\"--title <title>\", \"글 제목\")\n .requiredOption(\"--slug <slug>\", \"URL 슬러그\")\n .requiredOption(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--type <type>\", \"post 또는 page\", \"post\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--publish\", \"즉시 발행\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--category-ids <ids>\", \"쉼표로 구분한 카테고리 ID\")\n .option(\"--tag-ids <ids>\", \"쉼표로 구분한 태그 ID\")\n .action(async (options) => {\n await run(\"createCmsPost\", {\n ...options,\n bodyJson: await readJsonObject(options.bodyFile),\n categoryIds: commaSeparated(options.categoryIds),\n tagIds: commaSeparated(options.tagIds),\n bodyFile: undefined,\n });\n });\n\n posts\n .command(\"update\")\n .description(\"기존 글의 일부 필드 수정\")\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--title <title>\", \"글 제목\")\n .option(\"--slug <slug>\", \"URL 슬러그\")\n .option(\"--body-file <path>\", \"Tiptap 본문 JSON 파일\")\n .option(\"--excerpt <text>\", \"요약\")\n .option(\"--featured-media-id <id>\", \"썸네일 미디어 ID\")\n .option(\"--clear-featured-media\", \"썸네일 연결 해제\")\n .option(\"--scheduled-at <iso>\", \"예약 발행 ISO 시각\")\n .option(\"--clear-schedule\", \"예약 해제\")\n .action(async (postId: string, options) => {\n await run(\"updateCmsPost\", {\n postId,\n siteId: options.siteId,\n title: options.title,\n slug: options.slug,\n bodyJson: options.bodyFile\n ? await readJsonObject(options.bodyFile)\n : undefined,\n excerpt: options.excerpt,\n featuredMediaId: options.clearFeaturedMedia\n ? null\n : options.featuredMediaId,\n scheduledAt: options.clearSchedule ? null : options.scheduledAt,\n });\n });\n\n lifecycle(posts, \"publish\", \"글을 즉시 발행\", \"publishCmsPost\");\n lifecycle(posts, \"unpublish\", \"발행 글을 초안으로 전환\", \"unpublishCmsPost\");\n\n posts\n .command(\"set-terms\")\n .description(\"글의 카테고리 또는 태그 전체 교체\")\n .argument(\"<post-id>\", \"글 ID\")\n .requiredOption(\"--taxonomy <type>\", \"category 또는 tag\")\n .requiredOption(\"--term-ids <ids>\", \"쉼표로 구분한 term ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options) => {\n await run(\"setCmsPostTerms\", {\n postId,\n siteId: options.siteId,\n taxonomy: options.taxonomy,\n termIds: commaSeparated(options.termIds) ?? [],\n });\n });\n}\n\nfunction registerMediaCommands(cli: Command): void {\n const media = cli.command(\"media\").description(\"CMS 미디어 자동화\");\n\n media\n .command(\"list\")\n .description(\"미디어 라이브러리 조회\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--limit <number>\", \"조회 개수\", integer, 20)\n .option(\"--offset <number>\", \"조회 시작 위치\", integer, 0)\n .action(async (options) => {\n await run(\"listCmsMedia\", options);\n });\n\n media\n .command(\"upload\")\n .description(\"로컬 파일을 업로드하고 CMS 미디어로 등록\")\n .argument(\"<file>\", \"jpg, png, webp, gif 또는 pdf 파일\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--content-type <type>\", \"MIME type 직접 지정\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (filePath: string, options) => {\n await run(\"uploadCmsMedia\", { filePath, ...options });\n });\n\n media\n .command(\"update\")\n .description(\"미디어 대체 텍스트 또는 캡션 수정\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .option(\"--alt <text>\", \"대체 텍스트\")\n .option(\"--caption <text>\", \"캡션\")\n .action(async (mediaId: string, options) => {\n await run(\"updateCmsMedia\", { mediaId, ...options });\n });\n\n media\n .command(\"delete\")\n .description(\"미디어 파일과 라이브러리 항목 삭제\")\n .argument(\"<media-id>\", \"미디어 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (mediaId: string, options: CommonOptions) => {\n await run(\"deleteCmsMedia\", { mediaId, ...options });\n });\n}\n\nfunction lifecycle(\n posts: Command,\n commandName: string,\n description: string,\n toolName: \"publishCmsPost\" | \"unpublishCmsPost\",\n): void {\n posts\n .command(commandName)\n .description(description)\n .argument(\"<post-id>\", \"글 ID\")\n .option(\"--site-id <id>\", \"사이트 ID\")\n .action(async (postId: string, options: CommonOptions) => {\n await run(toolName, { postId, ...options });\n });\n}\n\nasync function run(\n name: Parameters<typeof callAutomationTool>[0],\n args: Record<string, unknown>,\n): Promise<void> {\n const definedArgs = Object.fromEntries(\n Object.entries(args).filter(([, value]) => value !== undefined),\n );\n const result = await callAutomationTool(name, definedArgs);\n process.stdout.write(`${JSON.stringify(result, null, 2)}\\n`);\n}\n\nasync function readJsonObject(filePath: string): Promise<Record<string, unknown>> {\n const parsed: unknown = JSON.parse(await readFile(filePath, \"utf8\"));\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new InvalidArgumentError(\"본문 파일은 Tiptap JSON 객체여야 합니다.\");\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction commaSeparated(value: string | undefined): string[] | undefined {\n if (value === undefined) return undefined;\n return value.split(\",\").map((item) => item.trim()).filter(Boolean);\n}\n\nfunction integer(value: string): number {\n const parsed = Number.parseInt(value, 10);\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError(\"0 이상의 정수를 입력하세요.\");\n }\n return parsed;\n}\n","import ky from \"ky\";\n\nconst DEFAULT_API_BASE = \"https://api.roottale.com\";\n\nexport class CmsAutomationError extends Error {\n readonly name = \"CmsAutomationError\";\n}\n\nexport type ApiRequest = {\n readonly method: \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\";\n readonly pathname: string;\n readonly query?: Readonly<Record<string, string | undefined>>;\n readonly body?: unknown;\n};\n\nfunction apiConfig(): { readonly apiKey: string; readonly baseUrl: string } {\n const apiKey = process.env.ROOTTALE_API_KEY;\n if (!apiKey) {\n throw new CmsAutomationError(\n \"ROOTTALE_API_KEY 환경변수가 없습니다. 어드민 설정 > 사이트 연결 키에서 read_write 키를 발급해 MCP env에 추가하세요.\",\n );\n }\n return {\n apiKey,\n baseUrl: (process.env.ROOTTALE_API_BASE ?? DEFAULT_API_BASE).replace(\n /\\/+$/,\n \"\",\n ),\n };\n}\n\nexport async function requestRootTaleApi(\n request: ApiRequest,\n): Promise<unknown> {\n const { apiKey, baseUrl } = apiConfig();\n const url = new URL(`${baseUrl}${request.pathname}`);\n for (const [key, value] of Object.entries(request.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n return ky(url, {\n method: request.method,\n headers: { authorization: `Bearer ${apiKey}` },\n json: request.body,\n retry: 0,\n timeout: 30_000,\n }).json();\n}\n\nexport async function putUpload(\n uploadUrl: string,\n bytes: Uint8Array,\n contentType: string,\n): Promise<void> {\n const body = new Uint8Array(bytes.byteLength);\n body.set(bytes);\n await ky.put(uploadUrl, {\n body: body.buffer,\n headers: { \"content-type\": contentType },\n retry: 0,\n timeout: 120_000,\n });\n}\n","type JsonSchemaProperty = Readonly<Record<string, unknown>>;\n\nexport const AUTOMATION_TOOL_DEFINITIONS = [\n tool(\"listManagedCmsPosts\", \"초안·예약·발행 글을 포함한 관리용 글 목록을 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n type: enumProperty([\"post\", \"page\"]),\n status: enumProperty([\"draft\", \"published\", \"scheduled\", \"private\"]),\n cursor: stringProperty(\"이전 응답의 next_cursor\"),\n }),\n tool(\n \"createCmsPost\",\n \"Tiptap bodyJson으로 글 또는 페이지를 생성합니다. 기본은 초안이며 publish=true면 즉시 발행합니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n type: enumProperty([\"post\", \"page\"]),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n publish: { type: \"boolean\" },\n scheduledAt: stringProperty(\"미래 ISO 8601 발행 시각\"),\n categoryIds: arrayProperty(\"카테고리 term id\"),\n tagIds: arrayProperty(\"태그 term id\"),\n },\n [\"title\", \"slug\", \"bodyJson\"],\n ),\n tool(\"updateCmsPost\", \"기존 CMS 글을 부분 수정합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n title: stringProperty(\"제목\"),\n slug: stringProperty(\"슬러그\"),\n bodyJson: { type: \"object\", description: \"Tiptap 문서 JSON\" },\n excerpt: nullableStringProperty(\"요약\"),\n featuredMediaId: nullableStringProperty(\"대표 이미지 media id\"),\n scheduledAt: nullableStringProperty(\"예약 시각\"),\n }, [\"postId\"]),\n tool(\"publishCmsPost\", \"초안 또는 예약 글을 즉시 발행하고 웹훅을 전송합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"unpublishCmsPost\", \"발행 글을 초안으로 되돌립니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"postId\"]),\n tool(\"setCmsPostTerms\", \"글의 카테고리 또는 태그 연결을 교체합니다.\", {\n postId: stringProperty(\"글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n taxonomy: enumProperty([\"category\", \"tag\"]),\n termIds: arrayProperty(\"term id\"),\n }, [\"postId\", \"taxonomy\", \"termIds\"]),\n tool(\"listCmsMedia\", \"CMS 미디어 라이브러리를 조회합니다.\", {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100\"),\n offset: numberProperty(\"0 이상\"),\n }),\n tool(\n \"uploadCmsMedia\",\n \"로컬 이미지/PDF 파일을 업로드하고 CMS media id와 공개 URL을 반환합니다.\",\n {\n filePath: stringProperty(\"로컬 파일 절대/상대 경로\"),\n siteId: stringProperty(\"사이트 ID\"),\n contentType: enumProperty([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ]),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n },\n [\"filePath\"],\n ),\n tool(\"updateCmsMedia\", \"CMS 미디어의 대체 텍스트 또는 캡션을 수정합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n alt: nullableStringProperty(\"대체 텍스트\"),\n caption: nullableStringProperty(\"캡션\"),\n }, [\"mediaId\"]),\n tool(\"deleteCmsMedia\", \"CMS 미디어를 R2와 라이브러리에서 삭제합니다.\", {\n mediaId: stringProperty(\"media id\"),\n siteId: stringProperty(\"사이트 ID\"),\n }, [\"mediaId\"]),\n] as const;\n\nexport type AutomationToolName =\n (typeof AUTOMATION_TOOL_DEFINITIONS)[number][\"name\"];\n\nexport function isAutomationToolName(name: string): name is AutomationToolName {\n return AUTOMATION_TOOL_DEFINITIONS.some(\n (toolDefinition) => toolDefinition.name === name,\n );\n}\n\nfunction tool<const TName extends string>(\n name: TName,\n description: string,\n properties: Readonly<Record<string, JsonSchemaProperty>>,\n required: readonly string[] = [],\n) {\n return {\n name,\n description,\n inputSchema: {\n type: \"object\",\n properties,\n ...(required.length > 0 ? { required } : {}),\n additionalProperties: false,\n },\n } as const;\n}\n\nfunction stringProperty(description: string): JsonSchemaProperty {\n return { type: \"string\", description };\n}\n\nfunction nullableStringProperty(description: string): JsonSchemaProperty {\n return { type: [\"string\", \"null\"], description };\n}\n\nfunction numberProperty(description: string): JsonSchemaProperty {\n return { type: \"number\", description };\n}\n\nfunction enumProperty(values: readonly string[]): JsonSchemaProperty {\n return { type: \"string\", enum: values };\n}\n\nfunction arrayProperty(description: string): JsonSchemaProperty {\n return { type: \"array\", items: { type: \"string\" }, description };\n}\n","import { readFile } from \"node:fs/promises\";\nimport { basename, extname, resolve } from \"node:path\";\n\nimport { z } from \"zod\";\n\nimport { CmsAutomationError, putUpload, requestRootTaleApi } from \"./api.js\";\nimport { UploadMediaInput } from \"./automation-schemas.js\";\n\nexport async function uploadCmsMedia(args: Record<string, unknown>): Promise<unknown> {\n const input = UploadMediaInput.parse(args);\n const filePath = resolve(input.filePath);\n const bytes = await readFile(filePath);\n const contentType = input.contentType ?? inferContentType(filePath);\n if (!contentType) {\n throw new CmsAutomationError(\n \"contentType을 추론할 수 없습니다. jpg/png/webp/gif/pdf 파일이 아니면 contentType을 지정하세요.\",\n );\n }\n const requested = z\n .object({\n upload_url: z.string().url(),\n r2_key: z.string().min(1),\n })\n .parse(\n await requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads\",\n body: {\n site_id: input.siteId,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n },\n }),\n );\n await putUpload(requested.upload_url, bytes, contentType);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/media/uploads/complete\",\n body: {\n site_id: input.siteId,\n r2_key: requested.r2_key,\n original_name: basename(filePath),\n content_type: contentType,\n size_bytes: bytes.byteLength,\n alt: input.alt,\n caption: input.caption,\n },\n });\n}\n\nfunction inferContentType(filePath: string): string | undefined {\n switch (extname(filePath).toLowerCase()) {\n case \".jpg\":\n case \".jpeg\":\n return \"image/jpeg\";\n case \".png\":\n return \"image/png\";\n case \".webp\":\n return \"image/webp\";\n case \".gif\":\n return \"image/gif\";\n case \".pdf\":\n return \"application/pdf\";\n default:\n return undefined;\n }\n}\n","import { z } from \"zod\";\n\nconst SiteId = z.string().min(1).optional();\nconst PostId = z.string().min(1);\nconst NullableText = z.string().nullable().optional();\n\nexport const CreatePostInput = z\n .object({\n siteId: SiteId,\n type: z.enum([\"post\", \"page\"]).optional(),\n title: z.string().min(1).max(500),\n slug: z.string().min(1).max(120),\n bodyJson: z.record(z.string(), z.unknown()),\n excerpt: z.string().max(1000).nullable().optional(),\n featuredMediaId: z.string().nullable().optional(),\n publish: z.boolean().optional(),\n scheduledAt: z.string().datetime({ offset: true }).optional(),\n categoryIds: z.array(z.string()).optional(),\n tagIds: z.array(z.string()).optional(),\n })\n .strict();\n\nexport const UpdatePostInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n title: z.string().min(1).max(500).optional(),\n slug: z.string().min(1).max(120).optional(),\n bodyJson: z.record(z.string(), z.unknown()).optional(),\n excerpt: NullableText,\n featuredMediaId: z.string().nullable().optional(),\n scheduledAt: z.string().datetime({ offset: true }).nullable().optional(),\n })\n .strict();\n\nexport const PostLifecycleInput = z\n .object({ postId: PostId, siteId: SiteId })\n .strict();\n\nexport const ListManagedPostsInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n status: z\n .enum([\"draft\", \"published\", \"scheduled\", \"private\"])\n .optional(),\n cursor: z.string().optional(),\n })\n .strict();\n\nexport const SetTermsInput = z\n .object({\n postId: PostId,\n siteId: SiteId,\n taxonomy: z.enum([\"category\", \"tag\"]),\n termIds: z.array(z.string()),\n })\n .strict();\n\nexport const UploadMediaInput = z\n .object({\n filePath: z.string().min(1),\n siteId: SiteId,\n contentType: z\n .enum([\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/gif\",\n \"application/pdf\",\n ])\n .optional(),\n alt: NullableText,\n caption: NullableText,\n })\n .strict();\n\nexport const ListMediaInput = z\n .object({\n siteId: SiteId,\n limit: z.number().int().min(1).max(100).optional(),\n offset: z.number().int().nonnegative().optional(),\n })\n .strict();\n\nexport const UpdateMediaInput = z\n .object({\n mediaId: z.string().min(1),\n siteId: SiteId,\n alt: NullableText,\n caption: NullableText,\n })\n .strict()\n .refine((input) => input.alt !== undefined || input.caption !== undefined, {\n message: \"alt 또는 caption 중 하나 이상이 필요합니다.\",\n });\n\nexport const DeleteMediaInput = z\n .object({ mediaId: z.string().min(1), siteId: SiteId })\n .strict();\n","import { CmsAutomationError, requestRootTaleApi } from \"./api.js\";\nimport {\n type AutomationToolName,\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n} from \"./automation-definitions.js\";\nimport { uploadCmsMedia } from \"./automation-media.js\";\nimport {\n CreatePostInput,\n DeleteMediaInput,\n ListManagedPostsInput,\n ListMediaInput,\n PostLifecycleInput,\n SetTermsInput,\n UpdateMediaInput,\n UpdatePostInput,\n} from \"./automation-schemas.js\";\n\nexport {\n AUTOMATION_TOOL_DEFINITIONS,\n isAutomationToolName,\n};\n\nexport async function callAutomationTool(\n name: AutomationToolName,\n args: Record<string, unknown>,\n): Promise<unknown> {\n switch (name) {\n case \"listManagedCmsPosts\": {\n const input = ListManagedPostsInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/posts\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n type: input.type,\n status: input.status,\n cursor: input.cursor,\n }),\n });\n }\n case \"createCmsPost\": {\n const input = CreatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"POST\",\n pathname: \"/v1/cms/posts\",\n body: {\n site_id: input.siteId,\n type: input.type,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n publish: input.publish,\n scheduled_at: input.scheduledAt,\n category_ids: input.categoryIds,\n tag_ids: input.tagIds,\n },\n });\n }\n case \"updateCmsPost\": {\n const input = UpdatePostInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}`,\n body: {\n site_id: input.siteId,\n title: input.title,\n slug: input.slug,\n body_json: input.bodyJson,\n excerpt: input.excerpt,\n featured_media_id: input.featuredMediaId,\n scheduled_at: input.scheduledAt,\n },\n });\n }\n case \"publishCmsPost\":\n case \"unpublishCmsPost\": {\n const input = PostLifecycleInput.parse(args);\n const action = name === \"publishCmsPost\" ? \"publish\" : \"unpublish\";\n return requestRootTaleApi({\n method: \"POST\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/${action}`,\n body: { site_id: input.siteId },\n });\n }\n case \"setCmsPostTerms\": {\n const input = SetTermsInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/posts/${encodeURIComponent(input.postId)}/terms`,\n body: {\n site_id: input.siteId,\n taxonomy: input.taxonomy,\n term_ids: input.termIds,\n },\n });\n }\n case \"listCmsMedia\": {\n const input = ListMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/media\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n offset: input.offset,\n }),\n });\n }\n case \"uploadCmsMedia\":\n return uploadCmsMedia(args);\n case \"updateCmsMedia\": {\n const input = UpdateMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"PATCH\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n body: {\n site_id: input.siteId,\n alt: input.alt,\n caption: input.caption,\n },\n });\n }\n case \"deleteCmsMedia\": {\n const input = DeleteMediaInput.parse(args);\n return requestRootTaleApi({\n method: \"DELETE\",\n pathname: `/v1/cms/media/${encodeURIComponent(input.mediaId)}`,\n query: optionalQuery({ site_id: input.siteId }),\n });\n }\n default:\n return assertNever(name);\n }\n}\n\nfunction optionalQuery(\n values: Readonly<Record<string, string | number | undefined>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(values)\n .filter((entry): entry is [string, string | number] => entry[1] !== undefined)\n .map(([key, value]) => [key, String(value)]),\n );\n}\n\nfunction assertNever(value: never): never {\n throw new CmsAutomationError(`Unknown automation tool: ${String(value)}`);\n}\n","import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\n\nimport { registerTools } from \"./tools.js\";\n\n// tsup define 으로 빌드타임 주입 — 비번들 실행(vitest 등)에서는 \"dev\".\ndeclare const __CMS_MCP_VERSION__: string | undefined;\nconst VERSION =\n typeof __CMS_MCP_VERSION__ === \"string\" ? __CMS_MCP_VERSION__ : \"dev\";\n\nexport const SERVER_INSTRUCTIONS = `\nroottale-cms-mcp는 RootTale CMS를 외부 사이트(주로 Next.js)에 연동하고\n글·썸네일·본문 이미지를 자동화하기 위한 문서·예시 코드·API tool을 제공합니다.\n\n지켜야 할 규칙:\n- RootTale 연동 관련 작업은 가장 먼저 listRootTaleDocs를 호출해 문서 목록을 파악하세요.\n- 연동 코드를 작성하기 전 반드시 readRootTaleNextjsExampleCode를 호출해 예시 코드를\n 참고한 후 그 패턴을 따라 작성하세요.\n- API 키(rtlk_cust_*)는 서버 전용입니다. NEXT_PUBLIC_* 등 브라우저로 노출되는\n 환경변수에 절대 넣지 마세요.\n- 글쓰기 전에 getSiteKnowledge로 브랜드 보이스와 용어 규칙을 확인하세요.\n- 새 글은 기본적으로 draft로 만들고, 사용자가 즉시 발행을 명시했을 때만\n publish=true 또는 publishCmsPost를 사용하세요.\n- 이미지는 uploadCmsMedia로 먼저 등록한 뒤 반환된 id를 featuredMediaId에,\n 반환된 URL을 Tiptap image 노드의 attrs.src에 사용하세요.\n- 글을 수정하거나 발행하기 전 analyzeCmsPostSeo로 SEO와 가독성을 확인하세요.\n- 개선할 글을 찾을 때 getCmsPostSeoScores를, 실제 유입 검색어를 고를 때\n getTopSeoKeywords를 사용하세요.\n- 이미 학습된 내용이더라도 본 서버의 문서로 더블체크 후 작업하세요.\n`.trim();\n\nexport function createServer(): Server {\n const server = new Server(\n { name: \"roottale-cms\", version: VERSION },\n { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS },\n );\n registerTools(server);\n return server;\n}\n","import type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n ErrorCode,\n ListToolsRequestSchema,\n McpError,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport {\n AUTOMATION_TOOL_DEFINITIONS,\n callAutomationTool,\n isAutomationToolName,\n} from \"./automation-tools.js\";\nimport { requestRootTaleApi } from \"./api.js\";\nimport { listDocs, readDoc, readNextjsExampleCode, searchDocs } from \"./docs.js\";\nimport {\n callSeoTool,\n isSeoToolName,\n SEO_TOOL_DEFINITIONS,\n} from \"./seo-tools.js\";\n\nconst ReadDocInput = z.object({ path: z.string().min(1) }).strict();\nconst SearchDocsInput = z.object({ query: z.string().min(1) }).strict();\nconst ListPostsInput = z\n .object({\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n cursor: z.string().optional(),\n })\n .strict();\nconst GetPostInput = z.object({ slugOrId: z.string().min(1) }).strict();\nconst GetSiteKnowledgeInput = z\n .object({ siteId: z.string().optional() })\n .strict();\n\nconst DOCUMENTATION_TOOL_DEFINITIONS = [\n {\n name: \"listRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체 목록(경로·제목·설명)을 반환합니다. \" +\n \"RootTale 연동 작업을 시작하기 전 가장 먼저 호출하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"readRootTaleDoc\",\n description:\n \"RootTale CMS 통합 문서 1개를 경로로 읽습니다. \" +\n \"경로는 listRootTaleDocs 가 반환한 path 값을 사용하세요.\",\n inputSchema: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"문서 경로 (예: blog.md)\" },\n },\n required: [\"path\"],\n additionalProperties: false,\n },\n },\n {\n name: \"searchRootTaleDocs\",\n description:\n \"RootTale CMS 통합 문서 전체에서 정규식으로 검색합니다 (대소문자 무시). \" +\n \"매치된 파일 경로·라인 번호·라인 내용을 반환합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", description: \"정규식 패턴 (예: revalidate|webhook)\" },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: \"readRootTaleNextjsExampleCode\",\n description:\n \"RootTale CMS를 외부 Next.js(App Router) 사이트에 연동하는 완전한 예시 코드 세트를 반환합니다 \" +\n \"(블로그 목록/상세, 웹훅 revalidate 라우트, RSS/사이트맵, 상담문의 서버 액션 등). \" +\n \"연동 코드를 작성하기 전 반드시 호출해 패턴을 따라 작성하세요.\",\n inputSchema: { type: \"object\", properties: {}, additionalProperties: false },\n },\n {\n name: \"listPublishedPosts\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts)로 발행된 글 목록을 조회합니다. \" +\n \"환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n limit: { type: \"number\", description: \"1-100, 기본 20\" },\n type: { type: \"string\", enum: [\"post\", \"page\"] },\n cursor: { type: \"string\", description: \"이전 응답의 next_cursor\" },\n },\n additionalProperties: false,\n },\n },\n {\n name: \"getPublishedPost\",\n description:\n \"연동 검증용 — RootTale 공개 CMS API(GET /v1/cms/public/posts/:identifier)로 발행된 글 1개를 \" +\n \"slug 또는 UUID로 조회합니다. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n slugOrId: { type: \"string\", description: \"글 slug 또는 UUID\" },\n },\n required: [\"slugOrId\"],\n additionalProperties: false,\n },\n },\n {\n name: \"getSiteKnowledge\",\n description:\n \"이 RootTale 사이트의 브랜드 보이스(어조·톤·화자)와 용어 규칙(금지어·교정어)을 조회합니다 \" +\n \"(GET /v1/cms/public/site-knowledge). 이 사이트에 맞는 글을 쓰기 전에 호출해 톤·표현을 맞추세요. \" +\n \"금지어는 쓰지 말고, 교정어는 권장 표현으로 바꿔 쓰세요. 환경변수 ROOTTALE_API_KEY(rtlk_cust_*)가 필요합니다.\",\n inputSchema: {\n type: \"object\",\n properties: {\n siteId: {\n type: \"string\",\n description: \"사이트 ID (선택, 미설정 시 기본 사이트)\",\n },\n },\n additionalProperties: false,\n },\n },\n] as const;\n\nexport const TOOL_DEFINITIONS = [\n ...DOCUMENTATION_TOOL_DEFINITIONS,\n ...AUTOMATION_TOOL_DEFINITIONS,\n ...SEO_TOOL_DEFINITIONS,\n] as const;\n\nfunction textResponse(text: string) {\n return { content: [{ type: \"text\" as const, text }] };\n}\n\nfunction jsonResponse(value: unknown) {\n return textResponse(JSON.stringify(value, null, 2));\n}\n\nexport function registerTools(server: Server): void {\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: TOOL_DEFINITIONS.map((t) => ({ ...t })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const args = (request.params.arguments ?? {}) as Record<string, unknown>;\n try {\n if (isAutomationToolName(request.params.name)) {\n return jsonResponse(\n await callAutomationTool(request.params.name, args),\n );\n }\n if (isSeoToolName(request.params.name)) {\n return jsonResponse(await callSeoTool(request.params.name, args));\n }\n switch (request.params.name) {\n case \"listRootTaleDocs\":\n return jsonResponse(await listDocs());\n case \"readRootTaleDoc\": {\n const input = ReadDocInput.parse(args);\n const content = await readDoc(input.path);\n if (content === null) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `문서를 찾을 수 없습니다: ${input.path} — listRootTaleDocs로 경로를 확인하세요.`,\n );\n }\n return textResponse(content);\n }\n case \"searchRootTaleDocs\": {\n const input = SearchDocsInput.parse(args);\n return jsonResponse(await searchDocs(input.query));\n }\n case \"readRootTaleNextjsExampleCode\":\n return textResponse(await readNextjsExampleCode());\n case \"listPublishedPosts\": {\n const input = ListPostsInput.parse(args);\n const params: Record<string, string> = {};\n if (input.limit) params.limit = String(input.limit);\n if (input.type) params.type = input.type;\n if (input.cursor) params.cursor = input.cursor;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/posts\",\n query: params,\n }),\n );\n }\n case \"getPublishedPost\": {\n const input = GetPostInput.parse(args);\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: `/v1/cms/public/posts/${encodeURIComponent(input.slugOrId)}`,\n }),\n );\n }\n case \"getSiteKnowledge\": {\n const input = GetSiteKnowledgeInput.parse(args);\n const params: Record<string, string> = {};\n if (input.siteId) params.site_id = input.siteId;\n return jsonResponse(\n await requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/public/site-knowledge\",\n query: params,\n }),\n );\n }\n default:\n throw new McpError(\n ErrorCode.InvalidParams,\n `Unknown RootTale tool: ${request.params.name}`,\n );\n }\n } catch (error) {\n if (error instanceof McpError) throw error;\n const message = error instanceof Error ? error.message : String(error);\n return { isError: true, content: [{ type: \"text\" as const, text: message }] };\n }\n });\n}\n","/**\n * 패키지에 번들된 통합 문서(`docs/`)·예시 코드(`examples/`) 로더.\n *\n * dist/index.js 와 src/docs.ts 모두 패키지 루트에서 한 단계 아래이므로\n * `..` 상대 경로 해석이 빌드 전후 동일하다. npm publish 시 `files` 에\n * docs/, examples/ 가 포함되어 npx 환경에서도 같은 구조로 설치된다.\n */\nimport { readdir, readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst PKG_ROOT = fileURLToPath(new URL(\"..\", import.meta.url));\nconst DOCS_ROOT = path.join(PKG_ROOT, \"docs\");\nconst EXAMPLES_ROOT = path.join(PKG_ROOT, \"examples\");\n\nexport interface DocEntry {\n /** docs/ 기준 상대 경로 (예: \"blog.md\"). */\n path: string;\n title: string;\n description: string;\n}\n\ninterface Frontmatter {\n title: string;\n description: string;\n}\n\n/** `---\\ntitle: ...\\ndescription: ...\\n---` 형태의 단순 frontmatter 파서. */\nexport function parseFrontmatter(markdown: string): Frontmatter {\n const result: Frontmatter = { title: \"\", description: \"\" };\n const match = markdown.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return result;\n for (const line of match[1].split(\"\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx === -1) continue;\n const key = line.slice(0, idx).trim();\n const value = line.slice(idx + 1).trim();\n if (key === \"title\") result.title = value;\n if (key === \"description\") result.description = value;\n }\n return result;\n}\n\nasync function walkFiles(root: string, dir = root): Promise<string[]> {\n const entries = await readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...(await walkFiles(root, full)));\n } else {\n files.push(path.relative(root, full));\n }\n }\n return files.sort();\n}\n\nexport async function listDocs(): Promise<DocEntry[]> {\n const files = (await walkFiles(DOCS_ROOT)).filter((f) => f.endsWith(\".md\"));\n return Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(DOCS_ROOT, rel), \"utf8\");\n const fm = parseFrontmatter(content);\n return { path: rel, title: fm.title || rel, description: fm.description };\n }),\n );\n}\n\n/** 경로 탈출(../) 방지 후 단일 문서 읽기. 없으면 null. */\nexport async function readDoc(relPath: string): Promise<string | null> {\n const resolved = path.resolve(DOCS_ROOT, relPath);\n if (!resolved.startsWith(DOCS_ROOT + path.sep)) return null;\n try {\n return await readFile(resolved, \"utf8\");\n } catch {\n return null;\n }\n}\n\nexport interface SearchMatch {\n path: string;\n line: number;\n text: string;\n}\n\n/** 모든 문서에 대해 정규식 검색 (대소문자 무시). 매치 라인을 반환. */\nexport async function searchDocs(query: string): Promise<SearchMatch[]> {\n const regex = new RegExp(query, \"i\");\n const matches: SearchMatch[] = [];\n for (const doc of await listDocs()) {\n const content = await readDoc(doc.path);\n if (!content) continue;\n content.split(\"\\n\").forEach((text, idx) => {\n if (regex.test(text)) {\n matches.push({ path: doc.path, line: idx + 1, text: text.trim() });\n }\n });\n }\n return matches;\n}\n\n/**\n * Next.js 통합 예시 코드 전체를 `--- FILE: <path> ---` 헤더로 이어붙여 반환.\n * 파일 수가 적어(통합 1세트) 한 번에 주는 쪽이 에이전트 왕복을 줄인다.\n */\nexport async function readNextjsExampleCode(): Promise<string> {\n const root = path.join(EXAMPLES_ROOT, \"nextjs\");\n const files = await walkFiles(root);\n const sections = await Promise.all(\n files.map(async (rel) => {\n const content = await readFile(path.join(root, rel), \"utf8\");\n return `--- FILE: ${rel} ---\\n${content}`;\n }),\n );\n return sections.join(\"\\n\\n\");\n}\n","import { z } from \"zod\";\n\nimport { requestRootTaleApi } from \"./api.js\";\nimport {\n isSeoToolName,\n SEO_TOOL_DEFINITIONS,\n type SeoToolName,\n} from \"./seo-definitions.js\";\n\nconst SiteIdInput = z.object({ siteId: z.string().optional() });\n\nconst TopKeywordsInput = SiteIdInput.extend({\n dateRange: z\n .enum([\n \"last_7_days\",\n \"last_30_days\",\n \"last_3_months\",\n \"last_6_months\",\n \"last_12_months\",\n ])\n .optional(),\n limit: z.number().int().min(1).max(100).optional(),\n}).strict();\n\nconst AnalyzePostInput = SiteIdInput.extend({\n postId: z.string().min(1),\n focusKeyword: z.string().max(200).optional(),\n}).strict();\n\nconst ScoresInput = SiteIdInput.extend({\n limit: z.number().int().min(1).max(100).optional(),\n type: z.enum([\"post\", \"page\"]).optional(),\n seoFilter: z.enum([\"bad\", \"ok\", \"good\", \"empty_keyword\"]).optional(),\n}).strict();\n\nexport { isSeoToolName, SEO_TOOL_DEFINITIONS };\n\nexport async function callSeoTool(\n name: SeoToolName,\n args: Record<string, unknown>,\n): Promise<unknown> {\n switch (name) {\n case \"getTopSeoKeywords\": {\n const input = TopKeywordsInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/seo/keywords\",\n query: optionalQuery({\n site_id: input.siteId,\n date_range: input.dateRange,\n limit: input.limit,\n }),\n });\n }\n case \"analyzeCmsPostSeo\": {\n const input = AnalyzePostInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname:\n `/v1/cms/seo/posts/${encodeURIComponent(input.postId)}/analysis`,\n query: optionalQuery({\n site_id: input.siteId,\n focus_keyword: input.focusKeyword,\n }),\n });\n }\n case \"getCmsPostSeoScores\": {\n const input = ScoresInput.parse(args);\n return requestRootTaleApi({\n method: \"GET\",\n pathname: \"/v1/cms/seo/scores\",\n query: optionalQuery({\n site_id: input.siteId,\n limit: input.limit,\n type: input.type,\n seo_filter: input.seoFilter,\n }),\n });\n }\n }\n}\n\nfunction optionalQuery(\n values: Readonly<Record<string, string | number | undefined>>,\n): Record<string, string> {\n return Object.fromEntries(\n Object.entries(values)\n .filter(\n (entry): entry is [string, string | number] =>\n entry[1] !== undefined,\n )\n .map(([key, value]) => [key, String(value)]),\n );\n}\n","type JsonSchemaProperty = Readonly<Record<string, unknown>>;\n\nconst DATE_RANGES = [\n \"last_7_days\",\n \"last_30_days\",\n \"last_3_months\",\n \"last_6_months\",\n \"last_12_months\",\n] as const;\n\nexport const SEO_TOOL_DEFINITIONS = [\n tool(\n \"getTopSeoKeywords\",\n \"Google Search Console에서 사이트의 상위 검색어와 클릭·노출·CTR·평균 순위를 조회합니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n dateRange: enumProperty(DATE_RANGES, \"조회 기간, 기본 last_30_days\"),\n limit: numberProperty(\"1-100, 기본 25\"),\n },\n ),\n tool(\n \"analyzeCmsPostSeo\",\n \"ROOT-ADMIN과 같은 규칙으로 글의 SEO와 한국어 가독성을 다시 검사합니다. 글을 수정하거나 발행하기 전에 사용하세요.\",\n {\n postId: stringProperty(\"분석할 글 ID\"),\n siteId: stringProperty(\"사이트 ID\"),\n focusKeyword: stringProperty(\"이번 분석에 사용할 집중 키워드\"),\n },\n [\"postId\"],\n ),\n tool(\n \"getCmsPostSeoScores\",\n \"발행된 글의 최신 SEO 점수와 등급을 조회합니다. 개선이 필요한 글이나 집중 키워드가 없는 글을 찾을 수 있습니다.\",\n {\n siteId: stringProperty(\"사이트 ID\"),\n limit: numberProperty(\"1-100, 기본 10\"),\n type: enumProperty([\"post\", \"page\"], \"콘텐츠 유형\"),\n seoFilter: enumProperty(\n [\"bad\", \"ok\", \"good\", \"empty_keyword\"],\n \"SEO 상태 필터\",\n ),\n },\n ),\n] as const;\n\nexport type SeoToolName = (typeof SEO_TOOL_DEFINITIONS)[number][\"name\"];\n\nexport function isSeoToolName(name: string): name is SeoToolName {\n return SEO_TOOL_DEFINITIONS.some((definition) => definition.name === name);\n}\n\nfunction tool<const TName extends string>(\n name: TName,\n description: string,\n properties: Readonly<Record<string, JsonSchemaProperty>>,\n required: readonly string[] = [],\n) {\n return {\n name,\n description,\n inputSchema: {\n type: \"object\",\n properties,\n ...(required.length > 0 ? { required } : {}),\n additionalProperties: false,\n },\n } as const;\n}\n\nfunction stringProperty(description: string): JsonSchemaProperty {\n return { type: \"string\", description };\n}\n\nfunction numberProperty(description: string): JsonSchemaProperty {\n return { type: \"number\", description };\n}\n\nfunction enumProperty(\n values: readonly string[],\n description: string,\n): JsonSchemaProperty {\n return { type: \"string\", enum: values, description };\n}\n"],"mappings":"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","names":["readFile","z","z","readFile","z","readFile","z","tool","stringProperty","enumProperty","numberProperty","z","optionalQuery","z"]}
|
package/docs/api-reference.md
CHANGED
|
@@ -332,7 +332,8 @@ settings.siteProfile.defaultOgImageUrl` 순으로 우선합니다. `logo_url`·
|
|
|
332
332
|
| `lead_kind` | | `patient`(기본) \| `sales` |
|
|
333
333
|
| `_redirect_url` | | 완료 후 redirect base (allowlist 검증) |
|
|
334
334
|
| `cf-turnstile-response` | | Cloudflare Turnstile 토큰. tenant-api에 `LEAD_INTAKE_TURNSTILE_SECRET_KEY`가 설정된 경우 필수 |
|
|
335
|
-
| `attr_landing_path`, `attr_rt_src`, `attr_utm_source`, `attr_utm_medium`, `attr_utm_campaign`, `attr_referrer`, `attr_first_touch_at` | | 유입 어트리뷰션 — CRM에 유입 경로 표시 (`inquiries.md` 참고). 그 외 `attr_*` 키는 무시 |
|
|
335
|
+
| `attr_landing_path`, `attr_rt_src`, `attr_utm_source`, `attr_utm_medium`, `attr_utm_campaign`, `attr_utm_term`, `attr_utm_content`, `attr_ad_click`, `attr_referrer`, `attr_first_touch_at`, `attr_last_touch_at`, `attr_last_touch_channel`, `attr_last_touch_source`, `attr_last_touch_medium`, `attr_last_touch_campaign`, `attr_last_touch_referrer` | | 유입 어트리뷰션 — CRM에 유입 경로 표시 (`inquiries.md` 참고). 그 외 `attr_*` 키는 무시 |
|
|
336
|
+
| `attr_journey` | | 방문 여정 JSON 원문(서버 zod 검증, 최대 30항목) — CRM "방문 여정" 패널에 표시 (`inquiries.md` 참고) |
|
|
336
337
|
| 기타 임의 필드 | | 최대 50개, 암호화 보관, CRM 상세 노출 |
|
|
337
338
|
|
|
338
339
|
응답: `302` redirect — 성공 `?ok=1`, 실패 `?err=<code>`
|
package/docs/blog.md
CHANGED
|
@@ -38,6 +38,15 @@ export default function BlogPage() {
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
> **카테고리 허브 라우트(`/blog/categories/{slug}`)를 구현하세요.**
|
|
42
|
+
> `showCategoryFilter` 의 카테고리 칩, `RootTaleBlogCategories`, `RootTaleBlogPost`
|
|
43
|
+
> 의 `breadcrumb` 카테고리 세그먼트가 모두 기본적으로 이 경로를 링크합니다
|
|
44
|
+
> (`defaultCategoryHref`). **단일 블로그 사이트도 예외가 아닙니다** — 이 라우트가
|
|
45
|
+
> 없으면 카테고리 칩·브레드크럼을 클릭했을 때 404 가 됩니다. 공지·블로그처럼
|
|
46
|
+
> 섹션을 나눈 사이트는 `{basePath}/categories/{slug}` 패턴(`collections.md`
|
|
47
|
+
> "URL이 어떻게 정해지나" 절)을 대신 씁니다. 참조 구현은 예시 코드
|
|
48
|
+
> `app/blog/categories/[slug]/page.tsx`(MCP tool `readRootTaleNextjsExampleCode`).
|
|
49
|
+
|
|
41
50
|
> **공지·블로그를 나눈 사이트(ADR-0060)는 목록을 반드시 섹션으로 스코프하세요.**
|
|
42
51
|
> `RootTaleBlogList` 는 기본적으로 **모든 글**을 렌더하므로, 섹션을 나눴는데
|
|
43
52
|
> `collection` 을 안 주면 공지 글이 `/blog` 목록에 섞여 나옵니다. 각 목록 페이지에서
|
|
@@ -134,6 +143,46 @@ export default async function PostPage({
|
|
|
134
143
|
경우 `@roottale/cms-core` 의 `extractFaqEntries(doc)` / `faqPageJsonLd(entries)`
|
|
135
144
|
로 동일한 JSON-LD 를 생성할 수 있습니다.
|
|
136
145
|
|
|
146
|
+
#### 저자 링크 + 브레드크럼 (avcd 구조)
|
|
147
|
+
|
|
148
|
+
발행 글에 `author_slug`(작가 아카이브 slug)가 있으면 `RootTaleBlogPost` 가
|
|
149
|
+
헤더 메타·저자 카드의 작성자 이름을 자동으로 `<a href="/blog/author/{slug}">`
|
|
150
|
+
로 감쌉니다. slug 가 없는 작가는 지금처럼 plain text 로 렌더됩니다. 링크
|
|
151
|
+
대상을 바꾸려면 `authorHref`:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<RootTaleBlogPost
|
|
155
|
+
apiKey={apiKey}
|
|
156
|
+
slugOrId={slug}
|
|
157
|
+
authorHref={(slug) => `/authors/${slug}`} // 기본 /blog/author/{slug}
|
|
158
|
+
/>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`breadcrumb` prop(opt-in, 기본 off)을 주면 시각 브레드크럼(홈 › 블로그 ›
|
|
162
|
+
카테고리 › 글 제목)을 본문 위에 렌더하고, `siteUrl` 을 함께 주면
|
|
163
|
+
`BreadcrumbList` JSON-LD 도 같이 emit합니다:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<RootTaleBlogPost
|
|
167
|
+
apiKey={apiKey}
|
|
168
|
+
slugOrId={slug}
|
|
169
|
+
breadcrumb={{ siteUrl: process.env.NEXT_PUBLIC_SITE_URL }}
|
|
170
|
+
// collections 를 함께 주면 "블로그" 세그먼트가 글의 소속 섹션 basePath로
|
|
171
|
+
// 자동 해석됩니다(공지 글 → /notice). showCategory:false 로 카테고리
|
|
172
|
+
// 세그먼트 생략 가능, categoryHref로 링크 override 가능.
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
마지막 항목(카테고리 링크, `showCategory` 기본 `true`)은 `defaultCategoryHref`
|
|
177
|
+
(`/blog/categories/{slug}`)를 씁니다 — **이 라우트를 구현해야 브레드크럼·
|
|
178
|
+
카테고리 칩 클릭이 404 가 나지 않습니다**(아래 "카테고리 허브 라우트" 참고).
|
|
179
|
+
|
|
180
|
+
> **마크업 변경 주의** — 헤더 발행일이 `<span data-rt-cms-meta="date">` 에서
|
|
181
|
+
> `<time dateTime="…" data-rt-cms-meta="date">` 로 바뀌었습니다. `class`·
|
|
182
|
+
> `data-rt-cms-meta` 속성은 그대로라 속성 선택자로 스타일링했다면 영향 없지만,
|
|
183
|
+
> 태그명(`span`)을 직접 선택자로 쓴 고객 CSS/스크립트가 있다면 `time` 으로
|
|
184
|
+
> 갱신하세요.
|
|
185
|
+
|
|
137
186
|
### 다국어 (ADR-0052 A안, W4-6 PR C1/C2)
|
|
138
187
|
|
|
139
188
|
번역 사이트(어드민에서 "번역 추가"로 언어판을 만든 글)는 `RootTaleBlogList`/
|
|
@@ -229,6 +278,7 @@ export async function getPost(slug: string) {
|
|
|
229
278
|
| `terms` | 분류 용어 배열 (`taxonomy: "category" \| "tag"`, `name`, `slug`) |
|
|
230
279
|
| `featuredImageUrl` | 대표 이미지 |
|
|
231
280
|
| `authorName` | 작성자 표시명 |
|
|
281
|
+
| `authorSlug` | 작가 아카이브 slug(`/blog/author/{slug}`) — 미발급 작가는 `null` |
|
|
232
282
|
| `metaJson` | 부가 메타 — `metaJson.seo`에 SEO 오버라이드 |
|
|
233
283
|
|
|
234
284
|
### 정적 경로 사전 생성 + 메타데이터
|
package/docs/inquiries.md
CHANGED
|
@@ -85,9 +85,12 @@ export async function submitContact(
|
|
|
85
85
|
| `leadKind` | | `patient`(기본) \| `sales` |
|
|
86
86
|
| `turnstileToken` | | Cloudflare Turnstile 토큰. tenant-api secret 검증을 켠 경우 전달 |
|
|
87
87
|
| `extras` | | 임의 추가 항목 (최대 50개, 암호화 보관, CRM 상세에 노출) |
|
|
88
|
-
| `attribution` | | 유입 first-touch — 아래 [유입 어트리뷰션](#유입-어트리뷰션-attribution) 참고 |
|
|
88
|
+
| `attribution` | | 유입 first-touch/last-touch — 아래 [유입 어트리뷰션](#유입-어트리뷰션-attribution) 참고 |
|
|
89
|
+
| `journey` | | 방문 여정 JSON 원문 — 아래 [방문 여정](#방문-여정-journey) 참고 |
|
|
89
90
|
|
|
90
91
|
`extras`에 개인정보가 담길 수 있으므로 폼의 동의 고지에 수집 항목을 반영하세요.
|
|
92
|
+
`journey`를 함께 보낼 때는 개인정보 수집·이용 동의 문구에 "사이트 이용 기록
|
|
93
|
+
(방문 페이지·상호작용)"을 추가하세요.
|
|
91
94
|
|
|
92
95
|
## 유입 어트리뷰션 (`attribution`)
|
|
93
96
|
|
|
@@ -134,12 +137,33 @@ const result = await submitInquiry({
|
|
|
134
137
|
| `landing_path` | 첫 방문 landing pathname |
|
|
135
138
|
| `rt_src` | 단축링크/QR 토큰 (`roottale.link` 경유 시) |
|
|
136
139
|
| `utm_source` / `utm_medium` / `utm_campaign` | UTM 파라미터 |
|
|
140
|
+
| `utm_term` / `utm_content` | UTM 파라미터(세부) |
|
|
141
|
+
| `ad_click` | 광고 클릭 유입 존재 플래그(boolean, true일 때만 존재) — gclid/fbclid 원값은 저장하지 않습니다 |
|
|
137
142
|
| `referrer` | 외부 referrer **호스트명** (raw URL 아님) |
|
|
138
143
|
| `first_touch_at` | first-touch 시각 (ISO 8601) |
|
|
144
|
+
| `last_touch_at` | 가장 최근 non-direct 방문 시각(ISO 8601) — first-touch와 별개로 갱신 |
|
|
145
|
+
| `last_touch_channel` | last-touch를 유발한 채널 — `rs`(단축링크/QR) \| `utm` \| `gcl` \| `rf` |
|
|
146
|
+
| `last_touch_source` / `last_touch_medium` / `last_touch_campaign` / `last_touch_referrer` | last-touch 시점의 UTM/referrer |
|
|
139
147
|
|
|
140
148
|
직접 HTTP 연동 시에는 같은 값을 `attr_landing_path`, `attr_rt_src`,
|
|
141
|
-
`attr_utm_source`, `attr_utm_medium`, `attr_utm_campaign`, `
|
|
142
|
-
`
|
|
149
|
+
`attr_utm_source`, `attr_utm_medium`, `attr_utm_campaign`, `attr_utm_term`,
|
|
150
|
+
`attr_utm_content`, `attr_ad_click`, `attr_referrer`, `attr_first_touch_at`,
|
|
151
|
+
`attr_last_touch_at`, `attr_last_touch_channel`, `attr_last_touch_source`,
|
|
152
|
+
`attr_last_touch_medium`, `attr_last_touch_campaign`,
|
|
153
|
+
`attr_last_touch_referrer` 폼 필드로 보내면 됩니다.
|
|
154
|
+
|
|
155
|
+
## 방문 여정 (`journey`)
|
|
156
|
+
|
|
157
|
+
문의 제출 직전까지의 방문 경로(페이지 이동·주요 전환 이벤트)를 CRM 상세의
|
|
158
|
+
"방문 여정" 패널에서 시간순으로 확인할 수 있습니다.
|
|
159
|
+
`@roottale/analytics-runtime`의 `readJourney()`로 읽은 sessionStorage 링버퍼를
|
|
160
|
+
`JSON.stringify`한 문자열을 그대로 `journey` 필드(직접 HTTP는 `attr_journey`
|
|
161
|
+
폼 필드)로 보내면 됩니다 — 파싱·검증은 서버가 담당합니다.
|
|
162
|
+
|
|
163
|
+
의료(`medical`)·법률(`legal`) 업종 문의는 서버가 자동으로 "마일스톤
|
|
164
|
+
모드"(이벤트 유형·시각만, 방문 페이지 정보 제거)로 강제 전환합니다 —
|
|
165
|
+
증상·질환 관심사가 노출되지 않도록 하는 안전장치이므로 별도 설정이 필요
|
|
166
|
+
없습니다.
|
|
143
167
|
|
|
144
168
|
## 에러 처리
|
|
145
169
|
|
package/docs/seo.md
CHANGED
|
@@ -73,12 +73,14 @@ export default sitemap;
|
|
|
73
73
|
작가 사이트맵(`/sitemap/authors.xml`)을 만들 수 있습니다. 어드민 **설정 > 블로그 >
|
|
74
74
|
사이트맵**에서 "작가 사이트맵"을 켜면 인덱스에 `authors` 섹션이 추가됩니다.
|
|
75
75
|
|
|
76
|
-
```
|
|
76
|
+
```tsx
|
|
77
77
|
// app/blog/author/[slug]/page.tsx
|
|
78
78
|
import { notFound } from "next/navigation";
|
|
79
|
-
import { fetchAuthors } from "@roottale/cms-client/server";
|
|
79
|
+
import { fetchAuthors, profilePageSchema } from "@roottale/cms-client/server";
|
|
80
80
|
import { RootTaleBlogList } from "@roottale/cms-renderer-next/server";
|
|
81
81
|
|
|
82
|
+
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL!;
|
|
83
|
+
|
|
82
84
|
export default async function AuthorArchive({
|
|
83
85
|
params,
|
|
84
86
|
}: {
|
|
@@ -89,8 +91,19 @@ export default async function AuthorArchive({
|
|
|
89
91
|
const author = authors.find((a) => a.slug === slug);
|
|
90
92
|
if (!author) notFound();
|
|
91
93
|
|
|
94
|
+
const jsonLd = profilePageSchema({
|
|
95
|
+
url: `${SITE_URL}/blog/author/${slug}`,
|
|
96
|
+
name: author.name,
|
|
97
|
+
image: author.imageUrl,
|
|
98
|
+
description: author.bio,
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
return (
|
|
93
102
|
<>
|
|
103
|
+
<script
|
|
104
|
+
type="application/ld+json"
|
|
105
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
106
|
+
/>
|
|
94
107
|
<h1>{author.name}</h1>
|
|
95
108
|
{author.bio ? <p>{author.bio}</p> : null}
|
|
96
109
|
{/* author= 로 그 작가의 발행 글만 렌더 */}
|
|
@@ -104,6 +117,31 @@ export default async function AuthorArchive({
|
|
|
104
117
|
(`GET /v1/cms/public/authors`).
|
|
105
118
|
- `RootTaleBlogList`에 `author={slug}` 를 주면 그 작가의 글만 가져옵니다
|
|
106
119
|
(`GET /v1/cms/public/posts?author={slug}`).
|
|
120
|
+
- 글 상세의 `articleSchema` 에 `authorUrl: ${SITE_URL}/blog/author/${post.authorSlug}`
|
|
121
|
+
를 넘기면 글의 author 엔티티가 이 작가 페이지의 `ProfilePage.mainEntity`
|
|
122
|
+
와 같은 `@id`(`{authorUrl}#person`)로 연결됩니다.
|
|
123
|
+
|
|
124
|
+
### 카테고리 허브 (`/blog/categories/{slug}`)
|
|
125
|
+
|
|
126
|
+
`RootTaleBlogList`의 카테고리 칩, `RootTaleBlogCategories`, `RootTaleBlogPost`
|
|
127
|
+
의 `breadcrumb` 카테고리 세그먼트가 모두 기본적으로 `/blog/categories/{slug}`
|
|
128
|
+
를 링크합니다(`defaultCategoryHref`) — **단일 블로그 사이트도 이 라우트를
|
|
129
|
+
구현해야** 링크가 404 나지 않습니다. 참조 구현은 예시 코드
|
|
130
|
+
`app/blog/categories/[slug]/page.tsx`(MCP tool `readRootTaleNextjsExampleCode`).
|
|
131
|
+
공지·블로그처럼 섹션을 나눈 사이트는 `{basePath}/categories/{slug}` 패턴
|
|
132
|
+
(`collections.md` "URL이 어떻게 정해지나" 절)을 대신 씁니다.
|
|
133
|
+
|
|
134
|
+
목록 페이지용 JSON-LD는 `collectionPageSchema`:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { collectionPageSchema } from "@roottale/cms-client/server";
|
|
138
|
+
|
|
139
|
+
const jsonLd = collectionPageSchema({
|
|
140
|
+
url: `${SITE_URL}/blog/categories/${category.slug}`,
|
|
141
|
+
name: `${category.name} 글 모음`,
|
|
142
|
+
siteUrl: SITE_URL,
|
|
143
|
+
});
|
|
144
|
+
```
|
|
107
145
|
|
|
108
146
|
### 다국어 (hreflang) — ADR-0052 A안, 콘텐츠 다국어
|
|
109
147
|
|
|
@@ -165,8 +203,27 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
165
203
|
|
|
166
204
|
## 브레드크럼 (BreadcrumbList)
|
|
167
205
|
|
|
168
|
-
사이트 구조를 검색엔진에 전달하고 검색결과에 경로가 표시됩니다.
|
|
169
|
-
|
|
206
|
+
사이트 구조를 검색엔진에 전달하고 검색결과에 경로가 표시됩니다.
|
|
207
|
+
|
|
208
|
+
**빠른 경로** — `RootTaleBlogPost` 를 쓰면 `breadcrumb` prop 하나로 시각
|
|
209
|
+
브레드크럼 + `BreadcrumbList` JSON-LD 를 함께 얻습니다(opt-in, 기본 off):
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
<RootTaleBlogPost
|
|
213
|
+
apiKey={apiKey}
|
|
214
|
+
slugOrId={slug}
|
|
215
|
+
breadcrumb={{ siteUrl: SITE_URL }} // siteUrl 없으면 시각 브레드크럼만
|
|
216
|
+
/>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
카테고리 세그먼트는 글의 첫 category term 을 자동으로 쓰고
|
|
220
|
+
(`showCategory: false` 로 생략 가능), 링크는 `defaultCategoryHref`
|
|
221
|
+
(`/blog/categories/{slug}`)를 씁니다 — `blog.md` "카테고리 허브 라우트" 참고.
|
|
222
|
+
자세한 옵션(`homeLabel`/`listHref`/`categoryHref`/`collections` 연동)은
|
|
223
|
+
`blog.md` "저자 링크 + 브레드크럼" 절 참고.
|
|
224
|
+
|
|
225
|
+
**커스텀 경로** — 직접 마크업하는 경우 `breadcrumbSchema` 로 JSON-LD 만
|
|
226
|
+
생성하세요(UI 브레드크럼과 구조 일치 권장):
|
|
170
227
|
|
|
171
228
|
```tsx
|
|
172
229
|
import { breadcrumbSchema } from "@roottale/cms-client/server";
|
|
@@ -225,6 +282,29 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
225
282
|
`siteUrl`/`path` 를 안 넘기면 override 가 있을 때만 canonical 출력.
|
|
226
283
|
- `seo.noindex`/`seo.nofollow` 중 하나라도 켜지면 `robots` 출력
|
|
227
284
|
- `openGraph`(type:`article`·`publishedTime`·`images`) 자동 구성
|
|
285
|
+
- **avcd 구조** — `siteUrl` 이 있으면 canonical 해석 성공 여부와 무관하게
|
|
286
|
+
`alternates.types["application/rss+xml"]`(기본 `/feed.xml`, `feedPath`
|
|
287
|
+
옵션으로 override)을 항상 emit합니다. `twitter`(`summary_large_image`)도
|
|
288
|
+
og 와 같은 소스(seo override → 글 값)로 항상 채워집니다 — `card`+`title`
|
|
289
|
+
은 og 이미지가 없어도 emit, `images` 는 og 이미지가 있을 때만.
|
|
290
|
+
- **avcd 구조 — og article 확장** — 입력에 `modified`(→ `openGraph.modifiedTime`)·
|
|
291
|
+
`section`(→ `openGraph.section`)·`tags`(문자열 배열 → `openGraph.tags`)·
|
|
292
|
+
`authors`(저자 프로필 URL 배열 → `openGraph.authors`)를 넘기면 그대로
|
|
293
|
+
추가됩니다. 전부 선택 — 미지정/빈 배열은 생략(기존 호출부 diff 0).
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
return buildPostMetadata(post, {
|
|
297
|
+
siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
|
|
298
|
+
path: `/blog/${post.slug}`,
|
|
299
|
+
modified: post.modified, // updatedAt ISO
|
|
300
|
+
section: post.category || undefined,
|
|
301
|
+
tags: post.tags.map((t) => t.name),
|
|
302
|
+
authors: post.authorSlug
|
|
303
|
+
? [`${SITE_URL}/blog/author/${post.authorSlug}`]
|
|
304
|
+
: undefined,
|
|
305
|
+
feedPath: "/feed.xml", // 기본값 — 다중 스트림이면 섹션별 피드로 override
|
|
306
|
+
});
|
|
307
|
+
```
|
|
228
308
|
|
|
229
309
|
입력은 `{ title, description?, date?, image?, seo? }` 구조면 됩니다(`getPost`
|
|
230
310
|
의 `BlogPostMeta` 호환). 원본 post 를 쓸 땐 `seo: (post.metaJson as {
|
|
@@ -372,12 +452,14 @@ const hits = await searchPosts({
|
|
|
372
452
|
|
|
373
453
|
| 함수 | 용도 |
|
|
374
454
|
|---|---|
|
|
375
|
-
| `articleSchema(input)` | 블로그 글 상세 페이지 Article |
|
|
455
|
+
| `articleSchema(input)` | 블로그 글 상세 페이지 Article/BlogPosting |
|
|
376
456
|
| `breadcrumbSchema(items)` | 빵부스러기 |
|
|
377
457
|
| `organizationSchema(input)` | 조직/사업체 |
|
|
378
458
|
| `localBusinessSchema(profile, opts)` | 사업장 LocalBusiness (로컬 SEO — 아래 섹션) |
|
|
379
459
|
| `websiteSchema(input)` | 웹사이트 |
|
|
380
460
|
| `faqSchema(items)` | FAQ |
|
|
461
|
+
| `profilePageSchema(input)` | 작가 프로필 페이지 (`/blog/author/{slug}`) |
|
|
462
|
+
| `collectionPageSchema(input)` | 글 목록/카테고리 허브 (`/blog`, `/blog/categories/{slug}`) |
|
|
381
463
|
|
|
382
464
|
```tsx
|
|
383
465
|
import { articleSchema } from "@roottale/cms-client/server";
|
|
@@ -396,6 +478,29 @@ const jsonLd = articleSchema({
|
|
|
396
478
|
/>;
|
|
397
479
|
```
|
|
398
480
|
|
|
481
|
+
**avcd 구조 — 확장 옵션(전부 선택)**:
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
const jsonLd = articleSchema({
|
|
485
|
+
type: "BlogPosting", // 기본 "Article" — 미지정 시 기존 출력 그대로
|
|
486
|
+
id: `${url}#article`, // 지정 시 @id emit
|
|
487
|
+
headline: post.title,
|
|
488
|
+
url,
|
|
489
|
+
image: post.featuredImageUrl ?? undefined,
|
|
490
|
+
imageWidth: 1200, // 둘 다 있으면 image 를 ImageObject 로 승격
|
|
491
|
+
imageHeight: 630,
|
|
492
|
+
authorName: post.authorName,
|
|
493
|
+
authorUrl: `${SITE_URL}/blog/author/${post.authorSlug}`, // author.@id 연결
|
|
494
|
+
publisherName: "예시 사이트",
|
|
495
|
+
publisherId: `${SITE_URL}#organization`,
|
|
496
|
+
section: category?.name, // → articleSection
|
|
497
|
+
inLanguage: "ko",
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`profilePageSchema`/`collectionPageSchema` 사용법은 각각 위 "작가 아카이브"·
|
|
502
|
+
"카테고리 허브" 절 참고.
|
|
503
|
+
|
|
399
504
|
저수준 RSS가 필요하면 `generateRssXml` / `rssItemsFromPosts`를 직접 사용할 수
|
|
400
505
|
있습니다.
|
|
401
506
|
|
|
@@ -8,9 +8,12 @@ import {
|
|
|
8
8
|
buildPostMetadata,
|
|
9
9
|
postRedirectPath,
|
|
10
10
|
} from "@roottale/cms-renderer-next/routes";
|
|
11
|
+
import { articleSchema } from "@roottale/cms-client/server";
|
|
11
12
|
|
|
12
13
|
import { getAllPosts, getPost } from "@/lib/blog";
|
|
13
14
|
|
|
15
|
+
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL!;
|
|
16
|
+
|
|
14
17
|
export const revalidate = 1800;
|
|
15
18
|
|
|
16
19
|
type Props = { params: Promise<{ slug: string }> };
|
|
@@ -24,10 +27,15 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
|
24
27
|
const { slug } = await params;
|
|
25
28
|
const post = await getPost(slug);
|
|
26
29
|
if (!post) return {};
|
|
27
|
-
// 어드민 SEO 패널(metaJson.seo) override + self-canonical
|
|
30
|
+
// 어드민 SEO 패널(metaJson.seo) override + self-canonical + avcd 구조(RSS
|
|
31
|
+
// alternate·Twitter Card·og article 확장)까지 1줄로. modified/section/tags 는
|
|
32
|
+
// 있는 만큼만 넘기면 됩니다 — 없으면 해당 필드만 생략.
|
|
28
33
|
return buildPostMetadata(post, {
|
|
29
|
-
siteUrl:
|
|
34
|
+
siteUrl: SITE_URL,
|
|
30
35
|
path: `/blog/${post.slug}`,
|
|
36
|
+
modified: post.modified,
|
|
37
|
+
section: post.category || undefined,
|
|
38
|
+
tags: post.tags.map((t) => t.name),
|
|
31
39
|
});
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -39,14 +47,41 @@ export default async function PostPage({ params }: Props) {
|
|
|
39
47
|
const redirect = postRedirectPath(post, slug);
|
|
40
48
|
if (redirect) permanentRedirect(redirect);
|
|
41
49
|
|
|
50
|
+
const url = `${SITE_URL}/blog/${post.slug}`;
|
|
51
|
+
// avcd 구조 — BlogPosting + 확장 옵션(전부 선택, 있는 값만 채우면 됩니다).
|
|
52
|
+
const jsonLd = articleSchema({
|
|
53
|
+
type: "BlogPosting",
|
|
54
|
+
id: `${url}#article`,
|
|
55
|
+
headline: post.title,
|
|
56
|
+
description: post.description,
|
|
57
|
+
url,
|
|
58
|
+
image: post.image ?? undefined,
|
|
59
|
+
datePublished: post.date,
|
|
60
|
+
dateModified: post.modified,
|
|
61
|
+
authorName: post.authorName,
|
|
62
|
+
authorUrl: post.authorSlug
|
|
63
|
+
? `${SITE_URL}/blog/author/${post.authorSlug}`
|
|
64
|
+
: undefined,
|
|
65
|
+
publisherName: "예시 사이트",
|
|
66
|
+
section: post.category || undefined,
|
|
67
|
+
inLanguage: "ko",
|
|
68
|
+
});
|
|
69
|
+
|
|
42
70
|
return (
|
|
43
71
|
<main>
|
|
72
|
+
<script
|
|
73
|
+
type="application/ld+json"
|
|
74
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
75
|
+
/>
|
|
44
76
|
<h1>{post.title}</h1>
|
|
45
77
|
<RootTaleBlogPost
|
|
46
78
|
apiKey={process.env.ROOTTALE_API_KEY!}
|
|
47
79
|
baseUrl={process.env.ROOTTALE_API_BASE}
|
|
48
80
|
slugOrId={post.id}
|
|
49
81
|
showTitle={false}
|
|
82
|
+
// opt-in — 시각 브레드크럼 + BreadcrumbList JSON-LD. siteUrl 없으면
|
|
83
|
+
// 시각 브레드크럼만(JSON-LD 미emit).
|
|
84
|
+
breadcrumb={{ siteUrl: SITE_URL }}
|
|
50
85
|
/>
|
|
51
86
|
</main>
|
|
52
87
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 카테고리 허브 — RootTaleBlogList/RootTaleBlogCategories(카테고리 칩·목록)와
|
|
2
|
+
// RootTaleBlogPost의 breadcrumb prop(카테고리 세그먼트) 기본 링크가 모두
|
|
3
|
+
// `/blog/categories/{slug}`를 가리킵니다(defaultCategoryHref). 이 라우트가
|
|
4
|
+
// 없으면 그 링크들을 클릭했을 때 404가 됩니다 — 단일 블로그 사이트도 예외
|
|
5
|
+
// 아닙니다. 공지·블로그를 나눈 섹션형 사이트는 `{basePath}/categories/{slug}`
|
|
6
|
+
// 패턴(collections.md 참고)을 대신 쓰세요.
|
|
7
|
+
import type { Metadata } from "next";
|
|
8
|
+
import Link from "next/link";
|
|
9
|
+
import { notFound } from "next/navigation";
|
|
10
|
+
import { collectionPageSchema } from "@roottale/cms-client/server";
|
|
11
|
+
|
|
12
|
+
import { getPostsByCategory } from "@/lib/blog";
|
|
13
|
+
|
|
14
|
+
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL!;
|
|
15
|
+
|
|
16
|
+
type Props = { params: Promise<{ slug: string }> };
|
|
17
|
+
|
|
18
|
+
export const revalidate = 1800;
|
|
19
|
+
|
|
20
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
21
|
+
const { slug } = await params;
|
|
22
|
+
const posts = await getPostsByCategory(slug);
|
|
23
|
+
if (posts.length === 0) return { title: "카테고리" };
|
|
24
|
+
return {
|
|
25
|
+
title: `${posts[0]!.category} 글 모음`,
|
|
26
|
+
alternates: { canonical: `/blog/categories/${slug}` },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default async function CategoryArchivePage({ params }: Props) {
|
|
31
|
+
const { slug } = await params;
|
|
32
|
+
const posts = await getPostsByCategory(slug);
|
|
33
|
+
if (posts.length === 0) notFound();
|
|
34
|
+
|
|
35
|
+
const categoryName = posts[0]!.category;
|
|
36
|
+
const url = `${SITE_URL}/blog/categories/${slug}`;
|
|
37
|
+
// 글 목록 페이지용 CollectionPage JSON-LD (articleSchema는 글 상세 전용).
|
|
38
|
+
const jsonLd = collectionPageSchema({
|
|
39
|
+
url,
|
|
40
|
+
name: `${categoryName} 글 모음`,
|
|
41
|
+
siteUrl: SITE_URL,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<main>
|
|
46
|
+
<script
|
|
47
|
+
type="application/ld+json"
|
|
48
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
49
|
+
/>
|
|
50
|
+
<h1>{categoryName}</h1>
|
|
51
|
+
<ul>
|
|
52
|
+
{posts.map((post) => (
|
|
53
|
+
<li key={post.id}>
|
|
54
|
+
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
|
|
55
|
+
</li>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
</main>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -20,10 +20,13 @@ export interface BlogPostMeta {
|
|
|
20
20
|
title: string;
|
|
21
21
|
description: string;
|
|
22
22
|
date: string; // publishedAt ISO
|
|
23
|
+
modified: string; // updatedAt ISO — avcd 구조 og:article:modified_time·dateModified
|
|
23
24
|
category: string;
|
|
25
|
+
categorySlug: string; // "" = 카테고리 미지정 — /blog/categories/{slug} 허브 필터링용
|
|
24
26
|
tags: { name: string; slug: string }[];
|
|
25
27
|
image: string | null;
|
|
26
28
|
authorName?: string;
|
|
29
|
+
authorSlug?: string | null; // /blog/author/{slug} — 미발급 작가는 null
|
|
27
30
|
seo?: {
|
|
28
31
|
title?: string;
|
|
29
32
|
description?: string;
|
|
@@ -43,13 +46,16 @@ function toMeta(post: CmsPostContent): BlogPostMeta {
|
|
|
43
46
|
title: post.title,
|
|
44
47
|
description: post.excerpt ?? "",
|
|
45
48
|
date: post.publishedAt ?? "",
|
|
49
|
+
modified: post.updatedAt,
|
|
46
50
|
category: category?.name ?? "",
|
|
51
|
+
categorySlug: category?.slug ?? "",
|
|
47
52
|
tags:
|
|
48
53
|
post.terms
|
|
49
54
|
?.filter((t) => t.taxonomy === "tag")
|
|
50
55
|
.map((t) => ({ name: t.name, slug: t.slug })) ?? [],
|
|
51
56
|
image: post.featuredImageUrl ?? null,
|
|
52
57
|
authorName: post.authorName ?? undefined,
|
|
58
|
+
authorSlug: post.authorSlug,
|
|
53
59
|
seo: meta.seo as BlogPostMeta["seo"],
|
|
54
60
|
};
|
|
55
61
|
}
|
|
@@ -71,3 +77,14 @@ export async function getPost(
|
|
|
71
77
|
if (!post) return null;
|
|
72
78
|
return { ...toMeta(post), bodyJson: post.bodyJson };
|
|
73
79
|
}
|
|
80
|
+
|
|
81
|
+
// 카테고리 허브(`/blog/categories/{slug}`)용 — RootTaleBlogList/RootTaleBlogCategories
|
|
82
|
+
// 의 기본 카테고리 링크가 이 경로를 가리키므로(defaultCategoryHref), 사이트는 이
|
|
83
|
+
// 라우트를 반드시 구현해야 합니다(안 하면 카테고리 칩·브레드크럼 클릭이 404).
|
|
84
|
+
// `/v1/cms/public/posts` 는 카테고리 서버 필터가 없어 클라이언트에서 거릅니다.
|
|
85
|
+
export async function getPostsByCategory(
|
|
86
|
+
categorySlug: string,
|
|
87
|
+
): Promise<BlogPostMeta[]> {
|
|
88
|
+
const posts = await getAllPosts();
|
|
89
|
+
return posts.filter((p) => p.categorySlug === categorySlug);
|
|
90
|
+
}
|