diff --git a/apps/web/src/components/input/chip-input.tsx b/apps/web/src/components/input/chip-input.tsx index 196ba6428..45a59da70 100644 --- a/apps/web/src/components/input/chip-input.tsx +++ b/apps/web/src/components/input/chip-input.tsx @@ -59,8 +59,6 @@ function ChipDragOverlay({ activeChip }: ChipDragOverlayProps) { {activeChip ? : null} ); - if (typeof document === "undefined") return overlay; - return createPortal(overlay, document.body); } diff --git a/apps/web/src/components/input/rich-text-extensions.ts b/apps/web/src/components/input/rich-text-extensions.ts index d85265f34..f4ce6fc09 100644 --- a/apps/web/src/components/input/rich-text-extensions.ts +++ b/apps/web/src/components/input/rich-text-extensions.ts @@ -359,7 +359,6 @@ const parsedTablesMatchSource = (sourceTables: readonly string[], tables: readon }; export const hasUnsupportedTableMarkup = (html: string) => { - if (typeof DOMParser === "undefined") return false; const tableMarkers = tableMarkersFrom(html); if (tableMarkers.length === 0) return false; const document = new DOMParser().parseFromString(html, "text/html"); diff --git a/apps/web/src/features/auth/pages/login.tsx b/apps/web/src/features/auth/pages/login.tsx index 78a60cebb..b7ce6bf07 100644 --- a/apps/web/src/features/auth/pages/login.tsx +++ b/apps/web/src/features/auth/pages/login.tsx @@ -95,7 +95,6 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) { useEffect(() => { if (!("passkey" in providers)) return; - if (typeof window === "undefined") return; if (!("PublicKeyCredential" in window)) return; if (!PublicKeyCredential.isConditionalMediationAvailable) return; if (hasStartedConditionalPasskeyRef.current) return; diff --git a/apps/web/src/features/resume/builder/draft.ts b/apps/web/src/features/resume/builder/draft.ts index 32b3c9625..3d4e8dbd2 100644 --- a/apps/web/src/features/resume/builder/draft.ts +++ b/apps/web/src/features/resume/builder/draft.ts @@ -182,7 +182,6 @@ function getResumeQueryKey(id: string): QueryKey { } export function isEditableElementFocused(): boolean { - if (typeof document === "undefined") return false; const element = document.activeElement as HTMLElement | null; if (!element) return false; return ( @@ -211,7 +210,7 @@ function applyDeferredRemoteResume(id: string) { const resume = runtime.deferredRemoteResume; runtime.deferredRemoteResume = undefined; - if (runtime.deferredFocusHandler && typeof document !== "undefined") { + if (runtime.deferredFocusHandler) { document.removeEventListener("focusout", runtime.deferredFocusHandler, true); runtime.deferredFocusHandler = undefined; } @@ -228,7 +227,7 @@ function deferRemoteResumeUntilBlur(id: string, resume: Resume) { const runtime = getRuntime(id); runtime.deferredRemoteResume = resume; - if (runtime.deferredFocusHandler || typeof document === "undefined") return; + if (runtime.deferredFocusHandler) return; const handler = () => { // Let focus settle (e.g. tabbing between fields) before deciding editing has ended. @@ -337,17 +336,15 @@ function createRuntime(): Runtime { sessionId: crypto.randomUUID(), }; - if (typeof window !== "undefined") { - runtime.beforeUnloadHandler = () => runtime.syncResume.flush(); - window.addEventListener("beforeunload", runtime.beforeUnloadHandler); - // Changes made offline are sent as soon as the connection comes back. - runtime.onlineHandler = () => { - const current = useResumeStore.getState().resume; - if (!runtime.hasPendingLocalChanges || !current) return; - queueResumeSave(current); - }; - window.addEventListener("online", runtime.onlineHandler); - } + runtime.beforeUnloadHandler = () => runtime.syncResume.flush(); + window.addEventListener("beforeunload", runtime.beforeUnloadHandler); + // Changes made offline are sent as soon as the connection comes back. + runtime.onlineHandler = () => { + const current = useResumeStore.getState().resume; + if (!runtime.hasPendingLocalChanges || !current) return; + queueResumeSave(current); + }; + window.addEventListener("online", runtime.onlineHandler); return runtime; } @@ -376,15 +373,15 @@ function cleanupRuntime(id: string) { runtime.syncResume.flush(); runtime.abortController.abort(); - if (runtime.beforeUnloadHandler && typeof window !== "undefined") { + if (runtime.beforeUnloadHandler) { window.removeEventListener("beforeunload", runtime.beforeUnloadHandler); } - if (runtime.onlineHandler && typeof window !== "undefined") { + if (runtime.onlineHandler) { window.removeEventListener("online", runtime.onlineHandler); } - if (runtime.deferredFocusHandler && typeof document !== "undefined") { + if (runtime.deferredFocusHandler) { document.removeEventListener("focusout", runtime.deferredFocusHandler, true); } diff --git a/apps/web/src/features/resume/editor/write/picture-settings.tsx b/apps/web/src/features/resume/editor/write/picture-settings.tsx index 236800382..5df9f4ea6 100644 --- a/apps/web/src/features/resume/editor/write/picture-settings.tsx +++ b/apps/web/src/features/resume/editor/write/picture-settings.tsx @@ -489,7 +489,7 @@ type CropState = { /** Every photo option: upload with crop, address, show or hide, delete, fit, size, rotation, shape, border and shadow. */ export function PictureSettings() { const fileInputRef = useRef(null); - const appOrigin = typeof window === "undefined" ? "" : window.location.origin; + const appOrigin = window.location.origin; const [cropState, setCropState] = useState(null); const [crop, setCrop] = useState({ x: 0, y: 0 }); diff --git a/apps/web/src/features/resume/preview/preview.shared.utils.ts b/apps/web/src/features/resume/preview/preview.shared.utils.ts index bf7df3686..ad7139e82 100644 --- a/apps/web/src/features/resume/preview/preview.shared.utils.ts +++ b/apps/web/src/features/resume/preview/preview.shared.utils.ts @@ -15,7 +15,7 @@ export const DEFAULT_PDF_PAGE_SIZE: PreviewPageSize = { }; export const getPreviewCanvasScale = (width: number, height: number) => { - const devicePixelRatio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1; + const devicePixelRatio = window.devicePixelRatio || 1; const desiredScale = Math.max(PDF_PAGE_RENDER_SCALE, devicePixelRatio); const desiredPixels = width * height * desiredScale * desiredScale; diff --git a/apps/web/src/features/resume/preview/resume-accessible-text.tsx b/apps/web/src/features/resume/preview/resume-accessible-text.tsx index 627d82caa..5ba71338f 100644 --- a/apps/web/src/features/resume/preview/resume-accessible-text.tsx +++ b/apps/web/src/features/resume/preview/resume-accessible-text.tsx @@ -122,7 +122,6 @@ function renderRichTextNode(node: ChildNode, key: string): ReactNode { function RichText({ html }: { html: string }) { if (!html.trim()) return null; - if (typeof DOMParser === "undefined") return stripHtml(html); const body = new DOMParser().parseFromString(html, "text/html").body; return Array.from(body.childNodes).map((node, index) => renderRichTextNode(node, `rich-text-${index}`)); @@ -130,7 +129,6 @@ function RichText({ html }: { html: string }) { function hasRenderableRichText(html: string): boolean { if (!html.trim()) return false; - if (typeof DOMParser === "undefined") return stripHtml(html).trim().length > 0; const body = new DOMParser().parseFromString(html, "text/html").body; const getText = (node: ChildNode): string => { diff --git a/apps/web/src/features/settings/account/profile.tsx b/apps/web/src/features/settings/account/profile.tsx index da9a04875..3461f3663 100644 --- a/apps/web/src/features/settings/account/profile.tsx +++ b/apps/web/src/features/settings/account/profile.tsx @@ -128,7 +128,7 @@ export function ProfileSection({ session }: ProfileSectionProps) { const context = useRouteContext({ strict: false }); const smtpEnabled = context.flags?.smtpEnabled ?? false; const user = session.user; - const host = typeof window === "undefined" ? "" : window.location.host; + const host = window.location.host; const updateUser = async (patch: Parameters[0]) => { const { error } = await authClient.updateUser(patch); diff --git a/apps/web/src/features/settings/ai/mcp.tsx b/apps/web/src/features/settings/ai/mcp.tsx index 94067419c..bf61f3c42 100644 --- a/apps/web/src/features/settings/ai/mcp.tsx +++ b/apps/web/src/features/settings/ai/mcp.tsx @@ -5,7 +5,7 @@ import { Button } from "@reactive-resume/ui/components/button"; import { SettingsSection } from "../section"; export function McpSection() { - const address = typeof window === "undefined" ? "/mcp" : `${window.location.origin}/mcp`; + const address = `${window.location.origin}/mcp`; const [copied, setCopied] = useState(false); const [, copy] = useCopyToClipboard(); diff --git a/apps/web/src/libs/auth/client.ts b/apps/web/src/libs/auth/client.ts index eb8241bd0..87d954d75 100644 --- a/apps/web/src/libs/auth/client.ts +++ b/apps/web/src/libs/auth/client.ts @@ -18,16 +18,14 @@ export const authClient = createAuthClient({ twoFactorClient({ onTwoFactorRedirect() { // Redirect to 2FA verification page - if (typeof window !== "undefined") { - const { callbackURL, reauthenticate } = authSearchSchema.parse({ - reauthenticate: new URLSearchParams(window.location.search).get("reauthenticate") === "true", - callbackURL: new URLSearchParams(window.location.search).get("callbackURL"), - }); - const search = callbackURL - ? `?${new URLSearchParams({ callbackURL, ...(reauthenticate ? { reauthenticate: "true" } : {}) })}` - : ""; - window.location.href = `/auth/verify-2fa${search}`; - } + const { callbackURL, reauthenticate } = authSearchSchema.parse({ + reauthenticate: new URLSearchParams(window.location.search).get("reauthenticate") === "true", + callbackURL: new URLSearchParams(window.location.search).get("callbackURL"), + }); + const search = callbackURL + ? `?${new URLSearchParams({ callbackURL, ...(reauthenticate ? { reauthenticate: "true" } : {}) })}` + : ""; + window.location.href = `/auth/verify-2fa${search}`; }, }), oauthProviderClient(), diff --git a/apps/web/src/libs/orpc/client.ts b/apps/web/src/libs/orpc/client.ts index 6354346e4..b4efb581b 100644 --- a/apps/web/src/libs/orpc/client.ts +++ b/apps/web/src/libs/orpc/client.ts @@ -6,14 +6,11 @@ import { BatchLinkPlugin } from "@orpc/client/plugins"; import { createTanstackQueryUtils } from "@orpc/tanstack-query"; import { rpcFetch } from "./fetch"; -const getRpcUrl = () => { - if (typeof window === "undefined") return "http://localhost:3000/api/rpc"; - return `${window.location.origin}/api/rpc`; -}; +const getRpcUrl = () => `${window.location.origin}/api/rpc`; export const client: RouterClient = createORPCClient( new RPCLink({ - url: getRpcUrl(), + url: getRpcUrl, fetch: rpcFetch, plugins: [ new BatchLinkPlugin({ @@ -32,7 +29,7 @@ export const client: RouterClient = createORPCClient( export const streamClient: RouterClient = createORPCClient( new RPCLink({ - url: getRpcUrl(), + url: getRpcUrl, fetch: rpcFetch, interceptors: [ onError((error) => { diff --git a/apps/web/src/libs/seo.test.ts b/apps/web/src/libs/seo.test.ts index 5e0fdb8c5..0f0d8defb 100644 --- a/apps/web/src/libs/seo.test.ts +++ b/apps/web/src/libs/seo.test.ts @@ -7,10 +7,6 @@ import { } from "./seo"; describe("getCanonicalRootUrl", () => { - it("uses the production root when no origin is available", () => { - expect(getCanonicalRootUrl()).toBe("https://rxresu.me/"); - }); - it("normalizes an app origin to the root URL", () => { expect(getCanonicalRootUrl("http://localhost:3000")).toBe("http://localhost:3000/"); expect(getCanonicalRootUrl("https://rxresu.me/")).toBe("https://rxresu.me/"); diff --git a/apps/web/src/libs/seo.ts b/apps/web/src/libs/seo.ts index c52f563ec..20e52cf69 100644 --- a/apps/web/src/libs/seo.ts +++ b/apps/web/src/libs/seo.ts @@ -1,19 +1,9 @@ -const productionRootUrl = "https://rxresu.me/"; const appName = "Reactive Resume"; const repositoryUrl = "https://github.com/reactive-resume/reactive-resume"; type JsonLd = Record; -export const getCanonicalRootUrl = (origin?: string): string => { - if (!origin) return productionRootUrl; - - const url = new URL(origin); - url.pathname = "/"; - url.search = ""; - url.hash = ""; - - return url.toString(); -}; +export const getCanonicalRootUrl = (origin: string): string => new URL("/", origin).href; export const createNoindexFollowMeta = () => ({ name: "robots", content: "noindex, follow" }); diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 1de1789cf..de9ad48d2 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -21,8 +21,4 @@ document.querySelectorAll(serverSeoSelectors.join(",")).forEach((element) => { element.remove(); }); -if (!rootElement.innerHTML) { - const root = ReactDOM.createRoot(rootElement); - - root.render(); -} +ReactDOM.createRoot(rootElement).render(); diff --git a/apps/web/src/routes/$username/$slug.tsx b/apps/web/src/routes/$username/$slug.tsx index c6af9ab68..f775e9dc0 100644 --- a/apps/web/src/routes/$username/$slug.tsx +++ b/apps/web/src/routes/$username/$slug.tsx @@ -9,7 +9,6 @@ import { createNoindexFollowMeta, createResumeSocialMeta, getCanonicalRootUrl } type LoaderData = Omit & { data: ResumeData }; export const Route = createFileRoute("/$username/$slug")({ - ssr: "data-only", component: lazyRouteComponent(() => import("@/features/resume/public/public-resume"), "PublicResumeRoute"), notFoundComponent: lazyRouteComponent( () => import("@/features/resume/public/public-resume"), @@ -38,7 +37,7 @@ export const Route = createFileRoute("/$username/$slug")({ const social = getResumeSocialMeta(resume.data, resume.name || "Resume"); - const base = getCanonicalRootUrl(typeof window === "undefined" ? undefined : window.location.origin); + const base = getCanonicalRootUrl(window.location.origin); const canonicalUrl = `${base}${params.username}/${params.slug}`; const imageUrl = `${base}opengraph/banner.jpg`; diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 81b38e381..72829342c 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -48,7 +48,7 @@ const iconContextValue: IconProps = { size: 16, weight: "regular" }; export const Route = createRootRouteWithContext()({ component: RootComponent, head: () => { - const appUrl = typeof window !== "undefined" ? window.location.origin : "https://rxresu.me"; + const appUrl = window.location.origin; return { links: [ diff --git a/apps/web/src/routes/_home/-index.test.ts b/apps/web/src/routes/_home/-index.test.ts index 4b0b9abb9..c6b4815dc 100644 --- a/apps/web/src/routes/_home/-index.test.ts +++ b/apps/web/src/routes/_home/-index.test.ts @@ -1,3 +1,5 @@ +// @vitest-environment happy-dom + import { describe, expect, it, vi } from "vitest"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; diff --git a/apps/web/src/routes/_home/ats-checker.tsx b/apps/web/src/routes/_home/ats-checker.tsx index 5b3531f9e..4974a9402 100644 --- a/apps/web/src/routes/_home/ats-checker.tsx +++ b/apps/web/src/routes/_home/ats-checker.tsx @@ -17,7 +17,7 @@ export const Route = createFileRoute("/_home/ats-checker")({ // Back from signing up with a checked file to import. validateSearch: z.object({ import: z.coerce.boolean().optional().catch(undefined) }), head: () => { - const origin = typeof window === "undefined" ? "https://rxresu.me" : window.location.origin; + const origin = window.location.origin; const canonicalUrl = new URL("/ats-checker", origin).toString(); const imageUrl = new URL("/opengraph/ats-checker.png", origin).toString(); diff --git a/apps/web/src/routes/_home/index.tsx b/apps/web/src/routes/_home/index.tsx index 08cafbbda..66fffac02 100644 --- a/apps/web/src/routes/_home/index.tsx +++ b/apps/web/src/routes/_home/index.tsx @@ -1,7 +1,6 @@ import { ORPCError } from "@orpc/client"; -import { ClientOnly, createFileRoute, lazyRouteComponent, redirect } from "@tanstack/react-router"; +import { createFileRoute, lazyRouteComponent, redirect } from "@tanstack/react-router"; import { getResumeSocialMeta } from "@reactive-resume/resume/social-meta"; -import { LoadingScreen } from "@/components/layout/loading-screen"; import { NotFoundScreen } from "@/components/layout/not-found-screen"; import { Homepage } from "@/features/homepage/page"; import { orpc } from "@/libs/orpc/client"; @@ -50,7 +49,7 @@ export const Route = createFileRoute("/_home/")({ links: [{ rel: "canonical", href: canonicalUrl }], }; } - const appUrl = typeof window !== "undefined" ? window.location.origin : "https://rxresu.me"; + const appUrl = window.location.origin; const canonicalUrl = getCanonicalRootUrl(appUrl); return { @@ -70,11 +69,7 @@ function RouteComponent() { ); if (root.status === "public") { - return ( - }> - - - ); + return ; } return ; diff --git a/apps/web/src/routes/auth/consent.tsx b/apps/web/src/routes/auth/consent.tsx index aa3f12723..774c953d7 100644 --- a/apps/web/src/routes/auth/consent.tsx +++ b/apps/web/src/routes/auth/consent.tsx @@ -2,13 +2,12 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; import { OAuthConsentPage } from "@/features/auth/pages/consent"; export const Route = createFileRoute("/auth/consent")({ - ssr: false, - beforeLoad: ({ context, location }) => { + beforeLoad: ({ context }) => { if (!context.session) { throw redirect({ to: "/auth/login", search: { - callbackURL: `/api/auth/oauth${typeof window === "undefined" ? location.searchStr : window.location.search}`, + callbackURL: `/api/auth/oauth${window.location.search}`, }, }); }