Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ASSETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ direction — since the current site shows almost everyone except him in that ro
| `assets/img/media-glamour.jpg` | Speak (`media` credits) | Poster frame for Glamour's styling competition *Dress to Kill* (16:9). Source video: https://www.youtube.com/watch?v=_KzZ2pqT_6g |
| `assets/img/about-portrait.jpg` | About | A strong editorial portrait of Quentin. |

## The Archives page

`/archives` is the portfolio, driven by `content/archives.yaml` (`sections[].images`),
not by fixed slots: each entry is a `{ src, alt, cap }` pointing at any file under
`public/assets/img/`. Most of them currently live in `assets/img/gallery/`.

- **Budget 5 to 10 photos per section**, chosen for name recognition first, then
for how dynamic the image is. Past that, adding means replacing.
- **Name the person in `alt` and `cap` when the shoot is a named client.** That, plus
the `clients.names` line on the page, is what makes a search for that name reach
the site. Get the spelling exactly right.
- Sections: `celebrity`, `editorial`, `commercial`, `red-carpet`. A section with no
images is skipped on the page, so one can be defined before its photos exist.
- **Run `python3 tools/optimize_images.py` after adding files.** It now covers the
`gallery/` subfolder, and without it the new photos ship as single oversized JPEGs.
- **Rights:** only publish photos the owner has the rights to. Press-agency comps
(Getty, WireImage, Shutterstock) carry visible watermarks and are licensed for
review, not publication. Ask for the clean licensed file instead of shipping one.

## Video — the speaking reel
The Speak page has a play-button poster linking to `contact.html` as a placeholder.
Replace it with the real two-minute reel cut from the **Tommy Hilfiger interview**,
Expand Down
76 changes: 71 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,40 @@ category keys and shoot slugs in sync with the YAML (CI checks the keys). Never
shoots from different categories into one set: Celebrity men and Celebrity women are
separate categories, as they were on the old site.

Note the Work page no longer carries the old archive tile strip; it links to The
Archives instead. `galleries` still backs the Work case-study lightbox buttons.

## The Archives page

`/archives` ([src/pages/archives.astro](src/pages/archives.astro),
`content/archives.yaml`) is the portfolio: `sections` of `{ key, title, intro,
images[] }`, one section per area of work (Celebrity, Editorial, Commercial, Red
carpet). It is the owner's front door for portfolio material, so it is the first
place new work should land.

- **Every photo is a real `<img>` on the page.** That is the point of the page, and
it is also the only reliable way image search sees the portfolio (the Work
lightbox builds its DOM from JSON, which no crawler executes).
- **A section with no photos is skipped**, so an area of work can be defined in the
YAML before its images exist without shipping an empty grid.
- **The lightbox is reused unchanged.** Each section is emitted as a one-shoot
category in `<script id="gallery-data">`, which `main.js` already treats as "open
that shoot directly", so a click opens the clicked frame and prev/next walk the
section.
- **Budget: 5 to 10 photos per section**, picked for name recognition first and
dynamism second. Past that, adding means replacing.
- **`clients.names` is a plain-text list of named clients.** Alt text alone is a weak
signal for a person's name; a real sentence naming them, plus `mentions` in the
page's JSON-LD, is what lets a search for a client's name reach this site. Keep
spellings exactly right, since a misspelling matches nothing.
- Photos here may sit in `assets/img/gallery/`; `tools/optimize_images.py` covers
that subfolder, so run it after adding files or they ship unoptimized.

**Only publish photos the owner has the rights to.** Press and agency comps
(Getty, WireImage, Shutterstock) are watermarked and licensed for review, not for
publication on a commercial site. If a supplied file carries a visible watermark or
credit bug, do not ship it: ask for the clean, licensed copy.

## SEO layer

The full technical-SEO layer is generated in code, not authored per page, so it stays
Expand All @@ -175,10 +209,40 @@ consistent. Editable per page: `seo.title`, `seo.description`, `seo.ogTitle`,
`ProfilePage`, `ContactPage`, `CollectionPage`, or `WebPage`, plus a `BreadcrumbList`,
on the others).
- **Assets & crawl:** `public/assets/img/og-cover.jpg` (1200×630), favicons / PWA
icons via `public/site.webmanifest`, and `public/robots.txt` → `public/sitemap.xml`.
icons via `public/site.webmanifest`, and `public/robots.txt` →
[src/pages/sitemap.xml.ts](src/pages/sitemap.xml.ts).

Everything is keyed to `https://quentinfears.com` as the single source of truth.

### The sitemap is generated, and it is an image sitemap

[src/pages/sitemap.xml.ts](src/pages/sitemap.xml.ts) builds `dist/sitemap.xml` at
build time: the six page URLs, plus an `<image:image>` entry for every photo each
page shows, collected by walking the same `content/*.yaml` the pages read. Adding or
trimming a photo updates the sitemap on the next build, with nothing to keep in sync
by hand.

The image entries matter because most of the portfolio is invisible to crawlers
otherwise: gallery photos only enter the DOM when the lightbox builds it from the
JSON in `<script id="gallery-data">`, and a crawler never opens a gallery. An image
sitemap is Google's supported way to declare exactly those images, so it is what
gets the portfolio into Google Images. Do not replace it with a static
`public/sitemap.xml` again.

### Old Wix URLs redirect

quentinfears.com was a Wix site before this one, with its portfolio on category
pages (`/celebrity`, `/editorial`, `/about-me`, ...). Those URLs are still in
Google's index, so [src/pages/[legacy].astro](src/pages/%5Blegacy%5D.astro) emits a
stub at each one that declares the new page canonical and meta-refreshes to it;
the map lives in
[src/lib/legacy-redirects.ts](src/lib/legacy-redirects.ts). GitHub Pages cannot
serve a 301, and a canonical plus a 0-second refresh is the redirect signal Google
honours in its place. Stubs are deliberately out of the sitemap and the nav;
`tools/seo_check.py` detects the meta refresh, skips the page-level SEO layer for
them, and instead checks that the target resolves, the canonical matches it, and
the stub is not `noindex`. To retire one, delete its line from the map.

## Analytics

The site uses **Google Analytics 4** (`gtag.js`, stream `Quentin Fears`,
Expand Down Expand Up @@ -227,15 +291,16 @@ contained.
and a `jsonLd` graph (use `personMinimal` + `breadcrumb` from `src/lib/jsonld.ts`).
`BaseLayout` supplies the rest of the `<head>` — do not hand-write it.
3. Use exactly one `<h1>` and give every `<img>` an `alt`.
4. Add the page's URL to [public/sitemap.xml](public/sitemap.xml).
4. Add the page to `PAGES` in [src/pages/sitemap.xml.ts](src/pages/sitemap.xml.ts)
(its path plus the singleton to pull image entries from).
5. Run `npm run build && npm run validate` and fix anything reported.

### When the domain changes

`https://quentinfears.com` is the single source of truth. Update `SITE_ORIGIN` in
[src/lib/content.ts](src/lib/content.ts), `BASE` in
[tools/seo_check.py](tools/seo_check.py), and find-and-replace the domain in
`public/robots.txt`, `public/sitemap.xml`, and `public/CNAME` (the Pages custom-domain
`public/robots.txt` and `public/CNAME` (the Pages custom-domain
pin, served verbatim as `dist/CNAME`). The public site is `https://quentinfears.com`;
GitHub Pages publishes it at `https://qafears.github.io/website/` (the publish URL), and
the canonical/`og:` URLs intentionally point at `quentinfears.com`.
Expand Down Expand Up @@ -390,11 +455,12 @@ Instagram tooling in `tools/` (stdlib-only except the authed one):
Steady-state sizes — the signal to replace rather than append. These are ceilings
for unattended growth, not targets to fill:

- Archives sections (`content/archives.yaml` `sections[].images`): **5 to 10 each**
- Gallery categories (`content/galleries.yaml`): **8 images each**, across that
category's shoots
- Ideas notes (`content/ideas.yaml` `notes.items`): **4–6** · Ideas reels
(`reels.items`): **3–4**
- Work cases (`content/work.yaml`): **4** · styling archive: **8**
- Work cases (`content/work.yaml`): **4**

### Guardrails

Expand Down Expand Up @@ -425,7 +491,7 @@ Quick guardrail recap; the reasoning is in the sections above.
- No external fonts, scripts, or CDNs (breaks the offline / CSP guarantee). Build-time deps are fine. The one sanctioned exception is Google Analytics (see "Analytics"); do not add others, and do not remove GA to "restore" the offline rule.
- No root-absolute internal links or assets; keep them relative.
- No em dashes in anything shipped to `dist/`; restructure instead. CI fails on them.
- Do not hand-write or desync the `<head>`, JSON-LD, or sitemap; `BaseLayout` generates them.
- Do not hand-write or desync the `<head>` and JSON-LD (`BaseLayout` generates them) or the sitemap (`src/pages/sitemap.xml.ts` does, image entries included).
- No employer claim in metadata or any `seo.*` field.
- Keep the public `npm run build` static and adapter-free; the Netlify adapter belongs only to `build:admin`.
- Move or rename the crawl files or validators only alongside `tools/*.py` and `.github/workflows/` updates.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ builds and publishes `dist/` on every push to `main`. The public site is
- [ ] Have Quentin's manager/comms review the Walmart bio language.
- [ ] Set the contact form endpoint and update the contact email.
- [x] Set the real domain in the `og:` / canonical / sitemap tags (`https://quentinfears.com`). If the
site launches on a different domain, find-and-replace `quentinfears.com` across the HTML,
`robots.txt`, and `sitemap.xml`.
site launches on a different domain, update `SITE_ORIGIN` in `src/lib/content.ts` and
find-and-replace `quentinfears.com` in `robots.txt` and `CNAME`.
- [x] **Flip indexing on at launch.** The password gate has been removed and every page
ships `robots: index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1`.
- [ ] Submit `sitemap.xml` in Google Search Console and Bing Webmaster Tools after launch.
Expand Down
108 changes: 108 additions & 0 deletions content/archives.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
seo:
title: "The Archives | Quentin Fears"
description: "The portfolio of stylist and fashion creative director Quentin Fears: celebrity, editorial, commercial, and red carpet work."
ogTitle: "The Archives | Quentin Fears"
ogDescription: "A decade of styling: celebrity, editorial, commercial, and red carpet."
ogType: "website"
hero:
eyebrow: "The Archives"
heading: "A decade of execution."
intro: "Celebrity, editorial, commercial, red carpet. The point of view on the rest of this site is earned, and this is the work it is built on. Click any photo to open it full size."
sections:
- key: "celebrity"
title: "Celebrity"
intro: "Covers, portraits, and the looks that get photographed."
images:
- src: "assets/img/gallery/g-celebrity-1.jpg"
alt: "Glamour digital edition cover, a pink ostrich feather gown styled by Quentin Fears"
cap: "Glamour, digital edition cover"
- src: "assets/img/gallery/g-celebrity-4.jpg"
alt: "Actor Norman Reedus in a black leather jacket and grey tee, styled by Quentin Fears"
cap: "Norman Reedus"
- src: "assets/img/gallery/g-commercial-5.jpg"
alt: "A sculptural printed ball gown photographed on a residential street"
cap: "Printed ball gown"
- src: "assets/img/gallery/g-celebrity-3.jpg"
alt: "A lavender ostrich feather gown with a full train on a garden staircase"
cap: "Lavender feathers"
- src: "assets/img/gallery/g-celebrity-2.jpg"
alt: "A pink ostrich feather gown and matching bag on garden steps"
cap: "The season of glamour"
- src: "assets/img/gallery/g-celebrity-5.jpg"
alt: "Menswear styling: a patterned knit polo with tailored camel trousers"
cap: "Menswear styling"
- src: "assets/img/gallery/g-celebrity-6.jpg"
alt: "Menswear styling: a grey checked blazer, black trousers, and white sneakers"
cap: "Tailoring, off duty"
- src: "assets/img/gallery/g-celebrity-7.jpg"
alt: "Menswear styling: printed drawstring trousers with a white tank and plaid layer"
cap: "Print and proportion"
- key: "editorial"
title: "Editorial"
intro: "Concept, mood, styling, and art direction as one authored point of view."
images:
- src: "assets/img/gallery/g-editorial-3.jpg"
alt: "Editorial: a sequinned bodysuit and thigh-high boots with a rainbow pool float"
cap: "Poolside story"
- src: "assets/img/gallery/g-editorial-7.jpg"
alt: "Editorial: a bold printed gown caught in motion on a sunlit street"
cap: "Color and movement"
- src: "assets/img/gallery/g-editorial-2.jpg"
alt: "Editorial: a fringed tweed jacket and skirt with a pool cue and pink beret"
cap: "House rules"
- src: "assets/img/gallery/g-editorial-6.jpg"
alt: "Editorial: a gold sequinned jumpsuit on a palm-lined street"
cap: "Gold hour"
- src: "assets/img/gallery/g-editorial-5.jpg"
alt: "Editorial: a bold printed co-ord and laced heels against desert palms"
cap: "Print on print"
- src: "assets/img/gallery/g-editorial-1.jpg"
alt: "Editorial: a neon satin set styled on an inflatable poolside"
cap: "Poolside story"
- src: "assets/img/gallery/g-menswear-1.jpg"
alt: "Menswear editorial: a leather harness and long feather skirt, black and white"
cap: "Fringe and harness"
- src: "assets/img/gallery/g-menswear-4.jpg"
alt: "Menswear editorial: an open white shirt and denim shorts against open sky"
cap: "Wide open"
- src: "assets/img/gallery/g-menswear-6.jpg"
alt: "Menswear editorial: an oversized checked coat against a mirrored mosaic wall"
cap: "Mirror, mosaic"
- src: "assets/img/gallery/g-menswear-8.jpg"
alt: "Menswear editorial: a leather jacket and white shorts beneath an American flag"
cap: "Roadside Americana"
- key: "commercial"
title: "Commercial"
intro: "Campaign work built to sell a product and hold a brand's point of view."
images:
- src: "assets/img/gallery/g-commercial-3.jpg"
alt: "Swim campaign: a competitive swimmer mid-stroke in a racing lane"
cap: "Swim campaign"
- src: "assets/img/gallery/g-commercial-1.jpg"
alt: "Swim campaign: two athletes on the deck of a competition pool"
cap: "Swim campaign"
- src: "assets/img/gallery/g-commercial-2.jpg"
alt: "Swim campaign: two athletes in printed racing suits poolside"
cap: "Swim campaign"
- src: "assets/img/gallery/g-commercial-7.jpg"
alt: "Swim campaign: two children in printed life vests jumping into a pool"
cap: "Swim campaign, family"
- src: "assets/img/gallery/g-commercial-4.jpg"
alt: "Evening wear campaign: two navy sequinned gowns with high slits"
cap: "Evening wear"
- src: "assets/img/gallery/g-commercial-8.jpg"
alt: "Accessories campaign: a dusty pink suit with structured tan luggage"
cap: "Accessories"
- key: "red-carpet"
title: "Red carpet"
intro: "On a carpet the image is the whole message, and there is exactly one chance to get it right."
images: []
clients:
label: "Selected clients"
names: "Alfre Woodard, Norman Reedus, Garcelle Beauvais, Macy Gray, Isis King, Skai Jackson, Kristina Arielle, Ray Santiago, Loren Lott, Keesha Sharp, EJ Johnson"
footnote: "A selection from a decade of work. The full archive is available on request."
cta:
heading: "I take on a few brand projects at a time."
body: "Creative direction, visual strategy, and campaign work for brands and agencies."
buttonLabel: "Start a project"
buttonHref: "contact"
54 changes: 0 additions & 54 deletions content/work.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,60 +89,6 @@ cases:
footerKind: "none"
footerText: ""
footerHref: ""
archive:
eyebrow: "Selected styling archive"
heading: "A decade of execution."
intro: "Celebrity, red carpet, editorial, commercial. It's here so you know the point of view is earned. I've done the work it's built on."
items:
- src: "assets/img/archive-01.jpg"
alt: "Menswear styling"
gallery: "celebrity-men"
shoot: "menswear-styling"
capTitle: "Celebrity men"
capSub: "Menswear styling"
- src: "assets/img/archive-02.jpg"
alt: "Glamour cover styling"
gallery: "celebrity-women"
shoot: "glamour-cover"
capTitle: "Cover"
capSub: "Glamour"
- src: "assets/img/archive-03.jpg"
alt: "Editorial styling"
gallery: "editorial"
shoot: "poolside-story"
capTitle: "Editorial"
capSub: "Poolside story"
- src: "assets/img/archive-04.jpg"
alt: "Editorial art direction"
gallery: "menswear"
shoot: "fringe-harness"
capTitle: "Art direction"
capSub: "Menswear editorial"
- src: "assets/img/archive-05.jpg"
alt: "Norman Reedus"
gallery: "celebrity-men"
shoot: "norman-reedus"
capTitle: "Celebrity men"
capSub: "Norman Reedus"
- src: "assets/img/archive-06.jpg"
alt: "Campaign styling"
gallery: "commercial"
shoot: "print-commercial"
capTitle: "Campaign"
capSub: "Print & commercial"
- src: "assets/img/archive-07.jpg"
alt: "Red carpet styling"
gallery: "celebrity-women"
shoot: "glamour-cover"
capTitle: "Red carpet"
capSub: "Event dressing"
- src: "assets/img/archive-08.jpg"
alt: "Editorial styling"
gallery: "editorial"
shoot: "color-movement"
capTitle: "Editorial"
capSub: "Color & movement"
footnote: "A selection from a decade of work. Full archive available on request."
video:
eyebrow: "In motion"
heading: "On the floor"
Expand Down
10 changes: 6 additions & 4 deletions docs/astro-keystatic-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ src/layouts/BaseLayout.astro <- per-page <head>: SEO, OG, JSON-LD, favicons
src/components/*.astro <- Header, Footer, Cta
src/pages/*.astro <- one per page; renders content into the original markup
public/ <- assets/css, assets/js, assets/img, favicons, manifest,
robots.txt, sitemap.xml (copied verbatim into dist/)
robots.txt (copied verbatim into dist/)
src/pages/sitemap.xml.ts <- generates dist/sitemap.xml, image entries included
src/pages/[legacy].astro <- redirect stubs for the old Wix URLs
|
| npm run build (Astro, static; Keystatic admin excluded)
v
Expand Down Expand Up @@ -94,10 +96,10 @@ separate `build:admin` output on Netlify.
"shoot" field deep-links to one shoot's image set.
- **Add a whole new page:** add a singleton to `keystatic.config.ts`, a
`content/<page>.yaml`, and a `src/pages/<page>.astro` that renders it through
`BaseLayout` (which supplies the SEO/JSON-LD); add the URL to
`public/sitemap.xml`.
`BaseLayout` (which supplies the SEO/JSON-LD); add the page to `PAGES` in
`src/pages/sitemap.xml.ts`.
- **Change the domain:** update `SITE_ORIGIN` in `src/lib/content.ts`, `BASE` in
`tools/seo_check.py`, and `public/robots.txt` + `public/sitemap.xml`.
`tools/seo_check.py`, and `public/robots.txt`.

## Tradeoffs

Expand Down
Loading
Loading