UNPKG

@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
/** * 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