pabal-web-mcp 1.1.1 → 1.2.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/dist/bin/mcp-server.js +137 -173
- package/dist/index.d.ts +7 -2
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -1889,12 +1889,13 @@ async function handleInitProject(input) {
|
|
|
1889
1889
|
}
|
|
1890
1890
|
|
|
1891
1891
|
// src/tools/create-blog-html.ts
|
|
1892
|
-
import
|
|
1892
|
+
import fs8 from "fs";
|
|
1893
1893
|
import path8 from "path";
|
|
1894
1894
|
import { z as z5 } from "zod";
|
|
1895
1895
|
import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
1896
1896
|
|
|
1897
1897
|
// src/utils/blog.util.ts
|
|
1898
|
+
import fs7 from "fs";
|
|
1898
1899
|
import path7 from "path";
|
|
1899
1900
|
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
1900
1901
|
var BLOG_ROOT = "blogs";
|
|
@@ -1915,7 +1916,6 @@ function normalizeDate(date) {
|
|
|
1915
1916
|
}
|
|
1916
1917
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1917
1918
|
}
|
|
1918
|
-
var isKoreanLocale = (locale) => locale.trim().toLowerCase().startsWith("ko");
|
|
1919
1919
|
var toPublicBlogBase = (appSlug, slug) => `/${BLOG_ROOT}/${appSlug}/${slug}`;
|
|
1920
1920
|
function resolveCoverImagePath(appSlug, slug, coverImage) {
|
|
1921
1921
|
if (!coverImage || !coverImage.trim()) {
|
|
@@ -1931,20 +1931,6 @@ function resolveCoverImagePath(appSlug, slug, coverImage) {
|
|
|
1931
1931
|
}
|
|
1932
1932
|
return cleaned;
|
|
1933
1933
|
}
|
|
1934
|
-
function resolveRelativeImagePath(appSlug, slug, relativePath) {
|
|
1935
|
-
const raw = relativePath?.trim() || "./images/hero.png";
|
|
1936
|
-
const normalized = raw.replace(/^\.\//, "");
|
|
1937
|
-
return {
|
|
1938
|
-
raw,
|
|
1939
|
-
absolute: `${toPublicBlogBase(appSlug, slug)}/${normalized}`
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
function buildDescription(locale, topic, appSlug) {
|
|
1943
|
-
if (isKoreanLocale(locale)) {
|
|
1944
|
-
return `${topic}\uB97C \uC8FC\uC81C\uB85C ${appSlug}\uAC00 ASO\uC640 SEO\uB97C \uC5B4\uB5BB\uAC8C \uC5F0\uACB0\uD558\uACE0 \uBE14\uB85C\uADF8 \uD2B8\uB798\uD53D\uC744 \uC81C\uD488 \uD398\uC774\uC9C0\uB85C \uC774\uC5B4\uC8FC\uB294\uC9C0 \uC815\uB9AC\uD588\uC2B5\uB2C8\uB2E4.`;
|
|
1945
|
-
}
|
|
1946
|
-
return `How ${appSlug} teams turn "${topic}" into a bridge between ASO pages and SEO blogs without losing consistency.`;
|
|
1947
|
-
}
|
|
1948
1934
|
function deriveTags(topic, appSlug) {
|
|
1949
1935
|
const topicParts = topic.toLowerCase().split(/[^a-z0-9+]+/).filter(Boolean).slice(0, 6);
|
|
1950
1936
|
const set = /* @__PURE__ */ new Set([...topicParts, appSlug.toLowerCase(), "blog"]);
|
|
@@ -1958,9 +1944,14 @@ function buildBlogMeta(options) {
|
|
|
1958
1944
|
options.slug,
|
|
1959
1945
|
options.coverImage
|
|
1960
1946
|
);
|
|
1947
|
+
if (!options.description || !options.description.trim()) {
|
|
1948
|
+
throw new Error(
|
|
1949
|
+
"Description is required. The LLM must generate a meta description based on the topic and locale."
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1961
1952
|
return {
|
|
1962
1953
|
title: options.title,
|
|
1963
|
-
description: options.description
|
|
1954
|
+
description: options.description.trim(),
|
|
1964
1955
|
appSlug: options.appSlug,
|
|
1965
1956
|
slug: options.slug,
|
|
1966
1957
|
locale: options.locale,
|
|
@@ -1978,140 +1969,18 @@ function renderBlogMetaBlock(meta) {
|
|
|
1978
1969
|
${serialized}
|
|
1979
1970
|
-->`;
|
|
1980
1971
|
}
|
|
1981
|
-
function renderEnglishBody(args) {
|
|
1982
|
-
const {
|
|
1983
|
-
meta,
|
|
1984
|
-
topic,
|
|
1985
|
-
appSlug,
|
|
1986
|
-
includeRelativeImageExample,
|
|
1987
|
-
relativeImagePath
|
|
1988
|
-
} = args;
|
|
1989
|
-
const lines = [];
|
|
1990
|
-
lines.push(`<h1>${meta.title}</h1>`);
|
|
1991
|
-
lines.push(
|
|
1992
|
-
`<p>${appSlug} keeps product pages and blogs aligned. This article shows how to use "${topic}" as a shared story so ASO and SEO stay in sync.</p>`
|
|
1993
|
-
);
|
|
1994
|
-
if (includeRelativeImageExample && relativeImagePath) {
|
|
1995
|
-
lines.push(
|
|
1996
|
-
`<img src="${relativeImagePath.raw}" alt="${appSlug} ${topic} cover" />`
|
|
1997
|
-
);
|
|
1998
|
-
lines.push(
|
|
1999
|
-
`<p>The image above is stored next to this file and resolves to <code>${relativeImagePath.absolute}</code> when published.</p>`
|
|
2000
|
-
);
|
|
2001
|
-
}
|
|
2002
|
-
lines.push(`<h2>Why the gap appears</h2>`);
|
|
2003
|
-
lines.push(
|
|
2004
|
-
`<p>ASO pages are tuned for storefronts while SEO posts speak to search crawlers. Teams often duplicate work, drift on messaging, and miss internal links back to /products/${appSlug}.</p>`
|
|
2005
|
-
);
|
|
2006
|
-
lines.push(`<h3>Signals that drift</h3>`);
|
|
2007
|
-
lines.push(
|
|
2008
|
-
`<p>Different headlines, mismatched screenshots, and stale dates make ranking harder. "${topic}" is a strong bridge topic because it touches both acquisition paths.</p>`
|
|
2009
|
-
);
|
|
2010
|
-
lines.push(`<h2>How to bridge with ${appSlug}</h2>`);
|
|
2011
|
-
lines.push(
|
|
2012
|
-
`<p>Start with the product story, then reuse it in blog form. Keep the same core claim, swap storefront keywords for search intent, and reference the canonical product slug.</p>`
|
|
2013
|
-
);
|
|
2014
|
-
lines.push(`<h3>Mini playbook</h3>`);
|
|
2015
|
-
lines.push(
|
|
2016
|
-
`<ul>
|
|
2017
|
-
<li>Reuse the app store hero claim inside the intro.</li>
|
|
2018
|
-
<li>Map ASO keywords to SEO phrases for the "${topic}" angle.</li>
|
|
2019
|
-
<li>Link feature blurbs to product screenshots and changelog notes.</li>
|
|
2020
|
-
<li>Close with a CTA back to <code>/products/${appSlug}</code>.</li>
|
|
2021
|
-
</ul>`
|
|
2022
|
-
);
|
|
2023
|
-
lines.push(`<h2>Example flow to copy</h2>`);
|
|
2024
|
-
lines.push(
|
|
2025
|
-
`<p>Pick one release, outline how it helps with "${topic}", then add a short proof (metric, quote, or screenshot). Keep h2/h3 hierarchy stable so translations stay predictable.</p>`
|
|
2026
|
-
);
|
|
2027
|
-
lines.push(`<h2>Wrap up</h2>`);
|
|
2028
|
-
lines.push(
|
|
2029
|
-
`<p>${appSlug} keeps ASO and SEO talking to each other. Publish this HTML under <code>/blogs/${appSlug}/${meta.slug}/${meta.locale}.html</code> and link it from the product page so traffic flows both ways.</p>`
|
|
2030
|
-
);
|
|
2031
|
-
lines.push(
|
|
2032
|
-
`<p><strong>CTA:</strong> Explore the product page at <a href="/products/${appSlug}">/products/${appSlug}</a>.</p>`
|
|
2033
|
-
);
|
|
2034
|
-
return lines.join("\n\n");
|
|
2035
|
-
}
|
|
2036
|
-
function renderKoreanBody(args) {
|
|
2037
|
-
const {
|
|
2038
|
-
meta,
|
|
2039
|
-
topic,
|
|
2040
|
-
appSlug,
|
|
2041
|
-
includeRelativeImageExample,
|
|
2042
|
-
relativeImagePath
|
|
2043
|
-
} = args;
|
|
2044
|
-
const lines = [];
|
|
2045
|
-
lines.push(`<h1>${meta.title}</h1>`);
|
|
2046
|
-
lines.push(
|
|
2047
|
-
`<p>${appSlug}\uB294 \uC81C\uD488 \uD398\uC774\uC9C0\uC640 \uBE14\uB85C\uADF8\uC758 \uD750\uB984\uC774 \uB04A\uAE30\uC9C0 \uC54A\uB3C4\uB85D "${topic}"\uC744 \uAC19\uC740 \uC774\uC57C\uAE30\uB85C \uBB36\uC5B4\uB0C5\uB2C8\uB2E4. \uC774 \uAE00\uC740 ASO \uC2E0\uD638\uC640 SEO \uCF58\uD150\uCE20\uB97C \uD558\uB098\uC758 \uBA54\uC2DC\uC9C0\uB85C \uC5F0\uACB0\uD558\uB294 \uBC29\uBC95\uC744 \uC124\uBA85\uD569\uB2C8\uB2E4.</p>`
|
|
2048
|
-
);
|
|
2049
|
-
if (includeRelativeImageExample && relativeImagePath) {
|
|
2050
|
-
lines.push(
|
|
2051
|
-
`<img src="${relativeImagePath.raw}" alt="${appSlug} ${topic} \uD45C\uC9C0 \uC774\uBBF8\uC9C0" />`
|
|
2052
|
-
);
|
|
2053
|
-
lines.push(
|
|
2054
|
-
`<p>\uC704 \uC774\uBBF8\uC9C0\uB294 \uAE00\uACFC \uAC19\uC740 \uD3F4\uB354\uC5D0 \uC800\uC7A5\uB418\uC5B4 \uD37C\uBE14\uB9AC\uC2DC \uC2DC <code>${relativeImagePath.absolute}</code> \uACBD\uB85C\uB85C \uB178\uCD9C\uB429\uB2C8\uB2E4.</p>`
|
|
2055
|
-
);
|
|
2056
|
-
}
|
|
2057
|
-
lines.push(`<h2>ASO\uC640 SEO\uAC00 \uAC08\uB77C\uC9C0\uB294 \uC9C0\uC810</h2>`);
|
|
2058
|
-
lines.push(
|
|
2059
|
-
`<p>\uC2A4\uD1A0\uC5B4 \uCD5C\uC801\uD654\uB294 \uC804\uD658\uC5D0 \uC9D1\uC911\uD558\uACE0, \uBE14\uB85C\uADF8\uB294 \uAC80\uC0C9 \uB178\uCD9C\uC744 \uB178\uB9BD\uB2C8\uB2E4. \uAC19\uC740 \uC81C\uD488\uC774\uB77C\uB3C4 \uC81C\uBAA9, \uC2A4\uD06C\uB9B0\uC0F7, \uB0A0\uC9DC\uAC00 \uB2EC\uB77C\uC9C0\uBA74 \uC2E0\uB8B0\uB3C4\uAC00 \uB5A8\uC5B4\uC9C0\uACE0 /products/${appSlug}\uB85C \uC774\uC5B4\uC9C0\uB294 \uB9C1\uD06C\uB3C4 \uC57D\uD574\uC9D1\uB2C8\uB2E4.</p>`
|
|
2060
|
-
);
|
|
2061
|
-
lines.push(`<h3>\uD754\uD788 \uB193\uCE58\uB294 \uC2E0\uD638</h3>`);
|
|
2062
|
-
lines.push(
|
|
2063
|
-
`<p>\uC2A4\uD1A0\uC5B4 \uBA54\uC2DC\uC9C0\uC640 \uBE14\uB85C\uADF8 \uCE74\uD53C\uAC00 \uB530\uB85C \uB180\uAC70\uB098, \uCD9C\uC2DC \uB9E5\uB77D\uC774 \uBE60\uC9C0\uB294 \uACBD\uC6B0\uC785\uB2C8\uB2E4. "${topic}" \uAC19\uC740 \uC8FC\uC81C\uB294 \uB450 \uCC44\uB110\uC744 \uBAA8\uB450 \uAC74\uB4DC\uB9AC\uAE30 \uB54C\uBB38\uC5D0 \uC77C\uAD00\uC131\uC774 \uB354 \uC911\uC694\uD569\uB2C8\uB2E4.</p>`
|
|
2064
|
-
);
|
|
2065
|
-
lines.push(`<h2>${appSlug}\uB85C \uB2E4\uB9AC \uB193\uAE30</h2>`);
|
|
2066
|
-
lines.push(
|
|
2067
|
-
`<p>\uC81C\uD488 \uC774\uC57C\uAE30\uB97C \uBA3C\uC800 \uC815\uB9AC\uD558\uACE0 \uBE14\uB85C\uADF8 \uBC84\uC804\uC73C\uB85C \uC7AC\uC0AC\uC6A9\uD569\uB2C8\uB2E4. \uD575\uC2EC \uC8FC\uC7A5\uACFC \uC99D\uAC70\uB294 \uC720\uC9C0\uD558\uB418, \uAC80\uC0C9 \uC758\uB3C4\uC5D0 \uB9DE\uCDB0 \uD0A4\uC6CC\uB4DC\uC640 \uC608\uC2DC\uB97C \uC870\uC815\uD558\uACE0 \uC81C\uD488 \uC2AC\uB7EC\uADF8\uB97C \uD568\uAED8 \uB178\uCD9C\uD569\uB2C8\uB2E4.</p>`
|
|
2068
|
-
);
|
|
2069
|
-
lines.push(`<h3>\uC801\uC6A9 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8</h3>`);
|
|
2070
|
-
lines.push(
|
|
2071
|
-
`<ul>
|
|
2072
|
-
<li>\uC2A4\uD1A0\uC5B4 \uD5E4\uB4DC\uB77C\uC778\uC744 \uC778\uD2B8\uB85C\uC5D0 \uC7AC\uC0AC\uC6A9\uD558\uACE0 \uB3D9\uC77C\uD55C \uC8FC\uC7A5\uC73C\uB85C \uD480\uC5B4\uAC00\uAE30</li>
|
|
2073
|
-
<li>"${topic}"\uB97C \uC704\uD55C SEO \uD0A4\uC6CC\uB4DC\uB97C ASO \uD0A4\uC6CC\uB4DC\uC640 \uB9E4\uD551\uD558\uAE30</li>
|
|
2074
|
-
<li>\uC2E0\uAE30\uB2A5/\uC2A4\uD06C\uB9B0\uC0F7/\uBCC0\uACBD \uC0AC\uD56D\uC744 \uBE14\uB85C\uADF8 \uBCF8\uBB38\uC5D0 \uC9E7\uAC8C \uC5F0\uACB0\uD558\uAE30</li>
|
|
2075
|
-
<li>\uB9C8\uC9C0\uB9C9 \uBB38\uB2E8\uC5D0\uC11C <code>/products/${appSlug}</code>\uB85C \uC790\uC5F0\uC2A4\uB7EC\uC6B4 CTA \uBC30\uCE58</li>
|
|
2076
|
-
</ul>`
|
|
2077
|
-
);
|
|
2078
|
-
lines.push(`<h2>\uC0AC\uB840 \uD750\uB984 \uC608\uC2DC</h2>`);
|
|
2079
|
-
lines.push(
|
|
2080
|
-
`<p>\uCD5C\uADFC \uB9B4\uB9AC\uC2A4\uB97C \uD558\uB098 \uACE8\uB77C "${topic}"\uACFC \uC5B4\uB5BB\uAC8C \uB9DE\uBB3C\uB9AC\uB294\uC9C0 \uC124\uBA85\uD558\uACE0, \uC22B\uC790\xB7\uC778\uC6A9\xB7\uC2A4\uD06C\uB9B0\uC0F7 \uC911 \uD558\uB098\uB85C \uC99D\uAC70\uB97C \uB0A8\uAE30\uC138\uC694. h2/h3 \uAD6C\uC870\uB97C \uACE0\uC815\uD558\uBA74 \uB2E4\uAD6D\uC5B4 \uD655\uC7A5\uB3C4 \uC218\uC6D4\uD574\uC9D1\uB2C8\uB2E4.</p>`
|
|
2081
|
-
);
|
|
2082
|
-
lines.push(`<h2>\uB9C8\uBB34\uB9AC</h2>`);
|
|
2083
|
-
lines.push(
|
|
2084
|
-
`<p>${appSlug}\uB294 \uBE14\uB85C\uADF8\uC640 \uC81C\uD488 \uD398\uC774\uC9C0\uAC00 \uC11C\uB85C \uD2B8\uB798\uD53D\uC744 \uC8FC\uACE0\uBC1B\uB3C4\uB85D \uC124\uACC4\uD588\uC2B5\uB2C8\uB2E4. \uC774 HTML\uC744 <code>/blogs/${appSlug}/${meta.slug}/${meta.locale}.html</code> \uC704\uCE58\uC5D0 \uC800\uC7A5\uD558\uACE0 \uC81C\uD488 \uC0C1\uC138\uC5D0\uC11C \uB9C1\uD06C\uB97C \uAC78\uC5B4\uB450\uC138\uC694.</p>`
|
|
2085
|
-
);
|
|
2086
|
-
lines.push(
|
|
2087
|
-
`<p><strong>CTA:</strong> \uC81C\uD488 \uD398\uC774\uC9C0 <a href="/products/${appSlug}">/products/${appSlug}</a>\uC5D0\uC11C \uB354 \uC790\uC138\uD788 \uC0B4\uD3B4\uBCF4\uC138\uC694.</p>`
|
|
2088
|
-
);
|
|
2089
|
-
return lines.join("\n\n");
|
|
2090
|
-
}
|
|
2091
|
-
function renderBlogBody(options) {
|
|
2092
|
-
if (isKoreanLocale(options.meta.locale)) {
|
|
2093
|
-
return renderKoreanBody(options);
|
|
2094
|
-
}
|
|
2095
|
-
return renderEnglishBody(options);
|
|
2096
|
-
}
|
|
2097
1972
|
function buildBlogHtmlDocument(options) {
|
|
2098
1973
|
const metaBlock = renderBlogMetaBlock(options.meta);
|
|
2099
|
-
const body =
|
|
2100
|
-
meta: options.meta,
|
|
2101
|
-
topic: options.topic,
|
|
2102
|
-
appSlug: options.appSlug,
|
|
2103
|
-
includeRelativeImageExample: options.includeRelativeImageExample,
|
|
2104
|
-
relativeImagePath: options.relativeImagePath
|
|
2105
|
-
});
|
|
1974
|
+
const body = options.content.trim();
|
|
2106
1975
|
return `${metaBlock}
|
|
2107
1976
|
${body}`;
|
|
2108
1977
|
}
|
|
2109
|
-
function resolveTargetLocales(input
|
|
1978
|
+
function resolveTargetLocales(input) {
|
|
2110
1979
|
if (input.locales?.length) {
|
|
2111
1980
|
const locales = input.locales.map((loc) => loc.trim()).filter(Boolean);
|
|
2112
1981
|
return Array.from(new Set(locales));
|
|
2113
1982
|
}
|
|
2114
|
-
const fallback = input.locale?.trim()
|
|
1983
|
+
const fallback = input.locale?.trim();
|
|
2115
1984
|
return fallback ? [fallback] : [];
|
|
2116
1985
|
}
|
|
2117
1986
|
function getBlogOutputPaths(options) {
|
|
@@ -2125,6 +1994,63 @@ function getBlogOutputPaths(options) {
|
|
|
2125
1994
|
const publicBasePath = toPublicBlogBase(options.appSlug, options.slug);
|
|
2126
1995
|
return { baseDir, filePath, publicBasePath };
|
|
2127
1996
|
}
|
|
1997
|
+
function parseBlogHtml(htmlContent) {
|
|
1998
|
+
const metaBlockRegex = /<!--BLOG_META\s*\n([\s\S]*?)\n-->/;
|
|
1999
|
+
const match = htmlContent.match(metaBlockRegex);
|
|
2000
|
+
if (!match) {
|
|
2001
|
+
return { meta: null, body: htmlContent.trim() };
|
|
2002
|
+
}
|
|
2003
|
+
try {
|
|
2004
|
+
const metaJson = match[1].trim();
|
|
2005
|
+
const meta = JSON.parse(metaJson);
|
|
2006
|
+
const body = htmlContent.replace(metaBlockRegex, "").trim();
|
|
2007
|
+
return { meta, body };
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
return { meta: null, body: htmlContent.trim() };
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
function findExistingBlogPosts({
|
|
2013
|
+
appSlug,
|
|
2014
|
+
locale,
|
|
2015
|
+
publicDir,
|
|
2016
|
+
limit = 2
|
|
2017
|
+
}) {
|
|
2018
|
+
const blogAppDir = path7.join(publicDir, BLOG_ROOT, appSlug);
|
|
2019
|
+
if (!fs7.existsSync(blogAppDir)) {
|
|
2020
|
+
return [];
|
|
2021
|
+
}
|
|
2022
|
+
const posts = [];
|
|
2023
|
+
const subdirs = fs7.readdirSync(blogAppDir, { withFileTypes: true });
|
|
2024
|
+
for (const subdir of subdirs) {
|
|
2025
|
+
if (!subdir.isDirectory()) continue;
|
|
2026
|
+
const localeFile = path7.join(blogAppDir, subdir.name, `${locale}.html`);
|
|
2027
|
+
if (!fs7.existsSync(localeFile)) continue;
|
|
2028
|
+
try {
|
|
2029
|
+
const htmlContent = fs7.readFileSync(localeFile, "utf-8");
|
|
2030
|
+
const { meta, body } = parseBlogHtml(htmlContent);
|
|
2031
|
+
if (meta && meta.locale === locale) {
|
|
2032
|
+
posts.push({
|
|
2033
|
+
filePath: localeFile,
|
|
2034
|
+
meta,
|
|
2035
|
+
body,
|
|
2036
|
+
publishedAt: meta.publishedAt
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
posts.sort((a, b) => {
|
|
2044
|
+
const dateA = new Date(a.publishedAt).getTime();
|
|
2045
|
+
const dateB = new Date(b.publishedAt).getTime();
|
|
2046
|
+
return dateB - dateA;
|
|
2047
|
+
});
|
|
2048
|
+
return posts.slice(0, limit).map(({ filePath, meta, body }) => ({
|
|
2049
|
+
filePath,
|
|
2050
|
+
meta,
|
|
2051
|
+
body
|
|
2052
|
+
}));
|
|
2053
|
+
}
|
|
2128
2054
|
|
|
2129
2055
|
// src/tools/create-blog-html.ts
|
|
2130
2056
|
var toJsonSchema4 = zodToJsonSchema5;
|
|
@@ -2135,14 +2061,17 @@ var createBlogHtmlInputSchema = z5.object({
|
|
|
2135
2061
|
"English title used for slug (kebab-case). Falls back to topic when omitted."
|
|
2136
2062
|
),
|
|
2137
2063
|
topic: z5.string().trim().min(1, "topic is required").describe("Topic/angle to write about in the blog body"),
|
|
2138
|
-
locale: z5.string().trim().
|
|
2139
|
-
"Primary locale (
|
|
2064
|
+
locale: z5.string().trim().min(1, "locale is required").describe(
|
|
2065
|
+
"Primary locale (e.g., 'en-US', 'ko-KR'). Required to determine the language for blog content generation."
|
|
2140
2066
|
),
|
|
2141
2067
|
locales: z5.array(z5.string().trim().min(1)).optional().describe(
|
|
2142
|
-
"Optional list of locales to generate. Each locale gets its own HTML file."
|
|
2068
|
+
"Optional list of locales to generate. Each locale gets its own HTML file. If provided, locale parameter is ignored."
|
|
2069
|
+
),
|
|
2070
|
+
content: z5.string().trim().min(1, "content is required").describe(
|
|
2071
|
+
"HTML content for the blog body. You (the LLM) must generate this HTML content based on the topic and locale. Structure should follow the pattern in public/en-US.html: paragraphs (<p>), headings (<h2>, <h3>), images (<img>), lists (<ul>, <li>), horizontal rules (<hr>), etc. The content should be written in the language corresponding to the locale."
|
|
2143
2072
|
),
|
|
2144
|
-
description: z5.string().trim().
|
|
2145
|
-
"Meta description
|
|
2073
|
+
description: z5.string().trim().min(1, "description is required").describe(
|
|
2074
|
+
"Meta description for the blog post. You (the LLM) must generate this based on the topic and locale. Should be a concise summary of the blog content in the language corresponding to the locale."
|
|
2146
2075
|
),
|
|
2147
2076
|
tags: z5.array(z5.string().trim().min(1)).optional().describe(
|
|
2148
2077
|
"Optional tags for BLOG_META. Defaults to tags derived from topic."
|
|
@@ -2150,12 +2079,6 @@ var createBlogHtmlInputSchema = z5.object({
|
|
|
2150
2079
|
coverImage: z5.string().trim().optional().describe(
|
|
2151
2080
|
"Cover image path. Relative paths rewrite to /blogs/<app>/<slug>/..., default is /products/<appSlug>/og-image.png."
|
|
2152
2081
|
),
|
|
2153
|
-
includeRelativeImageExample: z5.boolean().optional().default(false).describe(
|
|
2154
|
-
"Inject a relative image example (./images/hero.png) into the body to demonstrate path rewriting."
|
|
2155
|
-
),
|
|
2156
|
-
relativeImagePath: z5.string().trim().optional().describe(
|
|
2157
|
-
"Override the relative image path (default ./images/hero.png)."
|
|
2158
|
-
),
|
|
2159
2082
|
publishedAt: z5.string().trim().regex(DATE_REGEX2, "publishedAt must use YYYY-MM-DD").optional().describe("Publish date (YYYY-MM-DD). Defaults to today."),
|
|
2160
2083
|
modifiedAt: z5.string().trim().regex(DATE_REGEX2, "modifiedAt must use YYYY-MM-DD").optional().describe("Last modified date (YYYY-MM-DD). Defaults to publishedAt."),
|
|
2161
2084
|
overwrite: z5.boolean().optional().default(false).describe("Overwrite existing files when true (default: false).")
|
|
@@ -2169,19 +2092,33 @@ var createBlogHtmlTool = {
|
|
|
2169
2092
|
name: "create-blog-html",
|
|
2170
2093
|
description: `Generate HTML blog posts under public/blogs/<appSlug>/<slug>/<locale>.html with a BLOG_META block.
|
|
2171
2094
|
|
|
2095
|
+
CRITICAL: WRITING STYLE CONSISTENCY
|
|
2096
|
+
Before generating content, you MUST:
|
|
2097
|
+
1. Read existing blog posts from public/blogs/<appSlug>/*/<locale>.html (use findExistingBlogPosts utility or read files directly)
|
|
2098
|
+
2. Analyze the writing style, tone, and format from 2 existing posts in the same locale
|
|
2099
|
+
3. Match that exact writing style when generating the new blog post content and description
|
|
2100
|
+
4. Maintain consistency in: paragraph structure, heading usage, tone, formality level, and overall format
|
|
2101
|
+
|
|
2102
|
+
IMPORTANT REQUIREMENTS:
|
|
2103
|
+
1. The 'locale' parameter is REQUIRED. If the user does not provide a locale, you MUST ask them to specify which language/locale they want to write the blog in (e.g., 'en-US', 'ko-KR', 'ja-JP', etc.).
|
|
2104
|
+
2. The 'content' parameter is REQUIRED. You (the LLM) must generate the HTML content based on the 'topic' and 'locale' provided by the user. The content should be written in the language corresponding to the locale AND match the writing style of existing blog posts for that locale.
|
|
2105
|
+
3. The 'description' parameter is REQUIRED. You (the LLM) must generate this based on the topic, locale, AND the writing style of existing blog posts.
|
|
2106
|
+
|
|
2172
2107
|
Slug rules:
|
|
2173
2108
|
- slug = slugify(English title, kebab-case ASCII)
|
|
2174
2109
|
- path: public/blogs/<appSlug>/<slug>/<locale>.html
|
|
2175
2110
|
- coverImage default: /products/<appSlug>/og-image.png (relative paths are rewritten under /blogs/<app>/<slug>/)
|
|
2176
2111
|
- overwrite defaults to false (throws when file exists)
|
|
2177
2112
|
|
|
2178
|
-
|
|
2179
|
-
-
|
|
2180
|
-
-
|
|
2181
|
-
-
|
|
2182
|
-
- Conclusion + CTA linking to /products/<appSlug>
|
|
2113
|
+
HTML Structure (follows public/en-US.html pattern):
|
|
2114
|
+
- BLOG_META block at the top with JSON metadata
|
|
2115
|
+
- HTML body content: paragraphs (<p>), headings (<h2>, <h3>), images (<img>), lists (<ul>, <li>), horizontal rules (<hr>), etc.
|
|
2116
|
+
- You must generate the HTML content based on the topic, making it relevant and engaging for the target locale's language, while maintaining consistency with existing blog posts.
|
|
2183
2117
|
|
|
2184
|
-
Supports multiple locales when locales[] is provided
|
|
2118
|
+
Supports multiple locales when locales[] is provided. Each locale gets its own HTML file. For each locale, you must:
|
|
2119
|
+
1. Read existing posts in that locale to understand the writing style
|
|
2120
|
+
2. Generate appropriate content in that locale's language
|
|
2121
|
+
3. Match the writing style and format of existing posts`,
|
|
2185
2122
|
inputSchema: inputSchema5
|
|
2186
2123
|
};
|
|
2187
2124
|
async function handleCreateBlogHtml(input) {
|
|
@@ -2193,20 +2130,36 @@ async function handleCreateBlogHtml(input) {
|
|
|
2193
2130
|
description,
|
|
2194
2131
|
tags,
|
|
2195
2132
|
coverImage,
|
|
2196
|
-
includeRelativeImageExample = false,
|
|
2197
|
-
relativeImagePath,
|
|
2198
2133
|
publishedAt,
|
|
2199
2134
|
modifiedAt,
|
|
2200
|
-
overwrite = false
|
|
2135
|
+
overwrite = false,
|
|
2136
|
+
content
|
|
2201
2137
|
} = input;
|
|
2138
|
+
if (!content || !content.trim()) {
|
|
2139
|
+
throw new Error(
|
|
2140
|
+
"Content is required. Please provide HTML content for the blog body based on the topic and locale."
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2202
2143
|
const resolvedTitle = title && title.trim() || topic.trim();
|
|
2203
2144
|
const slug = slugifyTitle(resolvedTitle);
|
|
2204
2145
|
const targetLocales = resolveTargetLocales(input);
|
|
2205
2146
|
if (!targetLocales.length) {
|
|
2206
|
-
throw new Error(
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
"Locale is required. Please specify which language/locale you want to write the blog in (e.g., 'en-US', 'ko-KR', 'ja-JP')."
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
const existingPostsByLocale = {};
|
|
2152
|
+
for (const locale of targetLocales) {
|
|
2153
|
+
const existingPosts = findExistingBlogPosts({
|
|
2154
|
+
appSlug,
|
|
2155
|
+
locale,
|
|
2156
|
+
publicDir,
|
|
2157
|
+
limit: 2
|
|
2158
|
+
});
|
|
2159
|
+
if (existingPosts.length > 0) {
|
|
2160
|
+
existingPostsByLocale[locale] = existingPosts;
|
|
2161
|
+
}
|
|
2207
2162
|
}
|
|
2208
|
-
const shouldIncludeRelativeImage = includeRelativeImageExample || Boolean(relativeImagePath);
|
|
2209
|
-
const relativeImage = shouldIncludeRelativeImage ? resolveRelativeImagePath(appSlug, slug, relativeImagePath) : void 0;
|
|
2210
2163
|
const output = {
|
|
2211
2164
|
slug,
|
|
2212
2165
|
baseDir: path8.join(publicDir, "blogs", appSlug, slug),
|
|
@@ -2223,7 +2176,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2223
2176
|
})
|
|
2224
2177
|
);
|
|
2225
2178
|
const existing = plannedFiles.filter(
|
|
2226
|
-
({ filePath }) =>
|
|
2179
|
+
({ filePath }) => fs8.existsSync(filePath)
|
|
2227
2180
|
);
|
|
2228
2181
|
if (existing.length > 0 && !overwrite) {
|
|
2229
2182
|
const existingList = existing.map((f) => f.filePath).join("\n- ");
|
|
@@ -2232,7 +2185,7 @@ async function handleCreateBlogHtml(input) {
|
|
|
2232
2185
|
- ${existingList}`
|
|
2233
2186
|
);
|
|
2234
2187
|
}
|
|
2235
|
-
|
|
2188
|
+
fs8.mkdirSync(output.baseDir, { recursive: true });
|
|
2236
2189
|
for (const locale of targetLocales) {
|
|
2237
2190
|
const { filePath } = getBlogOutputPaths({
|
|
2238
2191
|
appSlug,
|
|
@@ -2256,12 +2209,9 @@ async function handleCreateBlogHtml(input) {
|
|
|
2256
2209
|
output.metaByLocale[locale] = meta;
|
|
2257
2210
|
const html = buildBlogHtmlDocument({
|
|
2258
2211
|
meta,
|
|
2259
|
-
|
|
2260
|
-
appSlug,
|
|
2261
|
-
includeRelativeImageExample: shouldIncludeRelativeImage,
|
|
2262
|
-
relativeImagePath: relativeImage
|
|
2212
|
+
content
|
|
2263
2213
|
});
|
|
2264
|
-
|
|
2214
|
+
fs8.writeFileSync(filePath, html, "utf-8");
|
|
2265
2215
|
output.files.push({ locale, path: filePath });
|
|
2266
2216
|
}
|
|
2267
2217
|
const summaryLines = [
|
|
@@ -2273,11 +2223,25 @@ async function handleCreateBlogHtml(input) {
|
|
|
2273
2223
|
"Files:",
|
|
2274
2224
|
...output.files.map((file) => `- ${file.locale}: ${file.path}`)
|
|
2275
2225
|
];
|
|
2226
|
+
const styleReferenceInfo = [];
|
|
2227
|
+
for (const [locale, posts] of Object.entries(existingPostsByLocale)) {
|
|
2228
|
+
if (posts.length > 0) {
|
|
2229
|
+
styleReferenceInfo.push(
|
|
2230
|
+
`
|
|
2231
|
+
Writing style reference for ${locale}: Found ${posts.length} existing post(s) used for style consistency.`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
if (styleReferenceInfo.length === 0) {
|
|
2236
|
+
styleReferenceInfo.push(
|
|
2237
|
+
"\nNote: No existing blog posts found for style reference. This is the first post for this app/locale combination."
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2276
2240
|
return {
|
|
2277
2241
|
content: [
|
|
2278
2242
|
{
|
|
2279
2243
|
type: "text",
|
|
2280
|
-
text: summaryLines.join("\n")
|
|
2244
|
+
text: summaryLines.join("\n") + styleReferenceInfo.join("")
|
|
2281
2245
|
}
|
|
2282
2246
|
]
|
|
2283
2247
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -499,13 +499,18 @@ interface CreateBlogHtmlInput {
|
|
|
499
499
|
*/
|
|
500
500
|
topic: string;
|
|
501
501
|
/**
|
|
502
|
-
* Single locale to generate (
|
|
502
|
+
* Single locale to generate (REQUIRED). Ignored when locales[] is provided.
|
|
503
503
|
*/
|
|
504
|
-
locale
|
|
504
|
+
locale: string;
|
|
505
505
|
/**
|
|
506
506
|
* Optional list of locales to generate. Each gets its own HTML file.
|
|
507
507
|
*/
|
|
508
508
|
locales?: string[];
|
|
509
|
+
/**
|
|
510
|
+
* HTML content for the blog body. REQUIRED. The LLM must generate this based on the topic and locale.
|
|
511
|
+
* Structure should follow public/en-US.html pattern.
|
|
512
|
+
*/
|
|
513
|
+
content: string;
|
|
509
514
|
/**
|
|
510
515
|
* Meta description override. If absent, a locale-aware summary is generated from topic/appSlug.
|
|
511
516
|
*/
|