Skip to content

Commit a782797

Browse files
committed
fix(site): basePath /GraphCompose for project Pages deployment
The v1.6.8 cut deployed the Next.js site to demchaav.github.io/GraphCompose/ — a PROJECT Pages URL — without enabling Next.js's basePath / assetPrefix. Result: every absolute path (/_next/static/css, /_next/static/chunks, /brand/logo, /examples.json, /showcase/pdf, /previews/*) resolved against the host root (demchaav.github.io) instead of the sub-path, returning 404 across the board. The browser rendered an unstyled, broken page. Fix is two-pronged: 1) next.config.mjs — set basePath, assetPrefix, and a matching NEXT_PUBLIC_BASE_PATH env var to "/GraphCompose" (override to "" via the env when running `next dev` locally). Next.js automatically prefixes its own _next/static URLs and any <Link> / <Image> hrefs. 2) site/lib/base-path.ts — withBasePath() helper that prefixes runtime string paths Next.js doesn't see at build time: plain <img src>, <object data>, fetch() URLs. Applied to every public/ asset reference in our code: - TopBar.tsx — /brand/graphcompose-logo.png - Gallery.tsx — fetch('/examples.json') + showcaseUrl() for every card's pdf + screenshot - lib/gallery.ts — 16 CV image + 15 letterImage covers - lib/presets.tsx — 3 Playground pdf + 3 poster paths External URLs (github.com, javadoc.io), hash anchors, mailto:, and data: URIs pass through untouched. The helper is idempotent — calling it with an already-prefixed path is a no-op. Verified with `next build` — 4/4 static pages prerendered green.
1 parent 9988b96 commit a782797

6 files changed

Lines changed: 65 additions & 15 deletions

File tree

site/components/Gallery.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
33
import { GALLERY, type TemplateSpec } from "@/lib/gallery";
44
import { PaperLetter } from "./PaperPage";
55
import { highlightJava } from "@/lib/highlight";
6+
import { withBasePath } from "@/lib/base-path";
67
import Reveal from "./Reveal";
78

89
/*
@@ -81,7 +82,7 @@ interface Card {
8182
}
8283

8384
const showcaseUrl = (relative: string) =>
84-
`/showcase/${relative.replace(/^showcase\//, "")}`;
85+
withBasePath(`/showcase/${relative.replace(/^showcase\//, "")}`);
8586

8687
function buildCards(manifest: Manifest | null): Card[] {
8788
if (!manifest) return [];
@@ -135,7 +136,7 @@ export default function Gallery() {
135136
const lastFocus = useRef<HTMLElement | null>(null);
136137

137138
useEffect(() => {
138-
fetch("/examples.json", { cache: "force-cache" })
139+
fetch(withBasePath("/examples.json"), { cache: "force-cache" })
139140
.then((r) => {
140141
if (!r.ok) throw new Error(`HTTP ${r.status}`);
141142
return r.json() as Promise<Manifest>;

site/components/TopBar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22
import { useEffect, useState } from "react";
3+
import { withBasePath } from "@/lib/base-path";
34

45
export default function TopBar() {
56
const [theme, setTheme] = useState<"light" | "dark">("light");
@@ -28,7 +29,7 @@ export default function TopBar() {
2829
<a className="brand" href="#top" aria-label="GraphCompose home">
2930
{/* eslint-disable-next-line @next/next/no-img-element */}
3031
<img
31-
src="/brand/graphcompose-logo.png"
32+
src={withBasePath("/brand/graphcompose-logo.png")}
3233
alt="GraphCompose"
3334
className="brand-logo"
3435
width={172}

site/lib/base-path.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Base-path prefixing for public/ assets.
3+
*
4+
* Next.js's `basePath` config auto-prefixes routes and `<Image>` /
5+
* `<Link>` href values, but plain `<img src>` / `<object data>` /
6+
* any string-literal asset path in our own code is left untouched
7+
* — it would resolve against the host root and 404 on a project
8+
* Pages deployment (e.g. `demchaav.github.io/GraphCompose/`).
9+
*
10+
* Use `withBasePath('/brand/logo.png')` so the same code works
11+
* locally (basePath = "") and on Pages (basePath = "/GraphCompose").
12+
*
13+
* The base path is sourced from `NEXT_PUBLIC_BASE_PATH` at build
14+
* time, which `next.config.mjs` keeps in lockstep with the
15+
* `basePath` / `assetPrefix` it sets on the Next.js config.
16+
*/
17+
const RAW = (process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/$/, "");
18+
19+
export const basePath: string = RAW;
20+
21+
export function withBasePath(path: string): string {
22+
// External / hash / data URLs pass through untouched.
23+
if (!path) return path;
24+
if (path.startsWith("http://") || path.startsWith("https://")) return path;
25+
if (path.startsWith("#") || path.startsWith("mailto:") || path.startsWith("data:")) return path;
26+
// Avoid double-prefixing when callers already did the work.
27+
if (RAW && path.startsWith(RAW + "/")) return path;
28+
// Ensure a leading slash on inputs that lack one.
29+
const normalised = path.startsWith("/") ? path : `/${path}`;
30+
return `${RAW}${normalised}`;
31+
}

site/lib/gallery.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface TemplateSpec {
2929
letterSource: string | null;
3030
}
3131

32+
import { withBasePath } from "./base-path";
33+
3234
const GH = "https://github.com/DemchaAV/GraphCompose/blob/main/src/main/java/com/demcha/compose/document/templates";
3335

3436
/*
@@ -80,8 +82,8 @@ export const GALLERY: TemplateSpec[] = SPECS.map(({ name, slug, hasLetter = true
8082
variant,
8183
desc,
8284
pair: hasLetter ? `${name}Letter` : null,
83-
image: `/previews/cv-v2/cv-${slug}-v2.png`,
84-
letterImage: hasLetter ? `/previews/coverletter-v2/cover-letter-${slug}-v2.png` : null,
85+
image: withBasePath(`/previews/cv-v2/cv-${slug}-v2.png`),
86+
letterImage: hasLetter ? withBasePath(`/previews/coverletter-v2/cover-letter-${slug}-v2.png`) : null,
8587
source: `${GH}/cv/v2/presets/${name}.java`,
8688
letterSource: hasLetter ? `${GH}/coverletter/v2/presets/${name}Letter.java` : null,
8789
code: `CvDocument cv = loadProfile();

site/lib/presets.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PaperHello, PaperInvoice, PaperCv } from "@/components/PaperPage";
2+
import { withBasePath } from "./base-path";
23
import type { ReactNode } from "react";
34

45
export type PresetId = "hello" | "invoice" | "cv";
@@ -38,8 +39,8 @@ export const PRESETS: Record<PresetId, Preset> = {
3839
hello: {
3940
id: "hello",
4041
label: "Hello world",
41-
pdf: "/previews/hello.pdf",
42-
poster: "/previews/hello.png",
42+
pdf: withBasePath("/previews/hello.pdf"),
43+
poster: withBasePath("/previews/hello.png"),
4344
code: `import com.demcha.compose.GraphCompose;
4445
import com.demcha.compose.document.api.DocumentPageSize;
4546
import com.demcha.compose.document.api.DocumentSession;
@@ -83,8 +84,8 @@ class Hello {
8384
invoice: {
8485
id: "invoice",
8586
label: "Invoice",
86-
pdf: "/previews/invoice.pdf",
87-
poster: "/previews/invoice.png",
87+
pdf: withBasePath("/previews/invoice.pdf"),
88+
poster: withBasePath("/previews/invoice.png"),
8889
code: `import com.demcha.compose.GraphCompose;
8990
import com.demcha.compose.document.api.DocumentPageSize;
9091
import com.demcha.compose.document.api.DocumentSession;
@@ -127,8 +128,8 @@ try (DocumentSession doc = GraphCompose.document(Path.of("invoice.pdf"))
127128
cv: {
128129
id: "cv",
129130
label: "CV · ModernProfessional",
130-
pdf: "/previews/cv.pdf",
131-
poster: "/previews/cv.png",
131+
pdf: withBasePath("/previews/cv.pdf"),
132+
poster: withBasePath("/previews/cv.png"),
132133
code: `import com.demcha.compose.GraphCompose;
133134
import com.demcha.compose.document.api.DocumentPageSize;
134135
import com.demcha.compose.document.api.DocumentSession;

site/next.config.mjs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
/** @type {import('next').NextConfig} */
2+
3+
// Sub-path under which the site is hosted on GitHub Pages.
4+
// This is a PROJECT page (demchaav.github.io/<repo>/), so every
5+
// emitted URL needs the /GraphCompose prefix — otherwise `_next/`
6+
// static chunks, the logo, and every public/ asset 404 against
7+
// the host root. Set the same value on `basePath`, `assetPrefix`,
8+
// and `NEXT_PUBLIC_BASE_PATH` so our own `withBasePath(...)` helper
9+
// (lib/base-path.ts) sees it at runtime.
10+
//
11+
// Override to "" for local `next dev` if you prefer accessing the
12+
// site at http://localhost:5173/ instead of /GraphCompose/.
13+
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "/GraphCompose";
14+
215
const nextConfig = {
316
// Fully static build — `next build` emits ./out, deployable to
417
// GitHub Pages / Vercel / any static host. No SSR, no server runtime.
518
output: "export",
619
images: { unoptimized: true },
7-
// If you deploy under a sub-path on GitHub Pages (e.g. user.github.io/graph-compose),
8-
// uncomment and set these:
9-
// basePath: "/graph-compose",
10-
// assetPrefix: "/graph-compose/",
20+
basePath: BASE_PATH || undefined,
21+
assetPrefix: BASE_PATH ? `${BASE_PATH}/` : undefined,
22+
env: {
23+
NEXT_PUBLIC_BASE_PATH: BASE_PATH,
24+
},
1125
trailingSlash: true,
1226
};
1327

0 commit comments

Comments
 (0)