Get found in search and see where sign-ups come from
@slashclone/website-traffic
Get your web app found in Google and AI search, and see which channels bring sign-ups. Sets up sitemap, robots, canonical URLs, share images, and structured data with a page checker, then consent-aware first-touch tracking from UTM tags and referral links, saved at sign-up. Includes a launch channel checklist. Engineering guidance, not legal advice.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1// JSON-LD builders. Render each result with:
- 2// <script type="application/ld+json">{safeJson(data)}</script>
- 3// Build from the same data the page shows, so markup never disagrees with the page.
- 5export type Json = Record<string, unknown>;
- 7/** Escape "<" so a value can never close the script tag. */
- 8export function safeJson(data: Json): string {
- 9 return JSON.stringify(data).replace(/</g, "\\u003c");
- 10}
- 12/**
- 13 * A product or app page. State the real price: "0" for free, the actual amount
- 14 * otherwise. For something only paying members can use, do not claim it is free.
- 15 */
- 16export function softwareApplication(input: {
- 17 name: string;
- 18 description: string;
- 19 url: string;
- 20 image?: string;
- 21 price: number; // in major units, for example 9 for $9
- 22 currency: string; // ISO 4217, for example "USD"
- 23 category?: string; // for example "DeveloperApplication"
- 24}): Json {
- 25 return {
- 26 "@context": "https://schema.org",
- 27 "@type": "SoftwareApplication",
- 28 name: input.name,
- 29 description: input.description,
- 30 url: input.url,
- 31 ...(input.image ? { image: input.image } : {}),
- 32 applicationCategory: input.category ?? "BusinessApplication",
- 33 operatingSystem: "Web",
- 34 offers: {
- 35 "@type": "Offer",
- 36 price: input.price.toFixed(2),
- 37 priceCurrency: input.currency,
- 38 },
- 39 };
- 40}
- 42/** A list page: the items actually shown, in the order shown. */
- 43export function itemList(items: { name: string; url: string }[]): Json {
- 44 return {
- 45 "@context": "https://schema.org",
- 46 "@type": "ItemList",
- 47 itemListElement: items.map((it, i) => ({
- 48 "@type": "ListItem",
- 49 position: i + 1,
- 50 name: it.name,
- 51 url: it.url,
- 52 })),
- 53 };
- 54}
- 56/** A help page. Answers must be plain text: strip links and tags first. */
- 57export function faqPage(faqs: { question: string; answer: string }[]): Json {
- 58 return {
- 59 "@context": "https://schema.org",
- 60 "@type": "FAQPage",
- 61 mainEntity: faqs.map((f) => ({
- 62 "@type": "Question",
- 63 name: f.question,
- 64 acceptedAnswer: { "@type": "Answer", text: plainText(f.answer) },
- 65 })),
- 66 };
- 67}
- 69/** Turn an HTML or Markdown-ish answer into plain text. */
- 70export function plainText(s: string): string {
- 71 return s
- 72 .replace(/<[^>]*>/g, " ")
- 73 .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
- 74 .replace(/[*_`]/g, "")
- 75 .replace(/\s+/g, " ")
- 76 .trim();
- 77}
- 79// If the answers are components rather than strings (JSX, templates), keep the FAQ
- 80// as data (question plus answer component) and derive the text by rendering each
- 81// answer to a string and passing it through plainText(). One list then drives both
- 82// the visible page and the markup, so a new question cannot be missed.
