pabal-resource-mcp 1.10.6 → 1.10.7
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 +259 -20
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -1480,9 +1480,17 @@ ${json}
|
|
|
1480
1480
|
// src/tools/aso/utils/improve/generate-aso-prompt.util.ts
|
|
1481
1481
|
var ASO_RULES_SUMMARY = [
|
|
1482
1482
|
`Use ${ASO_OVERVIEW_DOC_PATH} for keyword strategy and ${FIELD_LIMITS_DOC_PATH} for hard limits.`,
|
|
1483
|
-
"`
|
|
1483
|
+
"Keyword source priority: product-level manual CSV files in `.aso/keywordResearch/products/[slug]/*.csv` first. Check the target country Store Domain first; if missing, translate/localize US CSV keywords for the target locale. Then use locale saved keyword research alongside the CSV to validate and fill remaining opportunities.",
|
|
1484
|
+
"Distribute important terms in this order: title first, subtitle second, keywords third.",
|
|
1485
|
+
"Assign tracked keywords by relevance and current rank: best keyword that fits 30 chars -> title; next best keyword that fits 30 chars -> subtitle; remaining high-value keywords within 100 chars -> keyword field.",
|
|
1486
|
+
"`aso.title`: keep app name + the most relevant high-priority keyword, usually `App Name: Primary Keyword`; remember `&`, `:`, and `-` count as 2 characters in title limits.",
|
|
1484
1487
|
"`aso.subtitle`: use important keywords that do not repeat the title.",
|
|
1485
|
-
"`aso.keywords`: comma-separated with commas only, no spaces, no duplicates, no title/subtitle repetition.",
|
|
1488
|
+
"`aso.keywords`: comma-separated with commas only, no spaces, no duplicates, no title/subtitle repetition; every word can appear in only one of title, subtitle, or keywords.",
|
|
1489
|
+
"Do not split multi-word keywords across fields; keep the full phrase in one field.",
|
|
1490
|
+
"Use singular forms only because Apple indexes plurals automatically.",
|
|
1491
|
+
"Exclude stop words that Apple ignores, such as `a`, `and`, `the`, `for`, `with`, `app`, and `to`.",
|
|
1492
|
+
"Exclude company/app names and inherited category terms, such as the app brand and categories like `Health & Fitness`.",
|
|
1493
|
+
"For US Store keyword suggestions, find suggestions for the app and only add keywords with popularity >25 and difficulty <75.",
|
|
1486
1494
|
"`aso.keywords`: use as much of the 100-character limit as possible with relevant keywords; do not leave meaningful capacity unused and do not use filler.",
|
|
1487
1495
|
'`aso.keywords`: split phrase intent into reusable single terms when appropriate, e.g. `sound,relaxing,rain` for "relaxing sound" + "rain sound".',
|
|
1488
1496
|
"`aso.keywords`: prefer singular forms, allow real searched misspellings, and order by importance.",
|
|
@@ -1512,19 +1520,24 @@ function generatePrimaryOptimizationPrompt(args) {
|
|
|
1512
1520
|
prompt += `## Task
|
|
1513
1521
|
|
|
1514
1522
|
`;
|
|
1515
|
-
prompt += `Optimize the PRIMARY locale (${primaryLocale}) using **saved keyword research
|
|
1523
|
+
prompt += `Optimize the PRIMARY locale (${primaryLocale}) using **manual CSV priority keywords first**, then saved keyword research + full ASO field optimization.
|
|
1516
1524
|
|
|
1517
1525
|
`;
|
|
1518
|
-
prompt += `## Step 1: Use
|
|
1526
|
+
prompt += `## Step 1: Use Keyword Sources (${primaryLocale})
|
|
1519
1527
|
|
|
1520
1528
|
`;
|
|
1521
1529
|
const researchSections = keywordResearchByLocale[primaryLocale] || [];
|
|
1522
1530
|
const researchDir = keywordResearchDirByLocale[primaryLocale];
|
|
1523
1531
|
if (researchSections.length > 0) {
|
|
1524
|
-
prompt += `**CRITICAL: Use ONLY the
|
|
1532
|
+
prompt += `**CRITICAL: Use ONLY the keyword sources below. Do NOT invent or research new keywords.**
|
|
1533
|
+
|
|
1534
|
+
`;
|
|
1535
|
+
prompt += `**Source priority:** If a "Manual Priority Keywords CSV" section exists, apply those keywords before generated keyword research. If the CSV section says it is using US fallback rows, translate/localize those US keywords into ${primaryLocale} before applying them. Still review the saved locale keyword research alongside the CSV to validate relevance and fill remaining relevant opportunities.
|
|
1525
1536
|
|
|
1526
1537
|
`;
|
|
1527
1538
|
prompt += `The research data includes:
|
|
1539
|
+
`;
|
|
1540
|
+
prompt += `- **Manual Priority Keywords CSV:** Human-curated product-level keyword list. Check target country rows first; if missing, use US rows as a translation source. Apply before saved research, while still considering saved research.
|
|
1528
1541
|
`;
|
|
1529
1542
|
prompt += `- **Tier 1 (Core):** Use these in title and subtitle - highest traffic, best opportunity
|
|
1530
1543
|
`;
|
|
@@ -1541,7 +1554,7 @@ function generatePrimaryOptimizationPrompt(args) {
|
|
|
1541
1554
|
prompt += `- **User Language Patterns:** Phrases real users use in reviews - incorporate naturally
|
|
1542
1555
|
|
|
1543
1556
|
`;
|
|
1544
|
-
prompt += `
|
|
1557
|
+
prompt += `Keyword sources:
|
|
1545
1558
|
${researchSections.join("\n")}
|
|
1546
1559
|
|
|
1547
1560
|
`;
|
|
@@ -1560,6 +1573,18 @@ ${researchSections.join("\n")}
|
|
|
1560
1573
|
`;
|
|
1561
1574
|
prompt += `**Apply keywords strategically based on tier priority:**
|
|
1562
1575
|
|
|
1576
|
+
`;
|
|
1577
|
+
prompt += `**Assignment rule:** Tag tracked keywords by relevance and current rank: best keyword that fits the 30-character title limit as \`title\`, next best keyword that fits the 30-character subtitle limit as \`subtitle\`, and remaining high-value keywords that fit the 100-character keyword field as \`keyword field\`.
|
|
1578
|
+
`;
|
|
1579
|
+
prompt += `**Ordering rule:** Place the strongest important terms on the left in every field. A word used in \`aso.title\`, \`aso.subtitle\`, or \`aso.keywords\` must not appear in either of the other two fields.
|
|
1580
|
+
`;
|
|
1581
|
+
prompt += `**Phrase rule:** Do not split multi-word keywords across fields. Keep the complete phrase together in one field.
|
|
1582
|
+
`;
|
|
1583
|
+
prompt += `**Cleanup rule:** Use singular forms, remove Apple-ignored stop words (a/and/the/for/with/app/to/etc.), and exclude the company/app name plus inherited category keywords such as "Health & Fitness".
|
|
1584
|
+
|
|
1585
|
+
`;
|
|
1586
|
+
prompt += `**US Store suggestion rule:** For US Store keyword suggestions, find suggestions for this app and only add keywords with popularity >25 and difficulty <75.
|
|
1587
|
+
|
|
1563
1588
|
`;
|
|
1564
1589
|
prompt += `### Tier 1 Keywords (Core) \u2192 Title & Subtitle
|
|
1565
1590
|
`;
|
|
@@ -1620,10 +1645,16 @@ ${researchSections.join("\n")}
|
|
|
1620
1645
|
prompt += `Check all limits using ${FIELD_LIMITS_DOC_PATH}: title \u226430, subtitle \u226430, shortDescription \u226480, keywords \u2264100, intro \u2264300, outro \u2264200
|
|
1621
1646
|
`;
|
|
1622
1647
|
prompt += `- Apply ${ASO_OVERVIEW_DOC_PATH}: keyword popularity \u226520 where available, achievable difficulty, relevance, likely user intent, singular forms, important keywords first
|
|
1648
|
+
`;
|
|
1649
|
+
prompt += `- Confirm title/subtitle/keywords priority placement: title gets the most important fitting keyword, subtitle gets the next important fitting non-overlapping keyword, keywords gets remaining non-overlapping terms
|
|
1650
|
+
`;
|
|
1651
|
+
prompt += `- Confirm multi-word keywords are not split across fields and stop words/category/company terms were excluded
|
|
1652
|
+
`;
|
|
1653
|
+
prompt += `- For US Store keyword suggestions, confirm every added suggestion has popularity >25 and difficulty <75
|
|
1623
1654
|
`;
|
|
1624
1655
|
prompt += `- Maximize keyword field utilization: target 90-100/100 chars when enough relevant keywords exist; explain any lower count
|
|
1625
1656
|
`;
|
|
1626
|
-
prompt += `- Remove keyword duplicates
|
|
1657
|
+
prompt += `- Remove keyword duplicates across all metadata fields: no duplicate terms inside keywords and no overlap between title, subtitle, and keywords; no spaces after commas
|
|
1627
1658
|
`;
|
|
1628
1659
|
prompt += `- Ensure App Store/Play Store rules from ${FIELD_LIMITS_DOC_PATH} are satisfied (no disallowed characters/formatting)
|
|
1629
1660
|
|
|
@@ -1637,11 +1668,15 @@ ${researchSections.join("\n")}
|
|
|
1637
1668
|
prompt += `## Output Format
|
|
1638
1669
|
|
|
1639
1670
|
`;
|
|
1640
|
-
prompt += `**1. Keyword Research (from
|
|
1671
|
+
prompt += `**1. Keyword Research (from provided sources)**
|
|
1641
1672
|
`;
|
|
1642
1673
|
prompt += ` - Cite file(s) used and list the selected top 10 keywords (no new research)
|
|
1643
1674
|
`;
|
|
1644
|
-
prompt += ` -
|
|
1675
|
+
prompt += ` - If a Manual Priority Keywords CSV was provided, cite it first, state whether target-country or US fallback rows were used, and explain how those keywords were placed before generated research keywords
|
|
1676
|
+
`;
|
|
1677
|
+
prompt += ` - Tag each selected tracked keyword as \`title\`, \`subtitle\`, or \`keyword field\` according to relevance/current rank and field limits
|
|
1678
|
+
`;
|
|
1679
|
+
prompt += ` - Rationale: why these 10 were chosen from the provided sources
|
|
1645
1680
|
|
|
1646
1681
|
`;
|
|
1647
1682
|
prompt += `**2. Optimized JSON** (complete ${primaryLocale} locale structure)
|
|
@@ -1669,7 +1704,9 @@ ${researchSections.join("\n")}
|
|
|
1669
1704
|
`;
|
|
1670
1705
|
prompt += ` - shortDescription: X/80 \u2713/\u2717
|
|
1671
1706
|
`;
|
|
1672
|
-
prompt += ` - keywords: X/100 \u2713/\u2717 (target 90-100 when possible; deduped \u2713/\u2717)
|
|
1707
|
+
prompt += ` - keywords: X/100 \u2713/\u2717 (target 90-100 when possible; deduped \u2713/\u2717; no title/subtitle/keywords overlap \u2713/\u2717)
|
|
1708
|
+
`;
|
|
1709
|
+
prompt += ` - phrase integrity / singular / stop-word / category-name checks: \u2713/\u2717
|
|
1673
1710
|
`;
|
|
1674
1711
|
prompt += ` - intro: X/300 \u2713/\u2717
|
|
1675
1712
|
`;
|
|
@@ -1784,7 +1821,7 @@ ${optimizedPrimary}
|
|
|
1784
1821
|
prompt += `## Keyword Research (Per Locale)
|
|
1785
1822
|
|
|
1786
1823
|
`;
|
|
1787
|
-
prompt += `**Priority:** Use each locale's own keyword research. English fallback is ONLY used when locale-specific research is missing.
|
|
1824
|
+
prompt += `**Priority:** Use product-level Manual Priority Keywords CSV first when present: check target-country Store Domain rows first; if missing, translate/localize US CSV rows for the target locale. Then review each locale's own keyword research alongside the CSV. English fallback research is ONLY used when locale-specific research is missing.
|
|
1788
1825
|
`;
|
|
1789
1826
|
prompt += `When both iOS and Android research exist for a locale, treat iOS keywords as primary; use Android keywords only if space remains after fitting iOS keywords within character limits.
|
|
1790
1827
|
|
|
@@ -1852,6 +1889,18 @@ ${optimizedPrimary}
|
|
|
1852
1889
|
|
|
1853
1890
|
`;
|
|
1854
1891
|
prompt += `For EACH locale:
|
|
1892
|
+
`;
|
|
1893
|
+
prompt += `- Source priority: Manual Priority Keywords CSV first (target-country rows, or translated/localized US rows if target-country rows are missing), locale saved keyword research second, translated fallback research third.
|
|
1894
|
+
`;
|
|
1895
|
+
prompt += `- Even when CSV exists, still review the locale saved keyword research and use it to validate choices and fill remaining relevant capacity.
|
|
1896
|
+
`;
|
|
1897
|
+
prompt += `- Metadata placement priority: best fitting keyword by relevance/current rank to title, next best fitting keyword to subtitle, remaining high-value keywords to keyword field.
|
|
1898
|
+
`;
|
|
1899
|
+
prompt += `- Do not repeat the same word across title, subtitle, and keywords. Do not split multi-word keywords across fields.
|
|
1900
|
+
`;
|
|
1901
|
+
prompt += `- Use singular forms, remove Apple-ignored stop words, and exclude company/app/category names.
|
|
1902
|
+
`;
|
|
1903
|
+
prompt += `- For US Store keyword suggestions, find suggestions for this app and only add keywords with popularity >25 and difficulty <75.
|
|
1855
1904
|
`;
|
|
1856
1905
|
prompt += `- Priority: Keep iOS-sourced keywords first; add Android keywords only if there is remaining space after iOS keywords fit within field limits.
|
|
1857
1906
|
`;
|
|
@@ -1873,7 +1922,13 @@ ${optimizedPrimary}
|
|
|
1873
1922
|
`;
|
|
1874
1923
|
prompt += ` - **Only replace the keyword part** - keep the app name and format structure unchanged
|
|
1875
1924
|
`;
|
|
1876
|
-
prompt += `4. Deduplicate keywords: final \`aso.keywords\` must be unique, comma-only without spaces, as close to 100 chars as possible, and
|
|
1925
|
+
prompt += `4. Deduplicate keywords: final \`aso.keywords\` must be unique, comma-only without spaces, as close to 100 chars as possible, and must not repeat any title/subtitle keyword terms
|
|
1926
|
+
`;
|
|
1927
|
+
prompt += ` - Do not split multi-word keywords between title/subtitle/keywords; assign the full phrase to a single field
|
|
1928
|
+
`;
|
|
1929
|
+
prompt += ` - Use singular forms only; remove stop words such as a/and/the/for/with/app/to; exclude app/company/category names
|
|
1930
|
+
`;
|
|
1931
|
+
prompt += ` - For US Store keyword suggestions, only add suggestions with popularity >25 and difficulty <75
|
|
1877
1932
|
`;
|
|
1878
1933
|
prompt += `5. **Replace keywords in existing sentences** - swap ONLY the keywords, keep everything else:
|
|
1879
1934
|
`;
|
|
@@ -1938,7 +1993,7 @@ ${optimizedPrimary}
|
|
|
1938
1993
|
`;
|
|
1939
1994
|
prompt += `Process EACH locale in this batch sequentially:
|
|
1940
1995
|
`;
|
|
1941
|
-
prompt += `1. Use saved keyword research (in target language) OR **TRANSLATE English
|
|
1996
|
+
prompt += `1. Use Manual Priority Keywords CSV first when present. If target-country CSV rows are missing and US CSV rows are shown, **TRANSLATE/LOCALIZE those US CSV keywords** to the target locale before applying them. Then use saved keyword research (in target language), OR **TRANSLATE English fallback research from primary locale** if missing (see fallback strategy above - MUST translate, not use English directly)
|
|
1942
1997
|
`;
|
|
1943
1998
|
prompt += `2. **Replace keywords ONLY** in ALL fields (keep existing content structure unchanged):
|
|
1944
1999
|
`;
|
|
@@ -1960,7 +2015,9 @@ ${optimizedPrimary}
|
|
|
1960
2015
|
`;
|
|
1961
2016
|
prompt += `3. **CRITICAL**: Ensure ALL landing fields are translated (not English)
|
|
1962
2017
|
`;
|
|
1963
|
-
prompt += `4. After swapping keywords, validate ASO basics (${ASO_OVERVIEW_DOC_PATH}) + limits/store rules (${FIELD_LIMITS_DOC_PATH}) + keyword utilization (target 90-100/100 when enough relevant keywords exist) + keyword duplication (unique list;
|
|
2018
|
+
prompt += `4. After swapping keywords, validate ASO basics (${ASO_OVERVIEW_DOC_PATH}) + limits/store rules (${FIELD_LIMITS_DOC_PATH}) + keyword utilization (target 90-100/100 when enough relevant keywords exist) + keyword duplication (unique list; no overlap between title/subtitle/keywords; no spaces after commas)
|
|
2019
|
+
`;
|
|
2020
|
+
prompt += ` - Also validate no multi-word keyword was split across fields, singular forms are used, stop words are removed, and company/app/category names are excluded
|
|
1964
2021
|
`;
|
|
1965
2022
|
prompt += `5. **SAVE the updated JSON to file** using save-locale-file tool
|
|
1966
2023
|
`;
|
|
@@ -2001,10 +2058,14 @@ ${optimizedPrimary}
|
|
|
2001
2058
|
|
|
2002
2059
|
`;
|
|
2003
2060
|
prompt += `**1. Keyword Source**
|
|
2061
|
+
`;
|
|
2062
|
+
prompt += ` - If Manual Priority Keywords CSV exists: Cite it first; state whether target-country rows or translated US fallback rows were used; list selected CSV keywords and where they were placed
|
|
2004
2063
|
`;
|
|
2005
2064
|
prompt += ` - If saved research exists: Cite file(s) used; list selected top 10 keywords (in target language)
|
|
2006
2065
|
`;
|
|
2007
2066
|
prompt += ` - If using fallback: List **TRANSLATED** keywords from primary locale (English \u2192 target language) with translation rationale
|
|
2067
|
+
`;
|
|
2068
|
+
prompt += ` - Tag each selected tracked keyword as \`title\`, \`subtitle\`, or \`keyword field\` according to relevance/current rank and field limits
|
|
2008
2069
|
`;
|
|
2009
2070
|
prompt += ` - Show final 10 keywords **IN TARGET LANGUAGE** with tier assignments - DO NOT show English keywords
|
|
2010
2071
|
|
|
@@ -2040,7 +2101,9 @@ ${optimizedPrimary}
|
|
|
2040
2101
|
`;
|
|
2041
2102
|
prompt += ` - shortDescription: X/80 \u2713/\u2717
|
|
2042
2103
|
`;
|
|
2043
|
-
prompt += ` - keywords: X/100 \u2713/\u2717 (target 90-100 when possible; deduped \u2713/\u2717; comma-only/no spaces \u2713/\u2717;
|
|
2104
|
+
prompt += ` - keywords: X/100 \u2713/\u2717 (target 90-100 when possible; deduped \u2713/\u2717; comma-only/no spaces \u2713/\u2717; no title/subtitle/keywords overlap \u2713/\u2717)
|
|
2105
|
+
`;
|
|
2106
|
+
prompt += ` - phrase integrity / singular / stop-word / category-name checks: \u2713/\u2717
|
|
2044
2107
|
`;
|
|
2045
2108
|
prompt += ` - intro: X/300 \u2713/\u2717
|
|
2046
2109
|
`;
|
|
@@ -2094,6 +2157,168 @@ validate-aso(slug="${slug}")
|
|
|
2094
2157
|
// src/tools/aso/utils/improve/load-keyword-research.util.ts
|
|
2095
2158
|
import fs6 from "fs";
|
|
2096
2159
|
import path6 from "path";
|
|
2160
|
+
var LOCALE_STORE_DOMAIN_OVERRIDES = {
|
|
2161
|
+
ar: "sa",
|
|
2162
|
+
en: "us",
|
|
2163
|
+
"en-US": "us",
|
|
2164
|
+
"zh-Hans": "cn",
|
|
2165
|
+
"zh-Hant": "tw"
|
|
2166
|
+
};
|
|
2167
|
+
function parseCsvLine(line) {
|
|
2168
|
+
const values = [];
|
|
2169
|
+
let current = "";
|
|
2170
|
+
let isQuoted = false;
|
|
2171
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
2172
|
+
const char = line[index];
|
|
2173
|
+
const nextChar = line[index + 1];
|
|
2174
|
+
if (char === '"' && isQuoted && nextChar === '"') {
|
|
2175
|
+
current += '"';
|
|
2176
|
+
index += 1;
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
if (char === '"') {
|
|
2180
|
+
isQuoted = !isQuoted;
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
if (char === "," && !isQuoted) {
|
|
2184
|
+
values.push(current);
|
|
2185
|
+
current = "";
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
current += char;
|
|
2189
|
+
}
|
|
2190
|
+
values.push(current);
|
|
2191
|
+
return values;
|
|
2192
|
+
}
|
|
2193
|
+
function getLocaleStoreDomain(locale) {
|
|
2194
|
+
const override = LOCALE_STORE_DOMAIN_OVERRIDES[locale];
|
|
2195
|
+
if (override) {
|
|
2196
|
+
return override;
|
|
2197
|
+
}
|
|
2198
|
+
const localeParts = locale.split("-");
|
|
2199
|
+
return (localeParts[1] || localeParts[0] || "").toLowerCase();
|
|
2200
|
+
}
|
|
2201
|
+
function loadManualKeywordCsvRowsForStoreDomain(slug, storeDomain) {
|
|
2202
|
+
const productResearchDir = path6.join(getKeywordResearchDir(), "products", slug);
|
|
2203
|
+
if (!fs6.existsSync(productResearchDir)) {
|
|
2204
|
+
return [];
|
|
2205
|
+
}
|
|
2206
|
+
const csvFiles = fs6.readdirSync(productResearchDir).filter((file) => file.endsWith(".csv")).sort();
|
|
2207
|
+
if (csvFiles.length === 0) {
|
|
2208
|
+
return [];
|
|
2209
|
+
}
|
|
2210
|
+
const rows = [];
|
|
2211
|
+
for (const file of csvFiles) {
|
|
2212
|
+
const filePath = path6.join(productResearchDir, file);
|
|
2213
|
+
const raw = fs6.readFileSync(filePath, "utf-8").trim();
|
|
2214
|
+
if (!raw) {
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
const [headerLine, ...dataLines] = raw.split(/\r?\n/);
|
|
2218
|
+
if (!headerLine) {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
const headers = parseCsvLine(headerLine).map((header) => header.trim());
|
|
2222
|
+
const getValue = (values, header) => {
|
|
2223
|
+
const index = headers.indexOf(header);
|
|
2224
|
+
return index >= 0 ? values[index]?.trim() || "" : "";
|
|
2225
|
+
};
|
|
2226
|
+
for (const line of dataLines) {
|
|
2227
|
+
if (!line.trim()) {
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const values = parseCsvLine(line);
|
|
2231
|
+
const rowStoreDomain = getValue(values, "Store Domain").toLowerCase();
|
|
2232
|
+
if (rowStoreDomain !== storeDomain) {
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
const keyword = getValue(values, "Keyword");
|
|
2236
|
+
if (!keyword) {
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
rows.push({
|
|
2240
|
+
filePath,
|
|
2241
|
+
keyword,
|
|
2242
|
+
platform: getValue(values, "Platform"),
|
|
2243
|
+
storeDomain: rowStoreDomain,
|
|
2244
|
+
store: getValue(values, "Store"),
|
|
2245
|
+
ranking: getValue(values, "Ranking"),
|
|
2246
|
+
popularity: getValue(values, "Popularity"),
|
|
2247
|
+
difficulty: getValue(values, "Difficulty"),
|
|
2248
|
+
lastUpdate: getValue(values, "Last Update")
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
const seenKeywords = /* @__PURE__ */ new Set();
|
|
2253
|
+
return rows.filter((row) => {
|
|
2254
|
+
const normalized = row.keyword.toLowerCase();
|
|
2255
|
+
if (seenKeywords.has(normalized)) {
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
seenKeywords.add(normalized);
|
|
2259
|
+
return true;
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
function loadManualKeywordCsv(slug, locale) {
|
|
2263
|
+
const targetStoreDomain = getLocaleStoreDomain(locale);
|
|
2264
|
+
const localeRows = loadManualKeywordCsvRowsForStoreDomain(
|
|
2265
|
+
slug,
|
|
2266
|
+
targetStoreDomain
|
|
2267
|
+
);
|
|
2268
|
+
if (localeRows.length > 0) {
|
|
2269
|
+
return {
|
|
2270
|
+
rows: localeRows,
|
|
2271
|
+
isFallback: false,
|
|
2272
|
+
sourceStoreDomain: targetStoreDomain,
|
|
2273
|
+
targetStoreDomain
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
const usRows = targetStoreDomain === "us" ? [] : loadManualKeywordCsvRowsForStoreDomain(slug, "us");
|
|
2277
|
+
return {
|
|
2278
|
+
rows: usRows,
|
|
2279
|
+
isFallback: usRows.length > 0,
|
|
2280
|
+
sourceStoreDomain: "us",
|
|
2281
|
+
targetStoreDomain
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function formatManualKeywordCsvRows(result, locale) {
|
|
2285
|
+
const { rows, isFallback, sourceStoreDomain, targetStoreDomain } = result;
|
|
2286
|
+
const sourceFiles = [...new Set(rows.map((row) => row.filePath))];
|
|
2287
|
+
const lines = [];
|
|
2288
|
+
lines.push(
|
|
2289
|
+
`### Manual Priority Keywords CSV (${locale} / ${targetStoreDomain})`
|
|
2290
|
+
);
|
|
2291
|
+
lines.push(`Source: ${sourceFiles.join(", ")}`);
|
|
2292
|
+
if (isFallback) {
|
|
2293
|
+
lines.push(
|
|
2294
|
+
`Fallback: No CSV rows found for Store Domain "${targetStoreDomain}". Using "${sourceStoreDomain}" CSV keywords as translation source for ${locale}.`
|
|
2295
|
+
);
|
|
2296
|
+
lines.push(
|
|
2297
|
+
"Translation rule: translate/localize these US keywords into the target locale before placing them in title, subtitle, keywords, or landing copy."
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
lines.push(
|
|
2301
|
+
"Priority: Apply these CSV keywords before saved keyword research, but still use locale saved research alongside them to validate relevance and fill remaining opportunities."
|
|
2302
|
+
);
|
|
2303
|
+
lines.push(
|
|
2304
|
+
"Placement rule: put the strongest terms in title first, then subtitle, then the keywords field. Do not duplicate the same keyword across title, subtitle, and keywords."
|
|
2305
|
+
);
|
|
2306
|
+
lines.push("");
|
|
2307
|
+
lines.push("**CSV Priority Keywords:**");
|
|
2308
|
+
rows.slice(0, 30).forEach((row, index) => {
|
|
2309
|
+
const metrics = [
|
|
2310
|
+
row.platform ? `platform: ${row.platform}` : "",
|
|
2311
|
+
row.store ? `store: ${row.store}` : "",
|
|
2312
|
+
row.ranking ? `rank: ${row.ranking}` : "",
|
|
2313
|
+
row.popularity ? `popularity: ${row.popularity}` : "",
|
|
2314
|
+
row.difficulty ? `difficulty: ${row.difficulty}` : "",
|
|
2315
|
+
row.lastUpdate ? `updated: ${row.lastUpdate}` : ""
|
|
2316
|
+
].filter(Boolean);
|
|
2317
|
+
lines.push(`${index + 1}. **${row.keyword}**${metrics.length > 0 ? ` (${metrics.join(", ")})` : ""}`);
|
|
2318
|
+
});
|
|
2319
|
+
lines.push("\n----");
|
|
2320
|
+
return lines.join("\n");
|
|
2321
|
+
}
|
|
2097
2322
|
function extractRecommended(data) {
|
|
2098
2323
|
const summary = data?.summary || data?.data?.summary;
|
|
2099
2324
|
const recommended = summary?.recommendedKeywords;
|
|
@@ -2387,9 +2612,15 @@ function loadKeywordResearchForLocale(slug, locale) {
|
|
|
2387
2612
|
"locales",
|
|
2388
2613
|
locale
|
|
2389
2614
|
);
|
|
2615
|
+
const manualKeywordCsv = loadManualKeywordCsv(slug, locale);
|
|
2616
|
+
const manualKeywordSections = manualKeywordCsv.rows.length > 0 ? [formatManualKeywordCsvRows(manualKeywordCsv, locale)] : [];
|
|
2390
2617
|
const result = loadKeywordResearchForLocaleInternal(slug, locale);
|
|
2391
2618
|
if (result) {
|
|
2392
|
-
return {
|
|
2619
|
+
return {
|
|
2620
|
+
...result,
|
|
2621
|
+
sections: [...manualKeywordSections, ...result.sections],
|
|
2622
|
+
isFallback: false
|
|
2623
|
+
};
|
|
2393
2624
|
}
|
|
2394
2625
|
for (const fallbackLocale of FALLBACK_LOCALES) {
|
|
2395
2626
|
if (fallbackLocale === locale) continue;
|
|
@@ -2405,14 +2636,19 @@ function loadKeywordResearchForLocale(slug, locale) {
|
|
|
2405
2636
|
);
|
|
2406
2637
|
return {
|
|
2407
2638
|
entries: fallbackResult.entries,
|
|
2408
|
-
sections: sectionsWithNotice,
|
|
2639
|
+
sections: [...manualKeywordSections, ...sectionsWithNotice],
|
|
2409
2640
|
researchDir: fallbackResult.researchDir,
|
|
2410
2641
|
isFallback: true,
|
|
2411
2642
|
fallbackLocale
|
|
2412
2643
|
};
|
|
2413
2644
|
}
|
|
2414
2645
|
}
|
|
2415
|
-
return {
|
|
2646
|
+
return {
|
|
2647
|
+
entries: [],
|
|
2648
|
+
sections: manualKeywordSections,
|
|
2649
|
+
researchDir,
|
|
2650
|
+
isFallback: false
|
|
2651
|
+
};
|
|
2416
2652
|
}
|
|
2417
2653
|
|
|
2418
2654
|
// src/tools/aso/improve-public.ts
|
|
@@ -2444,6 +2680,7 @@ var improvePublicTool = {
|
|
|
2444
2680
|
|
|
2445
2681
|
## HOW THIS TOOL WORKS
|
|
2446
2682
|
This tool returns a PROMPT containing:
|
|
2683
|
+
- Product-level manual CSV keyword data from .aso/keywordResearch/products/[slug]/*.csv when present
|
|
2447
2684
|
- Saved keyword research data (Tier 1/2/3 keywords with traffic/difficulty scores)
|
|
2448
2685
|
- Current locale data
|
|
2449
2686
|
- Optimization instructions
|
|
@@ -2465,9 +2702,11 @@ This tool returns a PROMPT containing:
|
|
|
2465
2702
|
- **Stage 2:** Localize to other languages - **each locale uses its OWN keyword research**
|
|
2466
2703
|
|
|
2467
2704
|
## KEYWORD SOURCES (Per Locale)
|
|
2468
|
-
- **Priority 1:** Uses
|
|
2469
|
-
- **Priority 2
|
|
2705
|
+
- **Priority 1:** Uses product-level manual CSV keywords from .aso/keywordResearch/products/[slug]/*.csv. It checks the target locale's country Store Domain first; if missing, it uses US CSV keywords as a translation/localization source.
|
|
2706
|
+
- **Priority 2:** Uses each locale's SAVED keyword research from .aso/keywordResearch/products/[slug]/locales/[locale]/ alongside the CSV to validate relevance and fill remaining opportunities.
|
|
2707
|
+
- **Priority 3 (Fallback):** If locale-specific saved research is missing, falls back to en-US/en saved research and TRANSLATES it
|
|
2470
2708
|
- iOS and Android research are automatically combined per locale (iOS prioritized)
|
|
2709
|
+
- Title, subtitle, and keywords must be filled in that importance order, with no repeated keyword terms across the three fields
|
|
2471
2710
|
|
|
2472
2711
|
**CRITICAL:** Only processes existing locale files. Does NOT create new files.`,
|
|
2473
2712
|
inputSchema: inputSchema3
|