forked from akai/readest
381eed21cc
The Tauri build uses `output: 'export'`, so the dynamic `/runtime-config.js` route handler is never emitted. Requesting it returns the SPA fallback HTML and crashes with `Unexpected token '<'`. Gate the script tag on `NEXT_PUBLIC_APP_PLATFORM === 'web'`; Tauri consumers already fall back to the `NEXT_PUBLIC_*` envs baked in at build time. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
157 lines
5.6 KiB
TypeScript
157 lines
5.6 KiB
TypeScript
import * as React from 'react';
|
|
import type { Metadata, Viewport } from 'next';
|
|
import Script from 'next/script';
|
|
import { ViewTransitions } from 'next-view-transitions';
|
|
import { EnvProvider } from '@/context/EnvContext';
|
|
import Providers from '@/components/Providers';
|
|
|
|
import '../styles/globals.css';
|
|
|
|
const url = 'https://web.readest.com/';
|
|
const title = 'Readest — Where You Read, Digest and Get Insight';
|
|
const description =
|
|
'Discover Readest, the ultimate online ebook reader for immersive and organized reading. ' +
|
|
'Enjoy seamless access to your digital library, powerful tools for highlighting, bookmarking, ' +
|
|
'and note-taking, and support for multiple book views. ' +
|
|
'Perfect for deep reading, analysis, and understanding. Explore now!';
|
|
const previewImage = 'https://cdn.readest.com/images/open_graph_preview_read_now.png';
|
|
|
|
export const metadata: Metadata = {
|
|
metadataBase: new URL(url),
|
|
title: {
|
|
default: title,
|
|
template: '%s | Readest',
|
|
},
|
|
description,
|
|
generator: 'Next.js',
|
|
manifest: '/manifest.json',
|
|
keywords: ['epub', 'pdf', 'ebook', 'reader', 'readest', 'pwa'],
|
|
authors: [
|
|
{
|
|
name: 'readest',
|
|
url: 'https://github.com/readest/readest',
|
|
},
|
|
],
|
|
icons: {
|
|
icon: [{ url: '/icon.png' }, { url: '/favicon.ico' }],
|
|
apple: [{ url: '/apple-touch-icon.png', sizes: '180x180' }],
|
|
},
|
|
appleWebApp: {
|
|
capable: true,
|
|
title: 'Readest',
|
|
statusBarStyle: 'default',
|
|
},
|
|
openGraph: {
|
|
type: 'website',
|
|
url,
|
|
title,
|
|
description,
|
|
images: [previewImage],
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title,
|
|
description,
|
|
images: [previewImage],
|
|
},
|
|
other: {
|
|
'apple-mobile-web-app-capable': 'yes',
|
|
'twitter:domain': 'web.readest.com',
|
|
'twitter:url': url,
|
|
},
|
|
};
|
|
|
|
export const viewport: Viewport = {
|
|
width: 'device-width',
|
|
initialScale: 1,
|
|
minimumScale: 1,
|
|
maximumScale: 1,
|
|
userScalable: false,
|
|
viewportFit: 'cover',
|
|
// `interactive-widget=resizes-content` is appended client-side on
|
|
// Android only — see Providers.tsx. Other browsers warn about the
|
|
// unrecognized key on every page load, so we keep it out of SSR.
|
|
};
|
|
|
|
// In Tauri mobile dev the page origin doesn't match the dev server, so
|
|
// Next.js's `getSocketUrl` builds an unreachable HMR URL (see
|
|
// `next/dist/client/dev/hot-reloader/get-socket-url.js`):
|
|
// - iOS sim: page at `tauri://localhost` → `wss://localhost/_next/...`
|
|
// (no port, non-http scheme falls through to `wss:`)
|
|
// - Android emul.: page at `http://tauri.localhost` → `ws://tauri.localhost/_next/...`
|
|
// (`tauri.localhost` is intercepted by Tauri's asset handler, but
|
|
// WebSocket frames bypass the interceptor and the dev server is on the
|
|
// host machine, reachable from the emulator as `10.0.2.2`)
|
|
// Rewrite the WebSocket constructor before the HMR client runs.
|
|
// When `--host <ip>` is passed, tauri-cli exports `TAURI_DEV_HOST=<ip>`
|
|
// before invoking `beforeDevCommand`, so we forward that as `devHost` and
|
|
// use it for the rewrite (the dev server must also bind to the same address
|
|
// — typically `next dev -H 0.0.0.0`).
|
|
function patchTauriHmrWebSocket(devHost?: string) {
|
|
const isIosTauriProxy = location.protocol === 'tauri:' && location.hostname === 'localhost';
|
|
const isAndroidTauriProxy =
|
|
location.protocol === 'http:' && location.hostname === 'tauri.localhost';
|
|
if (!isIosTauriProxy && !isAndroidTauriProxy) return;
|
|
|
|
// Priority: explicit --host > platform default loopback alias.
|
|
// iOS Simulator can reach the host's localhost directly.
|
|
// Android emulator reaches the host machine via 10.0.2.2.
|
|
const hmrHost = devHost
|
|
? `${devHost}:3000`
|
|
: isIosTauriProxy
|
|
? 'localhost:3000'
|
|
: '10.0.2.2:3000';
|
|
const brokenHostPattern = /^wss?:\/\/(localhost|tauri\.localhost)(?=\/_next\/)/;
|
|
|
|
const OriginalWebSocket = window.WebSocket;
|
|
class PatchedWebSocket extends OriginalWebSocket {
|
|
constructor(url: string | URL, protocols?: string | string[]) {
|
|
const urlStr = url instanceof URL ? url.href : url;
|
|
const rewritten =
|
|
typeof urlStr === 'string' && brokenHostPattern.test(urlStr)
|
|
? urlStr.replace(brokenHostPattern, `ws://${hmrHost}`)
|
|
: url;
|
|
super(rewritten, protocols);
|
|
}
|
|
}
|
|
window.WebSocket = PatchedWebSocket;
|
|
}
|
|
|
|
const shouldInjectDevHmrPatch =
|
|
process.env['NODE_ENV'] === 'development' && process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'tauri';
|
|
const devHmrPatchScript = `(${patchTauriHmrWebSocket.toString()})(${JSON.stringify(
|
|
process.env['TAURI_DEV_HOST'],
|
|
)});`;
|
|
|
|
// `/runtime-config.js` is a dynamic route handler that only exists in the
|
|
// web/Docker build. The Tauri build is statically exported (`output:
|
|
// 'export'`), so the file isn't emitted — the request would return the SPA
|
|
// fallback HTML and crash with `Unexpected token '<'`. All runtime-config
|
|
// consumers fall back to `NEXT_PUBLIC_*` envs baked at build time on Tauri.
|
|
const shouldInjectRuntimeConfig = process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web';
|
|
|
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html
|
|
lang='en'
|
|
className={process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'tauri' ? 'edge-to-edge' : ''}
|
|
>
|
|
<head>
|
|
{shouldInjectRuntimeConfig ? (
|
|
<Script src='/runtime-config.js' strategy='beforeInteractive' />
|
|
) : null}
|
|
{shouldInjectDevHmrPatch ? (
|
|
<script dangerouslySetInnerHTML={{ __html: devHmrPatchScript }} />
|
|
) : null}
|
|
</head>
|
|
<body>
|
|
<ViewTransitions>
|
|
<EnvProvider>
|
|
<Providers>{children}</Providers>
|
|
</EnvProvider>
|
|
</ViewTransitions>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|