Fix browser tab titles on every route

Add usePageTitle so public and admin pages set document.title
to the current page (e.g. Portfolio · jmartgraphix).
This commit is contained in:
2026-07-24 10:56:34 -04:00
parent cef5738044
commit 36c008ac26
7 changed files with 59 additions and 3 deletions
+16
View File
@@ -0,0 +1,16 @@
import { useEffect } from "react";
export const SITE_TITLE = "jmartgraphix";
/** Set browser tab title: "Page · jmartgraphix" or just site name. */
export function setPageTitle(page?: string | null) {
const part = page?.trim();
document.title = part ? `${part} · ${SITE_TITLE}` : SITE_TITLE;
}
/** React hook — updates document.title whenever `page` changes. */
export function usePageTitle(page?: string | null) {
useEffect(() => {
setPageTitle(page);
}, [page]);
}
+3
View File
@@ -1,11 +1,14 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api, type SiteSettings } from "../lib/api";
import { usePageTitle } from "../lib/pageTitle";
import "./AboutPage.scss";
export function AboutPage() {
const [site, setSite] = useState<SiteSettings | null>(null);
usePageTitle("About");
useEffect(() => {
api.site().then(setSite).catch(() => {});
}, []);
+3
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api, type Project, type SiteSettings, type Category } from "../lib/api";
import { ProjectCard } from "../components/ProjectCard";
import { usePageTitle } from "../lib/pageTitle";
import "./HomePage.scss";
export function HomePage() {
@@ -10,6 +11,8 @@ export function HomePage() {
const [recent, setRecent] = useState<Project[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
usePageTitle(null); // site name only
useEffect(() => {
Promise.all([
api.site(),
+10
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useParams, useSearchParams, Link } from "react-router-dom";
import { api, type Project, type Category, type ListMeta } from "../lib/api";
import { ProjectCard } from "../components/ProjectCard";
import { usePageTitle } from "../lib/pageTitle";
import "./PortfolioPage.scss";
export function PortfolioPage() {
@@ -57,6 +58,15 @@ export function PortfolioPage() {
meta?.view?.description ||
"Browse projects by category, tag, or search. Share tailored views with clients and collaborators.";
const tabTitle = viewSlug
? meta?.view?.name || viewSlug
: category
? categories.find((c) => c.slug === category)?.name || category
: q
? `Search: ${q}`
: "Portfolio";
usePageTitle(tabTitle);
return (
<div className="portfolio page-enter container">
<header className="portfolio__header">
+5 -1
View File
@@ -3,6 +3,7 @@ import { Link, useParams } from "react-router-dom";
import Lightbox from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import { api, type Project, mediaSrc } from "../lib/api";
import { usePageTitle } from "../lib/pageTitle";
import "./ProjectPage.scss";
export function ProjectPage() {
@@ -11,13 +12,16 @@ export function ProjectPage() {
const [error, setError] = useState<string | null>(null);
const [lbIndex, setLbIndex] = useState(-1);
usePageTitle(project?.title || (error ? "Not found" : "Project"));
useEffect(() => {
if (!slug) return;
setProject(null);
setError(null);
api
.project(slug)
.then((p) => {
setProject(p);
document.title = `${p.title} · jmartgraphix`;
// Open Graph-ish meta updates
const setMeta = (prop: string, content: string) => {
let el = document.querySelector(`meta[property="${prop}"]`);
+5 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { api, type Resume } from "../lib/api";
import { usePageTitle } from "../lib/pageTitle";
import "./ResumePage.scss";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -9,12 +10,15 @@ export function ResumePage() {
const [resume, setResume] = useState<Resume | null>(null);
const [error, setError] = useState<string | null>(null);
usePageTitle(
resume ? `${resume.fullName} — Resume` : error ? "Resume" : "Resume"
);
useEffect(() => {
api
.resume()
.then((r) => {
setResume(r);
document.title = `${r.fullName} — Resume · jmartgraphix`;
})
.catch((e) => setError(e.message));
}, []);
+17 -1
View File
@@ -1,12 +1,28 @@
import { useEffect, useState } from "react";
import { Link, NavLink, Outlet, useNavigate } from "react-router-dom";
import { Link, NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import { api } from "../../lib/api";
import { usePageTitle } from "../../lib/pageTitle";
import "./Admin.scss";
function adminTitleFromPath(pathname: string): string {
if (pathname.endsWith("/projects/new")) return "New project · Admin";
if (/\/projects\/[^/]+$/.test(pathname) && !pathname.endsWith("/projects"))
return "Edit project · Admin";
if (pathname.includes("/projects")) return "Projects · Admin";
if (pathname.includes("/categories")) return "Categories · Admin";
if (pathname.includes("/views")) return "Portfolio views · Admin";
if (pathname.includes("/resume")) return "Resume · Admin";
if (pathname.includes("/settings")) return "Settings · Admin";
return "Admin";
}
export function AdminLayout() {
const [user, setUser] = useState<{ username: string; name?: string } | null>(null);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
const location = useLocation();
usePageTitle(adminTitleFromPath(location.pathname));
useEffect(() => {
api