Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@
"@puppeteer/browsers@>=2.0.0 <3.0.0": "3.2.2",
"source-map-js@>=1.2.0 <1.2.2": "1.2.2",
"proxy-addr@>=2.0.0 <2.0.8": "2.0.8",
"dompurify@<3.4.16": "3.4.16",
"qs@<6.16.0": "6.16.0",
"@ai-sdk/provider-utils@>=4.0.0 <4.0.33": "4.0.33",
"smol-toml@<1.9.0": "1.9.0",
"astro@7.2.8>vite": "8.2.4"
"astro@7.2.8>vite": "8.2.4",
"dompurify@<3.4.17": "3.4.17",
"http-cache-semantics": "4.3.0",
"glob@>=10.2.0 <10.5.0": "10.5.0"
},
"patchedDependencies": {
"@saas-maker/templates@0.1.18": "patches/saas-maker-templates-footer-retry-0.1.18.patch",
"http-cache-semantics@4.3.0": "patches/http-cache-semantics@4.3.0.patch"
}
},
"scripts": {
Expand Down Expand Up @@ -59,8 +65,9 @@
"quality:dependencies": "node scripts/check-code-health.mjs dependencies",
"quality:suppressions": "node scripts/check-code-health.mjs suppressions",
"quality:hygiene": "node scripts/check-code-health.mjs hygiene",
"quality": "pnpm check && pnpm typecheck && pnpm test:coverage && pnpm docs:check && pnpm --filter @rolepatch/extension typecheck && pnpm quality:unused && pnpm quality:complexity && pnpm quality:duplication && pnpm quality:cycles && pnpm quality:dependencies && pnpm quality:suppressions && pnpm quality:hygiene",
"resume:mcp": "node scripts/resume-mcp.mjs"
"quality": "pnpm quality:cache-security && pnpm check && pnpm typecheck && pnpm test:coverage && pnpm docs:check && pnpm --filter @rolepatch/extension typecheck && pnpm quality:unused && pnpm quality:complexity && pnpm quality:duplication && pnpm quality:cycles && pnpm quality:dependencies && pnpm quality:suppressions && pnpm quality:hygiene",
"resume:mcp": "node scripts/resume-mcp.mjs",
"quality:cache-security": "node scripts/check-http-cache-security.mjs && node --test scripts/http-cache-security.test.mjs"
},
"dependencies": {
"@ai-sdk/openai-compatible": "2.0.41",
Expand Down Expand Up @@ -93,7 +100,7 @@
"lucide-react": "^1.27.0",
"mammoth": "^1.12.2",
"marked": "^18.0.7",
"next": "16.3.8",
"next": "16.4.1",
"pdf-parse": "^2.4.5",
"posthog-js": "^1.407.3",
"puppeteer-core": "^24.42.0",
Expand All @@ -110,7 +117,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.5.5",
"@opennextjs/cloudflare": "^1.20.2",
"@opennextjs/cloudflare": "1.20.12",
"@playwright/test": "^1.62.0",
"@tailwindcss/postcss": "^4.3.3",
"@testing-library/jest-dom": "^6.9.1",
Expand All @@ -131,6 +138,6 @@
"typescript": "^5",
"ultracite": "7.10.2",
"vitest": "^4.1.11",
"wrangler": "^4.114.0"
"wrangler": "4.149.0"
}
}
15 changes: 15 additions & 0 deletions patches/http-cache-semantics@4.3.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/index.js b/index.js
--- a/index.js
+++ b/index.js
@@ -399,1 +399,10 @@
- if (this._rescc['must-revalidate']) {
+ if (
+ this._rescc['must-revalidate'] ||
+ !this.storable() ||
+ this._rescc['no-cache'] ||
+ (this._isShared && (
+ this._rescc['proxy-revalidate'] ||
+ (this._resHeaders['set-cookie'] &&
+ !this._rescc.public && !this._rescc.immutable)
+ ))
+ ) {
80 changes: 80 additions & 0 deletions patches/saas-maker-templates-footer-retry-0.1.18.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
--- a/src/Base.astro
+++ b/src/Base.astro
@@ -142,10 +142,22 @@
// Close any open popover (mobile menu) after in-page navigation.
addEventListener("hashchange", () => document.querySelectorAll(":popover-open").forEach((p) => p.hidePopover()));

+ if (!document.querySelector("form[data-subscribe], form[data-feedback]")) return;
+
// Footer forms post to SaaS Maker with a publishable key. Without one they run in preview mode.
const api = "https://api.sassmaker.com";
// A page address without query or fragment, which can carry private tokens.
const pageUrl = () => location.origin + location.pathname;
+ const feedbackIntents = new WeakMap();
+ function submissionId(form, feedback, shot, key) {
+ const signature = JSON.stringify([key, feedback]);
+ let intent = feedbackIntents.get(form);
+ if (!intent || intent.signature !== signature || intent.shot !== shot) {
+ intent = { signature, shot, id: crypto.randomUUID() };
+ feedbackIntents.set(form, intent);
+ }
+ return intent.id;
+ }
// The publishable project key: given directly, or resolved from the catalog id.
// Only a found key is cached; a miss or a failed lookup is retried on the next send.
// null means preview mode (no key, or the catalog id has no project); a failed
@@ -242,6 +254,7 @@
async function send(form, statusSel, request, ok) {
const status = form.querySelector(statusSel);
const button = form.querySelector("button[type=submit]");
+ if (button.disabled) return false;
const say = (text) => (status.textContent = text);
button.disabled = true;
say("Sending…");
@@ -258,7 +271,7 @@
} catch (err) {
say(err instanceof Unreachable
? "Couldn't reach SaaS Maker, so nothing was sent. Please try again in a moment."
- : "That did not send. Please try again in a moment.");
+ : "Couldn't confirm delivery. Please try again in a moment.");
} finally {
button.disabled = false;
}
@@ -272,24 +285,28 @@
e.preventDefault();
const data = new FormData(form);
const email = String(data.get("email") || "").trim();
- const shot = data.get("screenshot");
+ const shot = form.elements.namedItem("screenshot")?.files?.[0];
+ const feedback = {
+ type: data.get("type") || "feedback",
+ title: String(data.get("title") || "").trim(),
+ description: String(data.get("message") || "").trim(),
+ ...(email && { submitter_email: email }),
+ page: { url: pageUrl(), title: document.title },
+ ...(anchor && { anchor }),
+ source: "studio-footer",
+ };
const sent = await send(form, "[data-feedback-status]", async () => {
const key = await projectKey(form);
if (!key) return null;
const body = new FormData();
body.set("feedback", JSON.stringify({
- type: data.get("type") || "feedback",
- title: String(data.get("title") || "").trim(),
- description: String(data.get("message") || "").trim(),
- ...(email && { submitter_email: email }),
- page: { url: pageUrl(), title: document.title },
- ...(anchor && { anchor }),
- source: "studio-footer",
+ ...feedback,
+ submission_id: submissionId(form, feedback, shot, key),
}));
if (shot && shot.size) body.set("screenshot", shot);
return fetch(api + "/v1/feedback", { method: "POST", credentials: "omit", headers: { "X-Project-Key": key }, body });
}, "Thanks, it reached the person who builds " + form.dataset.product + ".");
- if (sent) { anchor = null; showAnchor(); form.querySelector("[data-feedback-file-label]").textContent = "Add a screenshot"; }
+ if (sent) { feedbackIntents.delete(form); anchor = null; showAnchor(); form.querySelector("[data-feedback-file-label]").textContent = "Add a screenshot"; }
}

if (form.hasAttribute("data-subscribe")) {
Loading
Loading