diff --git a/docs/ENGAGEMENT_NEXT_WAVE_2026-11-05.md b/docs/ENGAGEMENT_NEXT_WAVE_2026-11-05.md
new file mode 100644
index 00000000..f2c52c70
--- /dev/null
+++ b/docs/ENGAGEMENT_NEXT_WAVE_2026-11-05.md
@@ -0,0 +1,39 @@
+# Toolify365 — engagement improvement follow-up (2026-11-05)
+
+## Implemented in draft PR #39
+- Recently visited links on tool pages, limited to 3; now exclude both current/unknown/duplicate and any destination already shown by the existing related-tools row.
+- Percentage-calculator A/B comparison and explicit local draft save/restore/delete, 7-day expiry, 17-language text.
+- Analytics: `recent_tool_click` and `scenario_action` (IDs only; no user-entered numbers).
+- Standalone verification: pure-module strict TypeScript passed; 29 earlier independent assertions, 9 shared-state checks and 6 recent-link-selection checks passed separately. **Not full project Vitest/Next build**.
+
+## Current release blocks
+1. Full checkout cannot be fetched from this environment due to DNS resolution of github.com. Run `bash scripts/check-engagement-release.sh` on an owner-accessible clone.
+2. Manual en/ja desktop/mobile keyboard, share-link, unavailable browser storage, expired draft, consent and GA4 DebugView QA.
+3. Merge only after those checks; VPS deployment and post-deploy smoke test belong to owner.
+4. HYPD live-browser and Lighthouse endpoints were not available on the connected plan, so no real mobile performance score was obtained. HYPD GA4 account summaries returned no **selected** properties; do not claim measured engagement improvements.
+
+## Additional work worth doing after release, in impact order
+**P0 — use evidence, not time-on-page alone.** Establish last 28 days vs preceding 28 days: engaged sessions, engagement rate, average engagement time per session, pages/session, `calculate`, `recent_tool_click`, `related_click`, `scenario_action`, affiliate click and revenue. Break down by tool landing page, device, locale and source. Deduplicate returning navigation and avoid counting passive tabs as improvement. Register necessary GA4 custom dimensions (event-scoped `destination_tool`, `action`) if they are absent; verify consent first.
+
+**P1 — test on high-intent tools only.** Pick the top 5 landing pages from GA4 with enough traffic and the lowest engaged-session ratio; verify tool result is visible in first mobile viewport and no ad obscures it. Prioritize task completion, then a single logically related next action. Do not add generic link blocks or timers just to inflate average duration.
+
+**P2 — contextual workflows.** After successful % calculator comparison, test task-specific save/compare for loan, tip and other numeric tools only if a privacy review confirms their inputs are safe for local persistence. For free-text, health, financial identifiers or uploads, avoid blanket auto-save.
+
+**P3 — speed and A/B rollout.** Measure mobile Lighthouse LCP/INP/CLS, test reserved ad space and lazy loading, validate language navigation and ad placement. Roll out behind a tested feature gate or short evaluation window when analytics/sample size supports it. Roll back if tool completion or mobile performance worsens.
+
+## Operational release policy
+PR #39 stays draft / NO-GO until full build and real-device QA succeed. This document is a plan for further implementation, not a claim of production changes, conversion lifts or GA4 access.
+
+## Additional implementation: result-first navigation (2026-11-05)
+- `ToolFrame` now shows the existing related tool row and the new recent tool row **immediately after the workspace, before the below-result and banner ad units**. Both ad units, the in-article ad, and sidebar ad are retained; this is an ordering change, not an ad removal.
+- The previous related/recent deduplication means suggestions maximize distinct follow-up actions.
+- Added `site/src/components/tools/ToolFrame.order.test.ts` to enforce source-level order and retained ad components. This test was committed but has NOT been run inside the project's Vitest environment.
+- GitHub code readback confirmed the order after commit; visual, iPhone, ad placement and RPM impact still require real device QA and post-release measurement.
+- `percentage-calculator` was verified present in `MANUAL_SHARE_SLUGS`: its own `useShareableState` intentionally disables generic URL writing from `useAutoShareableState`, so this PR does not need to add a second serialization path.
+- GA4 cannot currently be queried with the selected connection: HYPD reports connected GA4 accounts but no selected property. Select the Toolify365 property before any baseline analysis; do not infer current engagement from other businesses.
+
+## ShareBar URL polling optimization (2026-11-05)
+- Replaced unconditional one-second URL polling with change events and history/BFCache resynchronization.
+- Both manual and automatic share hooks now notify the result-link control immediately after replaceState.
+- Added source-level regression tests in ShareBar.events.test.ts; full project Vitest and browser tests not run.
+- Validate result-link availability after edits, navigation and tab restoration before release. No performance uplift claimed.
diff --git a/docs/GROWTH_RELEASE_GATE_2026-11-05.md b/docs/GROWTH_RELEASE_GATE_2026-11-05.md
new file mode 100644
index 00000000..6ccb033b
--- /dev/null
+++ b/docs/GROWTH_RELEASE_GATE_2026-11-05.md
@@ -0,0 +1,86 @@
+# Toolify365 Growth Release Gate — 2026-11-05
+
+Related: #37, PR #38. No GitHub Actions are required or authorized for this rollout.
+
+## Existing SEO safeguards (do not duplicate)
+- `src/app/sitemap.ts`: generates only URLs selected by `isIndexableIn` / `indexableLocalesFor`.
+- `src/app/robots.ts`: publishes robots/sitemap configuration.
+- `src/lib/seo/metadata.ts`: canonical, robots and hreflang.
+- `src/lib/tools/registry.ts`: demand-ranked core index allowlist + selective locale exceptions.
+- `scripts/audit-seo.ts`, `scripts/report-index-policy.ts`, `scripts/indexnow.ts`: existing audits and index notification.
+
+**Do not** bulk-index all 223 tools × 17 locales. GSC index coverage is NOT the same as the count of URLs intended for indexing.
+
+## Local verification from repository root
+
+```bash
+cd site
+npm ci
+npm run typecheck
+npm run test
+npm run validate
+npm run audit:seo
+npm run audit:i18n
+npm run audit:index-policy -- --json
+npm run build
+```
+
+These checks need a machine with dependencies and network/required build environment. If any check fails, do not merge or deploy. Follow existing `prebuild` exchange-rate freshness policy; do not bypass it to force a green build.
+
+## Browser QA before production
+1. Check en and ja pages `/tools/character-frequency` and `/tools/reverse-text-generator`: free software CTA appears below tool and links to Text Batch Studio with correct locale.
+2. Check `/tools/csv-json-converter` and `/tools/json-formatter`: only currently available CSV Cleaner Pro and JSON Studio destinations appear.
+3. Confirm a tool with no mapping does not show an empty software panel.
+4. On mobile and keyboard navigation, confirm CTA focus, labels and layout.
+5. Check `/software/{csv-cleaner-pro,json-studio,text-batch-studio}` processing / downloading still work, and failed input still gives the previous error.
+6. Consent/debug QA: `software_click` fires on CTA, `software_process` on process success/failure and `software_download` on a download click. No user-entered text, file name or file contents are sent. Without GA configuration events are safe no-ops.
+7. Verify live canonical host is `https://toolify365.com`, and check HTTP 200, robots/sitemap and hreflang reciprocal URLs manually. Search-result snippets can lag deployment.
+
+## GA4 definitions
+Create event-scoped custom dimensions (where not already present): `tool_slug`, `locale`, `destination`, `software_slug`. Do not register redundant fields. Analyze:
+- Organic landing sessions → `calculate` → `software_click`
+- `software_click` → `software_process` successful
+- `software_process` → `software_download`
+Count users/sessions when possible, not only raw events. Exclude internal traffic. Not all click-through events guarantee successful navigation or file download.
+
+## Search prioritization
+- Obtain last 28 finalized days and prior 28 days from GSC / Bing, grouped by page + query + locale/country + device.
+- Work on high-impression, low-CTR indexed pages first; ensure unique title, accurate introduction, examples and FAQ.
+- Expand `INDEX_EXCEPTIONS` only for verified demand with localized content; 5–10 pairs at a time, assess after 28 days, rollback if indexing/crawl quality degrades.
+- Do not automatically post marketing links to unrelated communities or create scaled thin pages.
+
+## Deployment and rollback
+- After all gates pass, merge PR #38 and deploy by the site's owner-managed process.
+- Verify direct URLs and GA4 DebugView, then submit **changed, valid, canonical** URLs using existing IndexNow script (not Google).
+- Record release commit, timestamp, and post-release GA4/GSC/Bing baseline.
+- If UI/processing or analytics regress, revert PR #38 and redeploy; do not edit the sitemap allowlist as part of rollback unless it changed.
+
+## Current evidence
+Source-only reviews are not equivalent to running the above commands. The connected GSC access returned a 3-day sample (2026-10-31 to 2026-11-02) with no suggested opportunities; it cannot establish a 28-day comparison or zero traffic.
+
+## Post-deployment automated smoke check (new)
+Run only **after** a successful owner deployment:
+```bash
+cd site
+SITE_URL=https://toolify365.com npm run audit:live-growth
+```
+This performs read-only HTTP checks on key en/ja tool and software pages, expected contextual CTA HTML, canonical presence, robots and sitemap. HTTP connectivity is required; a failure from a restricted runner is NOT proof that the production site is down. This script is not a substitute for browser interaction tests, GA4 DebugView, live Search Console URL Inspection or a local production build.
+
+## Extended release safeguards (2026-11-05)
+The `audit:live-growth` script now also checks that known index-eligible core tool pages do **not** return a noindex meta tag and appear in the served sitemap, while verifying same-origin self-canonical and unexpected redirects. This is a *post-deployment* check; it intentionally fails when the current production deployment does not yet contain the PR's CTA. Do not run it as a pre-deploy gate against the old production version.
+
+## Dependency-free regression test (2026-11-05)
+`cd site && npm run test:smoke-html` runs the Node built-in test suite for HTML meta robots, canonical extraction, and actual on-domain, same-locale software CTA anchor detection. This was also exercised in an isolated Node.js 22 environment (3 test groups passing). It is **not** a substitute for Next.js/Vitest full tests, real browser QA or deploy verification.
+
+## Parallel PR integration guard — PR #39
+PR #39 (engagement) also edits `site/src/components/tools/ToolFrame.tsx` and `site/src/lib/analytics/events.ts`, both changed in this PR #38. Do **not** overwrite either file with an entire version copied from one PR while merging the other.
+
+When integrating:
+1. Rebase/merge both branches locally in a scratch branch (not `main`).
+2. Preserve in `ToolFrame.tsx` BOTH PR #39's engagement/navigation UI and PR #38's `ToolSoftwareRecommendations` section.
+3. Preserve in `events.ts` ALL existing event exports plus PR #39 event changes and PR #38 `trackSoftwareClick`, `trackSoftwareProcess`, `trackSoftwareDownload`.
+4. Resolve conflicts, run TypeScript, all tests including `test:smoke-html`, audits and build, and review DOM/keyboard order of CTA vs related/recents.
+5. Only after passing checks merge/release through the owner process. Avoid untested sequential merges.
+
+## Isolated execution evidence
+Node.js 22 `node --test` was run on exact reproduced copies of `growth-html-checks.mjs` and its test cases in the working container: 3/3 subtests passed (robots/googlebot; canonical; valid destination/locale/origin). This does **not** validate the complete Next.js application, GitHub PR head in a cloned checkout, nor the deployed website.
diff --git a/docs/PR39_VERIFICATION_2026-11-05.md b/docs/PR39_VERIFICATION_2026-11-05.md
new file mode 100644
index 00000000..4b59f22d
--- /dev/null
+++ b/docs/PR39_VERIFICATION_2026-11-05.md
@@ -0,0 +1,141 @@
+# PR #39 verification, 2026-11-05
+
+## Confirmed
+- Current main-to-feature comparison checked through GitHub connector.
+- Local isolated `tsc --strict --noEmit` passed for `scenario-comparison.ts`, `scenario-resume.ts`, and `recent-recommendations.ts`.
+- Local Node assertion harness passed 29 assertions for the same pure modules.
+- Additional Vitest cases for invalid draft timestamps added to the PR.
+- Public English percentage page currently renders the original calculator but not the new A/B and resume controls.
+
+## Still blocking release
+- Local `git ls-remote` could not resolve github.com, so a full repository checkout was unavailable.
+- Full `npm ci`, project Vitest, app-wide TypeScript checks, Next.js build, browser/mobile QA, and GA4 DebugView were not performed.
+- No GitHub Actions, PR merge, VPS deploy, or verified engagement uplift.
+
+## Next required action
+In an owner-accessible checkout run `bash scripts/check-engagement-release.sh`, then test English/Japanese desktop and mobile, blocked storage, expired draft, URL share, keyboard accessibility and consent. Merge and deploy only after all gates pass; measure 7/28-day GA4 engagement.
+
+This file is a checkpoint, not a passing full-release certificate.
+
+## Follow-up verification
+- Rechecked local Git access: DNS resolution of github.com still fails.
+- Re-ran isolated TypeScript strict compilation and 29 independent assertions: passed. This is not the app-wide build.
+- Added maxLength=40 to both percentage number inputs to match URL restore and saved draft limits.
+- English production page inspected: new compare and resume sections not found in fetched HTML.
+- No merge, deployment, or verified engagement uplift.
+
+## Shared-link restore hardening (2026-11-05)
+- Added `scenario-share.ts` and `scenario-share.test.ts` to reject incomplete, malformed, oversized, and non-calculable URL payloads as a whole. Calculator fields are no longer partially restored from an invalid `?s=` payload.
+- In the isolated local TypeScript harness, all four pure source modules passed strict checking. The existing 29 assertions and nine new share-state assertions passed (38 total). This is not a full repository Vitest run or Next.js build.
+- No GitHub Actions, production deployment, or verified engagement uplift.
+
+## 2026-11-05 — Related/recent navigation deduplication
+- Updated recent recommendation selection to omit slugs already present in the existing related-tools section. This avoids using the same screen real estate for duplicate destinations.
+- Added test cases for overlap and validated six cases with standalone Node assertions and TypeScript strict compilation. This is not a full React or Next build.
+- HYPD live-browser and mobile Lighthouse audits could not run: the linked service reported the tools unavailable on the current plan. No external mobile performance score or rendered DOM result was obtained.
+- Main merge and production deployment remain blocked by full-build/device verification, and no improvement in GA4 engagement has been measured.
+
+## Security-focused share URL regression (2026-11-05)
+- Generic share-state collection excludes password and hidden input types, in addition to file and action-only controls; the policy is centralized in share-input-policy.ts.
+- Both generic read and write are capped at 1800 encoded characters; the manual hook is capped at 4096 encoded characters in both directions. Oversized external payloads are rejected before decoding.
+- Added share-input-policy.test.ts; independently compiled this exact pure policy source with strict TypeScript and executed 18 standalone Node assertions, all passed. Full project Vitest/build and interactive browser QA remain pending.
+- Privacy caveat: other generic input values may still enter a shareable URL. Never describe all tool inputs as local-only or claim privacy audit completion.
+- Before release, test older ordinary shared links, password/hidden inputs, history navigation, copy-result-link availability, consent and mobile behavior. No rollout or engagement gain verified.
+
+## PII-oriented generic sharing guard (2026-11-05)
+- Automatic share URL collection now excludes email and telephone input types in addition to passwords/hidden controls.
+- It also rejects fields explicitly marked `data-private` or `data-share="off"`, and browser autofill fields such as name, address, postal code, date of birth, account username and payment card tokens (for inputs, selects and textareas).
+- Added `share-input-policy.integration.test.ts` alongside policy unit tests. New source verified via GitHub readback. A reconstructed local copy of the pure `share-input-policy.ts` passed strict TypeScript compilation and **36 independent Node assertions**. The new Vitest source was **not** run as a full project test.
+- Corrected both sharing hook comments: state placed in `?s=` is **not strictly device-local**; URL history, HTTP requests, copied links and referrers can expose it. Arbitrary remaining free-text values are not guaranteed non-sensitive; a tool-by-tool privacy assessment remains required before release.
+- Run the full local release gate, plus manual tests that standard numeric sharing still works, sensitive autofilled values never enter `?s=`, older links open acceptably, and no analytics payload includes entered values.
+
+## 2026-11-06 security follow-up
+- Fixed auto-sharing privacy exclusions to respect data-private or data-share="off" on enclosing field groups, not just on the input itself.
+- Updated share-input-policy.integration.test.ts to assert the ancestor exclusion selector. GitHub readback confirmed the change.
+- Full local test/build remains unavailable in the present execution environment; this update is not evidence of successful production deployment or GA4 uplift.
+
+## 2026-11-06: release blocker re-evaluation
+- Verified every npm script invoked by `scripts/check-engagement-release.sh` exists in `site/package.json`. This establishes script-name consistency only; it does **not** execute the scripts.
+- Six connector-readback consistency checks passed for release script names, ancestor privacy guard, input/textarea/select exclusion, URL length limits, lack of ShareBar polling and navigation-before-ad order.
+- Fixed a fragile string-escaping expectation in `share-input-policy.integration.test.ts`.
+- The generic URL restore path now ignores decoded field values unless they are strings or booleans, and skips non-integer or out-of-bounds mode button indices. Added source regression checks.
+- `git ls-remote` still cannot resolve github.com in the working container, preventing a full checkout and project-level npm ci, Vitest, Next build and browser QA.
+- Remains **NO-GO** until a full owner-accessible checkout passes all tests, browser/privacy/analytics checks, and an explicit production deploy. No live engagement improvement measured.
+
+## 2026-11-06 — URL-driven button activation risk
+- Identified that auto-shared URLs previously could trigger clicks on arbitrary tool buttons by index, not just mode controls.
+- Limited URL replay and recording of mode-button indices to sibling groups with at least two buttons and an existing active-mode styling marker; disabled and in-form submit controls cannot be replayed.
+- Preserved triangle-calculator's non-form mode buttons, which have browser-default submit type but are not form submission controls.
+- Added a source regression assertion in `share-input-policy.integration.test.ts`. GitHub readback confirmed code and test source; the project Vitest suite and browser QA **have not run**.
+- Remaining test: valid triangle right/sss/sas links, malicious links pointing to action buttons, mode toggles, alternate tools with button-driven modes, mobile UI and sharing result links.
+- Release stays NO-GO until the owner-accessible full build and manual QA pass. No performance or revenue lift has been demonstrated.
+
+## 2026-11-06 — Search-index localization and payload hardening
+- `useSearchIndex` now keys fetched data by the requested locale and returns null while an index for the new locale is loading, rather than displaying stale-language titles.
+- Extracted malformed search-index response handling into `site/src/lib/search/search-index-payload.ts`. Missing arrays and malformed entries are ignored; valid tool entries are preserved.
+- Added unit tests for parsed payloads plus a source-level regression test for locale-scoped display. Reconstructed the exact parser in a standalone local TypeScript harness with a minimal type stub: strict compilation and 10 Node assertions passed.
+- This is not a full Next.js app compile or UI-level language-switch test. No GitHub Actions, merge or deployment were performed.
+- Full-repo checkout remains blocked by DNS from this container, confirmed again. Run `bash scripts/check-engagement-release.sh` in the owner environment and verify locale switches in browser before release.
+
+## 2026-11-06 — Search index link validation
+- Strengthened the localized search-index parser used by recent-tool navigation: accept only hyphenated ASCII tool slugs (max 100 chars), known tool categories, nonempty localized titles (max 200 chars); drop duplicate slugs.
+- Reject malformed path traversal, percent-encoded separators, unknown categories and oversized titles without hiding valid tool suggestions.
+- Expanded `search-index-payload.test.ts` for malformed URL targets, duplicate slugs, and title/category validation.
+- Reconstructed the exact pure parser in an isolated local TypeScript harness with a minimal compatible SearchItem type stub. `tsc --strict` passed and 15 Node assertions passed.
+- **These are isolated checks, not the full repository Vitest/Next build**. No production deployment, mobile QA, or GA4 engagement uplift verified.
+- Next gate: full checkout and `bash scripts/check-engagement-release.sh`, then check actual en/ja locale switching and malformed index behavior on mobile.
+
+## 2026-11-06 — Next.js history state preservation
+- Fixed both `useShareableState` and `useAutoShareableState`: share URL updates now call `history.replaceState(history.state, "", url)` rather than replacing the history state with `null`.
+- This preserves the client router's pre-existing navigation state when adding/updating `?s=`. Avoid interpreting it as a verified navigation fix until tested in a browser.
+- Updated ShareBar.events.test.ts to assert preserved history state and reject the old null-overwrite pattern.
+- Read back both GitHub hook files and the test. Both hooks contain the new call and still dispatch share-state-change; neither retains the previous null overwrite.
+- Remaining manual QA: edit inputs, use back/forward, switch locale and tools through client navigation, restore shared links after BFCache return and confirm no hydration/router errors.
+- Source changes and static readback are verified. **Full Vitest, Next production build, real browser QA, merge and deploy are still pending.**
+
+## 2026-11-06 — Test fixture correction and isolated verification
+- Found misleading `search-index-payload.test.ts` malformed fixture: `slug: "s"` is structurally valid under the new parser; the fixture also lacked a title, masking the invalid-slug assumption. Replaced it with a genuinely unsafe path `../unsafe` plus a valid title so that exclusion is specifically attributable to the slug format.
+- Added a separate regression case that explicitly preserves a valid one-character slug; this checks that the validator is not over-restrictive.
+- Reproduced the parser logic in an isolated local TypeScript harness with a minimal compatible SearchItem type definition; `tsc --strict` passed and 10 grouped Node assertions passed, including normal, invalid, duplicate and short-slug scenarios. This is NOT a run of project Vitest or Next build.
+- Git CLI still cannot resolve github.com in the execution environment; continue requiring owner-accessible `bash scripts/check-engagement-release.sh`, browser QA and reviewed deployment before merging PR #39.
+
+## 2026-11-06 — GA4 synthetic restore measurement correction
+- `ToolInteractionTracker.tsx` now ignores untrusted DOM input/change and click events when tracking `calculate`, `copy_result`, and `preset_click`. Auto shared-URL restore uses programmatic events and `.click()`, so those should not count as actual user engagement.
+- Added `ToolInteractionTracker.trust.test.ts` to assert that the user-event boundary and event subscriptions remain in place.
+- GitHub readback confirms 5 source consistency checks (trusted input/click guards, listener registration, synthetic restore dispatch and test assertions) passed. **This is static readback, not running Vitest, a browser, or a GA4 debugging session.**
+- Browser acceptance: load an auto-shared URL, confirm no `calculate`/preset/copy event before user action; change a field manually and confirm one `calculate`; check consent denied/granted modes; verify no analytics values include user input.
+- Release remains NO-GO pending full tests/build and manual owner QA, without GitHub Actions.
+
+## 2026-11-06 — Client-side route transitions and delayed GA4 events
+- `ToolInteractionTracker` now resets one-shot `calculate` / `copy_result` / `preset_click` flags whenever the effect is recreated for a changed tool slug, locale or category. This avoids carrying dedup state from an earlier client-side route.
+- Cancelled the delayed 50 ms calculation event on unmount and added a disposed guard. Without this, a queued timer could send an event after a tool was left.
+- Added a corresponding `ToolInteractionTracker.trust.test.ts` source regression case and confirmed five source consistency checks by GitHub readback.
+- This static verification is NOT a real Vitest run or GA4 DebugView test. Browser acceptance still needs navigate tool A → B without a full reload, operate B and verify its first calculate, then leave before the pending event and verify no late analytics event.
+- Release stays draft/NO-GO; no full Next build, review, deployment or observed revenue/engagement improvement.
+
+## 2026-11-06 — Name/id-based sensitive input protection
+- Auto-generated `?s=` state formerly inspected input type, browser autocomplete and opt-out attributes, but did not check developer-assigned field `name` or `id`. Added an optional identifier parameter in `share-input-policy.ts` and wired `name` + `id` for input, textarea and select.
+- Filters credential/personal-data hint names including `apiKey`, `privateKey`, `refresh_token`, `creditCard`, `cardNumber`, `billingEmail`, `contactPhone`, and `postalCode`; camelCase is normalized. Regular numeric field identifiers such as `amount`, `valueA`, `angleC` remain eligible.
+- Added unit and integration test assertions. GitHub readback verifies the new source and test wiring. Reused the exact source pattern in a JavaScript regex check: **17 identifier cases passed** (12 excluded, 5 allowed). This check is limited to identifier matching, not full Vitest/React/Next behavior.
+- Remaining risk: arbitrary free-text can still contain sensitive content even without sensitive field names. Full tool-by-tool privacy review remains a release blocker. Real browser/GA4/consent checks and full `bash scripts/check-engagement-release.sh` are not complete.
+
+## 2026-11-06 — Oversized share URL stale-state protection
+- In both manual and generic sharing hooks, when new input state exceeds the URL size cap, the previous `?s=` is now removed rather than silently left in the URL. This prevents an older result from being offered as if it represented the current calculator input.
+- Both paths preserve `window.history.state` and dispatch `toolify:share-state-change` when removing a stale result URL, so the ShareBar "copy result link" control can disappear.
+- Updated `ShareBar.events.test.ts` with regression expectations for both hooks.
+- GitHub readback verified source/test presence of removal, history preservation and notification, but project-level Vitest, Next build and real browser behavior remain unverified.
+- Browser acceptance: generate result URL, exceed size cap, confirm result-link button disappears while normal share stays available; shorten input, confirm result link regenerates; verify back/forward and client route navigation.
+
+## 2026-11-06 — Private-by-default automatic free-text URL sharing
+- Significant privacy model change: `useAutoShareableState` no longer serializes `text`, `search`, `url` or `textarea` fields into `?s=` automatically. A reviewed tool must explicitly mark such a field `data-share="on"` to permit its inclusion.
+- Numeric fields, selects, checkboxes, dates and other non-free-text controls retain existing eligibility after private/type/autocomplete/name/id exclusions. Explicit privacy exclusions still override opt-in.
+- Added pure policy and hook integration regression assertions and confirmed **7 GitHub source consistency checks** for policy, opt-in wiring and tests. These checks DO NOT constitute running Vitest, TypeScript project typecheck or Next.js build.
+- Trade-off: text-processing tools may no longer preserve their entire text input when copying generic result links. This intentional reduction needs UX acceptance and tool-by-tool review before enabling any opt-in. Previously generated links can still be opened, but excluded text values are not restored by the generic hook.
+- The separate manually registered `useShareableState` hook is not automatically covered by this change; its tool-specific state must be reviewed for sensitive content.
+- Release remains NO-GO pending full owner-accessible build/test gate, text tool privacy review, mobile browser QA, analytics consent and actual deployment verification.
+
+## 2026-11-06 — Untrusted shared URL value type and option checks
+- Generic URL restore now validates decoded values against the target DOM input type before calling native setters or clicking checkbox/radio controls.
+- Only boolean values may restore checkboxes; radio controls require string values that exactly match the target radio's own value; select controls require an option present in the current select. Numeric/range fields require a finite numeric string or an empty string. String values are limited to 1800 characters.
+- Added unit and integration regression tests for the policy and hook wiring. GitHub source readback confirms **6 static consistency checks** (policy export, use, select/radio validation, tests). The Vitest tests have been added but **have not run**.
+- Remaining concerns: browser behavior when options change dynamically, old share link compatibility, and manually registered `useShareableState` consumers. Full owner checkout QA, typecheck/build, consent verification and desktop/mobile tests remain release blockers.
diff --git a/docs/PR40_INTEGRATION_VERIFICATION_2026-11-06.md b/docs/PR40_INTEGRATION_VERIFICATION_2026-11-06.md
new file mode 100644
index 00000000..b6792082
--- /dev/null
+++ b/docs/PR40_INTEGRATION_VERIFICATION_2026-11-06.md
@@ -0,0 +1,120 @@
+# PR #40 — Toolify growth + engagement integration gate
+
+Date: 2026-11-06 JST. Integration from PR #39 head plus PR #38 changes. Original PRs and `main` are unchanged.
+
+## Integration proof (GitHub source readback)
+- `site/src/components/tools/ToolFrame.tsx`: both `ContinueWithRecent` and `ToolSoftwareRecommendations` imported and rendered.
+- `site/src/lib/analytics/events.ts`: `trackRecentToolClick`, `trackScenarioAction`, `trackSoftwareClick`, `trackSoftwareProcess`, `trackSoftwareDownload` exports retained.
+- `site/package.json`: `test:smoke-html`, `audit:live-growth` retained.
+- GitHub compare against `main`: 91 commits ahead, zero behind, 35 changed files at PR #40 creation. This is **not** evidence of conflict-free runtime behavior.
+- Separate locally reconstructed copy of the PR #38 HTML helper/test: `node --test` = 3 passed, 0 failed under Node 22.16.0. This does not cover all of PR #40.
+
+## Mandatory local gate — no GitHub Actions
+```bash
+git fetch origin
+git checkout integration/toolify-growth-engagement-20261106
+cd site
+npm ci
+npm run test:smoke-html
+npm run typecheck
+npm run test
+npm run validate
+npm run audit:seo
+npm run audit:i18n
+npm run audit:index-policy -- --json
+npm run build
+```
+Stop on any failure. Make necessary fixes **on the integration branch**, then rerun the full suite.
+
+## Functional and privacy checks
+- Mobile/desktop en and ja pages: recent-tool controls and software CTA coexist without duplicated destinations or hidden primary tool.
+- Percentage calculator: 2-scenario comparison, optional save/resume/clear, share/restore, malformed URL and expired drafts.
+- ShareBar: no password/hidden value exposed in generic sharing, length cap enforced, event subscriptions behave correctly after back/forward.
+- Software: CSV Cleaner Pro, JSON Studio, Text Batch Studio process and download correctly; invalid input and max-size errors handled without data upload.
+- GA4: consent-aware events for recent click, scenario actions, software click/process/download. Ensure no user inputs, file contents or names are transmitted.
+- Accessibility: keyboard/focus and screenreader link labels in both locales.
+
+## Deployment gate
+Do **not** merge PR #38 and #39 separately after PR #40 merges. Review the combined PR and merge **only PR #40** once all checks pass, then close or supersede the original PRs as appropriate. Owner controls production deployment.
+```bash
+cd site
+SITE_URL=https://toolify365.com npm run audit:live-growth
+```
+Run this only after production deployment (the old site has no new CTA and correctly fails). Independently verify live GSC/Bing indexing; do not mass-index 17 locales without 28-day query evidence.
+
+## Current release result
+**NO-GO**: full checkout, TypeScript/Vitest/Next build, cross-feature browser checks and production deploy not executed in this environment. Direct clone to private GitHub repository fails DNS resolution here. No search traffic or revenue improvement verified.
+
+## 2026-11-06 incremental integration QA
+- The merged `ToolFrame` now presents contextual software CTAs **after** related/recent navigation but **before** advertising blocks. The corresponding `ToolFrame.order.test.ts` asserts this order.
+- Actual Node 22.16.0 isolated execution of exact HTML helpers and tests from the integration branch: **3 passed / 0 failed**. The full Next/Vitest suite remains unrun because `git ls-remote` still cannot resolve `github.com` from the local container.
+- PR #39 continued receiving changes while this integration was being prepared. Its updated `useAutoShareableState.ts` and sharing regression test have been copied into this branch; confirm fresh tree state again just before final merge because upstream may keep changing.
+- Source-readback verified `ToolFrame` positions: workspace, recent navigation, software CTA, and ad blocks in that order; five analytics exports intact. This does not establish working browser behavior.
+- Do not merge while PR #39 is actively evolving. Freeze both PR heads, recheck latest changed files, and run complete tests against the final integration tree.
+
+## One-command combined local gate (2026-11-06)
+The existing root `scripts/check-engagement-release.sh` now runs both PR #39's full checks and PR #38's dependency-free `test:smoke-html` before typecheck/Vitest/audits/build. On an owner-accessible checkout run `bash scripts/check-engagement-release.sh` from repository root; it uses `npm ci` and does not run GitHub Actions, merge or deploy. The shell script itself has not been run in this environment (private repo cannot be cloned). A passing run still requires real browser/GA4 checks before merging and owner-managed deployment.
+
+## 2026-11-06 morning sync (search safety + combined regression gate)
+- PR #39 added `search-index-payload.ts` and tests plus `useSearchIndex` validation. These were ported to PR #40, then the latest stricter payload validation (known categories, safe slug pattern, title limits, deduplication) and expanded tests were resynced.
+- Compared against category configuration: parser's eight categories match `ToolCategory`'s eight configured categories.
+- Added `site/scripts/growth-integration.test.mjs` with three dependency-free checks: workspace/navigation/CTA/ad order, five GA4 exports and consumers, and catalog-gated recommendation wiring.
+- Registered `npm run test:integration` and added it to `scripts/check-engagement-release.sh` along with `test:smoke-html`.
+- GitHub connector readback verified all three tests are present, the new npm command is wired, the UI order is workspace → recent → software CTA → ad, and the five tracking functions are retained. **This is source readback, not a completed run of the full integration gate.**
+- Direct clone still fails DNS from available execution environment. Avoid merging PR #40 until full npm install, Vitest, typecheck, browser/device checks and build run in an authorized checkout.
+
+## 2026-11-06 fresh PR39 parity and rendered HTML regression
+- Compared latest PR #39 changes to integration PR #40: 7 highlighted files (sharing policy/hook, search-index validation/hook/tests and verification record) were fetched by the GitHub connector and all seven file contents matched exactly.
+- Repeated local direct git clone probe: `Could not resolve host: github.com`; full repository checkout still not available in execution container.
+- Ran Node.js 22.16.0 with a local copy of the exact growth HTML helper/test modules: **3 test groups passed, zero failed**.
+- Identified a misleading success case: serialized HTML text inside hydration `'), false);
+});
+test('canonical regardless of attribute order', () => {
+ assert.equal(canonicalHref(''), 'https://toolify365.com/en/tools/foo');
+ assert.equal(canonicalHref(''), 'https://toolify365.com/en/tools/foo');
+ assert.equal(canonicalHref(''), null);
+ assert.equal(canonicalHref(''), '/right');
+});
+test('CTA must be an anchor, not an inert script string', () => {
+ assert.equal(hasSoftwareAnchor('Open', 'json-studio', 'en'), true);
+ assert.equal(hasSoftwareAnchor('', 'json-studio', 'en'), false);
+ assert.equal(hasSoftwareAnchor('', 'json-studio', 'en'), false);
+ assert.equal(hasSoftwareAnchor('', 'json-studio', 'en'), false);
+ assert.equal(hasSoftwareAnchor('', 'json-studio', 'en'), false);
+ assert.equal(hasSoftwareAnchor('Open', 'json-studio', 'en'), false);
+ assert.equal(hasSoftwareAnchor('Open', 'json-studio', 'ja'), false);
+ assert.equal(hasSoftwareAnchor('Open', 'json-studio', 'en'), false);
+ assert.equal(hasSoftwareAnchor('Open', 'json-studio', 'en', 'https://staging.example'), true);
+});
diff --git a/site/scripts/growth-integration.test.mjs b/site/scripts/growth-integration.test.mjs
new file mode 100644
index 00000000..094ab1ee
--- /dev/null
+++ b/site/scripts/growth-integration.test.mjs
@@ -0,0 +1,73 @@
+import { test } from 'node:test';
+import { strict as assert } from 'node:assert';
+import { readFileSync } from 'node:fs';
+
+const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8');
+const frame = read('../src/components/tools/ToolFrame.tsx');
+const events = read('../src/lib/analytics/events.ts');
+const workbench = read('../src/components/software/SoftwareWorkbench.tsx');
+const recent = read('../src/components/tools/ContinueWithRecent.tsx');
+const recs = read('../src/lib/software/recommendations.ts');
+
+test('growth and engagement navigation coexist ahead of advertising', () => {
+ for (const name of ['ToolInteractionTracker', 'ContinueWithRecent', 'ToolSoftwareRecommendations', 'AdBelowResult']) {
+ assert.ok(frame.includes('<' + name), 'missing ' + name);
+ }
+ const order = [' frame.indexOf(s));
+ assert.ok(order.every(i => i >= 0));
+ assert.ok(order.every((v, i) => i === 0 || order[i - 1] < v), 'navigation must precede ads');
+});
+
+test('all five integration analytics functions coexist', () => {
+ for (const name of ['trackRecentToolClick', 'trackScenarioAction', 'trackSoftwareClick', 'trackSoftwareProcess', 'trackSoftwareDownload']) {
+ assert.match(events, new RegExp('export function ' + name + '\\('));
+ }
+ assert.ok(workbench.includes('trackSoftwareProcess('));
+ assert.ok(workbench.includes('trackSoftwareDownload('));
+ assert.ok(recent.includes('trackRecentToolClick('));
+});
+
+test('catalog-gated software CTA mappings are present', () => {
+ assert.ok(recs.includes('AVAILABLE_SOFTWARE'));
+ assert.ok(recs.includes('availableRecommendations'));
+ assert.ok(frame.includes('slug={meta.slug} locale={locale}'));
+});
+
+test('synthetic restoration events cannot inflate active tool engagement', () => {
+ const tracker = read('../src/components/tools/ToolInteractionTracker.tsx');
+ const restore = read('../src/lib/hooks/useAutoShareableState.ts');
+ assert.ok(tracker.includes('if (!e.isTrusted || sent.current.calculate) return'));
+ assert.ok(tracker.includes('if (!e.isTrusted) return'));
+ assert.ok(tracker.includes('sent.current = { calculate: false, copy: false, preset: false }'));
+ assert.ok(tracker.includes('if (disposed) return'));
+ assert.ok(tracker.includes('window.clearTimeout(calculateDelay)'));
+ assert.ok(tracker.includes('}, [slug, locale, category])'));
+ assert.ok(restore.includes('dispatchEvent(new Event("input", { bubbles: true }))'));
+});
+
+test('automatic share URLs exclude sensitive named and autofilled fields', () => {
+ const policy = read('../src/lib/hooks/share-input-policy.ts');
+ const hook = read('../src/lib/hooks/useAutoShareableState.ts');
+ assert.ok(policy.includes('SENSITIVE_IDENTIFIER'));
+ assert.ok(policy.includes('SENSITIVE_AUTOCOMPLETE'));
+ assert.ok(policy.includes('SENSITIVE_IDENTIFIER.test(normalizedIdentifier)'));
+ assert.ok(policy.includes('explicitTextOptIn = false'));
+ assert.ok(policy.includes('freeTextType && !explicitTextOptIn'));
+ assert.ok(hook.includes('const identifier ='));
+ assert.ok(hook.includes('const explicitTextOptIn = el.getAttribute("data-share") === "on"'));
+ assert.ok(hook.includes('isShareableField(el.type, el.autocomplete, privateField, identifier, explicitTextOptIn)'));
+ assert.ok(hook.includes('isShareableField("textarea", el.autocomplete, privateField, identifier, explicitTextOptIn)'));
+ assert.ok(hook.includes('isShareableField("select", el.getAttribute("autocomplete") ?? "", privateField, identifier, explicitTextOptIn)'));
+});
+
+test('shared URL restoration validates untrusted input and clears stale oversized state', () => {
+ const policy = read('../src/lib/hooks/share-input-policy.ts');
+ const hook = read('../src/lib/hooks/useAutoShareableState.ts');
+ assert.ok(policy.includes('export function isValidRestoredFieldValue('));
+ assert.ok(policy.includes('Number.isFinite(Number(value))'));
+ assert.ok(hook.includes('if (!isValidRestoredFieldValue(kind, v)) continue'));
+ assert.ok(hook.includes('Array.from(el.options).some((option) => option.value === v)'));
+ assert.ok(hook.includes('el.type === "radio" && el.value !== v'));
+ assert.ok(hook.includes('stale.searchParams.delete("s")'));
+ assert.ok(hook.includes('window.dispatchEvent(new Event("toolify:share-state-change"))'));
+});
diff --git a/site/src/components/software/SoftwareWorkbench.tsx b/site/src/components/software/SoftwareWorkbench.tsx
index 546d0ce4..c3a3760c 100644
--- a/site/src/components/software/SoftwareWorkbench.tsx
+++ b/site/src/components/software/SoftwareWorkbench.tsx
@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
+import { trackSoftwareProcess, trackSoftwareDownload } from "@/lib/analytics/events";
import { FREE_SOFTWARE_LIMITS, isWithinSoftwareLimits } from "@/lib/software/limits";
import { cleanCsv, cleanTextLines, formatJson, prepareSoftwareDownload } from "@/lib/software/processors";
@@ -13,8 +14,13 @@ export function SoftwareWorkbench({ slug, labels }: {slug: ActiveSlug; labels: {
setError(""); setOutput(null);
try {
if (!isWithinSoftwareLimits(slug, input)) throw new Error(labels.limit);
- setOutput(slug === "csv-cleaner-pro" ? cleanCsv(input) : slug === "json-studio" ? formatJson(input, minify) : cleanTextLines(input));
- } catch (e) { setError(e instanceof Error && e.message === labels.limit ? labels.limit : labels.error); }
+ const result = slug === "csv-cleaner-pro" ? cleanCsv(input) : slug === "json-studio" ? formatJson(input, minify) : cleanTextLines(input);
+ setOutput(result);
+ trackSoftwareProcess({ software: slug, success: true });
+ } catch (e) {
+ trackSoftwareProcess({ software: slug, success: false });
+ setError(e instanceof Error && e.message === labels.limit ? labels.limit : labels.error);
+ }
};
const loadFile = async (file: File | undefined) => {
if (!file) return;
@@ -39,6 +45,7 @@ export function SoftwareWorkbench({ slug, labels }: {slug: ActiveSlug; labels: {
const ext = slug === "csv-cleaner-pro" ? "csv" : slug === "json-studio" ? "json" : "txt";
const url = URL.createObjectURL(new Blob([prepareSoftwareDownload(output, ext)], {type: mime + ";charset=utf-8"}));
const a = document.createElement("a"); a.href = url; a.download = "toolify365-output." + ext; document.body.appendChild(a); a.click(); a.remove();
+ trackSoftwareDownload({ software: slug });
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
return
diff --git a/site/src/components/tools/ContinueWithRecent.tsx b/site/src/components/tools/ContinueWithRecent.tsx
new file mode 100644
index 00000000..112a18c1
--- /dev/null
+++ b/site/src/components/tools/ContinueWithRecent.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { useLocale, useTranslations } from "next-intl";
+import { useRecent } from "@/lib/recent";
+import { useSearchIndex } from "@/lib/search/useSearchIndex";
+import { Link } from "@/lib/i18n/navigation";
+import { trackRecentToolClick } from "@/lib/analytics/events";
+import { selectRecentRecommendations } from "@/lib/tools/recent-recommendations";
+
+/**
+ * Offers a contextual return path after completing a tool.
+ * This is intentionally shown only to returning visitors and does not store inputs.
+ * Reuses the existing recent-slug store and lazily fetched localized search index.
+ */
+export function ContinueWithRecent({ currentSlug, category, relatedSlugs = [] }: { currentSlug: string; category: string; relatedSlugs?: string[] }) {
+ const locale = useLocale();
+ const t = useTranslations("tool");
+ const recent = useRecent();
+ const hasHistory = recent.some((slug) => slug !== currentSlug);
+ const index = useSearchIndex(locale, hasHistory);
+
+ if (!index || !hasHistory) return null;
+ const bySlug = new Map(index.map((item) => [item.slug, item]));
+ const alternatives = selectRecentRecommendations(recent, currentSlug, new Set(bySlug.keys()), 3, new Set(relatedSlugs));
+ const items = alternatives.map((slug) => bySlug.get(slug)).filter((item) => item !== undefined);
+ if (items.length === 0) return null;
+
+ return (
+
+