From 5367c403ab067add6e3ed1e84e7a1367a2848953 Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Mon, 20 Jul 2026 17:30:05 -0500 Subject: [PATCH] Source AuthorBio photo, name, and bio from the Ghost author profile The AuthorBio component on the home and about pages had a hardcoded placeholder circle and hardcoded name/bio strings, so updating the author photo required a code change and deploy. - Add getAuthor(slug) to the Ghost Content API client (authors/slug/ endpoint) and an authors field to GhostResponse - Add a useAuthor hook following the existing use-ghost patterns - AuthorBio now renders the Ghost profile image, name, and bio when present, falling back to the previous hardcoded content while the Ghost profile fields are empty or the API is unreachable - Component tests cover both the fallback and Ghost-data states The author can now update their photo and bio in Ghost Admin with no deploy; post footers already read the same profile. --- src/api/ghost.ts | 8 ++++++ src/components/author-bio.test.tsx | 41 ++++++++++++++++++++++++++++++ src/components/author-bio.tsx | 36 +++++++++++++++++++++----- src/lib/use-ghost.ts | 10 +++++++- 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 src/components/author-bio.test.tsx diff --git a/src/api/ghost.ts b/src/api/ghost.ts index 23f758b..724aa03 100644 --- a/src/api/ghost.ts +++ b/src/api/ghost.ts @@ -54,6 +54,7 @@ interface GhostResponse { posts?: T[] pages?: T[] tags?: T[] + authors?: T[] meta: { pagination: { page: number @@ -113,3 +114,10 @@ export async function getTags() { const data = await ghostFetch>("tags/?limit=all") return data.tags ?? [] } + +export async function getAuthor(slug: string) { + const data = await ghostFetch>( + `authors/slug/${slug}/` + ) + return data.authors?.[0] ?? null +} diff --git a/src/components/author-bio.test.tsx b/src/components/author-bio.test.tsx new file mode 100644 index 0000000..0615b55 --- /dev/null +++ b/src/components/author-bio.test.tsx @@ -0,0 +1,41 @@ +import { render, screen } from "@testing-library/react" +import { describe, expect, it, vi } from "vitest" + +import { AuthorBio } from "./author-bio" + +const useAuthorMock = vi.hoisted(() => vi.fn()) + +vi.mock("@/lib/use-ghost", () => ({ + useAuthor: useAuthorMock, +})) + +describe("AuthorBio", () => { + it("renders the fallback name, bio, and placeholder when Ghost has no data", () => { + useAuthorMock.mockReturnValue({ data: undefined }) + + render() + + expect(screen.getByText("Will Thomson")).toBeInTheDocument() + expect(screen.getByText(/certified arborist/i)).toBeInTheDocument() + expect(screen.queryByRole("img")).not.toBeInTheDocument() + }) + + it("renders the Ghost profile image, name, and bio when present", () => { + useAuthorMock.mockReturnValue({ + data: { + id: "1", + name: "Will Thomson", + slug: "will", + profile_image: "https://content.example.com/will.jpg", + bio: "Arborist and author.", + }, + }) + + render() + + const img = screen.getByRole("img", { name: "Will Thomson" }) + expect(img).toHaveAttribute("src", "https://content.example.com/will.jpg") + expect(screen.getByText("Arborist and author.")).toBeInTheDocument() + expect(screen.queryByText(/certified arborist/i)).not.toBeInTheDocument() + }) +}) diff --git a/src/components/author-bio.tsx b/src/components/author-bio.tsx index e96ebcc..3478adc 100644 --- a/src/components/author-bio.tsx +++ b/src/components/author-bio.tsx @@ -1,14 +1,38 @@ +import { useAuthor } from "@/lib/use-ghost" + +// Single-author site: the Ghost staff profile is the source of truth for the +// photo and bio, editable in Ghost Admin without a deploy. The hardcoded +// strings below are fallbacks for while the Ghost profile fields are empty +// (and for when the Content API is unreachable). +const AUTHOR_SLUG = "will" + +const FALLBACK_NAME = "Will Thomson" +const FALLBACK_BIO = + "Will is a certified arborist and landscape designer. He holds an MSc in " + + "Environmental Science from the American University of Beirut and an MLA " + + "from the Spitzer School of Architecture, and is a certified hater of " + + "using trees to score political points." + export function AuthorBio() { + const { data: author } = useAuthor(AUTHOR_SLUG) + return (
-
+ {author?.profile_image ? ( + {author.name} + ) : ( +
+ )}
-

Will Thomson

+

+ {author?.name ?? FALLBACK_NAME} +

- Will is a certified arborist and landscape designer. He holds an MSc - in Environmental Science from the American University of Beirut and an - MLA from the Spitzer School of Architecture, and is a certified hater - of using trees to score political points. + {author?.bio ?? FALLBACK_BIO}

diff --git a/src/lib/use-ghost.ts b/src/lib/use-ghost.ts index 1bda8c5..a6a4ea8 100644 --- a/src/lib/use-ghost.ts +++ b/src/lib/use-ghost.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query" -import { getPost, getPosts, getPage, getTags } from "@/api/ghost" +import { getAuthor, getPost, getPosts, getPage, getTags } from "@/api/ghost" export function usePosts(options?: { page?: number @@ -36,3 +36,11 @@ export function useTags() { queryFn: getTags, }) } + +export function useAuthor(slug: string) { + return useQuery({ + queryKey: ["ghost", "author", slug], + queryFn: () => getAuthor(slug), + enabled: !!slug, + }) +}