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:
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}"]`);
|
||||||
|
|||||||
@@ -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));
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user