feat: Publish Porfolio API - #65
Conversation
There was a problem hiding this comment.
Pull request overview
This PR publishes a portfolio content API by embedding JSON-LD into existing markdown content, generating a SQLite DB + static asset bundle at build time, and serving that data via a new Node/Hono server (with Docker + CI wiring).
Changes:
- Replace markdown frontmatter with per-page JSON-LD
<script type="application/ld+json">…</script>blocks across works/news/events/profile. - Add
.serverworkspace: build pipeline to parse markdown + extract JSON-LD, generateportfolio.db, copy assets, and serve an API (Hono + better-sqlite3). - Add Dockerfile, GitHub Actions workflow, and devcontainer updates to build/typecheck the server.
Reviewed changes
Copilot reviewed 131 out of 133 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| works/ut-collection-perspective/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/ushio-makabe/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/ushio-kameyama/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/twinkle/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/tsukulive19/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/tkbneu18/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/the-lost-thing/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/taurus/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/sympathy/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/suzuna/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/study/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/ssss/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/sohosai17/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/skip24/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/sgym-card/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/ri/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/residents/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/neuws18/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/neuws17/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/mastival/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/layers/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/laika/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/kasuga_ac/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/kamine-proto/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/kamine-baby/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/himap/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/hannan-guitar/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/ham19web/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/fyb/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/encounter/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/design-pattern/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/carillon/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/caf-kanno/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/battle-collection/index.md | Remove frontmatter; add JSON-LD metadata script |
| works/ariabella22/index.md | Remove frontmatter; add JSON-LD metadata script |
| profile/index.md | Add JSON-LD Person schema script |
| package.json | Declare .server as an npm workspace |
| news/251203_information-design/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/250704_tmaf25/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/250426_kamine/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/241007_mast-ad-lt-2024/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/230822_ushio-rpng-catalog/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/220912_gfest-guide-video/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/211128_caf21_award/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/211101_caf21/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/211028_at21/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/210728_tmaf21/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/210325_graduation/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/210324_tsukulive/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/210324_ham20/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/201207_isca/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/201108_tsukulive-note/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/200527_fyb/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/200228_kdcc/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/200124_making-notes/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/191101_tsukulive/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/190501_the-lost-thing/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/190501_ham19/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/190201_mastival/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/190121_sobo-award/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/181229_sobo-magazine/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/181007_the-lost-thing-on-cup/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/180901_t-tasu/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| news/180701_the-lost-thing/index.md | Remove frontmatter; add JSON-LD NewsArticle schema script |
| events/tsukuba24/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/tmaf25/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/tmaf21/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/t-tasu18/index.md | Replace content with JSON-LD ExhibitionEvent schema script |
| events/residents21/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/pond23/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/perspective/index.md | Remove frontmatter; add JSON-LD Event schema script |
| events/neo24/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/mastival/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/makabe/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/kmym21/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/kamine-neo25/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/kamine-expoc25/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/ham20/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/ham19/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/caf21/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/caf-ushio/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| events/asterism/index.md | Remove frontmatter; add JSON-LD ExhibitionEvent schema script |
| Dockerfile | Add container build for the API server |
| .server/tsconfig.json | Add TypeScript config for the server workspace |
| .server/src/types/content.ts | Add content + JSON-LD TypeScript types |
| .server/src/types/api.ts | Add API helper types (pagination, ProblemDetails) |
| .server/src/shared/response.ts | Add list pagination header helper |
| .server/src/shared/error.ts | Add RFC7807-style error responses |
| .server/src/shared/db-client.ts | Add SQLite client helper |
| .server/src/models/works/schema.ts | Define works table schema |
| .server/src/models/works/router.ts | Add works API routes |
| .server/src/models/works/queries.ts | Add works DB queries |
| .server/src/models/works/index.ts | Export works model wiring |
| .server/src/models/profile/schema.ts | Define profile table schema |
| .server/src/models/profile/router.ts | Add profile router (model-level) |
| .server/src/models/profile/queries.ts | Add profile DB query |
| .server/src/models/profile/index.ts | Export profile model wiring |
| .server/src/models/news/schema.ts | Define news table schema |
| .server/src/models/news/router.ts | Add news API routes |
| .server/src/models/news/queries.ts | Add news DB queries |
| .server/src/models/news/index.ts | Export news model wiring |
| .server/src/models/events/schema.ts | Define events table schema |
| .server/src/models/events/router.ts | Add events API routes |
| .server/src/models/events/queries.ts | Add events DB queries |
| .server/src/models/events/index.ts | Export events model wiring |
| .server/src/index.ts | Add Hono server entrypoint, routes, static, error handling |
| .server/package.json | Add server dependencies and build scripts |
| .server/build/normalize-paths.ts | Add HTML image path normalization helpers |
| .server/build/models/works/parser.ts | Add works markdown + JSON-LD parser |
| .server/build/models/profile/parser.ts | Add profile markdown + JSON-LD parser |
| .server/build/models/news/parser.ts | Add news markdown + JSON-LD parser |
| .server/build/models/events/parser.ts | Add events markdown + JSON-LD parser |
| .server/build/jsonld.ts | Add JSON-LD extraction + validation utilities |
| .server/build/index.ts | Add build pipeline entrypoint (parse → normalize → db → assets) |
| .server/build/db-populate.ts | Add SQLite schema creation + insertion |
| .server/build/copy-assets.ts | Add asset copy step into dist/static |
| .server/.gitignore | Ignore server build artifacts |
| .nvmrc | Pin Node major version |
| .gitignore | Ignore .claude dir |
| .github/workflows/server.yaml | Add CI workflow for server typecheck/build |
| .dockerignore | Add Docker build context filtering |
| .devcontainer/devcontainer.json | Enable Docker-in-Docker feature; add GH Actions extension |
| const insertWork = db.prepare(` | ||
| INSERT INTO works (id, jsonld, body_html) | ||
| VALUES (?, ?, ?) | ||
| `) | ||
|
|
||
| const insertEvent = db.prepare(` | ||
| INSERT INTO events (id, jsonld, body_html) | ||
| VALUES (?, ?, ?) | ||
| `) | ||
|
|
||
| const insertNews = db.prepare(` | ||
| INSERT INTO news (id, jsonld, body_html) | ||
| VALUES (?, ?, ?) | ||
| `) |
There was a problem hiding this comment.
The tables define sortable columns (works.release, events.session_start, news.release, etc.) and the query code orders by them, but populateDatabase() only inserts (id, jsonld, body_html). This leaves those columns NULL for every row, breaking ordering and making the indexes ineffective. Either populate the columns from JSON-LD during build, or update the list queries to sort based on JSON fields (e.g., json_extract(jsonld, ...)).
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| router.get("events", (c) => { | ||
| const { limit, offset } = parsePagination((name) => c.req.query(name)) | ||
|
|
||
| const result = getEvents({ limit, offset }) | ||
| const lang = c.req.query("lang") | ||
| const origin = new URL(c.req.url).origin | ||
|
|
||
| setListHeaders(c, result.total, limit, offset) | ||
| return c.json( | ||
| result.data.map((item) => | ||
| toApiJsonLd(item.jsonld, { | ||
| lang, | ||
| origin, | ||
| includeBodyHtml: false, | ||
| }), | ||
| ), | ||
| ) | ||
| }) | ||
|
|
||
| // GET /events/:id | ||
| router.get("events/:id", (c) => { | ||
| const id = c.req.param("id") |
There was a problem hiding this comment.
These routes are registered as "events" / "events/:id" (no leading /). This can prevent the endpoints from matching requests when mounted. Use /events and /events/:id to ensure correct routing.
| router.get("news", (c) => { | ||
| const { limit, offset } = parsePagination((name) => c.req.query(name)) | ||
|
|
||
| const result = getNews({ limit, offset }) | ||
| const lang = c.req.query("lang") | ||
| const origin = new URL(c.req.url).origin | ||
|
|
||
| setListHeaders(c, result.total, limit, offset) | ||
| return c.json( | ||
| result.data.map((item) => | ||
| toApiJsonLd(item.jsonld, { | ||
| lang, | ||
| origin, | ||
| includeBodyHtml: false, | ||
| }), | ||
| ), | ||
| ) | ||
| }) | ||
|
|
||
| // GET /news/:id | ||
| router.get("news/:id", (c) => { | ||
| const id = c.req.param("id") |
There was a problem hiding this comment.
These routes are registered as "news" / "news/:id" (no leading /). This can cause route matching issues when mounted. Use /news and /news/:id so the endpoints are reachable.
| .replace( | ||
| /<img([^>]*)\ssrc="(?!https?:\/\/)(?!\/)([^"]+)"/g, | ||
| (match, attrs, src) => { | ||
| return `<img${attrs} src="/${contentType}/${id}/${src}"` |
There was a problem hiding this comment.
normalizeImagePathsInHtml prepends /${contentType}/${id}/ to any relative <img src> value. For content like works/encounter/index.md where images are referenced as encounter/encounter_2.jpg but stored directly under works/encounter/encounter_2.jpg, this produces duplicated paths (e.g. /static/works/encounter/encounter/encounter_2.jpg) and breaks image URLs in body_html. Consider resolving relative paths against the content-type root (e.g. /${contentType}/${src}) or normalizing away a leading ${id}/ when present, then letting the later rewrite add /static.
| return `<img${attrs} src="/${contentType}/${id}/${src}"` | |
| let normalizedSrc = src as string | |
| const idPrefix = `${id}/` | |
| if (normalizedSrc.startsWith(idPrefix)) { | |
| normalizedSrc = normalizedSrc.slice(idPrefix.length) | |
| } | |
| return `<img${attrs} src="/${contentType}/${id}/${normalizedSrc}"` |
| router.get("works", (c) => { | ||
| const { limit, offset } = parsePagination((name) => c.req.query(name)) | ||
|
|
||
| const result = getWorks({ limit, offset }) | ||
| const lang = c.req.query("lang") | ||
| const origin = new URL(c.req.url).origin | ||
|
|
||
| setListHeaders(c, result.total, limit, offset) | ||
| return c.json( | ||
| result.data.map((item) => | ||
| toApiJsonLd(item.jsonld, { | ||
| lang, | ||
| origin, | ||
| includeBodyHtml: false, | ||
| }), | ||
| ), | ||
| ) | ||
| }) | ||
|
|
||
| // GET /works/:id | ||
| router.get("works/:id", (c) => { | ||
| const id = c.req.param("id") |
There was a problem hiding this comment.
These routes are registered as "works" / "works/:id" (no leading /). In Hono, route paths are typically absolute (e.g. /works). As written, this can result in the endpoints not matching requests (especially when mounted via app.route("/", worksRouter)). Use /works and /works/:id to ensure the routes are reachable.
No description provided.