Compare commits
2 Commits
9860213760
...
1ee61d8fe4
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ee61d8fe4 | |||
| 6c024342ba |
@@ -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/"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
]);
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
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 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>
|
||||
|
||||
@@ -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.`
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user