nook: fix the landing title, isolate /blog, grow the sitemap

The landing page spent its strongest on-page signal on the word "Home";
the title now carries the positioning line and the OG title carries the
full brand phrase. /blog on a product subdomain served the personal blog
in dev (vinxi ignores the vercel.json rewrites that protect production),
splitting the brand and crawl equity — subdomain hits now render the
shared 404. /agents, /changelog and /faq join the sitemap registry so
they are indexed as they land.

Tests: bun test (539 pass); vinxi build clean, no new tsc errors.

Issues: FRE-8, FRE-15, FRE-9
This commit is contained in:
2026-10-04 23:22:04 -04:00
parent 44bf21a868
commit dc959d971b
4 changed files with 27 additions and 3 deletions

View File

@@ -83,6 +83,9 @@ export const SITEMAP_ROUTES: Record<SiteId, SitemapEntry[]> = {
nook: [ nook: [
{ path: "/", changefreq: "weekly", priority: 1.0 }, { path: "/", changefreq: "weekly", priority: 1.0 },
{ path: "/checkout", changefreq: "monthly", priority: 0.5 }, { path: "/checkout", changefreq: "monthly", priority: 0.5 },
{ path: "/privacy", changefreq: "yearly", priority: 0.4 } { path: "/privacy", changefreq: "yearly", priority: 0.4 },
{ path: "/agents", changefreq: "weekly", priority: 0.9 },
{ path: "/changelog", changefreq: "daily", priority: 0.7 },
{ path: "/faq", changefreq: "monthly", priority: 0.8 }
] ]
}; };

View File

@@ -21,6 +21,8 @@ import { api } from "~/lib/api";
import CustomScrollbar from "~/components/CustomScrollbar"; import CustomScrollbar from "~/components/CustomScrollbar";
import "../post.css"; import "../post.css";
import { Post } from "~/db/types"; import { Post } from "~/db/types";
import { useSite } from "~/context/SiteContext";
import NotFound from "../../[...404]";
const getPostByTitle = query( const getPostByTitle = query(
async ( async (
@@ -262,6 +264,7 @@ const getPostByTitle = query(
); );
export default function PostPage() { export default function PostPage() {
const site = useSite();
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -297,6 +300,12 @@ export default function PostPage() {
return str.includes('data-type="mermaid"'); return str.includes('data-type="mermaid"');
}; };
// Host-dispatch guard: blog posts live on the apex domain only (see the
// guard in blog/index.tsx for the dev-server rationale).
if (site().id !== "main") {
return <NotFound />;
}
return ( return (
<Show <Show
when={data()} when={data()}

View File

@@ -9,6 +9,8 @@ import PostSorting from "~/components/blog/PostSorting";
import PublishStatusToggle from "~/components/blog/PublishStatusToggle"; import PublishStatusToggle from "~/components/blog/PublishStatusToggle";
import { TerminalSplash } from "~/components/TerminalSplash"; import { TerminalSplash } from "~/components/TerminalSplash";
import { CACHE_CONFIG } from "~/config"; import { CACHE_CONFIG } from "~/config";
import { useSite } from "~/context/SiteContext";
import NotFound from "../[...404]";
const getPosts = query(async () => { const getPosts = query(async () => {
"use server"; "use server";
@@ -77,6 +79,7 @@ const getPosts = query(async () => {
}, "posts"); }, "posts");
export default function BlogIndex() { export default function BlogIndex() {
const site = useSite();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const sort = () => searchParams.sort || "newest"; const sort = () => searchParams.sort || "newest";
@@ -89,6 +92,15 @@ export default function BlogIndex() {
const data = createAsync(() => getPosts(), { deferStream: true }); const data = createAsync(() => getPosts(), { deferStream: true });
// Host-dispatch guard: /blog is the personal blog on the apex domain only.
// Production rewrites already send `<sub>.freno.me/blog` → `/<sub>/blog`,
// which has no route — but in dev the vinxi server ignores vercel.json and
// would render the personal blog on a product subdomain, splitting the
// brand and crawl equity. Render the shared 404 instead.
if (site().id !== "main") {
return <NotFound />;
}
return ( return (
<> <>
<EdgeCacheHeaders maxAge={60} staleSeconds={3600} /> <EdgeCacheHeaders maxAge={60} staleSeconds={3600} />

View File

@@ -55,10 +55,10 @@ export default function NookLanding() {
<> <>
<EdgeCacheHeaders maxAge={300} /> <EdgeCacheHeaders maxAge={300} />
<PageHead <PageHead
title="Home" title="Agent control for your Mac"
ogTitle="The Nook — agent control for your Mac"
description="The Nook — a native macOS utility for coding-agent orchestration, fan and thermal control." description="The Nook — a native macOS utility for coding-agent orchestration, fan and thermal control."
ogImage={`https://${site().domain}/nook/og.png`} ogImage={`https://${site().domain}/nook/og.png`}
ogTitle="The Nook — native macOS agent orchestration"
ogDescription="A one-time-purchase macOS app for coding-agent orchestration, fan control, and thermal insight." ogDescription="A one-time-purchase macOS app for coding-agent orchestration, fan control, and thermal insight."
/> />