Skip to content
SlashClone

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.

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

Cookies that keep you signed in and remember these choices are always on. Everything else is off until you turn it on.