Renderer Split Implementation Plan
Renderer Split Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Split the public rendering layer into a dedicated Next.js app in this repo, with renderer-owned API routes for raw/domain access and renderer-owned data access (no shared library yet).
Architecture: Add apps/renderer as a standalone Next.js app (own deployment). Copy the current renderer routes and required server/lib/components into the renderer app, and add minimal glue for auth/likes. Leave the existing app intact for now, optionally removing or redirecting renderer routes after the new app is deployed.
Tech Stack: Next.js 14 App Router, tRPC (server + client), NextAuth, Prisma, AWS SDK for R2 (S3), Tailwind.
Pre-checks (before Task 1)
- Confirm adding
pnpm-workspace.yamlwon’t break existing scripts or tooling for the root app. - Decide whether to reuse the root Jest config for renderer tests or add a renderer-local Jest config.
Task 1: Add renderer app workspace scaffold
Files:
- Create:
pnpm-workspace.yaml - Create:
apps/renderer/package.json - Create:
apps/renderer/next.config.js - Create:
apps/renderer/tsconfig.json - Create:
apps/renderer/postcss.config.js - Create:
apps/renderer/tailwind.config.js - Create:
apps/renderer/app/layout.tsx - Create:
apps/renderer/app/page.tsx - Create:
apps/renderer/styles/globals.css - Create:
apps/renderer/styles/prism.css - Copy:
styles/fonts.ts->apps/renderer/styles/fonts.ts
Step 1: Write the failing test
Create apps/renderer/lib/__tests__/renderer-config.test.ts:
import { resolveRendererBaseUrl } from "../renderer-config";
const baseEnv = {
NEXT_PUBLIC_ROOT_DOMAIN: "example.com",
NEXT_PUBLIC_RENDERER_DOMAIN: "render.example.com",
};
test("uses renderer domain when provided", () => {
expect(resolveRendererBaseUrl(baseEnv)).toBe("https://render.example.com");
});
test("falls back to root domain when renderer domain is unset", () => {
expect(resolveRendererBaseUrl({ NEXT_PUBLIC_ROOT_DOMAIN: "example.com" })).toBe(
"https://example.com",
);
});
Step 2: Run test to verify it fails
Run: pnpm test -- apps/renderer/lib/__tests__/renderer-config.test.ts
Expected: FAIL with "Cannot find module '../renderer-config'".
Step 3: Write minimal implementation
Create apps/renderer/lib/renderer-config.ts:
export const resolveRendererBaseUrl = (
env: { NEXT_PUBLIC_ROOT_DOMAIN?: string; NEXT_PUBLIC_RENDERER_DOMAIN?: string },
) => {
const domain = env.NEXT_PUBLIC_RENDERER_DOMAIN || env.NEXT_PUBLIC_ROOT_DOMAIN;
if (!domain) return "";
return `https://${domain}`;
};
Step 4: Run test to verify it passes
Run: pnpm test -- apps/renderer/lib/__tests__/renderer-config.test.ts
Expected: PASS.
Step 5: Commit
git add pnpm-workspace.yaml apps/renderer/package.json apps/renderer/next.config.js apps/renderer/tsconfig.json apps/renderer/postcss.config.js apps/renderer/tailwind.config.js apps/renderer/app/layout.tsx apps/renderer/app/page.tsx apps/renderer/styles/globals.css apps/renderer/styles/prism.css apps/renderer/styles/fonts.ts apps/renderer/lib/renderer-config.ts apps/renderer/lib/__tests__/renderer-config.test.ts
git commit -m "chore(renderer): scaffold renderer app"
Task 2: Copy renderer routes into the renderer app
Files:
- Copy:
app/[user]/error.tsx->apps/renderer/app/[user]/error.tsx - Copy:
app/[user]/[project]/[[...slug]]/page.tsx->apps/renderer/app/[user]/[project]/[[...slug]]/page.tsx - Copy:
app/[user]/[project]/[[...slug]]/layout.tsx->apps/renderer/app/[user]/[project]/[[...slug]]/layout.tsx - Copy:
app/[user]/[project]/[[...slug]]/url-normalizer.tsx->apps/renderer/app/[user]/[project]/[[...slug]]/url-normalizer.tsx
Step 1: Write the failing test
Create apps/renderer/lib/__tests__/url-normalizer.test.ts:
import { normalizeSlug } from "../url-normalizer";
test("decodes %20 and preserves slashes", () => {
expect(normalizeSlug(["docs%20page", "index"])).toBe("docs+page/index");
});
Step 2: Run test to verify it fails
Run: pnpm test -- apps/renderer/lib/__tests__/url-normalizer.test.ts
Expected: FAIL with "Cannot find module '../url-normalizer'".
Step 3: Write minimal implementation
Create apps/renderer/lib/url-normalizer.ts:
export const normalizeSlug = (slug?: string[]) => {
const joined = slug && slug.length > 0 ? slug.join("/") : "/";
return joined.replace(/%20/g, "+");
};
Step 4: Run test to verify it passes
Run: pnpm test -- apps/renderer/lib/__tests__/url-normalizer.test.ts
Expected: PASS.
Step 5: Commit
git add apps/renderer/app/[user]/error.tsx apps/renderer/app/[user]/[project]/[[...slug]]/page.tsx apps/renderer/app/[user]/[project]/[[...slug]]/layout.tsx apps/renderer/app/[user]/[project]/[[...slug]]/url-normalizer.tsx apps/renderer/lib/url-normalizer.ts apps/renderer/lib/__tests__/url-normalizer.test.ts
git commit -m "feat(renderer): add public render routes"
Task 3: Add renderer-owned raw/domain API routes
Files:
- Copy:
app/api/raw/[username]/[projectName]/[branch]/[[...path]]/route.tsx->apps/renderer/app/api/raw/[username]/[projectName]/[branch]/[[...path]]/route.tsx - Copy:
app/api/domain/[domain]/[[...slug]]/route.tsx->apps/renderer/app/api/domain/[domain]/[[...slug]]/route.tsx - Create:
apps/renderer/lib/r2-url.ts - Create:
apps/renderer/lib/__tests__/r2-url.test.ts
Step 1: Write the failing test
Create apps/renderer/lib/__tests__/r2-url.test.ts:
import { buildR2RawUrl } from "../r2-url";
test("builds R2 raw file URL", () => {
expect(
buildR2RawUrl({
bucketDomain: "r2.example.com",
projectId: "site-1",
branch: "main",
path: "docs/readme.md",
protocol: "https",
}),
).toBe("https://r2.example.com/site-1/main/raw/docs/readme.md");
});
Step 2: Run test to verify it fails
Run: pnpm test -- apps/renderer/lib/__tests__/r2-url.test.ts
Expected: FAIL with "Cannot find module '../r2-url'".
Step 3: Write minimal implementation
Create apps/renderer/lib/r2-url.ts:
export const buildR2RawUrl = ({
bucketDomain,
projectId,
branch,
path,
protocol,
}: {
bucketDomain: string;
projectId: string;
branch: string;
path: string;
protocol: "http" | "https";
}) => {
const sanitizedPath = path.replace(/^\/+/, "");
return `${protocol}://${bucketDomain}/${projectId}/${branch}/raw/${sanitizedPath}`;
};
Step 4: Run test to verify it passes
Run: pnpm test -- apps/renderer/lib/__tests__/r2-url.test.ts
Expected: PASS.
Step 5: Commit
git add apps/renderer/app/api/raw/[username]/[projectName]/[branch]/[[...path]]/route.tsx apps/renderer/app/api/domain/[domain]/[[...slug]]/route.tsx apps/renderer/lib/r2-url.ts apps/renderer/lib/__tests__/r2-url.test.ts
git commit -m "feat(renderer): add raw/domain API routes"
Task 4: Copy renderer dependencies (content/render core)
Files:
- Copy:
components/MDX.tsx->apps/renderer/components/MDX.tsx - Copy:
components/MDX-layout.tsx->apps/renderer/components/MDX-layout.tsx - Copy:
components/mdx-components-factory.tsx->apps/renderer/components/mdx-components-factory.tsx - Copy:
components/layouts/*.tsx->apps/renderer/components/layouts/*.tsx - Copy:
components/comments.tsx->apps/renderer/components/comments.tsx - Copy:
components/edit-page-button.tsx->apps/renderer/components/edit-page-button.tsx - Copy:
components/error-message.tsx->apps/renderer/components/error-message.tsx - Copy:
components/client-components-wrapper.tsx->apps/renderer/components/client-components-wrapper.tsx - Copy:
lib/markdown.ts->apps/renderer/lib/markdown.ts - Copy:
lib/resolve-link.ts->apps/renderer/lib/resolve-link.ts - Copy:
lib/resolve-site-alias.ts->apps/renderer/lib/resolve-site-alias.ts - Copy:
lib/app-config.ts->apps/renderer/lib/app-config.ts - Copy:
lib/domains.ts->apps/renderer/lib/domains.ts - Copy:
styles/prism.css->apps/renderer/styles/prism.css - Copy:
styles/globals.css->apps/renderer/styles/globals.css
Step 1: Write the failing test
Create apps/renderer/lib/__tests__/resolve-link.test.ts:
import { resolveLink } from "../resolve-link";
test("resolves a relative link against a file path", () => {
expect(
resolveLink({
link: "../img.png",
filePath: "docs/guide/index.md",
prefixPath: "/@user/site/_r/-",
}),
).toBe("/@user/site/_r/-/docs/img.png");
});
Step 2: Run test to verify it fails
Run: pnpm test -- apps/renderer/lib/__tests__/resolve-link.test.ts
Expected: FAIL with "Cannot find module '../resolve-link'".
Step 3: Write minimal implementation
Copy lib/resolve-link.ts into apps/renderer/lib/resolve-link.ts and adjust imports to be local to apps/renderer.
Step 4: Run test to verify it passes
Run: pnpm test -- apps/renderer/lib/__tests__/resolve-link.test.ts
Expected: PASS.
Step 5: Commit
git add apps/renderer/components apps/renderer/lib apps/renderer/styles
git commit -m "feat(renderer): copy render core"
Task 5: Copy renderer dependencies (data access + auth + likes)
Files:
- Copy:
components/like-row.tsx->apps/renderer/components/like-row.tsx - Copy:
components/auth/login-panel.tsx->apps/renderer/components/auth/login-panel.tsx - Copy:
components/modal/*->apps/renderer/components/modal/* - Copy:
lib/content-store.ts->apps/renderer/lib/content-store.ts - Copy:
lib/feature-flags.ts->apps/renderer/lib/feature-flags.ts - Copy:
server/api/types.ts->apps/renderer/server/api/types.ts - Copy:
server/api/root.ts->apps/renderer/server/api/root.ts - Copy:
server/api/trpc.ts->apps/renderer/server/api/trpc.ts - Copy:
server/api/routers/site.ts->apps/renderer/server/api/routers/site.ts - Copy:
server/api/routers/like.ts->apps/renderer/server/api/routers/like.ts - Copy:
server/auth.ts->apps/renderer/server/auth.ts - Copy:
server/db.ts->apps/renderer/server/db.ts - Copy:
trpc/react.tsx->apps/renderer/trpc/react.tsx - Copy:
trpc/server.ts->apps/renderer/trpc/server.ts
Step 1: Write the failing test
Create apps/renderer/lib/__tests__/session-gate.test.ts:
import { needsAuthForLike } from "../session-gate";
test("likes require auth", () => {
expect(needsAuthForLike()).toBe(true);
});
Step 2: Run test to verify it fails
Run: pnpm test -- apps/renderer/lib/__tests__/session-gate.test.ts
Expected: FAIL with "Cannot find module '../session-gate'".
Step 3: Write minimal implementation
Create apps/renderer/lib/session-gate.ts:
export const needsAuthForLike = () => true;
Step 4: Run test to verify it passes
Run: pnpm test -- apps/renderer/lib/__tests__/session-gate.test.ts
Expected: PASS.
Step 5: Commit
git add apps/renderer/components apps/renderer/lib apps/renderer/server apps/renderer/trpc
git commit -m "feat(renderer): add data access and auth"
Task 6: Wire renderer env + layout + tRPC
Files:
- Create:
apps/renderer/env.mjs - Create:
apps/renderer/app/api/auth/[...nextauth]/route.ts - Create:
apps/renderer/middleware.ts - Modify:
apps/renderer/app/layout.tsx - Modify:
apps/renderer/app/[user]/[project]/[[...slug]]/layout.tsx - Modify:
apps/renderer/app/[user]/[project]/[[...slug]]/page.tsx
Step 1: Write the failing test
Create apps/renderer/lib/__tests__/render-env.test.ts:
import { resolveRendererBaseUrl } from "../renderer-config";
test("renderer base url uses renderer domain when set", () => {
expect(
resolveRendererBaseUrl({
NEXT_PUBLIC_RENDERER_DOMAIN: "render.example.com",
NEXT_PUBLIC_ROOT_DOMAIN: "example.com",
}),
).toBe("https://render.example.com");
});
Step 2: Run test to verify it fails
Run: pnpm test -- apps/renderer/lib/__tests__/render-env.test.ts
Expected: FAIL with "Cannot find module '../renderer-config'" or missing env wiring.
Step 3: Write minimal implementation
Wire apps/renderer/env.mjs and update layout to use it for metadata and app config (similar to root app/layout.tsx).
Step 4: Run test to verify it passes
Run: pnpm test -- apps/renderer/lib/__tests__/render-env.test.ts
Expected: PASS.
Step 5: Commit
git add apps/renderer/env.mjs apps/renderer/app/api/auth apps/renderer/middleware.ts apps/renderer/app/layout.tsx apps/renderer/app/[user]/[project]/[[...slug]]/layout.tsx apps/renderer/app/[user]/[project]/[[...slug]]/page.tsx apps/renderer/lib/__tests__/render-env.test.ts
git commit -m "feat(renderer): wire env and layout"
Task 7: Optional cutover in existing app
Files:
- Modify:
app/[user]/[project]/[[...slug]]/page.tsx - Modify:
app/[user]/[project]/[[...slug]]/layout.tsx - Modify:
app/[user]/error.tsx - Modify:
app/api/raw/[username]/[projectName]/[branch]/[[...path]]/route.tsx - Modify:
app/api/domain/[domain]/[[...slug]]/route.tsx
Step 1: Write the failing test
Create e2e/renderer-redirect.spec.ts:
import { test, expect } from "@playwright/test";
test("public render routes redirect to renderer", async ({ page }) => {
const response = await page.goto("/user/project");
expect(response?.status()).toBe(302);
});
Step 2: Run test to verify it fails
Run: pnpm test:e2e -- e2e/renderer-redirect.spec.ts
Expected: FAIL with 200 or 404 until redirect is added.
Step 3: Write minimal implementation
Add redirects in the existing app to NEXT_PUBLIC_RENDERER_DOMAIN and leave the renderer app to own these routes.
Step 4: Run test to verify it passes
Run: pnpm test:e2e -- e2e/renderer-redirect.spec.ts
Expected: PASS with 302 and Location header to renderer domain.
Step 5: Commit
git add app/[user]/[project]/[[...slug]]/page.tsx app/[user]/[project]/[[...slug]]/layout.tsx app/[user]/error.tsx app/api/raw/[username]/[projectName]/[branch]/[[...path]]/route.tsx app/api/domain/[domain]/[[...slug]]/route.tsx e2e/renderer-redirect.spec.ts
git commit -m "chore: redirect public render routes to renderer"