Skip to content

Fix SSR SEO blank output and hydration errors - #779

Open
augustmood wants to merge 5 commits into
deltaDAO:mainfrom
augustmood:fix/ssr-seo-hydration
Open

augustmood wants to merge 5 commits into
deltaDAO:mainfrom
augustmood:fix/ssr-seo-hydration

Conversation

@augustmood

@augustmood augustmood commented Sep 10, 2025 •

Copy link
Copy Markdown

fix(ssr/seo): Stabilize SSR rendering and canonical URIs; inject SSR‑safe URIs for dynamic routes

Primary goal: fix SEO so “View Page Source” shows the complete server‑rendered SEO markup. This PR restores stable SSR output, corrects canonical generation, and removes browser‑only dependencies during SSR. It also consolidates publish step canonicals and injects SSR‑stable URIs for dynamic routes.

Summary

  • Restore server‑side rendering of the SEO subtree by initializing the URQL client synchronously and ensuring the Provider does not gate server output.
  • Generate canonical links from SSR‑safe URIs (router.pathname or server‑injected ssrUri), never from asPath/query strings.
  • For dynamic routes, inject stable paths on the server (asset) and unify publish step canonicals to /publish.
  • Remove direct window usage in render paths; guard browser‑only code and stabilize components to prevent hydration mismatches.

Context & Root Cause

  • URQL client was previously constructed in useEffect and rendering was gated behind client ? <Provider> : <></>. During SSR, useEffect does not run, resulting in an empty render that stripped all SEO from the server output.
  • The SEO component accessed window.location to decide robots/twitter meta, producing different <Head> markup on server vs client and triggering hydration mismatches.
  • <html lang> lived inside <Head> instead of _document.tsx, contributing to divergent markup and hydration warnings.

Changes

  • URQL/Provider
    • Initialize the URQL client at module load (SSR‑safe).
    • Provider renders with the initialized client; if unavailable, it still renders {children} to avoid an empty SSR tree.
    • Subgraph querying uses a lazily created query client in @utils/subgraph to decouple from Provider at SSR time.
  • SEO/Head/_document
    • Remove all window access during render; derive environment from siteUrl and make Head deterministic on the server.
    • Move <Html lang="en"> to pages/_document.tsx.
  • Canonical
    • Use router.pathname instead of route/asPath and build canonical from props for static content.
    • Inject server‑stable canonical paths via getServerSideProps for dynamic routes (asset) and unify publish steps to /publish.
  • Hydration stability
    • Header wallet/network menu render client‑only; Tooltip only sets appendTo in the browser.
    • Search filter/sort and profile history parse URL params in an SSR‑friendly way.
    • Faucet: restore page SSR; use placeholders for address/network until mount to avoid wagmi hydration mismatch; keep canonical from router.pathname.
    • Publish navigation: show checkmarks only for past steps; enable validateOnMount so initial errors are computed on first render.

Verification & Impact

  • View page source and confirm canonical tags:
    • Asset details canonical is the exact path /asset/${did} (no "?" query part), e.g. /asset/did:op:abc123…: src/pages/asset/[did]/index.tsx:1.
    • Any publish step canonical is /publish: src/pages/publish/[step].tsx:1.
    • Markdown/privacy/cookies pages canonical matches their respective slugs.
  • No hydration warnings for wallet/network components during first render.
  • Search page filter/sort reads URL params correctly under SSR and in the browser.
  • robots meta:
    • Production host portal.pontus-x.eu allows indexing.
    • Non‑prod shows noindex,nofollow.
  • Cookies are written with secure and without direct window/location during SSR.
  • Most importantly, the server renders complete SEO (<title>, <meta>, Open Graph, Twitter) so “View Page Source” reflects the full content.

Scope and Risk

  • No breaking changes. Existing routes/APIs and user flows are unchanged.
  • No changes to data‑fetching strategy; CSR behavior remains the same.
  • Work is limited to SSR, SEO, and hydration stability.

Related Commits

  • 2a70075 — fix(ssr/seo): stabilize SSR rendering, canonical URIs, and hydration
  • 5236d78 — fix(seo/ssr): correct canonical for dynamic routes; SSR‑inject URIs; unify publish canonical

@augustmood
augustmood requested a review from a team as a code owner September 10, 2025 00:14
@vercel

vercel Bot commented Sep 10, 2025

Copy link
Copy Markdown

@augustmood is attempting to deploy a commit to the deltaDAO Team on Vercel.

A member of the Team first needs to authorize it.

@augustmood
augustmood marked this pull request as draft September 10, 2025 01:33
@moritzkirstein

Copy link
Copy Markdown
Contributor

Hey @augustmood,

thank you very much for your work here! I am not sure if this PR is ready for review / merge, as it is still in draft. Just in case you missed that, please mark as ready for review and we'll get back with feedback.

@augustmood

Copy link
Copy Markdown
Author

Hey @augustmood,

thank you very much for your work here! I am not sure if this PR is ready for review / merge, as it is still in draft. Just in case you missed that, please mark as ready for review and we'll get back with feedback.

Hey @moritzkirstein,

Sorry for the delayed response. I initially thought the PR was ready, but I identified some issues and bugs, so I have marked it as a draft. I will address these problems and mark it as ready for review once everything is resolved. I expect it to be ready by next week.

- Replace `router.route` with `router.pathname` across pages to use SSR‑safe
  template URIs (index, verify, 404, search, profile, bookmarks,
  asset/[did], publish/[step], privacy/[slug], cookies/[slug], [slug]).
- Make URL param parsing SSR‑safe:
  - Search Filter/Sort build `search` from `window.location.search` on client,
    with a `router.asPath` fallback on server, instead of direct `location.search`.
  - Profile/History defaultTab parsed from `URLSearchParams` over SSR‑safe `search`.
- Prevent header hydration mismatches:
  - Render `Wallet` and `NetworkMenu` client‑only (`ssr: false`) to avoid
    wagmi state differences between server and client.
- Harden browser‑only access and cookies utility:
  - Guard all `window`/`location` access with `typeof window !== 'undefined'`.
  - Use `secure` cookie flag and avoid direct `location` reads in `cookies.ts`.
- UserPreferences hydration for SSR stability:
  - Initialize `allowExternalContent` and `chainIds` with SSR‑stable defaults,
    then hydrate from cookies after mount.
- Tooltip stability:
  - Only set `appendTo=document.body` in the browser to keep SSR/CSR markup aligned.

Notes
- No changes to data‑fetching strategy; CSR behavior remains unchanged.
- Canonical tag generation now uses SSR‑stable URIs via `pathname` to avoid
  querystring leaks and hydration issues.

Signed-off-by: Binming Li <binming.li@outlook.com>
@augustmood
augustmood force-pushed the fix/ssr-seo-hydration branch from 5203685 to 2a70075 Compare September 24, 2025 18:07
augustmood and others added 2 commits September 24, 2025 14:04
…unify publish canonical

- SSG pages: build canonical from props.slug (no router)
  - privacy/[slug], cookies/[slug], [slug] now use `uri` derived from `page.slug`
  - Ensures SSR/CSR identical output; precise canonical without query

- Asset details (asset/[did]): add GSSP to SSR-inject real path
  - `getServerSideProps` returns `{ ssrUri: `/asset/${did}`, did }`
  - Page uses `ssrUri` for `<Seo>` canonical (no query), `did` for providers
  - Data fetching remains CSR; only canonical path is SSR-stable

- Publish steps (publish/[step]): add GSSP and unify canonical
  - `getServerSideProps` returns `{ ssrUri: '/publish' }` for all steps
  - Consolidates SEO weight on the entry page; avoids step-level duplicates

Notes
- Canonical URIs no longer include querystrings
- View Page Source now shows precise canonical for asset pages (and unified for publish)

Signed-off-by: Binming Li <binming.li@outlook.com>
@augustmood
augustmood marked this pull request as ready for review September 24, 2025 21:08
@augustmood

Copy link
Copy Markdown
Author

Hey @moritzkirstein,

Thanks for your patience! I’ve fixed the issues and the PR is now marked as ready for review. Please let me know if you have any feedback once you’ve had a chance to take a look.

- Restore SSR for Faucet page and keep canonical stable via router.pathname.
- Render the card on first paint; avoid hydration mismatch from wagmi state by
  rendering placeholders until mount:
  - Show "—" for address/network on SSR and the first client render.
  - Replace with actual values after mount; disable the button until mounted
    and an address is present.
- Ensures no hydration errors while preserving immediate UI render.

Files: src/pages/faucet.tsx, src/components/Faucet/index.tsx
Breaking changes: none

Signed-off-by: Binming Li <binming.li@outlook.com>
…ount

- Show "completed" checkmarks only for steps BEFORE the current step and only
  when that step has no validation errors.
- Enable Formik validateOnMount so initial errors are computed on first render.
- Prevents steps 2/3 from showing as checked on initial page load.

Files: src/components/Publish/Navigation/index.tsx, src/components/Publish/index.tsx
Breaking changes: none

Signed-off-by: Binming Li <binming.li@outlook.com>
@vercel

vercel Bot commented Sep 30, 2025 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
mvg-portal Ready Ready Preview Comment Sep 30, 2025 9:40am

@oceanByte

Copy link
Copy Markdown
Contributor

Hey @augustmood,

Thanks a lot for the updates, we really appreciate your contribution! 🙌

This PR is quite big, so it’ll take us a little time to go through it thoroughly. It’s definitely on our radar and we’ll get back to you with feedback as soon as possible. Thanks for your patience in the meantime!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes server-side rendering (SSR) issues that prevented SEO metadata from appearing in the HTML source, caused hydration mismatches, and generated incorrect canonical URLs. The changes ensure complete SEO markup is rendered server-side, stabilize component hydration, and establish consistent canonical URIs across all pages.

Changes:

  • Initialize URQL client at module load to enable SSR rendering instead of in useEffect
  • Replace window/location access in SEO and cookie utilities with SSR-safe alternatives
  • Standardize canonical URI generation using router.pathname for static pages and getServerSideProps for dynamic routes
  • Make Wallet and NetworkMenu client-only components, add mounted guards to Faucet, and compute URL parameters SSR-safely in Search and Profile
  • Move HTML lang attribute from Head to _document.tsx and add validateOnMount to Publish form

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/@context/UrqlProvider.tsx Initialize URQL client at module load for SSR availability
src/@utils/subgraph.ts Create separate query client decoupled from Provider
src/components/@shared/Page/Seo/index.tsx Derive environment from siteUrl instead of window.location
src/pages/_document.tsx Add Html lang="en" wrapper in _document
src/pages/asset/[did]/index.tsx Inject SSR-stable URI via getServerSideProps
src/pages/publish/[step].tsx Unify all publish step canonicals to /publish
src/pages/{verify,search,profile,index,404,bookmarks,faucet}.tsx Change to router.pathname for canonical
src/pages/{privacy,cookies}/[slug].tsx Construct canonical from slug prop
src/pages/[slug].tsx Use page.slug for canonical
src/components/Header/Menu.tsx Load Wallet and NetworkMenu client-only
src/components/@shared/atoms/Tooltip/index.tsx Guard appendTo with typeof window check
src/components/Search/{Filter,sort}.tsx Parse URL params SSR-safely
src/components/Faucet/index.tsx Add mounted state to prevent hydration mismatch
src/components/Profile/History/index.tsx Parse search params SSR-safely
src/components/Publish/Navigation/index.tsx Only show checkmarks for past steps
src/components/Publish/index.tsx Add validateOnMount to Formik
src/@utils/cookies.ts Check window availability before accessing location
src/@context/UserPreferences.tsx Initialize state SSR-safely and hydrate from cookies on mount

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +24
export async function getServerSideProps({
params
}: {
params: { step: string }
}): Promise<{ props: { ssrUri: string } }> {

Copilot AI Jan 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The params.step parameter is received but never used in the function. Since the canonical URI is unified to /publish for all steps, this parameter should either be used (if different steps need different handling) or the type should acknowledge it's intentionally unused. Consider removing it from the destructuring or adding a comment explaining why it's unified to /publish.

Suggested change
export async function getServerSideProps({
params
}: {
params: { step: string }
}): Promise<{ props: { ssrUri: string } }> {
export async function getServerSideProps(): Promise<{ props: { ssrUri: string } }> {

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +30
export async function getServerSideProps({
params
}: {
params: { step: string }
}): Promise<{ props: { ssrUri: string } }> {
return {
props: {
ssrUri: `/publish`
}
}
}

Copilot AI Jan 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getServerSideProps function should use Next.js's GetServerSideProps type instead of inline type annotations. Import GetServerSideProps from 'next' and type it as GetServerSideProps<{ ssrUri: string }> for better type safety and consistency with Next.js conventions.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +31
export async function getServerSideProps({
params
}: {
params: { did: string }
}): Promise<{ props: { ssrUri: string; did: string } }> {
const { did } = params
return {
props: {
ssrUri: `/asset/${did}`,
did
}
}
}

Copilot AI Jan 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getServerSideProps function should use Next.js's GetServerSideProps type instead of inline type annotations. Import GetServerSideProps from 'next' and type it as GetServerSideProps<{ ssrUri: string; did: string }> for better type safety and consistency with Next.js conventions.

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

This branch was successfully deployed

1 active deployment
Preview — 5fca72a3 Deployed Sep 30, 2025 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants