feat: Wave 2 — admin → public site wiring (4 medium features)#528
Open
tayebmokni wants to merge 5 commits into
Open
feat: Wave 2 — admin → public site wiring (4 medium features)#528tayebmokni wants to merge 5 commits into
tayebmokni wants to merge 5 commits into
Conversation
…#507) Four \`<Link>\` destinations in the admin sidebar + list views 404'd: - posts/page.tsx links to /posts/new and /posts/import - pages/page.tsx links to /pages/new - jobs/dlq/page.tsx links to /jobs as a "back" link Build the missing route files. Each is brand-polished (Headline with italic accent, card layout): - /posts/new — Client Component form (title, slug, status). Slug auto-derives from title via exported \`slugify()\` when blank. Submits POST /api/v1/posts with content_blocks: []. On success navigates to /posts/{id} (the existing editor takes over). Surfaces ApiError inline. Headline: "Write your *next* post." - /pages/new — Sibling shape with post_type: 'page' and a slash- prefixed slug (/about-us). Redirects to /pages/{id}. - /posts/import — Server-rendered explainer card linking to /migrate (where the WordPress importer lives). Lists the three supported sources (WXR, WP REST, ACF JSON). - /jobs — Static card grid, one card per queue from the canonical KNOWN_QUEUES set (critical, default, webhooks, media, search, reports, low — same list the DLQ chip filter reads). Each card links to /jobs/dlq?queue={name}. 22 new tests across the four pages: slugify unit cases, render, headline, submit POST body + redirect, error branch. Closes #507. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
The marketing site's nav + footer columns were hardcoded const arrays
(\`NAV_LINKS\`, footer-product, footer-resources, etc.). Admin → Appearance
→ Menus persisted menus into the menu store but nothing read them.
### API: new public-read endpoint
\`apps/api/internal/public/menus/\` (new):
- \`GET /api/v1/menus\` lists all configured menus
- \`GET /api/v1/menus/by-location/{location}\` returns items for the
menu whose slug matches {location}
- Anonymous-readable (no policy gate) — anonymous visitors render the
nav on the marketing landing
- Empty/missing menu → 200 \`{"items": []}\` (never 404 — graceful)
- Computes \`external\` flag server-side from URL shape (scheme,
scheme-relative \`//\`, mailto:/tel:)
7 tests cover the shape contract, missing menu, empty menu, list
endpoint, anonymous access, and the isExternalURL matrix.
### Web: consumer
apps/web/src/lib/api.ts now exports \`MenuItem\` + \`fetchMenu(location)\`.
Returns \`[]\` on any fetch error — graceful degrade, never throws.
Nav.tsx + Footer.tsx replace their NAV_LINKS / footer-* arrays with
\`await fetchMenu(...)\` calls (parallel-fetched with site-name via
Promise.all). Both fall back to a sensible default array when the menu
is empty, so a fresh install renders a usable nav rather than blank.
External links render as plain \`<a target="_blank" rel="noopener
noreferrer">\`; internal as next/link.
### Wired
main.go now Mounts publicmenus next to the admin menu mount, reusing
the existing menuStore so admin edits land on the public read path
immediately.
Closes #509.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Public site title, OG metadata, wordmark, footer copyright now read
from /api/v1/settings?group=core.site instead of hardcoded "GoNext".
### apps/web/src/lib/api.ts (extended in the previous commit)
\`fetchSiteOptions({revalidate, cookie})\` hits the API and returns
\`{name, tagline, url}\`. Returns documented defaults ("GoNext",
"A site powered by GoNext.", "") on any fetch error or non-200 —
the public site never crashes on a settings hiccup.
### apps/web/src/app/layout.tsx
Replaces static \`metadata\` export with \`async generateMetadata()\`
that calls fetchSiteOptions and populates:
- \`title.default\` (siteName)
- \`title.template\` (\`%s — \${siteName}\`)
- \`description\` (tagline)
- \`metadataBase\` (only when url is non-empty AND parses)
- \`openGraph.siteName\`
revalidate: 60s — settings change rarely, so a Next data-cache
window of one minute is plenty.
### Marketing chrome
Nav.tsx + Footer.tsx (committed in #509 together with menus) are
now async Server Components that pull site name + tagline. The
Wordmark primitive accepts a \`name\` prop, splits on the FIRST
space, renders first half display-bold + second half italic serif.
Single-word names render bold-only.
### PublicShell
Wrapped the async chrome in \`<Suspense fallback={null}>\` so route-
page tests in jsdom still render the themed body while the async
data hasn't resolved.
### Known follow-up
/api/v1/settings is currently RequireSession-gated, so the public
web container always gets 401 and falls back to defaults. A public
read carve-out lands in the next commit so admin-edited values
actually surface on the public site.
Closes #508 (web-side wiring). API public read in the next commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
PR #508's web-side wiring was rendering default values on every page load because /api/v1/settings is RequireSession-gated — the public web container has no session cookie. The graceful-fallback path was firing on every request, so admin edits never surfaced. New: \`apps/api/internal/public/settings/\` mounting: - \`GET /api/v1/public/site\` — anonymous-readable. Returns the flat projection \`{name, tagline, url}\` for the publicly-safe core.site.* keys. Store error returns documented defaults (\"GoNext\" / \"A site powered by GoNext.\" / \"\") with HTTP 200 — never 5xx. Wired into main.go next to the admin settings mount, reusing the same settingsStore so operator edits surface immediately on the public site. apps/web/src/lib/api.ts::fetchSiteOptions now hits the public endpoint and dropped the cookie-forwarding code (no longer needed). 6 tests cover empty store, partial keys, store error graceful path, no-auth contract, flat wire shape, nil-Deps Mount validation. Verified live: curl http://localhost:8080/api/v1/public/site → 200 {\"name\":\"Verified Live Save\",\"tagline\":\"...\",\"url\":\"...\"} curl http://localhost:3000/ → <title>Verified Live Save</title> in the rendered HTML → <meta property=\"og:site_name\" content=\"Verified Live Save\"> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
The TODO at apps/web/src/app/page.tsx:17-19 promised that admin
\`core.reading.homepage_type=static_page\` + \`homepage_page_id\` would
render the chosen page at /. The dispatcher is now wired.
### Public-read extension
apps/api/internal/public/settings/handler.go projects two more
fields next to the existing \`{name, tagline, url}\`:
"reading": {
"homepage_type": "latest_posts" | "static_page",
"homepage_page_id": "<slug>" | ""
}
Enum-guard: an invalid stored homepage_type clamps to
\`latest_posts\` rather than propagating the bad value.
Two new tests: TestReadingProjectionSurfacesStoredValues,
TestInvalidHomepageTypeFallsBackToDefault.
### Web
apps/web/src/lib/api.ts: SiteOptions grows a \`reading\` field
(camel-case keys). Defaults: type=latest_posts, id="".
apps/web/src/app/page.tsx: when homepage_type === 'static_page'
AND homepage_page_id is non-empty, dispatcher calls
renderSingular(id) and wraps with PublicShell. Otherwise, falls
through to the existing marketing landing.
Graceful degrade: empty id, renderSingular non-200, or any thrown
error → marketing landing. The dispatcher never crashes the home
route on misconfig.
page.test.tsx covers all 4 branches: default, static_page+valid,
static_page+empty-id (fall back), static_page+404 (fall back).
Closes #510.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on PR #527 (Wave 1) which stacked on PR #523 (post-session cleanup).
Closes 4 issues from the #522 tracker
core.reading.show_on_frontdispatcher: admin pins a Page → renders at /5 commits
feat(admin): build /posts/new, /pages/new, /posts/import, /jobs pages (#507)feat(api+web): wire admin menus → public site navigation (#509)— includes newapps/api/internal/public/menus/packagefeat(web): wire admin Settings → public site identity (#508)— web sidefeat(api): public-read /api/v1/public/site for site identity (#508)— API side, anonymous-readable so the web container can fetch without a sessionfeat(web): show_on_front dispatcher — admin pin → public homepage (#510)— extends public/site with reading.{homepage_type,homepage_page_id}Verification
go build ./...cleanpnpm exec tsc --noEmit0 errors in apps/admin and apps/webGET /api/v1/public/sitereturns 200 with seededcore.site.*+readingprojection (no auth)GET /api/v1/menus/by-location/primaryreturns 200 with empty array on fresh installcurl localhost:3000/HTML shows<title>and<meta og:site_name>from postgresWhat's next
🤖 Generated with Claude Code