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.0Tested
Activity
New: be the first to try it
  1. 1// Example tests (Vitest style). Adapt imports and the render helper to the project.
  2. 2// The point: render the real help page, then prove every visible question is marked up,
  3. 3// the JSON-LD parses, and no answer carries markup.
  4. 4
  5. 5import { describe, expect, it } from "vitest";
  6. 6import { faqPage, itemList, plainText, safeJson, softwareApplication } from "./structured-data";
  7. 7// import { renderHelpPage } from "../app/help/render"; // returns the page HTML as a string
  8. 8
  9. 9function jsonLdBlocks(html: string): unknown[] {
  10. 10 const re = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g;
  11. 11 return [...html.matchAll(re)].map((m) => JSON.parse(m[1]));
  12. 14describe("structured data", () => {
  13. 15 it("marks up every FAQ on the help page with plain-text answers", async () => {
  14. 16 const html =
  15. 17 "<h3>Is there a free plan?</h3>" +
  16. 18 `<script type="application/ld+json">${safeJson(
  17. 19 faqPage([
  18. 20 { question: "Is there a free plan?", answer: 'Yes. See <a href="/pricing">pricing</a>.' },
  19. 21 ]),
  20. 22 )}</script>`; // replace with: await renderHelpPage()
  21. 23 const visible = [...html.matchAll(/<h3>([^<]+)<\/h3>/g)].map((m) => m[1]);
  22. 24 const faq = jsonLdBlocks(html).find(
  23. 25 (b) => (b as { "@type"?: string })["@type"] === "FAQPage",
  24. 26 ) as {
  25. 27 mainEntity: { name: string; acceptedAnswer: { text: string } }[];
  26. 28 };
  27. 29 expect(faq.mainEntity.map((q) => q.name)).toEqual(visible);
  28. 30 for (const q of faq.mainEntity) expect(q.acceptedAnswer.text).not.toMatch(/<[a-z/]/i);
  29. 31 });
  30. 33 it("states the real price", () => {
  31. 34 const app = softwareApplication({
  32. 35 name: "Example",
  33. 36 description: "Plans your week.",
  34. 37 url: "https://app.example.com/",
  35. 38 price: 9,
  36. 39 currency: "USD",
  37. 40 });
  38. 41 expect(app.offers).toMatchObject({ price: "9.00", priceCurrency: "USD" });
  39. 42 });
  40. 44 it("numbers list items from 1 in display order", () => {
  41. 45 const list = itemList([
  42. 46 { name: "A", url: "https://app.example.com/a" },
  43. 47 { name: "B", url: "https://app.example.com/b" },
  44. 48 ]);
  45. 49 expect((list.itemListElement as { position: number }[]).map((i) => i.position)).toEqual([1, 2]);
  46. 50 });
  47. 52 it("cannot close the script tag", () => {
  48. 53 expect(safeJson({ name: "</script><script>x()</script>" })).not.toContain("</script>");
  49. 54 });
  50. 56 it("strips links to their words", () => {
  51. 57 expect(plainText("See [pricing](/pricing) or <b>email</b> us.")).toBe(
  52. 58 "See pricing or email us.",
  53. 59 );
  54. 60 });
  55. 61});

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