fixed privacy routing
This commit is contained in:
@@ -366,6 +366,11 @@ function MainRightBarContent() {
|
|||||||
Contact Me
|
Contact Me
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||||
|
<a href="/privacy-policy" onClick={handleLinkClick}>
|
||||||
|
Privacy
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/MikeFreno/"
|
href="https://github.com/MikeFreno/"
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ function handleChunkError(source: string): void {
|
|||||||
window.addEventListener("error", (event) => {
|
window.addEventListener("error", (event) => {
|
||||||
if (
|
if (
|
||||||
event.message?.includes("Importing a module script failed") ||
|
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();
|
event.preventDefault();
|
||||||
handleChunkError("error event");
|
handleChunkError("error event");
|
||||||
@@ -111,9 +112,8 @@ window.addEventListener("error", (event) => {
|
|||||||
window.addEventListener("unhandledrejection", (event) => {
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
if (
|
if (
|
||||||
event.reason?.message?.includes("Importing a module script failed") ||
|
event.reason?.message?.includes("Importing a module script failed") ||
|
||||||
event.reason?.message?.includes(
|
event.reason?.message?.includes("Failed to fetch dynamically imported module") ||
|
||||||
"Failed to fetch dynamically imported module"
|
event.reason?.message?.includes("error loading dynamically imported module")
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleChunkError("unhandled rejection");
|
handleChunkError("unhandled rejection");
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ import { SITE_CONFIG, type SiteId } from "./site-context";
|
|||||||
const ALL_SITES: SiteId[] = ["main", "nessa", "lineage", "gaze", "inputhalo", "nook"];
|
const ALL_SITES: SiteId[] = ["main", "nessa", "lineage", "gaze", "inputhalo", "nook"];
|
||||||
|
|
||||||
describe("NAV_CONFIG — per-site link sets", () => {
|
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([
|
expect(navLabelsFor("main")).toEqual([
|
||||||
"Home",
|
"Home",
|
||||||
"Blog",
|
"Blog",
|
||||||
"Downloads",
|
"Downloads",
|
||||||
"Resume",
|
"Resume",
|
||||||
"Contact",
|
"Contact",
|
||||||
|
"Privacy",
|
||||||
"GitHub",
|
"GitHub",
|
||||||
"LinkedIn"
|
"LinkedIn"
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const NAV_CONFIG: Record<SiteId, NavItem[]> = {
|
|||||||
{ label: "Downloads", href: "/downloads", icon: "downloads" },
|
{ label: "Downloads", href: "/downloads", icon: "downloads" },
|
||||||
{ label: "Resume", href: "/resume", icon: "resume" },
|
{ label: "Resume", href: "/resume", icon: "resume" },
|
||||||
{ label: "Contact", href: "/contact", icon: "contact" },
|
{ label: "Contact", href: "/contact", icon: "contact" },
|
||||||
|
{ label: "Privacy", href: "/privacy-policy", icon: "privacy" },
|
||||||
{
|
{
|
||||||
label: "GitHub",
|
label: "GitHub",
|
||||||
href: "https://github.com/MikeFreno/",
|
href: "https://github.com/MikeFreno/",
|
||||||
|
|||||||
@@ -40,14 +40,15 @@ describe("generateSitemap", () => {
|
|||||||
expect(locs).toContain("https://freno.me/login");
|
expect(locs).toContain("https://freno.me/login");
|
||||||
expect(locs).toContain("https://freno.me/resume");
|
expect(locs).toContain("https://freno.me/resume");
|
||||||
expect(locs).toContain("https://freno.me/downloads");
|
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("</urlset>");
|
expect(xml).toContain("</urlset>");
|
||||||
const urlOpens = (xml.match(/<url>/g) || []).length;
|
const urlOpens = (xml.match(/<url>/g) || []).length;
|
||||||
const urlCloses = (xml.match(/<\/url>/g) || []).length;
|
const urlCloses = (xml.match(/<\/url>/g) || []).length;
|
||||||
expect(urlOpens).toBe(urlCloses);
|
expect(urlOpens).toBe(urlCloses);
|
||||||
expect(urlOpens).toBe(6);
|
expect(urlOpens).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("generates valid parseable XML for lineage site", () => {
|
it("generates valid parseable XML for lineage site", () => {
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ export const SITEMAP_ROUTES: Record<SiteId, SitemapEntry[]> = {
|
|||||||
{ path: "/contact", changefreq: "monthly", priority: 0.7 },
|
{ path: "/contact", changefreq: "monthly", priority: 0.7 },
|
||||||
{ path: "/login", changefreq: "monthly", priority: 0.5 },
|
{ path: "/login", changefreq: "monthly", priority: 0.5 },
|
||||||
{ path: "/resume", changefreq: "yearly", priority: 0.6 },
|
{ 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 ──────────────────────────────────────────────────
|
// ── Subdomain sites ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* UUID + device name for trial/activation limits, the email used for purchase,
|
* UUID + device name for trial/activation limits, the email used for purchase,
|
||||||
* and Stripe payment processing.
|
* and Stripe payment processing.
|
||||||
*/
|
*/
|
||||||
import { A } from "@solidjs/router";
|
import { buildMainSiteUrl } from "~/lib/subdomain-url";
|
||||||
import { PageHead } from "~/components/PageHead";
|
import { PageHead } from "~/components/PageHead";
|
||||||
import SubdomainHeader from "~/components/SubdomainHeader";
|
import SubdomainHeader from "~/components/SubdomainHeader";
|
||||||
|
|
||||||
@@ -94,9 +94,14 @@ export default function NookPrivacyPolicy() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="pb-2 pl-4">
|
<div class="pb-2 pl-4">
|
||||||
If you have any questions about this policy, you can contact us{" "}
|
If you have any questions about this policy, you can contact us{" "}
|
||||||
<A href="/contact" class="text-blue hover-underline-animation">
|
<a
|
||||||
|
href={buildMainSiteUrl("/contact")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-blue hover-underline-animation"
|
||||||
|
>
|
||||||
here
|
here
|
||||||
</A>
|
</a>
|
||||||
.
|
.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
156
src/routes/privacy-policy/index.tsx
Normal file
156
src/routes/privacy-policy/index.tsx
Normal file
@@ -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 `<product>.freno.me/privacy`.
|
||||||
|
*
|
||||||
|
* The legacy `/privacy-policy/shapes-with-abigail` 308-redirects here.
|
||||||
|
*/
|
||||||
|
export default function PrivacyPolicy() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHead
|
||||||
|
title="Privacy Policy"
|
||||||
|
description="Privacy policy for the freno.me blog and personal site, covering accounts, comments, and contact forms."
|
||||||
|
/>
|
||||||
|
<div class="bg-base">
|
||||||
|
<div class="min-h-screen px-[8vw] py-[8vh]">
|
||||||
|
<div class="py-4 text-xl">
|
||||||
|
freno.me's Privacy Policy
|
||||||
|
</div>
|
||||||
|
<div class="py-2">Last Updated: August 28, 2026</div>
|
||||||
|
<div class="py-2">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
<ol>
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">1.</span> Data We Collect
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Account:</div> 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.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Blog:</div> 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.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(c) Contact:</div> If you submit the
|
||||||
|
contact form, we store your name, email, and message so we
|
||||||
|
can reply.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">2.</span> How We Use It
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">3.</span> Cookies
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">4.</span> Third-Party Services
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Email:</div> Transactional email
|
||||||
|
(login links, password resets, contact replies) is sent by
|
||||||
|
Sendinblue. We share only the email address needed to
|
||||||
|
deliver the message.
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(b) Error monitoring:</div> We use Sentry
|
||||||
|
to collect crash reports, which include your browser type
|
||||||
|
and page URL. Your account details are never sent.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">5.</span> Data Retention &
|
||||||
|
Deletion
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
You can delete your account and your comments or messages any
|
||||||
|
time by contacting us. We delete the data when we receive your
|
||||||
|
request.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">6.</span> Security
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
All connections use TLS. Passwords are stored as salted
|
||||||
|
hashes. Access to the database is restricted to the site
|
||||||
|
operator.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">7.</span> Changes to the Privacy
|
||||||
|
Policy
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
We may update this policy periodically. Any changes are posted
|
||||||
|
on this page, so please review it from time to time.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-2">
|
||||||
|
<div class="pb-2 text-lg">
|
||||||
|
<span class="-ml-4 pr-2">8.</span> Contact Us
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
If you have any questions about this policy, you can contact
|
||||||
|
us{" "}
|
||||||
|
<A
|
||||||
|
href="/contact"
|
||||||
|
class="text-blue hover-underline-animation"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</A>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ import NotFound from "./[...404]";
|
|||||||
import NessaPrivacyPolicy from "./nessa/privacy";
|
import NessaPrivacyPolicy from "./nessa/privacy";
|
||||||
import LineagePrivacyPolicy from "./lineage/privacy";
|
import LineagePrivacyPolicy from "./lineage/privacy";
|
||||||
import GazePrivacyPolicy from "./gaze/privacy";
|
import GazePrivacyPolicy from "./gaze/privacy";
|
||||||
|
import InputHaloPrivacyPolicy from "./inputhalo/privacy";
|
||||||
import NookPrivacyPolicy from "./nook/privacy";
|
import NookPrivacyPolicy from "./nook/privacy";
|
||||||
|
import MainPrivacyPolicy from "./privacy-policy/index";
|
||||||
/**
|
/**
|
||||||
* Host-aware privacy policy route — `/privacy`.
|
* 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
|
* (SolidStart's client `Router` matches on `window.location.pathname`, not the
|
||||||
* rewritten server path).
|
* rewritten server path).
|
||||||
*
|
*
|
||||||
* The main site has no `/privacy` page (it uses `/privacy-policy/*`), so
|
* The main site's policy lives at `/privacy-policy` (a legacy path, so the
|
||||||
* `main` falls back to the 404 handler — preserving its pre-rewrite behavior.
|
* 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/<product>` routes 308-redirect to the corresponding
|
* Legacy `/privacy-policy/<product>` routes 308-redirect to the
|
||||||
* subdomain `/privacy` (see `src/routes/privacy-policy/*.tsx`).
|
* corresponding subdomain `/privacy` (see `src/routes/privacy-policy/*.tsx`).
|
||||||
*/
|
*/
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
const site = useSite();
|
const site = useSite();
|
||||||
return (
|
return (
|
||||||
<Switch fallback={<NotFound />}>
|
<Switch fallback={<NotFound />}>
|
||||||
|
<Match when={site().id === "main"}>
|
||||||
|
<MainPrivacyPolicy />
|
||||||
|
</Match>
|
||||||
<Match when={site().id === "nessa"}>
|
<Match when={site().id === "nessa"}>
|
||||||
<NessaPrivacyPolicy />
|
<NessaPrivacyPolicy />
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
Reference in New Issue
Block a user