@extenshi/mcp
Version:
Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools
350 lines • 15.7 kB
TypeScript
/**
* Page generator, schema v2: a full, declarative extension homepage.
*
* A v1 form (no `schemaVersion`) is still rendered by `buildLandingPage` in
* landing-page.ts, unchanged and byte-identical — `renderLandingForm` below
* only takes the v2 path when `schemaVersion === 2`. Everything v2 adds is an
* optional field on the same LandingForm, so an old row, an old client and an
* old server all keep working (see landing-page-v2.test.ts).
*
* What v2 adds, all declarative data — never markup:
* - ordered sections with `hidden` (hero, about, features, screenshots,
* how-it-works, pricing, faq, testimonials, store-badges, changelog, links);
* - theme tokens: palette per light/dark (or `auto` = both, by
* prefers-color-scheme), fonts from LANDING_FONTS, radius, width;
* - per-locale content (`locales`) rendered as one page per locale with
* hreflang alternates and `dir="rtl"` for right-to-left languages;
* - SEO / Open Graph fields and JSON-LD (SoftwareApplication + FAQPage),
* whose `offers` carry the real prices of plans bound to Pay offers.
*
* Output rules are the v1 rules: no JavaScript, every string HTML-escaped,
* links scheme-clamped (safeHref), images clamped (safeImageSrc), CSS built
* only from allowlists and validated hex colours. The hosted CSP
* (`default-src 'none'`, inline styles only) therefore holds unchanged.
*
* Testimonials must be attributed: a quote without an author and a public
* https source is refused, never rendered. Drafting tools never write
* testimonials — only the developer supplies them.
*
* SINGLE SOURCE, THREE COPIES — same scheme as landing-page.ts:
* shared-types/landing-sections.ts (canonical — edit here)
* tools/extenshi-mcp/src/landing-sections.ts (copy)
* tools/extenshi-cli/src/landing-sections.ts (copy)
* After editing, run:
* sed "s#from 'shared-types\/#from './#" shared-types/landing-sections.ts > tools/extenshi-mcp/src/landing-sections.ts
* sed "s#from 'shared-types\/#from './#" shared-types/landing-sections.ts > tools/extenshi-cli/src/landing-sections.ts
*/
import { type LandingBrowser, type LandingForm } from './landing-page.js';
export declare const LANDING_SCHEMA_VERSION: 2;
export declare const LANDING_SECTION_TYPES: readonly ["hero", "about", "features", "screenshots", "how-it-works", "pricing", "faq", "testimonials", "store-badges", "changelog", "links"];
export type LandingSectionType = (typeof LANDING_SECTION_TYPES)[number];
export declare const LANDING_V2_LIMITS: {
readonly sections: 20;
readonly sectionId: 40;
readonly title: 120;
readonly intro: 600;
readonly body: 6000;
readonly items: 24;
readonly screenshots: 12;
readonly steps: 8;
readonly plans: 6;
readonly planFeatures: 12;
readonly faq: 20;
readonly testimonials: 12;
readonly quote: 600;
readonly changelog: 20;
readonly changelogNotes: 12;
readonly links: 12;
readonly shortText: 120;
readonly text: 500;
readonly locales: 10;
readonly seoTitle: 70;
readonly seoDescription: 300;
};
/** A section id: lowercase slug, used as the HTML id and as the locale override key. */
export declare const LANDING_SECTION_ID: RegExp;
interface SectionBase<T extends LandingSectionType> {
id: string;
type: T;
/** Kept in the form, left out of the page. */
hidden?: boolean;
/** Section heading; each type has a default (LANDING_UI_DEFAULTS). */
title?: string;
intro?: string;
}
export interface LandingHeroSection extends SectionBase<'hero'> {
/** Defaults to extensionName. */
headline?: string;
/** Defaults to tagline. */
subheadline?: string;
/** Store buttons under the headline. Default true. */
showBadges?: boolean;
}
export interface LandingAboutSection extends SectionBase<'about'> {
/** Plain text; blank lines separate paragraphs. */
body: string;
}
export interface LandingFeaturesSection extends SectionBase<'features'> {
items: {
title: string;
description?: string;
}[];
}
export interface LandingScreenshotsSection extends SectionBase<'screenshots'> {
items: {
url: string;
alt?: string;
caption?: string;
}[];
}
export interface LandingHowItWorksSection extends SectionBase<'how-it-works'> {
steps: {
title: string;
description?: string;
}[];
}
export type LandingPlanPeriod = 'one-time' | 'month' | 'year' | 'lifetime';
export interface LandingPricingPlan {
name?: string;
/** Display price ("$4.99", "Free"). Overwritten from the Pay offer when offerSku is set. */
price?: string;
period?: LandingPlanPeriod;
description?: string;
features?: string[];
highlighted?: boolean;
/**
* SKU of an Extenshi Pay offer of this project. When set, the hosted page
* takes price, period and — when not given here — name, description and
* features from the live offer, so the page cannot advertise a price the
* checkout does not charge.
*/
offerSku?: string;
ctaLabel?: string;
/** Where the plan button goes. Default: the first store listing. */
ctaUrl?: string;
}
export interface LandingPricingSection extends SectionBase<'pricing'> {
plans: LandingPricingPlan[];
note?: string;
}
export interface LandingFaqSection extends SectionBase<'faq'> {
items: {
question: string;
answer: string;
}[];
}
export interface LandingTestimonial {
quote: string;
/** The person's name as published at the source. Required. */
author: string;
role?: string;
/** Public https URL where they said it (store review, post). Required. */
sourceUrl: string;
sourceLabel?: string;
}
export interface LandingTestimonialsSection extends SectionBase<'testimonials'> {
items: LandingTestimonial[];
}
export interface LandingStoreBadgesSection extends SectionBase<'store-badges'> {
/** Order and subset of stores; default every store with a URL. */
browsers?: LandingBrowser[];
}
export interface LandingChangelogSection extends SectionBase<'changelog'> {
entries: {
version: string;
date?: string;
notes: string[];
}[];
}
export interface LandingLinksSection extends SectionBase<'links'> {
items?: {
label: string;
url: string;
}[];
/** Also list privacy / support / feedback / terms from the form. Default true. */
includeProjectLinks?: boolean;
}
export type LandingSection = LandingHeroSection | LandingAboutSection | LandingFeaturesSection | LandingScreenshotsSection | LandingHowItWorksSection | LandingPricingSection | LandingFaqSection | LandingTestimonialsSection | LandingStoreBadgesSection | LandingChangelogSection | LandingLinksSection;
/**
* System font stacks only (after modern-font-stacks, CC0). A web font would
* need a font-src exception in the hosted CSP and a third-party request from
* every visitor; a stack renders instantly and requests nothing.
*/
export declare const LANDING_FONTS: {
readonly default: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif";
readonly 'system-ui': "system-ui, sans-serif";
readonly 'neo-grotesque': "Inter, Roboto, \"Helvetica Neue\", \"Arial Nova\", \"Nimbus Sans\", Arial, sans-serif";
readonly humanist: "Seravek, \"Gill Sans Nova\", Ubuntu, Calibri, \"DejaVu Sans\", source-sans-pro, sans-serif";
readonly 'geometric-humanist': "Avenir, Montserrat, Corbel, \"URW Gothic\", source-sans-pro, sans-serif";
readonly 'classical-humanist': "Optima, Candara, \"Noto Sans\", source-sans-pro, sans-serif";
readonly 'rounded-sans': "ui-rounded, \"Hiragino Maru Gothic ProN\", Quicksand, Comfortaa, Manjari, \"Arial Rounded MT\", \"Arial Rounded MT Bold\", Calibri, source-sans-pro, sans-serif";
readonly hyperlegible: "\"Atkinson Hyperlegible Next\", \"Atkinson Hyperlegible\", Lexend, Verdana, Tahoma, sans-serif";
readonly industrial: "Bahnschrift, \"DIN Alternate\", \"Franklin Gothic Medium\", \"Nimbus Sans Narrow\", sans-serif-condensed, sans-serif";
readonly transitional: "Charter, \"Bitstream Charter\", \"Sitka Text\", Cambria, serif";
readonly 'old-style': "\"Iowan Old Style\", \"Palatino Linotype\", \"URW Palladio L\", P052, serif";
readonly 'slab-serif': "Rockwell, \"Rockwell Nova\", \"Roboto Slab\", \"DejaVu Serif\", \"Sitka Small\", serif";
readonly didone: "Didot, \"Bodoni MT\", \"Noto Serif Display\", \"URW Palladio L\", P052, Sylfaen, serif";
readonly monospace: "ui-monospace, \"Cascadia Code\", \"Source Code Pro\", Menlo, Consolas, \"DejaVu Sans Mono\", monospace";
};
export type LandingFontId = keyof typeof LANDING_FONTS;
export interface LandingPalette {
accent?: string;
/** Text on accent-coloured buttons. */
accentText?: string;
background?: string;
surface?: string;
text?: string;
muted?: string;
border?: string;
}
export interface LandingThemeTokens {
/** `auto` ships both palettes and follows prefers-color-scheme. Default: form.theme. */
mode?: 'light' | 'dark' | 'auto';
light?: LandingPalette;
dark?: LandingPalette;
headingFont?: LandingFontId;
bodyFont?: LandingFontId;
radius?: 'none' | 'small' | 'medium' | 'large';
width?: 'narrow' | 'normal' | 'wide';
}
export interface LandingSeo {
/** <title> and og:title. Default "{name} — {tagline}". */
title?: string;
/** meta description and og:description. Default description or tagline. */
description?: string;
/** og:image; default the first screenshot. https only. */
ogImageUrl?: string;
/** Twitter/X handle for twitter:site. */
twitterSite?: string;
/** Publisher name for JSON-LD author. */
authorName?: string;
/** Adds robots noindex — for a page that must stay out of search. */
noindex?: boolean;
}
export declare const LANDING_UI_DEFAULTS: {
readonly addToChrome: "Add to Chrome";
readonly getForFirefox: "Get for Firefox";
readonly getForEdge: "Get for Edge";
readonly screenshots: "Screenshots";
readonly features: "Features";
readonly howItWorks: "How it works";
readonly pricing: "Pricing";
readonly faq: "Frequently asked questions";
readonly testimonials: "What people say";
readonly storeBadges: "Get it for your browser";
readonly changelog: "Changelog";
readonly links: "Links";
readonly privacy: "Privacy policy";
readonly support: "Support";
readonly feedback: "Share feedback";
readonly terms: "License & refunds";
readonly install: "Install";
readonly popular: "Most popular";
readonly perMonth: "/ month";
readonly perYear: "/ year";
readonly oneTime: "one-time";
readonly lifetime: "lifetime";
readonly source: "Source";
readonly languages: "Languages";
/** Alt text of the hero logo; `{name}` is replaced by the extension name. */
readonly logoAlt: "{name} logo";
};
export type LandingUiKey = keyof typeof LANDING_UI_DEFAULTS;
export interface LandingLocaleContent {
extensionName?: string;
tagline?: string;
description?: string;
seo?: {
title?: string;
description?: string;
ogImageUrl?: string;
};
ui?: Partial<Record<LandingUiKey, string>>;
/**
* Per-section overrides keyed by section id, in the section's own shape.
* Only string values are taken (arrays merge by index), so a translation
* can change words and localized screenshot URLs, never structure.
*/
sections?: Record<string, Record<string, unknown>>;
}
/** The optional fields v2 adds to LandingForm. */
export interface LandingV2Fields {
schemaVersion?: 2;
sections?: LandingSection[];
tokens?: LandingThemeTokens;
seo?: LandingSeo;
/** Locale of the base content. Default `en`. */
defaultLocale?: string;
locales?: Record<string, LandingLocaleContent>;
ui?: Partial<Record<LandingUiKey, string>>;
/** Hosted uninstall-feedback form (extenshi hosted artifact). */
uninstallUrl?: string;
/** License terms & refunds page (Extenshi Pay hosted artifact). */
termsUrl?: string;
}
export interface LandingProblem {
/** JSON path into the form, e.g. `sections[3].items[0].sourceUrl`. */
path: string;
severity: 'error' | 'warning';
message: string;
/** What to change to fix it. */
fix: string;
}
/**
* `pt_BR`, `pt-br`, `PT-BR` → `pt-BR` (BCP 47, as hreflang and <html lang>
* want it). Accepts the Chrome `_locales` spelling used by the localization
* workflow. Null when the value is not a language tag.
*/
export declare function normalizeLandingLocale(raw: unknown): string | null;
/** `<html dir>` for a locale page: `rtl` for Arabic, Hebrew, Persian, Urdu and other RTL scripts. */
export declare function landingLocaleDir(locale: string): 'ltr' | 'rtl';
/** URL path segment for a locale: lowercase BCP 47 (`pt-br`). */
export declare function landingLocaleSlug(locale: string): string;
export declare function landingDefaultLocale(form: LandingForm): string;
/** Default locale first, then every valid translated locale (deduplicated, capped). */
export declare function landingLocales(form: LandingForm): string[];
/** Absolute URL of a locale's page: the default locale lives at `base`, others at `base/{slug}`. */
export declare function landingLocaleUrl(base: string, locale: string, defaultLocale: string): string;
/** The form as it reads in `locale` (the base form for the default locale). */
export declare function localizeLandingForm(form: LandingForm, locale: string): LandingForm;
/** WCAG 2 contrast ratio of two hex colours, or null when either is not a hex colour. */
export declare function contrastRatio(a: string, b: string): number | null;
/**
* Everything wrong with a form, as actionable problems. `error` means the
* hosted publish refuses the form; `warning` means it renders but something
* was dropped, defaulted or is likely unintended. For a v1 form this is
* landingPageWarnings() as warnings — v1 validation is unchanged.
*/
export declare function landingFormProblems(form: LandingForm): LandingProblem[];
/** A live Extenshi Pay offer as the renderer needs it (the BFF's LandingPayOffer fits). */
export interface LandingRenderOffer {
sku: string;
/** Minor units, as stored on the offer (formatLandingPrice divides by 100). */
amountMinor: number;
currency: string;
}
export interface RenderLandingOptions {
/** Locale to render; default the form's default locale. */
locale?: string;
/**
* The project's active Pay offers (hosted rendering only). Plans bound to
* one by offerSku get a JSON-LD Offer with its real price; without them a
* page that sells something claims no price rather than a free one.
*/
payOffers?: readonly LandingRenderOffer[];
}
/**
* The one entry point every surface calls (dojo preview, BFF hosting, MCP and
* CLI generate). A v1 form goes to buildLandingPage untouched.
*/
export declare function renderLandingForm(form: LandingForm, opts?: RenderLandingOptions): string;
/** Every locale's page: `[{ locale, path, html }]`, default locale first (path ''). */
export declare function renderLandingLocales(form: LandingForm, opts?: Omit<RenderLandingOptions, 'locale'>): {
locale: string;
slug: string;
html: string;
}[];
/** Format a Pay offer amount for a pricing card ("$4.99", "4,99 €"). */
export declare function formatLandingPrice(amountMinor: number, currency: string, locale: string): string;
export {};
//# sourceMappingURL=landing-sections.d.ts.map