diff --git a/packages/app/next.config.ts b/packages/app/next.config.ts
index 8942a52..d1e332f 100644
--- a/packages/app/next.config.ts
+++ b/packages/app/next.config.ts
@@ -1,6 +1,12 @@
import type { NextConfig } from 'vinext'
+import { STATIC_EXPORT_DEPLOYMENT_ID } from './src/lib/static-export-rsc-transport'
+
+const isPagesDemo = process.env.VITE_YEOLLIN_DEMO === 'true'
+
const nextConfig: NextConfig = {
+ assetPrefix: isPagesDemo ? process.env.VITE_YEOLLIN_BASE_PATH : undefined,
+ deploymentId: isPagesDemo ? STATIC_EXPORT_DEPLOYMENT_ID : undefined,
output: 'export',
}
diff --git a/packages/app/src/app/layout.tsx b/packages/app/src/app/layout.tsx
index ac684fd..4556bb1 100644
--- a/packages/app/src/app/layout.tsx
+++ b/packages/app/src/app/layout.tsx
@@ -3,6 +3,22 @@ import { resetCss } from '@devup-ui/reset-css'
import type { Metadata } from 'next'
import { Providers } from '@/components/providers'
+import {
+ installStaticExportRscTransport,
+ STATIC_EXPORT_DEPLOYMENT_ID,
+} from '@/lib/static-export-rsc-transport'
+
+function escapeScriptClosingTag(script: string): string {
+ return script.replace(/<\/script/giu, '\\u003c/script')
+}
+
+const staticExportRscTransport = escapeScriptClosingTag(
+ `(${installStaticExportRscTransport.toString()})(${JSON.stringify(STATIC_EXPORT_DEPLOYMENT_ID)}, ${JSON.stringify(process.env.VITE_YEOLLIN_BASE_PATH ?? '')})`,
+)
+
+const needsStaticExportRscTransport =
+ process.env.NODE_ENV === 'production' &&
+ process.env.VITE_YEOLLIN_DEMO === 'true'
export const metadata: Metadata = {
title: 'Yeollin CMS',
@@ -19,6 +35,11 @@ export default function RootLayout({
return (
+ {needsStaticExportRscTransport && (
+
+ )}
diff --git a/packages/app/src/lib/mock-api.ts b/packages/app/src/lib/mock-api.ts
index 8add8a4..295a308 100644
--- a/packages/app/src/lib/mock-api.ts
+++ b/packages/app/src/lib/mock-api.ts
@@ -658,59 +658,6 @@ function mockResponse(url: URL, method: string, init?: RequestInit): Response {
)
}
-function staticRscRequestUrl(
- input: RequestInfo | URL,
- init: RequestInit | undefined,
- url: URL,
-): URL | null {
- if (
- url.origin !== window.location.origin ||
- (DEMO_BASE_PATH !== '' &&
- url.pathname !== DEMO_BASE_PATH &&
- !url.pathname.startsWith(`${DEMO_BASE_PATH}/`))
- ) {
- return null
- }
-
- const headers = new Headers(input instanceof Request ? input.headers : {})
- new Headers(init?.headers).forEach((value, name) => headers.set(name, value))
- if (
- headers.get('RSC') !== '1' &&
- !headers.get('Accept')?.includes('text/x-component')
- ) {
- return null
- }
-
- const staticUrl = new URL(url)
- if (!staticUrl.pathname.endsWith('.rsc')) {
- staticUrl.pathname = staticUrl.pathname.endsWith('/')
- ? `${staticUrl.pathname}index.rsc`
- : `${staticUrl.pathname}.rsc`
- }
- return staticUrl
-}
-
-function normalizeStaticRscResponse(response: Response): Response {
- if (
- !response.ok ||
- response.headers.get('Content-Type')?.startsWith('text/x-component')
- ) {
- return response
- }
-
- const headers = new Headers(response.headers)
- headers.set('Content-Type', 'text/x-component')
-
- // GitHub Pages serves unknown extensions as application/octet-stream.
- // Shadowing the immutable header collection preserves the fetched response's
- // URL and body, which vinext uses when deciding whether to navigate in-app.
- Object.defineProperty(response, 'headers', {
- configurable: true,
- value: headers,
- })
- return response
-}
-
/** Installs the browser-only API simulator used by the public GitHub Pages demo. */
export function installMockApi(): void {
if (installed || typeof window === 'undefined') return
@@ -729,12 +676,7 @@ export function installMockApi(): void {
) {
return mockResponse(url, method, init)
}
- const staticRscUrl = staticRscRequestUrl(input, init, url)
- if (staticRscUrl === null) return nativeFetch(input, init)
-
- const staticInput =
- input instanceof Request ? new Request(staticRscUrl, input) : staticRscUrl
- return normalizeStaticRscResponse(await nativeFetch(staticInput, init))
+ return nativeFetch(input, init)
}
}
diff --git a/packages/app/src/lib/static-export-rsc-transport.ts b/packages/app/src/lib/static-export-rsc-transport.ts
new file mode 100644
index 0000000..e30a8d1
--- /dev/null
+++ b/packages/app/src/lib/static-export-rsc-transport.ts
@@ -0,0 +1,65 @@
+export const STATIC_EXPORT_DEPLOYMENT_ID = 'yeollin-cms-pages-demo'
+
+/**
+ * Adapts vinext RSC requests to artifacts emitted by `output: 'export'` on a
+ * plain static host such as GitHub Pages.
+ */
+export function installStaticExportRscTransport(
+ deploymentId: string,
+ basePath: string,
+): void {
+ const nativeFetch = globalThis.fetch.bind(globalThis)
+ const normalizedBasePath = basePath.replace(/\/$/u, '')
+
+ globalThis.fetch = async (input, init) => {
+ const request = new Request(input, init)
+ if (request.method !== 'GET' || request.headers.get('RSC') !== '1') {
+ return nativeFetch(input, init)
+ }
+
+ const visibleUrl = new URL(request.url)
+ if (
+ visibleUrl.origin !== globalThis.location.origin ||
+ (normalizedBasePath !== '' &&
+ visibleUrl.pathname !== normalizedBasePath &&
+ !visibleUrl.pathname.startsWith(`${normalizedBasePath}/`))
+ ) {
+ return nativeFetch(input, init)
+ }
+
+ const artifactUrl = new URL(visibleUrl)
+ if (!artifactUrl.pathname.endsWith('.rsc')) {
+ const rootPath =
+ normalizedBasePath === '' ? '/' : `${normalizedBasePath}/`
+ artifactUrl.pathname =
+ artifactUrl.pathname === rootPath
+ ? `${rootPath}index.rsc`
+ : `${artifactUrl.pathname.replace(/\/$/u, '')}.rsc`
+ }
+ artifactUrl.searchParams.delete('_rsc')
+
+ const artifactResponse = await nativeFetch(artifactUrl, {
+ credentials: request.credentials,
+ headers: request.headers,
+ signal: request.signal,
+ })
+ if (!artifactResponse.ok) {
+ await artifactResponse.body?.cancel()
+ return nativeFetch(input, init)
+ }
+
+ const headers = new Headers(artifactResponse.headers)
+ headers.set('Content-Type', 'text/x-component')
+ headers.set('X-Vinext-RSC-Compatibility-Id', deploymentId)
+
+ const response = new Response(artifactResponse.body, {
+ headers,
+ status: artifactResponse.status,
+ statusText: artifactResponse.statusText,
+ })
+
+ // The `.rsc` artifact is a transport detail, not a redirect destination.
+ Object.defineProperty(response, 'url', { value: visibleUrl.href })
+ return response
+ }
+}