mcp-scraper 0.39.0 → 0.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +382 -44
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +6 -6
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +4 -4
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +3 -3
  16. package/dist/{chunk-DO6WGZRS.js → chunk-3PIWJS6Y.js} +2 -2
  17. package/dist/{chunk-YUE46JGV.js → chunk-7XBBFBYY.js} +2 -2
  18. package/dist/{chunk-AFY6DNL7.js → chunk-GUVKHCKE.js} +13 -3
  19. package/dist/chunk-GUVKHCKE.js.map +1 -0
  20. package/dist/{chunk-TJRCAP7C.js → chunk-LP6E462I.js} +8 -8
  21. package/dist/chunk-LP6E462I.js.map +1 -0
  22. package/dist/{chunk-4WAE4AV5.js → chunk-NVXNEOUQ.js} +2 -2
  23. package/dist/{chunk-3HOANNNR.js → chunk-X2LKCX6H.js} +2 -2
  24. package/dist/{chunk-ATWQRW3K.js → chunk-XORPNO3Z.js} +2 -2
  25. package/dist/chunk-ZUJLSICT.js +7 -0
  26. package/dist/chunk-ZUJLSICT.js.map +1 -0
  27. package/dist/{db-6AJW47NF.js → db-W3CP562I.js} +2 -2
  28. package/dist/{extract-bundle-CWG35SKV.js → extract-bundle-K4PG3RZJ.js} +3 -3
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/{location-data-repository-BGKZF7DV.js → location-data-repository-RLQX6SNM.js} +3 -3
  32. package/dist/{server-BXNVNAMT.js → server-KM3CFWCF.js} +373 -52
  33. package/dist/server-KM3CFWCF.js.map +1 -0
  34. package/dist/{site-extract-repository-QZKZSQLQ.js → site-extract-repository-2SMMFKKL.js} +3 -3
  35. package/dist/{worker-YTIMPE57.js → worker-FXAGFYOE.js} +5 -5
  36. package/package.json +7 -7
  37. package/dist/chunk-6NWL3MVU.js +0 -7
  38. package/dist/chunk-6NWL3MVU.js.map +0 -1
  39. package/dist/chunk-AFY6DNL7.js.map +0 -1
  40. package/dist/chunk-TJRCAP7C.js.map +0 -1
  41. package/dist/server-BXNVNAMT.js.map +0 -1
  42. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +0 -58
  43. package/docs/adr/0002-hybrid-smart-rag-vault-retrieval.md +0 -62
  44. package/docs/adr/0003-waive-unrecoverable-scheduled-model-cost.md +0 -22
  45. package/docs/adr/2026-07-28-owned-aug-webinar-registration.md +0 -61
  46. package/docs/adr/README.md +0 -13
  47. package/docs/final-tooling-spec.md +0 -206
  48. package/docs/hosted-location-data.md +0 -108
  49. package/docs/kernel-proxy-future-enhancements.md +0 -80
  50. package/docs/mcp-tool-craft-lint.generated.md +0 -186
  51. package/docs/mcp-tool-design-guide.md +0 -225
  52. package/docs/mcp-tool-manifest.generated.json +0 -23305
  53. package/docs/mcp-tool-quality-spec.md +0 -240
  54. package/docs/oauth-legal-review.md +0 -38
  55. package/docs/seo-crawl-report-spec.md +0 -287
  56. package/docs/specs/api-forge-spec.md +0 -234
  57. package/docs/specs/connected-services-control-plane-decoupling-spec.md +0 -1044
  58. package/docs/specs/deferred-work-spec.md +0 -86
  59. package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +0 -1689
  60. package/docs/specs/kernel-stealth-captcha-test-matrix.md +0 -278
  61. package/docs/specs/main-mcp-integration-ownership-spec.md +0 -1164
  62. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +0 -1602
  63. package/docs/specs/meta-ad-creative-media-resolution-spec.md +0 -31
  64. package/docs/specs/multimodal-image-memory-architecture-spec.md +0 -1022
  65. package/docs/specs/oauth-mcp-spec.md +0 -213
  66. package/docs/specs/plan-visibility-and-management-roadmap.md +0 -293
  67. package/docs/specs/query-fanout-transport-contract-fix.md +0 -45
  68. package/docs/specs/relationship-workspace-ai-behavior-plan.md +0 -26
  69. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +0 -995
  70. package/docs/tool-catalog-spec.md +0 -388
  71. /package/dist/{chunk-DO6WGZRS.js.map → chunk-3PIWJS6Y.js.map} +0 -0
  72. /package/dist/{chunk-YUE46JGV.js.map → chunk-7XBBFBYY.js.map} +0 -0
  73. /package/dist/{chunk-4WAE4AV5.js.map → chunk-NVXNEOUQ.js.map} +0 -0
  74. /package/dist/{chunk-3HOANNNR.js.map → chunk-X2LKCX6H.js.map} +0 -0
  75. /package/dist/{chunk-ATWQRW3K.js.map → chunk-XORPNO3Z.js.map} +0 -0
  76. /package/dist/{db-6AJW47NF.js.map → db-W3CP562I.js.map} +0 -0
  77. /package/dist/{extract-bundle-CWG35SKV.js.map → extract-bundle-K4PG3RZJ.js.map} +0 -0
  78. /package/dist/{location-data-repository-BGKZF7DV.js.map → location-data-repository-RLQX6SNM.js.map} +0 -0
  79. /package/dist/{site-extract-repository-QZKZSQLQ.js.map → site-extract-repository-2SMMFKKL.js.map} +0 -0
  80. /package/dist/{worker-YTIMPE57.js.map → worker-FXAGFYOE.js.map} +0 -0
@@ -3849,6 +3849,12 @@ async function migrate() {
3849
3849
  `);
3850
3850
  await db.execute(`CREATE INDEX IF NOT EXISTS jobs_user_id ON jobs(user_id)`);
3851
3851
  await db.execute(`CREATE INDEX IF NOT EXISTS jobs_status ON jobs(status)`);
3852
+ await db.execute(
3853
+ `CREATE INDEX IF NOT EXISTS jobs_created_at_status_user ON jobs(created_at, status, user_id)`
3854
+ );
3855
+ await db.execute(
3856
+ `CREATE INDEX IF NOT EXISTS jobs_user_created_at_status ON jobs(user_id, created_at, status)`
3857
+ );
3852
3858
  await db.execute(`
3853
3859
  CREATE TABLE IF NOT EXISTS harvest_attempts (
3854
3860
  id TEXT PRIMARY KEY,
@@ -4565,6 +4571,8 @@ async function migrate() {
4565
4571
  zoom_join_url TEXT,
4566
4572
  resend_contact_id TEXT,
4567
4573
  confirmation_email_id TEXT,
4574
+ field_notes_email_id TEXT,
4575
+ reminder_48h_email_id TEXT,
4568
4576
  reminder_24h_email_id TEXT,
4569
4577
  reminder_1h_email_id TEXT,
4570
4578
  last_error TEXT,
@@ -4573,7 +4581,7 @@ async function migrate() {
4573
4581
  UNIQUE(event_slug, email)
4574
4582
  )
4575
4583
  `);
4576
- for (const col of ["reminder_24h_email_id TEXT", "reminder_1h_email_id TEXT"]) {
4584
+ for (const col of ["field_notes_email_id TEXT", "reminder_48h_email_id TEXT", "reminder_24h_email_id TEXT", "reminder_1h_email_id TEXT"]) {
4577
4585
  try {
4578
4586
  await db.execute(`ALTER TABLE workshop_registrations ADD COLUMN ${col}`);
4579
4587
  } catch {
@@ -6555,7 +6563,9 @@ var init_db = __esm({
6555
6563
  "harvest_attempts_job_id",
6556
6564
  "harvest_attempts_user_created_at",
6557
6565
  "inbox_messages_user_sent_at",
6566
+ "jobs_created_at_status_user",
6558
6567
  "jobs_status",
6568
+ "jobs_user_created_at_status",
6559
6569
  "jobs_user_id",
6560
6570
  "kpo_jobs_status",
6561
6571
  "kpo_jobs_user_id",
@@ -6622,7 +6632,7 @@ var init_db = __esm({
6622
6632
  "subscription_id",
6623
6633
  "subscription_tier"
6624
6634
  ],
6625
- workshop_registrations: ["reminder_24h_email_id", "reminder_1h_email_id"],
6635
+ workshop_registrations: ["field_notes_email_id", "reminder_48h_email_id", "reminder_24h_email_id", "reminder_1h_email_id"],
6626
6636
  workflow_artifacts: ["content"],
6627
6637
  workflow_runs: ["step_index", "step_log_json", "step_state_json", "total_steps", "webhook_url"]
6628
6638
  };
@@ -37981,7 +37991,7 @@ var PACKAGE_VERSION;
37981
37991
  var init_version = __esm({
37982
37992
  "src/version.ts"() {
37983
37993
  "use strict";
37984
- PACKAGE_VERSION = "0.39.0";
37994
+ PACKAGE_VERSION = "0.40.2";
37985
37995
  }
37986
37996
  });
37987
37997
 
@@ -41719,10 +41729,10 @@ var init_http_mcp_tool_executor = __esm({
41719
41729
  return this.call("/billing/credits", input);
41720
41730
  }
41721
41731
  listServiceConnections(input) {
41722
- return this.getJson("/integrations");
41732
+ return this.getJson("/schedule-connections");
41723
41733
  }
41724
41734
  testServiceConnection(input) {
41725
- return this.call(`/integrations/${encodeURIComponent(input.connectionId)}/test`, {
41735
+ return this.call(`/schedule-connections/${encodeURIComponent(input.connectionId)}/test`, {
41726
41736
  ...input.providerConfigKey ? { providerConfigKey: input.providerConfigKey } : {}
41727
41737
  });
41728
41738
  }
@@ -41742,13 +41752,13 @@ var init_http_mcp_tool_executor = __esm({
41742
41752
  return this.callConnectedMutation("/schedule-connections/actions/zoom/create-meeting", input);
41743
41753
  }
41744
41754
  readServiceConnection(input) {
41745
- return this.call("/integrations/actions/read", input);
41755
+ return this.call("/schedule-connections/actions/read", input);
41746
41756
  }
41747
41757
  importServiceConnectionToMemory(input) {
41748
41758
  return this.call("/schedule-connections/actions/import-memory", input);
41749
41759
  }
41750
41760
  describeServiceConnectionTool(input) {
41751
- return this.call("/integrations/actions/describe", input);
41761
+ return this.call("/schedule-connections/actions/describe", input);
41752
41762
  }
41753
41763
  exportConnectedServiceData(input) {
41754
41764
  const timeoutMs = this.httpTimeoutOverrideMs ?? 29e4;
@@ -41762,7 +41772,7 @@ var init_http_mcp_tool_executor = __esm({
41762
41772
  return this.call("/schedule-connections/actions/export-download", input);
41763
41773
  }
41764
41774
  callServiceConnectionAction(input) {
41765
- return this.callConnectedMutation("/integrations/actions/call", input);
41775
+ return this.callConnectedMutation("/schedule-connections/actions/call", input);
41766
41776
  }
41767
41777
  setScheduledActionConnections(input) {
41768
41778
  return this.call(`/schedule-actions/${encodeURIComponent(input.scheduleActionId)}/connections`, {
@@ -51911,6 +51921,259 @@ var init_resend_inbound_routes = __esm({
51911
51921
  }
51912
51922
  });
51913
51923
 
51924
+ // src/email/workshop-field-notes.ts
51925
+ function escapeHtml4(value) {
51926
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
51927
+ }
51928
+ function workshopFieldNoteUrl(slug) {
51929
+ return `${WORKSHOP_FIELD_NOTES_URL}?article=${encodeURIComponent(slug)}`;
51930
+ }
51931
+ function renderWorkshopFieldNotesBody() {
51932
+ let activeCategory = "";
51933
+ return [
51934
+ '<p style="margin:0">The written companion to our live workshop is already available in MCP Scraper Field Notes. You do not need to read all of it beforehand\u2014browse the subjects that match the work you already do.</p>',
51935
+ ...workshopFieldNotes.flatMap((note, index) => {
51936
+ const category = note.category === activeCategory ? [] : [`<h2 style="margin:${index === 0 ? 32 : 40}px 0 12px;color:#2563eb;font-size:13px;line-height:1.3;letter-spacing:.08em;text-transform:uppercase">${escapeHtml4(note.category)}</h2>`];
51937
+ activeCategory = note.category;
51938
+ const url = escapeHtml4(workshopFieldNoteUrl(note.slug));
51939
+ const sentences = note.snippet.map(escapeHtml4).join(" ");
51940
+ return [
51941
+ ...category,
51942
+ `<div style="padding:20px 0;border-top:1px solid #e5e7eb"><div style="color:#6b7280;font-size:12px;font-weight:700;letter-spacing:.06em">${String(index + 1).padStart(2, "0")}</div><h3 style="margin:6px 0 8px;font-size:19px;line-height:1.3"><a href="${url}" style="color:#101114;text-decoration:none">${escapeHtml4(note.title)}</a></h3><p style="margin:0;color:#454952;font-size:15px;line-height:1.65">${sentences}</p><p style="margin:10px 0 0"><a href="${url}" style="color:#2563eb;font-size:14px;font-weight:700;text-decoration:none">Read this note \u2192</a></p></div>`
51943
+ ];
51944
+ })
51945
+ ].join("");
51946
+ }
51947
+ var WORKSHOP_FIELD_NOTES_URL, workshopFieldNotes;
51948
+ var init_workshop_field_notes = __esm({
51949
+ "src/email/workshop-field-notes.ts"() {
51950
+ "use strict";
51951
+ WORKSHOP_FIELD_NOTES_URL = "https://mcp-field-notes.vercel.app/";
51952
+ workshopFieldNotes = [
51953
+ {
51954
+ slug: "operating-model",
51955
+ category: "Start Here",
51956
+ title: "The Operating Model",
51957
+ snippet: [
51958
+ "MCP Scraper is an operating system for evidence work, not simply a page fetcher.",
51959
+ "It connects a question to the right sources, method, and durable output.",
51960
+ "This note gives you the mental model for everything else."
51961
+ ]
51962
+ },
51963
+ {
51964
+ slug: "context-aware-vault-capture",
51965
+ category: "Start Here",
51966
+ title: "Context-Aware Vault Capture",
51967
+ snippet: [
51968
+ "This demonstrates what a governed memory write actually looks like.",
51969
+ "The agent inspects the vault, chooses the destination, writes with supported links and tags, then reads it back.",
51970
+ "The final retrieval check proves the material can be found later."
51971
+ ]
51972
+ },
51973
+ {
51974
+ slug: "complete-competitive-surface-scan",
51975
+ category: "Start Here",
51976
+ title: "The Complete Competitive Surface Scan",
51977
+ snippet: [
51978
+ "A competitive scan is complete when it covers the evidence needed for a decision.",
51979
+ "That might combine search, sites, reviews, community language, ads, or video without scraping everything.",
51980
+ "This note shows how to define coverage and report gaps honestly."
51981
+ ]
51982
+ },
51983
+ {
51984
+ slug: "search-serp-and-paa",
51985
+ category: "Research Surfaces & Systems",
51986
+ title: "Search, SERPs, and People Also Ask",
51987
+ snippet: [
51988
+ "Rankings, questions, local results, sources, and AI answers are different kinds of search evidence.",
51989
+ "A fast SERP and a deeper People Also Ask harvest answer different questions.",
51990
+ "This note shows how to choose the least expensive search depth that changes the decision."
51991
+ ]
51992
+ },
51993
+ {
51994
+ slug: "reddit-and-community-research",
51995
+ category: "Research Surfaces & Systems",
51996
+ title: "Reddit and Community Research",
51997
+ snippet: [
51998
+ "Community research exposes language, objections, workarounds, and disagreement that polished pages often hide.",
51999
+ "It should preserve thread context and dissent instead of turning comments into a market-wide vote.",
52000
+ "This note explains discovery, deep reading, and careful synthesis."
52001
+ ]
52002
+ },
52003
+ {
52004
+ slug: "browser-profiles-and-ai-fanout",
52005
+ category: "Research Surfaces & Systems",
52006
+ title: "Browser Profiles and AI Citation Fan-Out",
52007
+ snippet: [
52008
+ "A browser profile preserves delegated access to sites that require login.",
52009
+ "Citation fan-out studies the subqueries, browsed sources, and citations inside an AI answer.",
52010
+ "This note keeps the authenticated surface and the research method conceptually separate."
52011
+ ]
52012
+ },
52013
+ {
52014
+ slug: "youtube-and-video-research",
52015
+ category: "Research Surfaces & Systems",
52016
+ title: "YouTube and Video Research",
52017
+ snippet: [
52018
+ "Video research can stop at discovery, continue to transcription, or go deeper into frame analysis.",
52019
+ "Each level costs more and answers a different question.",
52020
+ "This note shows how to choose the cheapest sufficient depth."
52021
+ ]
52022
+ },
52023
+ {
52024
+ slug: "advertising-intelligence",
52025
+ category: "Research Surfaces & Systems",
52026
+ title: "Advertising Intelligence",
52027
+ snippet: [
52028
+ "Advertising intelligence studies the messages, offers, proof, formats, and creative patterns a company distributes.",
52029
+ "It does not prove spend, profitability, or performance.",
52030
+ "This note maps the right evidence path for Meta, Google, public libraries, and authorized account creative."
52031
+ ]
52032
+ },
52033
+ {
52034
+ slug: "search-console-and-analytics-skills",
52035
+ category: "Connected Data & Automation",
52036
+ title: "Search Console and Analytics Skills",
52037
+ snippet: [
52038
+ "Search Console and Analytics supply first-party evidence, not strategy by themselves.",
52039
+ "A user-owned skill adds business doctrine, comparison rules, and priorities.",
52040
+ "This note shows how connected data, public research, Memory, and scheduling become a repeatable decision system."
52041
+ ]
52042
+ },
52043
+ {
52044
+ slug: "scheduled-gmail-zoom-and-research-digestion",
52045
+ category: "Connected Data & Automation",
52046
+ title: "Scheduled Gmail, Zoom, and Research Digestion",
52047
+ snippet: [
52048
+ "Scheduled digestion turns accumulating email, meetings, and research into bounded durable updates.",
52049
+ "It should not silently bulk-import an account or invent people, tasks, deals, or projects.",
52050
+ "This note defines the source, time window, filters, classifications, and destination that make a digest safe."
52051
+ ]
52052
+ },
52053
+ {
52054
+ slug: "rank-tracking-and-change-monitoring",
52055
+ category: "Connected Data & Automation",
52056
+ title: "Rank Tracking and Change Monitoring",
52057
+ snippet: [
52058
+ "A tracker compares observations, and a monitor remembers a baseline before looking again.",
52059
+ "Repeating the same prompt does not create either system.",
52060
+ "This note explains stable subjects, comparable inputs, materiality thresholds, stored history, and change receipts."
52061
+ ]
52062
+ },
52063
+ {
52064
+ slug: "research-artifacts-and-reading-rooms",
52065
+ category: "Artifacts, Memory & Skills",
52066
+ title: "Research Artifacts and Reading Rooms",
52067
+ snippet: [
52068
+ "Research should end in something people can inspect, reopen, and use.",
52069
+ "MCP Scraper can choose among inline reports, private artifacts, tables, governed Memory, and editorial reading rooms.",
52070
+ "This note explains which output belongs where."
52071
+ ]
52072
+ },
52073
+ {
52074
+ slug: "skills-as-user-owned-judgment",
52075
+ category: "Artifacts, Memory & Skills",
52076
+ title: "Skills as User-Owned Judgment",
52077
+ snippet: [
52078
+ "MCP Scraper supplies evidence surfaces and operating primitives.",
52079
+ "A skill supplies the user\u2019s doctrine, sequence, standards, rejection rules, storage choices, and verification.",
52080
+ "This note shows how repeated corrections become reusable judgment."
52081
+ ]
52082
+ },
52083
+ {
52084
+ slug: "seo-and-content",
52085
+ category: "Applications & Contexts",
52086
+ title: "SEO & Content",
52087
+ snippet: [
52088
+ "Start with a real search decision, then inspect the live SERP, questions, and competing pages.",
52089
+ "Preserve the evidence before turning it into a source-backed brief.",
52090
+ "Memory and monitoring make the method reusable instead of disposable."
52091
+ ]
52092
+ },
52093
+ {
52094
+ slug: "local-markets",
52095
+ category: "Applications & Contexts",
52096
+ title: "Local Markets",
52097
+ snippet: [
52098
+ "Local research combines businesses, services, reviews, positioning, search visibility, and geography.",
52099
+ "The goal is to find testable gaps without flattening every source into one score.",
52100
+ "This note shows how to preserve a market baseline and monitor meaningful change."
52101
+ ]
52102
+ },
52103
+ {
52104
+ slug: "sales-and-client-work",
52105
+ category: "Applications & Contexts",
52106
+ title: "Sales & Client Work",
52107
+ snippet: [
52108
+ "This is a relationship graph, not a contact database.",
52109
+ "People, organizations, opportunities, projects, conversations, meetings, decisions, and follow-ups remain separate records connected by verified links.",
52110
+ "This note shows why that distinction prevents invented CRM context."
52111
+ ]
52112
+ },
52113
+ {
52114
+ slug: "competitor-monitoring",
52115
+ category: "Applications & Contexts",
52116
+ title: "Competitor Monitoring",
52117
+ snippet: [
52118
+ "Monitoring begins with a dated baseline and a defined set of things worth watching.",
52119
+ "Each run compares trustworthy evidence and classifies only material change.",
52120
+ "The output reports the difference, including source failure and meaningful no-change."
52121
+ ]
52122
+ },
52123
+ {
52124
+ slug: "learning-and-research",
52125
+ category: "Applications & Contexts",
52126
+ title: "Learning & Research",
52127
+ snippet: [
52128
+ "Raw articles, transcripts, pages, and datasets belong in Library with provenance intact.",
52129
+ "Reusable lessons belong separately in Knowledge and link back to their sources.",
52130
+ "This note explains recall by meaning, time, or graph without confusing relevance with truth."
52131
+ ]
52132
+ },
52133
+ {
52134
+ slug: "build-your-own-tools",
52135
+ category: "Applications & Contexts",
52136
+ title: "Build Your Own Tools",
52137
+ snippet: [
52138
+ "The same acquisition, browser, connection, Memory, artifact, workflow, and scheduling primitives can sit behind something you build.",
52139
+ "That could be a skill, internal process, client report, monitoring service, or narrow SaaS.",
52140
+ "This note starts from one recurring decision and defines the product contract around it."
52141
+ ]
52142
+ },
52143
+ {
52144
+ slug: "improve-originating-skill",
52145
+ category: "How MCP Scraper Learned",
52146
+ title: "Improve the Originating Skill",
52147
+ snippet: [
52148
+ "A corrected output often reveals a problem in the workflow that produced it.",
52149
+ "This reflection method traces the failure to the highest owning layer and improves the originating skill or validator.",
52150
+ "Future work then inherits the lesson instead of repeating the same repair."
52151
+ ]
52152
+ },
52153
+ {
52154
+ slug: "landing-page-problem-solving",
52155
+ category: "How MCP Scraper Learned",
52156
+ title: "Problem-Solving Lessons for Use-Case Pages",
52157
+ snippet: [
52158
+ "This is the concrete receipt from applying that reflection method to MCP Scraper landing pages.",
52159
+ "It captures the fixes for generic copy, weak proof, wrong archetypes, visible seams, inconsistent calls to action, and missed mobile defects.",
52160
+ "The result is a stronger generator, not merely one repaired page."
52161
+ ]
52162
+ },
52163
+ {
52164
+ slug: "workshop-playbook",
52165
+ category: "Workshop",
52166
+ title: "MCP Scraper Workshop Prompt Playbook",
52167
+ snippet: [
52168
+ "This is the practical prompt set for the live session.",
52169
+ "It covers research surfaces, connected data, authenticated browser work, scheduling, skills, completeness receipts, and governed Memory.",
52170
+ "You can replace the examples with your own business, competitors, accounts, and questions."
52171
+ ]
52172
+ }
52173
+ ];
52174
+ }
52175
+ });
52176
+
51914
52177
  // src/api/workshop-registration-repository.ts
51915
52178
  function rowToRegistration(row) {
51916
52179
  return {
@@ -51924,6 +52187,8 @@ function rowToRegistration(row) {
51924
52187
  zoomJoinUrl: row.zoom_join_url == null ? null : String(row.zoom_join_url),
51925
52188
  resendContactId: row.resend_contact_id == null ? null : String(row.resend_contact_id),
51926
52189
  confirmationEmailId: row.confirmation_email_id == null ? null : String(row.confirmation_email_id),
52190
+ fieldNotesEmailId: row.field_notes_email_id == null ? null : String(row.field_notes_email_id),
52191
+ reminder48hEmailId: row.reminder_48h_email_id == null ? null : String(row.reminder_48h_email_id),
51927
52192
  reminder24hEmailId: row.reminder_24h_email_id == null ? null : String(row.reminder_24h_email_id),
51928
52193
  reminder1hEmailId: row.reminder_1h_email_id == null ? null : String(row.reminder_1h_email_id),
51929
52194
  lastError: row.last_error == null ? null : String(row.last_error)
@@ -52023,6 +52288,8 @@ async function updateWorkshopRegistration(id, patch) {
52023
52288
  zoom_join_url: patch.zoomJoinUrl,
52024
52289
  resend_contact_id: patch.resendContactId,
52025
52290
  confirmation_email_id: patch.confirmationEmailId,
52291
+ field_notes_email_id: patch.fieldNotesEmailId,
52292
+ reminder_48h_email_id: patch.reminder48hEmailId,
52026
52293
  reminder_24h_email_id: patch.reminder24hEmailId,
52027
52294
  reminder_1h_email_id: patch.reminder1hEmailId,
52028
52295
  last_error: patch.lastError
@@ -52082,10 +52349,19 @@ function workshopReminderSchedule(startAt) {
52082
52349
  const start = Date.parse(startAt);
52083
52350
  if (!Number.isFinite(start)) throw new Error("Workshop start time is invalid.");
52084
52351
  return {
52352
+ reminder48h: new Date(start - 48 * 60 * 60 * 1e3).toISOString(),
52085
52353
  reminder24h: new Date(start - 24 * 60 * 60 * 1e3).toISOString(),
52086
52354
  reminder1h: new Date(start - 60 * 60 * 1e3).toISOString()
52087
52355
  };
52088
52356
  }
52357
+ function workshopRemindersComplete(registration, schedule, now = Date.now()) {
52358
+ const reminders = [
52359
+ [registration.reminder48hEmailId, schedule.reminder48h],
52360
+ [registration.reminder24hEmailId, schedule.reminder24h],
52361
+ [registration.reminder1hEmailId, schedule.reminder1h]
52362
+ ];
52363
+ return reminders.every(([emailId, scheduledAt]) => Boolean(emailId) || Date.parse(scheduledAt) <= now);
52364
+ }
52089
52365
  function allowedOrigins() {
52090
52366
  const origins = /* @__PURE__ */ new Set(["https://mcpscraper.dev", "https://www.mcpscraper.dev"]);
52091
52367
  for (const value of (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",")) {
@@ -52100,12 +52376,12 @@ function requestOriginAllowed(origin) {
52100
52376
  function clientIp(headers) {
52101
52377
  return headers.get("cf-connecting-ip") || headers.get("x-real-ip") || headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
52102
52378
  }
52103
- function escapeHtml4(value) {
52379
+ function escapeHtml5(value) {
52104
52380
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
52105
52381
  }
52106
- function emailFrame(input) {
52107
- const button = input.button ? `<p style="margin:28px 0"><a href="${escapeHtml4(input.button.url)}" style="display:inline-block;padding:14px 22px;border-radius:999px;background:#0a0a0a;color:#fff;text-decoration:none;font-weight:700">${escapeHtml4(input.button.label)}</a></p>` : "";
52108
- return `<!doctype html><html><body style="margin:0;background:#f5f7fb;color:#101114;font-family:Arial,sans-serif"><div style="display:none;max-height:0;overflow:hidden">${escapeHtml4(input.eyebrow)}</div><table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr><td align="center" style="padding:32px 16px"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:620px;background:#fff;border-radius:24px;overflow:hidden"><tr><td style="padding:42px;background:#2563eb;color:#fff"><div style="font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase">${escapeHtml4(input.eyebrow)}</div><h1 style="margin:12px 0 0;font-size:38px;line-height:1.04;letter-spacing:-.04em">${escapeHtml4(input.title)}</h1></td></tr><tr><td style="padding:36px;font-size:16px;line-height:1.65">${input.body}${button}<p style="margin:30px 0 0;color:#686b73;font-size:13px">You received this because you registered for this workshop. It does not subscribe you to general marketing emails.</p></td></tr></table></td></tr></table></body></html>`;
52382
+ function renderWorkshopEmailHtml(input) {
52383
+ const button = input.button ? `<table role="presentation" cellspacing="0" cellpadding="0" style="margin:30px 0 0"><tr><td bgcolor="#2563eb" style="border-radius:999px"><a href="${escapeHtml5(input.button.url)}" style="display:inline-block;padding:15px 24px;border-radius:999px;background:#2563eb;color:#ffffff;text-decoration:none;font-size:16px;font-weight:700;line-height:1">${escapeHtml5(input.button.label)}</a></td></tr></table>` : "";
52384
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>@media only screen and (max-width:620px){.email-shell{padding:16px 10px!important}.email-header{padding:30px 24px!important}.email-content{padding:30px 24px!important}.email-title{font-size:34px!important}}</style></head><body style="margin:0;background:#f5f7fb;color:#101114;font-family:Arial,sans-serif"><div style="display:none;max-height:0;overflow:hidden">${escapeHtml5(input.eyebrow)}</div><table role="presentation" width="100%" cellspacing="0" cellpadding="0" bgcolor="#f5f7fb"><tr><td class="email-shell" align="center" style="padding:32px 16px"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" bgcolor="#ffffff" style="max-width:620px;background:#ffffff;border-radius:24px;overflow:hidden"><tr><td class="email-header" bgcolor="#2563eb" style="padding:42px;background:#2563eb;color:#ffffff"><table role="presentation" cellspacing="0" cellpadding="0"><tr><td><img src="https://mcpscraper.dev/email/mcp-brain-white.png" width="48" height="48" alt="MCP Scraper" style="display:block;width:48px;height:48px;border:0"></td><td style="padding-left:12px;color:#ffffff;font-size:17px;font-weight:700;letter-spacing:-.01em">MCP Scraper</td></tr></table><div style="margin-top:32px;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#dbeafe">${escapeHtml5(input.eyebrow)}</div><h1 class="email-title" style="margin:12px 0 0;color:#ffffff;font-size:42px;line-height:1.04;letter-spacing:-.04em">${escapeHtml5(input.title)}</h1></td></tr><tr><td class="email-content" style="padding:36px;color:#101114;font-size:16px;line-height:1.65">${input.body}${button}<p style="margin:32px 0 0;padding-top:24px;border-top:1px solid #e5e7eb;color:#686b73;font-size:13px;line-height:1.5">You received this because you registered for this workshop. It does not subscribe you to general marketing emails.</p></td></tr></table></td></tr></table></body></html>`;
52109
52385
  }
52110
52386
  function icsTimestamp(value) {
52111
52387
  return new Date(value).toISOString().replaceAll("-", "").replaceAll(":", "").replace(/\.\d{3}Z$/, "Z");
@@ -52155,21 +52431,26 @@ async function addEventContact(resend, registration, config) {
52155
52431
  email: registration.email,
52156
52432
  segmentId: config.resendSegmentId
52157
52433
  });
52158
- if (segment.error) throw new Error(`Resend could not add the event contact: ${segment.error.message}`);
52434
+ if (segment.error) {
52435
+ console.warn("[workshop-registration] event contact sync skipped:", segment.error.message);
52436
+ return null;
52437
+ }
52159
52438
  if (config.resendTopicId) {
52160
52439
  const topic = await resend.contacts.topics.update({
52161
52440
  email: registration.email,
52162
52441
  topics: [{ id: config.resendTopicId, subscription: "opt_in" }]
52163
52442
  });
52164
- if (topic.error) throw new Error(`Resend could not update the event topic: ${topic.error.message}`);
52443
+ if (topic.error) {
52444
+ console.warn("[workshop-registration] event topic sync skipped:", topic.error.message);
52445
+ }
52165
52446
  }
52166
52447
  return null;
52167
52448
  }
52168
52449
  async function sendConfirmation(resend, registration, config) {
52169
52450
  if (!registration.zoomJoinUrl) throw new Error("Workshop access details are not ready.");
52170
- const date = escapeHtml4(config.dateLabel);
52171
- const timezone = escapeHtml4(config.timezoneLabel);
52172
- const duration = escapeHtml4(config.durationLabel);
52451
+ const date = escapeHtml5(config.dateLabel);
52452
+ const timezone = escapeHtml5(config.timezoneLabel);
52453
+ const duration = escapeHtml5(config.durationLabel);
52173
52454
  const sent = await resend.emails.send({
52174
52455
  from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
52175
52456
  replyTo: "support@updates.mcpscraper.dev",
@@ -52180,7 +52461,7 @@ async function sendConfirmation(resend, registration, config) {
52180
52461
  content: workshopInvitation(registration, config),
52181
52462
  contentType: "text/calendar; method=REQUEST; charset=UTF-8"
52182
52463
  }],
52183
- html: emailFrame({
52464
+ html: renderWorkshopEmailHtml({
52184
52465
  eyebrow: "Your live workshop seat is saved",
52185
52466
  title: `You\u2019re in, ${registration.firstName}.`,
52186
52467
  body: `<p style="margin:0">I\u2019ll show you how the pieces of MCP Scraper fit together around real work: research, Memory, connected accounts, Scheduled Actions, and reusable workflows.</p><p><strong>${date}</strong><br>${timezone}<br>${duration}</p><p>Your calendar invitation is attached, and your Zoom access link is below.</p>`,
@@ -52190,21 +52471,64 @@ async function sendConfirmation(resend, registration, config) {
52190
52471
  if (sent.error || !sent.data?.id) throw new Error(`Resend could not send confirmation: ${sent.error?.message ?? "unknown response"}`);
52191
52472
  return sent.data.id;
52192
52473
  }
52474
+ async function sendFieldNotesGuide(resend, registration) {
52475
+ const sent = await resend.emails.send({
52476
+ from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
52477
+ replyTo: "support@updates.mcpscraper.dev",
52478
+ to: registration.email,
52479
+ subject: "Your MCP Scraper workshop reading guide",
52480
+ html: renderWorkshopEmailHtml({
52481
+ eyebrow: "Before the live workshop",
52482
+ title: "The material is already here.",
52483
+ body: renderWorkshopFieldNotesBody(),
52484
+ button: { label: "Open the Field Notes", url: WORKSHOP_FIELD_NOTES_URL }
52485
+ })
52486
+ });
52487
+ if (sent.error || !sent.data?.id) {
52488
+ throw new Error(`Resend could not send the Field Notes guide: ${sent.error?.message ?? "unknown response"}`);
52489
+ }
52490
+ return sent.data.id;
52491
+ }
52492
+ function workshopReminderMessage(input) {
52493
+ const isTwoDays = input.kind === "48h";
52494
+ const isTomorrow = input.kind === "24h";
52495
+ const subject = isTwoDays ? "Two days out: bring the work you want MCP Scraper to handle" : isTomorrow ? "Tomorrow: see what MCP Scraper can actually do" : "One hour: your MCP Scraper workshop link";
52496
+ const eyebrow = isTwoDays ? "48 hours to go" : isTomorrow ? "Your workshop is tomorrow" : "We start in one hour";
52497
+ const title = isTwoDays ? `Bring one real problem, ${input.firstName}.` : isTomorrow ? "Tomorrow, we build around your work." : "Grab your coffee. Bring a real use case.";
52498
+ const opening = isTwoDays ? "This will not be a march through every feature. I\u2019ll show you how to choose the right combination of research, Memory, connected accounts, Scheduled Actions, and skills for work you already need done." : isTomorrow ? "The point is not to use every tool. It is to recognize which pieces belong together for research, monitoring, client work, learning, or something you are building." : "We go live in one hour. Keep one real project, question, or recurring task in mind\u2014I\u2019ll show you how to map the MCP Scraper pieces around it without turning the session into a dry feature tour.";
52499
+ const prompt = isTwoDays ? "A stubborn workflow, research question, client account, or product idea you want to move forward." : isTomorrow ? "Choose the one job you most want your AI to remember, research, monitor, or help you build." : "Keep one project, question, or recurring task in mind. We will map the right MCP Scraper pieces around it.";
52500
+ const promptCard = `<div style="margin:24px 0 0;padding:18px 20px;border:1px solid #bfdbfe;border-radius:16px;background:#eff6ff"><div style="color:#1d4ed8;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase">Bring this with you</div><p style="margin:7px 0 0;color:#1f2937;font-size:15px;line-height:1.55">${escapeHtml5(prompt)}</p></div>`;
52501
+ const details = `<p style="margin:24px 0 0"><strong>${escapeHtml5(input.dateLabel)}</strong><br>${escapeHtml5(input.timezoneLabel)}<br>${escapeHtml5(input.durationLabel)}</p>`;
52502
+ return {
52503
+ subject,
52504
+ html: renderWorkshopEmailHtml({
52505
+ eyebrow,
52506
+ title,
52507
+ body: `<p style="margin:0">${escapeHtml5(opening)}</p>${promptCard}${details}`,
52508
+ button: {
52509
+ label: input.kind === "1h" ? "Join the live workshop" : "Open your workshop link",
52510
+ url: input.zoomJoinUrl
52511
+ }
52512
+ })
52513
+ };
52514
+ }
52193
52515
  async function scheduleReminder(resend, registration, config, kind, scheduledAt) {
52194
52516
  if (!registration.zoomJoinUrl) throw new Error("Workshop access details are not ready.");
52195
- const isTomorrow = kind === "24h";
52517
+ const message = workshopReminderMessage({
52518
+ kind,
52519
+ firstName: registration.firstName,
52520
+ dateLabel: config.dateLabel,
52521
+ timezoneLabel: config.timezoneLabel,
52522
+ durationLabel: config.durationLabel,
52523
+ zoomJoinUrl: registration.zoomJoinUrl
52524
+ });
52196
52525
  const sent = await resend.emails.send({
52197
52526
  from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
52198
52527
  replyTo: "support@updates.mcpscraper.dev",
52199
52528
  to: registration.email,
52200
- subject: isTomorrow ? "Tomorrow: What You Can Actually Do With MCP Scraper" : "Starts in one hour: MCP Scraper live workshop",
52529
+ subject: message.subject,
52201
52530
  scheduledAt,
52202
- html: emailFrame({
52203
- eyebrow: isTomorrow ? "Your workshop is tomorrow" : "We start in one hour",
52204
- title: isTomorrow ? `See you tomorrow, ${registration.firstName}.` : `We\u2019re starting soon, ${registration.firstName}.`,
52205
- body: `<p style="margin:0">${isTomorrow ? "Tomorrow I\u2019ll show you how MCP Scraper\u2019s research, Memory, connected accounts, Scheduled Actions, and reusable skills fit together around real work." : "The live MCP Scraper workshop starts in one hour. Your Zoom access link is below."}</p><p><strong>${escapeHtml4(config.dateLabel)}</strong><br>${escapeHtml4(config.timezoneLabel)}<br>${escapeHtml4(config.durationLabel)}</p>`,
52206
- button: { label: "Join the live workshop", url: registration.zoomJoinUrl }
52207
- })
52531
+ html: message.html
52208
52532
  });
52209
52533
  if (sent.error || !sent.data?.id) {
52210
52534
  throw new Error(`Resend could not schedule the ${kind} reminder: ${sent.error?.message ?? "unknown response"}`);
@@ -52217,10 +52541,10 @@ async function sendWaitlistEmail(resend, registration, config) {
52217
52541
  replyTo: "support@updates.mcpscraper.dev",
52218
52542
  to: registration.email,
52219
52543
  subject: "You\u2019re on the MCP Scraper workshop waitlist",
52220
- html: emailFrame({
52544
+ html: renderWorkshopEmailHtml({
52221
52545
  eyebrow: "MCP Scraper live workshop",
52222
52546
  title: `You\u2019re on the list, ${registration.firstName}.`,
52223
- body: `<p style="margin:0">The live room is currently full. I have saved your place on the waitlist and will email you if a seat opens.</p><p><strong>${escapeHtml4(config.dateLabel)}</strong><br>${escapeHtml4(config.timezoneLabel)}<br>${escapeHtml4(config.durationLabel)}</p>`
52547
+ body: `<p style="margin:0">The live room is currently full. I have saved your place on the waitlist and will email you if a seat opens.</p><p><strong>${escapeHtml5(config.dateLabel)}</strong><br>${escapeHtml5(config.timezoneLabel)}<br>${escapeHtml5(config.durationLabel)}</p>`
52224
52548
  })
52225
52549
  });
52226
52550
  if (sent.error || !sent.data?.id) throw new Error(`Resend could not send waitlist confirmation: ${sent.error?.message ?? "unknown response"}`);
@@ -52232,7 +52556,7 @@ async function sendInterestEmail(resend, registration) {
52232
52556
  replyTo: "support@updates.mcpscraper.dev",
52233
52557
  to: registration.email,
52234
52558
  subject: "You\u2019re on the early list for the MCP Scraper workshop",
52235
- html: emailFrame({
52559
+ html: renderWorkshopEmailHtml({
52236
52560
  eyebrow: "MCP Scraper live workshop",
52237
52561
  title: `You\u2019re on the early list, ${registration.firstName}.`,
52238
52562
  body: '<p style="margin:0">I\u2019m putting together a live August workshop about what you can actually do with MCP Scraper\u2014and how research, Memory, connected accounts, Scheduled Actions, and reusable workflows fit together around real work.</p><p>I\u2019ll send the date, time, and registration details to this email as soon as the live session is scheduled.</p>'
@@ -52249,6 +52573,7 @@ var init_workshop_registration_routes = __esm({
52249
52573
  import_resend2 = require("resend");
52250
52574
  import_zod41 = require("zod");
52251
52575
  init_db();
52576
+ init_workshop_field_notes();
52252
52577
  init_workshop_registration_repository();
52253
52578
  EVENT_SLUG = "aug-webinar";
52254
52579
  registrationSchema = import_zod41.z.object({
@@ -52300,14 +52625,17 @@ var init_workshop_registration_routes = __esm({
52300
52625
  lastName: parsed.data.lastName,
52301
52626
  capacity: config.capacity
52302
52627
  });
52628
+ const reminderSchedule = config.mode === "registration" ? workshopReminderSchedule(config.startAt) : null;
52303
52629
  if (registration.confirmationEmailId && registration.status !== "pending") {
52304
52630
  const waitlisted = registration.status === "waitlisted";
52305
52631
  const interested = registration.status === "interested";
52306
- return c.json({
52307
- ok: true,
52308
- status: interested ? "interested" : waitlisted ? "waitlisted" : "confirmed",
52309
- message: interested ? "You are on the early list. Check your inbox for confirmation." : waitlisted ? "The live room is full, but you are on the waitlist. Check your inbox for details." : "You are registered. Check your inbox for the confirmation and workshop access details."
52310
- });
52632
+ if (interested || waitlisted || registration.fieldNotesEmailId && reminderSchedule && workshopRemindersComplete(registration, reminderSchedule)) {
52633
+ return c.json({
52634
+ ok: true,
52635
+ status: interested ? "interested" : waitlisted ? "waitlisted" : "confirmed",
52636
+ message: interested ? "You are on the early list. Check your inbox for confirmation." : waitlisted ? "The live room is full, but you are on the waitlist. Check your inbox for details." : "You are registered. Check your inbox for the confirmation and workshop access details."
52637
+ });
52638
+ }
52311
52639
  }
52312
52640
  const resend = new import_resend2.Resend(config.resendApiKey);
52313
52641
  try {
@@ -52356,7 +52684,17 @@ var init_workshop_registration_routes = __esm({
52356
52684
  });
52357
52685
  current = { ...current, resendContactId: contactId, confirmationEmailId: emailId };
52358
52686
  }
52359
- const schedule = workshopReminderSchedule(config.startAt);
52687
+ if (!current.fieldNotesEmailId) {
52688
+ const fieldNotesEmailId = await sendFieldNotesGuide(resend, current);
52689
+ await updateWorkshopRegistration(current.id, { fieldNotesEmailId, lastError: null });
52690
+ current = { ...current, fieldNotesEmailId };
52691
+ }
52692
+ const schedule = reminderSchedule;
52693
+ if (!current.reminder48hEmailId && Date.parse(schedule.reminder48h) > Date.now()) {
52694
+ const reminder48hEmailId = await scheduleReminder(resend, current, config, "48h", schedule.reminder48h);
52695
+ await updateWorkshopRegistration(current.id, { reminder48hEmailId, lastError: null });
52696
+ current = { ...current, reminder48hEmailId };
52697
+ }
52360
52698
  if (!current.reminder24hEmailId && Date.parse(schedule.reminder24h) > Date.now()) {
52361
52699
  const reminder24hEmailId = await scheduleReminder(resend, current, config, "24h", schedule.reminder24h);
52362
52700
  await updateWorkshopRegistration(current.id, { reminder24hEmailId, lastError: null });
@@ -52412,7 +52750,7 @@ function readAsset(name) {
52412
52750
  assetCache.set(name, value);
52413
52751
  return value;
52414
52752
  }
52415
- function escapeHtml5(value) {
52753
+ function escapeHtml6(value) {
52416
52754
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
52417
52755
  }
52418
52756
  function safeUrl(value, allowImage = false) {
@@ -52428,20 +52766,20 @@ function safeUrl(value, allowImage = false) {
52428
52766
  }
52429
52767
  function markdownRenderer() {
52430
52768
  const renderer = new import_marked.Renderer();
52431
- renderer.html = ({ text: text2 }) => `<pre><code>${escapeHtml5(text2)}</code></pre>`;
52769
+ renderer.html = ({ text: text2 }) => `<pre><code>${escapeHtml6(text2)}</code></pre>`;
52432
52770
  renderer.link = function({ href, title, tokens }) {
52433
52771
  const label = this.parser.parseInline(tokens);
52434
52772
  const safe2 = safeUrl(href);
52435
52773
  if (!safe2) return label;
52436
- const titleAttribute = title ? ` title="${escapeHtml5(title)}"` : "";
52774
+ const titleAttribute = title ? ` title="${escapeHtml6(title)}"` : "";
52437
52775
  const external = safe2.startsWith("http://") || safe2.startsWith("https://") ? ' target="_blank" rel="noopener noreferrer"' : "";
52438
- return `<a href="${escapeHtml5(safe2)}"${titleAttribute}${external}>${label}</a>`;
52776
+ return `<a href="${escapeHtml6(safe2)}"${titleAttribute}${external}>${label}</a>`;
52439
52777
  };
52440
52778
  renderer.image = ({ href, title, text: text2 }) => {
52441
52779
  const safe2 = safeUrl(href, true);
52442
- if (!safe2) return `<em>[Image omitted: ${escapeHtml5(text2)}]</em>`;
52443
- const titleAttribute = title ? ` title="${escapeHtml5(title)}"` : "";
52444
- return `<img src="${escapeHtml5(safe2)}" alt="${escapeHtml5(text2)}"${titleAttribute} loading="lazy">`;
52780
+ if (!safe2) return `<em>[Image omitted: ${escapeHtml6(text2)}]</em>`;
52781
+ const titleAttribute = title ? ` title="${escapeHtml6(title)}"` : "";
52782
+ return `<img src="${escapeHtml6(safe2)}" alt="${escapeHtml6(text2)}"${titleAttribute} loading="lazy">`;
52445
52783
  };
52446
52784
  return new import_marked.Marked({ gfm: true, breaks: false, renderer });
52447
52785
  }
@@ -52464,7 +52802,7 @@ function addHeadingIds(html) {
52464
52802
  used.set(base, count + 1);
52465
52803
  const id = count ? `${base}-${count + 1}` : base;
52466
52804
  toc.push({ id, title, level: Number(level) });
52467
- return `<h${level} id="${id}" data-jump-title="${escapeHtml5(title)}">${inner}</h${level}>`;
52805
+ return `<h${level} id="${id}" data-jump-title="${escapeHtml6(title)}">${inner}</h${level}>`;
52468
52806
  });
52469
52807
  return { rendered, toc };
52470
52808
  }
@@ -52507,7 +52845,7 @@ function renderEditorialReadingRoom(input, now = /* @__PURE__ */ new Date()) {
52507
52845
  const generatedAt = now.toISOString();
52508
52846
  const data = { ...input, generatedAt, articles };
52509
52847
  const template = readAsset("index.html");
52510
- const html = template.replace("<title>Editorial Reading Room</title>", `<title>${escapeHtml5(input.site.title)}</title>`).replace('content="A mobile-first editorial reading room."', `content="${escapeHtml5(input.deck || input.site.heroTitle)}"`).replace("/*__READING_ROOM_STYLES__*/", readAsset("styles.css")).replace("/*__READING_ROOM_DATA__*/", escapedJsonForHtml(data)).replace("/*__READING_ROOM_SCRIPT__*/", readAsset("app.js"));
52848
+ const html = template.replace("<title>Editorial Reading Room</title>", `<title>${escapeHtml6(input.site.title)}</title>`).replace('content="A mobile-first editorial reading room."', `content="${escapeHtml6(input.deck || input.site.heroTitle)}"`).replace("/*__READING_ROOM_STYLES__*/", readAsset("styles.css")).replace("/*__READING_ROOM_DATA__*/", escapedJsonForHtml(data)).replace("/*__READING_ROOM_SCRIPT__*/", readAsset("app.js"));
52511
52849
  const bytes = Buffer.byteLength(html);
52512
52850
  return {
52513
52851
  html,