Compare commits

...

2 Commits

Author SHA1 Message Date
1ee61d8fe4 remove "The Nook" in license key phrasing 2026-08-28 14:15:40 -04:00
6c024342ba fixed privacy routing 2026-08-28 13:16:28 -04:00
11 changed files with 206 additions and 25 deletions

View File

@@ -366,6 +366,11 @@ function MainRightBarContent() {
Contact Me
</a>
</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>
<a
href="https://github.com/MikeFreno/"

View File

@@ -101,7 +101,8 @@ function handleChunkError(source: string): void {
window.addEventListener("error", (event) => {
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");

View File

@@ -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"
]);

View File

@@ -80,6 +80,7 @@ export const NAV_CONFIG: Record<SiteId, NavItem[]> = {
{ 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/",

View File

@@ -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("</urlset>");
const urlOpens = (xml.match(/<url>/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", () => {

View File

@@ -47,7 +47,8 @@ export const SITEMAP_ROUTES: Record<SiteId, SitemapEntry[]> = {
{ 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 ──────────────────────────────────────────────────

View File

@@ -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() {
</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">
<a
href={buildMainSiteUrl("/contact")}
target="_blank"
rel="noopener noreferrer"
class="text-blue hover-underline-animation"
>
here
</A>
</a>
.
</div>
</div>

View File

@@ -55,10 +55,7 @@ export default function NookSuccess() {
return (
<>
<PageHead
title="Order complete"
description="Your The Nook license key."
/>
<PageHead title="Order complete" description="Your license key." />
<SubdomainHeader />
<div class="bg-base mx-auto max-w-xl px-4 py-16">
@@ -88,16 +85,24 @@ export default function NookSuccess() {
</Show>
}
>
<h1 class="text-text mb-2 text-2xl font-bold">Thanks for buying The Nook</h1>
<h1 class="text-text mb-2 text-2xl font-bold">
Thanks for buying The Nook
</h1>
<p class="text-subtext0 mb-6 text-sm">
Here is your license key. Open The Nook, go to Settings, and enter it to activate.
Here is your license key. Open The Nook, go to Settings, and enter
it to activate.
</p>
<div class="border-overlay0 bg-base mb-4 flex items-center justify-between gap-3 rounded-lg border p-4">
<code class="text-text break-all text-sm">{key()}</code>
<code class="text-text text-sm break-all">{key()}</code>
</div>
<Button variant="download" size="md" color={brandColor()} onClick={copyKey}>
<Button
variant="download"
size="md"
color={brandColor()}
onClick={copyKey}
>
{copied() ? "Copied" : "Copy key"}
</Button>

View 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&apos;s Privacy Policy
</div>
<div class="py-2">Last Updated: August 28, 2026</div>
<div class="py-2">
Welcome to freno.me (&apos;We&apos;, &apos;Us&apos;, &apos;Our&apos;).
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 &amp;
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>
</>
);
}

View File

@@ -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/<product>` routes 308-redirect to the corresponding
* subdomain `/privacy` (see `src/routes/privacy-policy/*.tsx`).
* Legacy `/privacy-policy/<product>` routes 308-redirect to the
* corresponding subdomain `/privacy` (see `src/routes/privacy-policy/*.tsx`).
*/
export default function PrivacyPage() {
const site = useSite();
return (
<Switch fallback={<NotFound />}>
<Match when={site().id === "main"}>
<MainPrivacyPolicy />
</Match>
<Match when={site().id === "nessa"}>
<NessaPrivacyPolicy />
</Match>

View File

@@ -233,9 +233,9 @@ export async function emailLicenseKey(
body: JSON.stringify({
sender: { name: "The Nook", email: "support@freno.me" },
to: [{ email: to }],
subject: "Your The Nook license key",
subject: "Your license key",
textContent:
`Your The Nook license key is:\n\n${licenseKey}\n\n` +
`Your license key is:\n\n${licenseKey}\n\n` +
`Open The Nook, go to Settings, and enter this key to activate.\n` +
`You can activate up to 3 devices.`
})