diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index b010194..d037e71 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -366,6 +366,11 @@ function MainRightBarContent() { Contact Me +
  • + + Privacy + +
  • { if ( event.message?.includes("Importing a module script failed") || - event.message?.includes("Failed to fetch dynamically imported module") + event.message?.includes("Failed to fetch dynamically imported module") || + event.message?.includes("error loading dynamically imported module") ) { event.preventDefault(); handleChunkError("error event"); @@ -111,9 +112,8 @@ window.addEventListener("error", (event) => { window.addEventListener("unhandledrejection", (event) => { if ( event.reason?.message?.includes("Importing a module script failed") || - event.reason?.message?.includes( - "Failed to fetch dynamically imported module" - ) + event.reason?.message?.includes("Failed to fetch dynamically imported module") || + event.reason?.message?.includes("error loading dynamically imported module") ) { event.preventDefault(); handleChunkError("unhandled rejection"); diff --git a/src/lib/nav-config.test.ts b/src/lib/nav-config.test.ts index 346df5d..73c4c64 100644 --- a/src/lib/nav-config.test.ts +++ b/src/lib/nav-config.test.ts @@ -19,13 +19,14 @@ import { SITE_CONFIG, type SiteId } from "./site-context"; const ALL_SITES: SiteId[] = ["main", "nessa", "lineage", "gaze", "inputhalo", "nook"]; describe("NAV_CONFIG — per-site link sets", () => { - it("main → Home, Blog, Downloads, Resume, Contact, GitHub, LinkedIn", () => { + it("main → Home, Blog, Downloads, Resume, Contact, Privacy, GitHub, LinkedIn", () => { expect(navLabelsFor("main")).toEqual([ "Home", "Blog", "Downloads", "Resume", "Contact", + "Privacy", "GitHub", "LinkedIn" ]); diff --git a/src/lib/nav-config.ts b/src/lib/nav-config.ts index f0a926e..5e33b96 100644 --- a/src/lib/nav-config.ts +++ b/src/lib/nav-config.ts @@ -80,6 +80,7 @@ export const NAV_CONFIG: Record = { { label: "Downloads", href: "/downloads", icon: "downloads" }, { label: "Resume", href: "/resume", icon: "resume" }, { label: "Contact", href: "/contact", icon: "contact" }, + { label: "Privacy", href: "/privacy-policy", icon: "privacy" }, { label: "GitHub", href: "https://github.com/MikeFreno/", diff --git a/src/lib/sitemap-routes.test.ts b/src/lib/sitemap-routes.test.ts index c3e3dfe..66d7983 100644 --- a/src/lib/sitemap-routes.test.ts +++ b/src/lib/sitemap-routes.test.ts @@ -40,14 +40,15 @@ describe("generateSitemap", () => { expect(locs).toContain("https://freno.me/login"); expect(locs).toContain("https://freno.me/resume"); expect(locs).toContain("https://freno.me/downloads"); + expect(locs).toContain("https://freno.me/privacy-policy"); - expect(locs.length).toBe(6); + expect(locs.length).toBe(7); expect(xml).toContain(""); const urlOpens = (xml.match(//g) || []).length; const urlCloses = (xml.match(/<\/url>/g) || []).length; expect(urlOpens).toBe(urlCloses); - expect(urlOpens).toBe(6); + expect(urlOpens).toBe(7); }); it("generates valid parseable XML for lineage site", () => { diff --git a/src/lib/sitemap-routes.ts b/src/lib/sitemap-routes.ts index 69a431e..16d9cbb 100644 --- a/src/lib/sitemap-routes.ts +++ b/src/lib/sitemap-routes.ts @@ -47,7 +47,8 @@ export const SITEMAP_ROUTES: Record = { { path: "/contact", changefreq: "monthly", priority: 0.7 }, { path: "/login", changefreq: "monthly", priority: 0.5 }, { path: "/resume", changefreq: "yearly", priority: 0.6 }, - { path: "/downloads", changefreq: "weekly", priority: 0.8 } + { path: "/downloads", changefreq: "weekly", priority: 0.8 }, + { path: "/privacy-policy", changefreq: "yearly", priority: 0.4 } ], // ── Subdomain sites ────────────────────────────────────────────────── diff --git a/src/routes/nook/privacy.tsx b/src/routes/nook/privacy.tsx index 18509ea..96191f1 100644 --- a/src/routes/nook/privacy.tsx +++ b/src/routes/nook/privacy.tsx @@ -5,7 +5,7 @@ * UUID + device name for trial/activation limits, the email used for purchase, * and Stripe payment processing. */ -import { A } from "@solidjs/router"; +import { buildMainSiteUrl } from "~/lib/subdomain-url"; import { PageHead } from "~/components/PageHead"; import SubdomainHeader from "~/components/SubdomainHeader"; @@ -94,9 +94,14 @@ export default function NookPrivacyPolicy() {
    If you have any questions about this policy, you can contact us{" "} - + here - + .
    diff --git a/src/routes/privacy-policy/index.tsx b/src/routes/privacy-policy/index.tsx new file mode 100644 index 0000000..9b63bb5 --- /dev/null +++ b/src/routes/privacy-policy/index.tsx @@ -0,0 +1,156 @@ +import { A } from "@solidjs/router"; +import { PageHead } from "~/components/PageHead"; + +/** + * Main-site privacy policy — `freno.me/privacy-policy`. + * + * Covers the personal site (freno.me): accounts, blog comments & likes, the + * contact form, transactional email, and error monitoring. Product + * subdomains (Nessa, Lineage, Gaze, InputHalo, The Nook) each publish their + * own policy at `.freno.me/privacy`. + * + * The legacy `/privacy-policy/shapes-with-abigail` 308-redirects here. + */ +export default function PrivacyPolicy() { + return ( + <> + +
    +
    +
    + freno.me's Privacy Policy +
    +
    Last Updated: August 28, 2026
    +
    + Welcome to freno.me ('We', 'Us', 'Our'). + This site is a personal site and blog. This policy explains what + we collect, why, and how it is handled. +
    +
      +
      +
      + 1. Data We Collect +
      +
      +
      +
      (a) Account:
      If you create an + account with an email and password, we store your email, a + hash of your password, and your profile name. If you sign in + with Google or GitHub, we store your name, email, and avatar + as provided by the provider. +
      +
      +
      (b) Blog:
      If you comment on a post, + we store your username, your comment, and its date. If you + like a post, we store that you liked it, so the like can be + removed. +
      +
      +
      (c) Contact:
      If you submit the + contact form, we store your name, email, and message so we + can reply. +
      +
      +
      + +
      +
      + 2. How We Use It +
      +
      + We use your data only to operate the site: to authenticate + you, to publish your comments, and to reply to your messages. + We do not use it for advertising and we do not sell it to + anyone. +
      +
      + +
      +
      + 3. Cookies +
      +
      + We use a session cookie to keep you signed in and short-lived + cookies to rate-limit the login and contact forms against + abuse. No tracking or advertising cookies are set. +
      +
      + +
      +
      + 4. Third-Party Services +
      +
      +
      +
      (a) Email:
      Transactional email + (login links, password resets, contact replies) is sent by + Sendinblue. We share only the email address needed to + deliver the message. +
      +
      +
      (b) Error monitoring:
      We use Sentry + to collect crash reports, which include your browser type + and page URL. Your account details are never sent. +
      +
      +
      + +
      +
      + 5. Data Retention & + Deletion +
      +
      + You can delete your account and your comments or messages any + time by contacting us. We delete the data when we receive your + request. +
      +
      + +
      +
      + 6. Security +
      +
      + All connections use TLS. Passwords are stored as salted + hashes. Access to the database is restricted to the site + operator. +
      +
      + +
      +
      + 7. Changes to the Privacy + Policy +
      +
      + We may update this policy periodically. Any changes are posted + on this page, so please review it from time to time. +
      +
      + +
      +
      + 8. Contact Us +
      +
      + If you have any questions about this policy, you can contact + us{" "} + + here + + . +
      +
      +
    +
    +
    + + ); +} diff --git a/src/routes/privacy.tsx b/src/routes/privacy.tsx index dd28a64..6d4db0d 100644 --- a/src/routes/privacy.tsx +++ b/src/routes/privacy.tsx @@ -4,7 +4,9 @@ import NotFound from "./[...404]"; import NessaPrivacyPolicy from "./nessa/privacy"; import LineagePrivacyPolicy from "./lineage/privacy"; import GazePrivacyPolicy from "./gaze/privacy"; +import InputHaloPrivacyPolicy from "./inputhalo/privacy"; import NookPrivacyPolicy from "./nook/privacy"; +import MainPrivacyPolicy from "./privacy-policy/index"; /** * Host-aware privacy policy route — `/privacy`. * @@ -18,16 +20,20 @@ import NookPrivacyPolicy from "./nook/privacy"; * (SolidStart's client `Router` matches on `window.location.pathname`, not the * rewritten server path). * - * The main site has no `/privacy` page (it uses `/privacy-policy/*`), so - * `main` falls back to the 404 handler — preserving its pre-rewrite behavior. + * The main site's policy lives at `/privacy-policy` (a legacy path, so the + * page stays there for SEO); `main` renders it here at the public path + * `/privacy`, so both URLs serve the same content on `freno.me`. * - * Legacy `/privacy-policy/` routes 308-redirect to the corresponding - * subdomain `/privacy` (see `src/routes/privacy-policy/*.tsx`). + * Legacy `/privacy-policy/` routes 308-redirect to the + * corresponding subdomain `/privacy` (see `src/routes/privacy-policy/*.tsx`). */ export default function PrivacyPage() { const site = useSite(); return ( }> + + +