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 { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { api, type SiteSettings } from "../lib/api"; import { api, type SiteSettings } from "../lib/api";
import { usePageTitle } from "../lib/pageTitle";
import "./AboutPage.scss"; import "./AboutPage.scss";
export function AboutPage() { export function AboutPage() {
const [site, setSite] = useState<SiteSettings | null>(null); const [site, setSite] = useState<SiteSettings | null>(null);
usePageTitle("About");
useEffect(() => { useEffect(() => {
api.site().then(setSite).catch(() => {}); api.site().then(setSite).catch(() => {});
}, []); }, []);
+3
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { api, type Project, type SiteSettings, type Category } from "../lib/api"; import { api, type Project, type SiteSettings, type Category } from "../lib/api";
import { ProjectCard } from "../components/ProjectCard"; import { ProjectCard } from "../components/ProjectCard";
import { usePageTitle } from "../lib/pageTitle";
import "./HomePage.scss"; import "./HomePage.scss";
export function HomePage() { export function HomePage() {
@@ -10,6 +11,8 @@ export function HomePage() {
const [recent, setRecent] = useState<Project[]>([]); const [recent, setRecent] = useState<Project[]>([]);
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
usePageTitle(null); // site name only
useEffect(() => { useEffect(() => {
Promise.all([ Promise.all([
api.site(), api.site(),
+10
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useParams, useSearchParams, Link } from "react-router-dom"; import { useParams, useSearchParams, Link } from "react-router-dom";
import { api, type Project, type Category, type ListMeta } from "../lib/api"; import { api, type Project, type Category, type ListMeta } from "../lib/api";
import { ProjectCard } from "../components/ProjectCard"; import { ProjectCard } from "../components/ProjectCard";
import { usePageTitle } from "../lib/pageTitle";
import "./PortfolioPage.scss"; import "./PortfolioPage.scss";
export function PortfolioPage() { export function PortfolioPage() {
@@ -57,6 +58,15 @@ export function PortfolioPage() {
meta?.view?.description || meta?.view?.description ||
"Browse projects by category, tag, or search. Share tailored views with clients and collaborators."; "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 ( return (
<div className="portfolio page-enter container"> <div className="portfolio page-enter container">
<header className="portfolio__header"> <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 Lightbox from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css"; import "yet-another-react-lightbox/styles.css";
import { api, type Project, mediaSrc } from "../lib/api"; import { api, type Project, mediaSrc } from "../lib/api";
import { usePageTitle } from "../lib/pageTitle";
import "./ProjectPage.scss"; import "./ProjectPage.scss";
export function ProjectPage() { export function ProjectPage() {
@@ -11,13 +12,16 @@ export function ProjectPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [lbIndex, setLbIndex] = useState(-1); const [lbIndex, setLbIndex] = useState(-1);
usePageTitle(project?.title || (error ? "Not found" : "Project"));
useEffect(() => { useEffect(() => {
if (!slug) return; if (!slug) return;
setProject(null);
setError(null);
api api
.project(slug) .project(slug)
.then((p) => { .then((p) => {
setProject(p); setProject(p);
document.title = `${p.title} · jmartgraphix`;
// Open Graph-ish meta updates // Open Graph-ish meta updates
const setMeta = (prop: string, content: string) => { const setMeta = (prop: string, content: string) => {
let el = document.querySelector(`meta[property="${prop}"]`); let el = document.querySelector(`meta[property="${prop}"]`);
+5 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { api, type Resume } from "../lib/api"; import { api, type Resume } from "../lib/api";
import { usePageTitle } from "../lib/pageTitle";
import "./ResumePage.scss"; import "./ResumePage.scss";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -9,12 +10,15 @@ export function ResumePage() {
const [resume, setResume] = useState<Resume | null>(null); const [resume, setResume] = useState<Resume | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
usePageTitle(
resume ? `${resume.fullName} — Resume` : error ? "Resume" : "Resume"
);
useEffect(() => { useEffect(() => {
api api
.resume() .resume()
.then((r) => { .then((r) => {
setResume(r); setResume(r);
document.title = `${r.fullName} — Resume · jmartgraphix`;
}) })
.catch((e) => setError(e.message)); .catch((e) => setError(e.message));
}, []); }, []);
+17 -1
View File
@@ -1,12 +1,28 @@
import { useEffect, useState } from "react"; 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 { api } from "../../lib/api";
import { usePageTitle } from "../../lib/pageTitle";
import "./Admin.scss"; 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() { export function AdminLayout() {
const [user, setUser] = useState<{ username: string; name?: string } | null>(null); const [user, setUser] = useState<{ username: string; name?: string } | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
usePageTitle(adminTitleFromPath(location.pathname));
useEffect(() => { useEffect(() => {
api api