@extenshi/mcp
Version:
Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools
118 lines • 4.51 kB
JavaScript
/**
* Schema.org SoftwareApplication JSON-LD for an extension's public site.
*
* Pure and dependency-free on purpose: the landing-page generator
* (./landing-page.ts) embeds it, and that generator ships in three places —
* dojo (through shared-types), the published @extenshi/mcp package and the
* published @extenshi/cli package. Those two cannot import workspace
* shared-types at runtime, so they carry copies of this file:
*
* shared-types/json-ld.ts (canonical — edit here)
* tools/extenshi-mcp/src/json-ld.ts (byte-identical copy)
* tools/extenshi-cli/src/json-ld.ts (byte-identical copy)
*
* `landing-page-sync.test.ts` in each package fails when a copy drifts. After
* editing, run:
* cp shared-types/json-ld.ts tools/extenshi-mcp/src/json-ld.ts
* cp shared-types/json-ld.ts tools/extenshi-cli/src/json-ld.ts
*
* `shared-types/ai-visibility.ts` re-exports everything here, so existing
* `shared-types/ai-visibility.js` imports keep working.
*/
function httpUrl(raw) {
const u = (raw ?? '').trim();
return /^https?:\/\//i.test(u) ? u : null;
}
const OS_FOR = {
chrome: 'Chrome',
firefox: 'Firefox',
edge: 'Microsoft Edge',
};
/**
* Schema.org SoftwareApplication. Only fields we have evidence for are emitted —
* empty author / URL / install links are omitted rather than faked.
*/
export function generateJsonLd(input) {
const name = input.extensionName.trim() || 'Extension';
const description = input.description.trim();
const site = httpUrl(input.websiteUrl);
const author = (input.authorName ?? '').trim();
const chrome = httpUrl(input.chromeUrl);
const firefox = httpUrl(input.firefoxUrl);
const edge = httpUrl(input.edgeUrl);
const privacy = httpUrl(input.privacyPolicyUrl);
const features = (input.capabilities ?? []).map((c) => c.trim()).filter(Boolean);
const screenshots = (input.screenshots ?? []).map((s) => s.trim()).filter((s) => /^https:\/\/\S+$/i.test(s));
const sameAs = [chrome, firefox, edge].filter((u) => Boolean(u));
const os = [
chrome ? OS_FOR.chrome : null,
firefox ? OS_FOR.firefox : null,
edge ? OS_FOR.edge : null,
].filter((x) => Boolean(x));
const json = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name,
applicationCategory: 'BrowserApplication',
};
if (description)
json.description = description;
if (site)
json.url = site;
if (os.length > 0)
json.operatingSystem = os.join(', ');
if (author)
json.author = { '@type': 'Organization', name: author };
if (input.offersPrice !== 'paid') {
json.offers = { '@type': 'Offer', price: '0', priceCurrency: 'USD' };
}
if (sameAs[0])
json.installUrl = sameAs[0];
if (sameAs.length > 0)
json.sameAs = sameAs;
if (features.length > 0)
json.featureList = features;
if (privacy)
json.privacyPolicy = privacy;
if (screenshots.length > 0)
json.screenshot = screenshots;
return json;
}
export function generateJsonLdSnippet(input) {
return `${JSON.stringify(generateJsonLd(input), null, 2)}\n`;
}
/**
* A `<script type="application/ld+json">` tag safe to drop into HTML.
* `<` is escaped so a description cannot close the script element.
*/
export function jsonLdScriptTag(input) {
const body = JSON.stringify(generateJsonLd(input)).replace(/</g, '\\u003c');
return `<script type="application/ld+json">${body}</script>`;
}
/** Map a landing-page form onto JSON-LD without a second source of truth. */
export function jsonLdFromLanding(input) {
const capabilities = input.features
.map((f) => {
const title = f.title.trim();
const desc = f.description.trim();
if (!title)
return '';
return desc ? `${title}: ${desc}` : title;
})
.filter(Boolean);
const out = {
extensionName: input.extensionName,
description: input.description.trim() || input.tagline.trim(),
chromeUrl: input.storeUrls.chrome,
firefoxUrl: input.storeUrls.firefox,
edgeUrl: input.storeUrls.edge,
privacyPolicyUrl: input.privacyPolicyUrl,
capabilities,
};
if (input.homepageUrl)
out.websiteUrl = input.homepageUrl;
if (input.screenshots && input.screenshots.length > 0)
out.screenshots = input.screenshots;
return out;
}
//# sourceMappingURL=json-ld.js.map