Ship: landing page + collab + custom domains + forms + analytics + clone + favicon#11
Merged
Conversation
Share now reachable from the editor TopBar instead of only the dashboard. Extracted ShareSiteModal into src/ui/ so both callers mount the same component, and added a role switcher per collaborator row. Collaborators can now remove themselves from a shared site via a Leave action; removeSiteCollaborator allows self-removal without admin rights. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a UserMenu dropdown (avatar + email + Sign out) into the shared AppHeader so every dashboard-level page has a visible logout affordance. Clerk mode signs out via useClerk().signOut(); dev-auth mode shows the item disabled with a hint. The Clerk-dependent child component is only mounted outside dev mode so useClerk() never runs without a provider. app/page.tsx is now a real landing page instead of a blind redirect — hero + features + how-it-works + file-protocol snippet + final CTA, built from Mantine components with the existing neon-on-dark theme. Signed-in users see a "Dashboard" CTA, signed-out users see "Sign in". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
System prompts for both browser models (Gemma + Qwen) and the server-side agent loop now: - require a Tabler icon next to the site name in _header.njk's brand link (never a plain string, never an emoji, never an <img>) - ban Unicode emoji characters anywhere in generated .md / .njk / front-matter fields — always use <i class="ti ti-*"></i> instead - ban colons inside YAML front-matter values (title, description, subtitle, …) because a second colon on the same line breaks the parser; use em-dash / hyphen instead, or wrap in double quotes The critic in the agent loop flags missing brand icons and stray emoji so the loop self-corrects. parseFrontMatter() now catches YAMLException and falls back to an empty data object + raw body, so the editor can still open a page saved with malformed front-matter instead of showing a runtime error overlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New site_domains table (domain PK, site_id FK). Owners/admins can attach or detach domains via GET/POST/DELETE /api/sites/:id/domains; addSiteDomain normalizes + validates, and rejects domains already pointing at another site. The middleware gains a host-based rewrite that runs before the existing auth gate. For hostnames outside APP_HOSTS (spaceforge.dev, localhost, *.vercel.app) it calls lookupSlugByDomain and rewrites the request to /s/<slug>/<path>, so any custom domain pointed at Spaceforge serves the published artifact without touching route handlers. Apex/app-host requests stay a pure passthrough (no DB read). ShareSiteModal gets a Tabs split — Collaborators + Custom domains — so the whole "who can see this / where does it live" story is in one place. DNS and TLS are terminated outside the app (Vercel / CDN); attaching a domain on the platform is still a manual step. This commit only records the mapping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…page
task dev blew up with:
Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it with "use server"
<... component={function LinkComponent} href=... >
The landing page (app/page.tsx) is a Server Component and was passing
next/link's Link function into Mantine <Anchor> / <Button> via the
"component" prop. Next 16's RSC serialization rejects that. Since the
landing page never gains from client-side navigation — one link per
surface, all plain hrefs — switching to native <a> is the cleanest fix:
- <Anchor component={Link} href=...> → <Anchor href=...>
- <Button component={Link} href=...> → <Button component="a" href=...>
Dropped the unused import Link from "next/link".
DashboardView, TrashView, TeamView, FormSubmissionsView keep their
component={Link} usage — all are 'use client' components, so the
Link function reference stays inside the client boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New form_submissions table (id, site_id, form_name, data jsonb,
user_agent, ip, created_at). Sites POST to
/api/forms/:slug/:name — a public endpoint that accepts multipart,
url-encoded, or JSON; stores the submission; and 303-redirects back
to the referer with ?submitted=<name> (or returns 201 JSON when the
caller asks for it).
Middleware publicPatterns now whitelists /api/forms/* so it works
for signed-out visitors. The custom-domain host rewrite skips
/api/* and /_next/* so forms still reach the handler under a
custom host.
Nunjucks render gains an optional `site` context ({ slug, name }).
The publish pipeline threads it through so layouts/markdown can
reference `{{ site.slug }}` — the canonical form action shape is
`/api/forms/{{ site.slug }}/<name>`.
Editor gets a Forms button in the TopBar and a dedicated
/sites/:id/forms view that lists submissions with per-form filters
and per-field rows. Middleware switches to the Node.js runtime
because pg/drizzle/@clerk/nextjs pull in node:crypto.
System prompts (Gemma + Qwen + server agent loop) now instruct the
model to wire forms to the endpoint, include a honeypot field, and
use proper labels / input types.
Acceptance verified end-to-end against a locally published site:
submit → row persists with all fields → surfaces correctly in the
UI table under the dark/neon theme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New page_views table (id, site_id, path, referrer, user_agent, ip, host, created_at). The public /s/:slug/... serving route records one row per HTML hit via recordViewBestEffort — fire-and-forget, never awaits, swallows DB errors so analytics cannot extend user-facing latency or take the request down with it. Non-HTML asset requests are ignored so we don't double-count the 20 secondary GETs that come with a page load. Obvious crawler UAs (googlebot, headless chrome, monitoring bots, …) are dropped at insert time. getAnalyticsSummary returns totals (24h / 7d / 30d), top pages and top referrers over 30 days, a daily series for the bar chart, and the 50 most recent hits. Surfaced at /sites/:id/analytics — editor TopBar gains an Analytics button next to Forms. Middleware config merged runtime: 'nodejs' into the config object so Next 16 + Turbopack actually honors it (a separate top-level runtime export was being ignored in dev, bringing back the edge crypto error). Acceptance verified end-to-end locally: 5 real-UA hits produce expected totals, top-page / top-referrer breakdowns, a daily chart entry, and a populated recent-hits table in the dark-themed UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cloneSite() takes a source site the caller can see (owning team or
collaborator grant), validates the requested slug is fresh, inserts a
new sites row with the source's templateId, and deep-copies every
site_files row via cloneFilesBetweenSites — downloads each source
blob and re-uploads it under drafts/<new-id>/<path> so the clone is
fully independent. Does NOT carry over site_versions, chat_messages,
site_collaborators, or site_domains — a clone starts as a clean
unpublished draft the new owner iterates on.
POST /api/sites/:id/clone { slug, name } returns 201 with the new
SiteSummary. Dashboard grows a duplicate icon on every site card
(both team-owned and shared-with-me) that opens a Mantine modal
pre-filling "<slug>-copy" and "<name> (copy)"; on confirm it
navigates straight to the new site's editor.
Acceptance verified end-to-end locally: cloned a 6-file published
site, confirmed the new row in the DB, all 6 file paths match the
source, and spot-checked three files byte-for-byte identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New favicon_icon column on sites (nullable text, stores a Tabler icon
name without the ti- prefix). Editors manage it via a curated
FaviconModal — ~55 brand-appropriate icons grouped into Brand marks /
Food & drink / Nature / Tech / Places & things / Letters & abstract,
with a search box, a live "current" preview chip, and a Reset action.
The favicon is served dynamically from
/s/<slug>/favicon.svg: the public site route special-cases the path,
looks up the chosen icon, and renders an SVG string on the fly via
lib/favicon/render. The SVG wraps the Tabler stroke-path in a dark
rounded square with the neon accent so it reads at tab size. The
response is s-maxage=300 with SWR so it's cheap but still updates
within a few minutes of a change.
Each published HTML now carries <link rel="icon"
href="/s/<slug>/favicon.svg"> injected by injectFrameworkServer so
the brand mark shows on the site without the user writing any head
markup themselves. The model prompts (Gemma + Qwen + server agent
loop) are updated to suggest a favicon name at the end of their
reply instead of writing a link tag.
Server-side SVG renderer loads each palette icon's __iconNode via
static imports from @tabler/icons-react/dist/esm/icons — React
stays out of the server bundle, only ~55 tiny iconNode arrays land.
A root-level src/tabler-icon-modules.d.ts declares the wildcard
module shape tsc was missing.
Acceptance verified end-to-end locally: unset → default rocket
served; PUT /api/sites/:id/favicon { icon: "bread" } → 200 and the
public /favicon.svg swaps to bread; PUT with an icon outside the
palette → 400. Screenshot confirms the rendered brand-styled bread
favicon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dashboard/team/trash pages already have UserMenu via AppHeader, but the editor's TopBar was missing it — anyone spending their time inside a site had no visible way to sign out. Thread the auth user and isDevAuth flag from app/sites/[siteId]/page.tsx → SiteEditor → App.chrome → TopBar, and mount <UserMenu> next to the color-scheme toggle. Absent props (the localStorage-only tests) just skip the menu, so the old embeddings still render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Summary
Bundles this session's work on top of
ab0b24d(the previousmain):UserMenuin every header (dashboard / team / trash / editor)/(was a redirect to/dashboard)site_domainstable, CRUD API, host-based middleware rewrite, domains tab in share modal. DNS + platform-side attachment still manual.form_submissionstable, publicPOST /api/forms/:slug/:name,/sites/:id/formsviewer with per-form filterpage_viewstable, fire-and-forget recording on HTML hits,/sites/:id/analytics(totals + top pages + top referrers + daily chart + recent hits)cloneSite,POST /api/sites/:id/clone, duplicate icon on every dashboard cardfavicon_iconcolumn, curated Tabler picker (~55 icons with search), dynamic/s/:slug/favicon.svg, injected into every published HTMLDB migrations 0002–0005 have been applied to production already; this PR only ships the code.
Test plan
spaceforge.dev/🤖 Generated with Claude Code