Dynamic content is not invisible content. A FastAPI application can serve fully crawlable bilingual pages without prebuilding HTML files, provided every request returns the complete user-facing document. The decisive signals are stable locale URLs, self-canonical pages, reciprocal hreflang, a Sitemap derived only from published snapshots, and JSON-LD that matches visible facts—not whether an .html file existed before the request. Request-time rendering is an operational choice; it neither guarantees discovery nor creates a separate class of “dynamic SEO.” Its success still depends on reliable delivery and useful content.
How do dynamic SSR, pre-rendering, and dynamic rendering differ?
The terms are often conflated even though they describe different delivery models.
| Model | When HTML is produced | Same content for crawlers and users? | Typical fit |
|---|---|---|---|
| Dynamic SSR | At request time, often with caching | Yes | Database-backed articles, products, and documentation |
| Pre-rendering or static generation | During a build or publication job | Yes | Slowly changing content with manageable build size |
| Dynamic rendering | A bot receives a rendered version while users receive a client-rendered app | Not necessarily | A temporary workaround for difficult JavaScript crawling |
This article focuses on the first model. MongoDB stores Markdown and a published snapshot. FastAPI reads the published data, and Jinja2 returns a complete HTML document. A crawler and a normal browser request the same URL and receive the same substantive content. No bot-specific copy is selected by User-Agent.
Google describes dynamic rendering as a workaround rather than a recommended long-term solution. That guidance does not make ordinary server-side rendering a workaround. Request-time SSR is also not “JavaScript-only” merely because no physical HTML file sits on disk.
Choose between request-time SSR and static generation based on deployment, caching, content volume, and recovery requirements—not on the myth that SEO requires a pre-existing file.
When is request-time SSR the better choice for a multilingual site?
The useful comparison is not “SSR is good for SEO and static pages are better.” Both can return equivalent HTML. The choice is about how a team wants content changes to move from its source of truth to a public URL.
Request-time SSR is attractive when editors publish frequently, content already lives in a database, and a release must update listings, topic pages, locale pairs, and Sitemaps immediately. A small template deployment can change the presentation of every article without rebuilding an archive. The cost is operational: the origin needs a database fallback strategy, bounded queries, cache revalidation, and honest 503 responses.
Static generation is attractive when the corpus changes slowly, build time remains predictable, and a static artifact is the desired recovery unit. It reduces runtime dependencies for article reads, but every content change must trigger and complete a build. Large multilingual collections can also create long invalidation queues or partial-deployment concerns unless the build system supports incremental output.
A hybrid is valid too: render article details statically while keeping search, personalization, or administrative previews dynamic. What matters to a crawler is the final public representation and its signals. What matters to operators is whether publication, rollback, cache invalidation, and language pairing remain observable.
Use these questions rather than an SEO slogan:
- Does an editor need a database commit to become visible without a site build?
- Can the system return a complete document if JavaScript fails?
- What happens when the cache, database, or build pipeline is unavailable?
- Can both locale URLs, their alternates, and the Sitemap change as one reviewed release?
- Is the same content served to a crawler and a person?
What happens when an article request reaches FastAPI?
A maintainable request path can remain small:
GET /en/articles/{slug}
│
├─ Redis hit → return cached complete HTML
│
└─ Redis miss
├─ Read a published-field projection from MongoDB
├─ Select the English snapshot
├─ Render the Jinja2 page shell
├─ Add canonical, hreflang, and JSON-LD
└─ Cache and return the document
FastAPI officially supports Jinja2 templates and template responses. In this implementation, the template owns the page shell, while Markdown is rendered and sanitized when an immutable publication snapshot is created. Public reads neither execute the Markdown pipeline again nor access the mutable draft.
That separation has two useful consequences:
- The initial HTML already contains the headline, summary, body, table of contents, and sources.
- Untrusted Markdown is handled once at the publication boundary, reducing repeated work and narrowing the public-read attack surface.
JavaScript may enhance reading progress, active headings, and code-copy controls. Disable it, and the body, navigation, language switcher, and evidence remain usable. This is progressive enhancement, not a separate crawler page.
Why does each language need a stable URL?
Avoid silently replacing the body on one URL according to Accept-Language, and avoid forcing visitors onto a guessed locale. A clearer mapping is:
/articles/{slug} → Simplified Chinese
/en/articles/{slug} → English
Google recommends discoverable language-specific versions and determines a page’s language primarily from visible content rather than the URL or lang attribute alone. Separate URLs also provide practical benefits:
- each locale owns its canonical, title, description, and cache entry;
- people can reliably share the edition they are reading;
- a Sitemap can state the language relationship explicitly;
- analytics can separate locale performance;
- the English edition can address English search intent rather than mirror Chinese sentence by sentence.
Sharing one slug means “the same topic,” not “identical prose.” What needs to be synchronized is the publication boundary and the language relationship. Atomic bilingual publishing with FastAPI, MongoDB, and Redis explains that state model.
How should hreflang and canonical work together?
The Chinese detail page should self-canonicalize to its Chinese URL. The English page should self-canonicalize to its English URL. Do not canonicalize the English edition to Chinese: when the main content has been translated, these are localized alternatives rather than duplicates that should be collapsed.
The Chinese page can emit:
<link rel="canonical"
href="https://www.example.com/articles/atomic-publishing">
<link rel="alternate" hreflang="zh-Hans"
href="https://www.example.com/articles/atomic-publishing">
<link rel="alternate" hreflang="en"
href="https://www.example.com/en/articles/atomic-publishing">
<link rel="alternate" hreflang="x-default"
href="https://www.example.com/en/articles/atomic-publishing">
The English page emits the same alternate set and changes only its canonical to the English URL. Five details matter:
- Every locale lists itself and every counterpart.
- Counterparts link back; missing return links can cause annotations to be ignored.
- Alternate
hrefvalues are fully qualified absolute URLs. zh-Hansidentifies Simplified Chinese, while genericenfits a global English audience.x-defaultis a fallback for unmatched languages, not a ranking weight for the “primary” edition.
Canonical answers “which URL should represent this page?” Hreflang answers “which localized alternatives correspond to it?” They serve different purposes, but their URL mapping must agree.
Google treats HTML, HTTP headers, and Sitemaps as equivalent ways to express language alternatives and says there is no search benefit to maintaining all three. A project may still emit both HTML and Sitemap mappings for operational clarity or other consumers. If it does, both should come from the same URL builder and be covered by consistency tests—not treated as additive ranking signals.
How can a dynamic Sitemap expose only live pages?
A Sitemap should not scan drafts or infer pages from the filesystem. Its query boundary should be:
recordType = article
state = published
published != null
The service then emits locale pairs for listings, topics, and detail pages:
<url>
<loc>https://www.example.com/en/articles/atomic-publishing</loc>
<xhtml:link rel="alternate" hreflang="zh-Hans"
href="https://www.example.com/articles/atomic-publishing"/>
<xhtml:link rel="alternate" hreflang="en"
href="https://www.example.com/en/articles/atomic-publishing"/>
<xhtml:link rel="alternate" hreflang="x-default"
href="https://www.example.com/en/articles/atomic-publishing"/>
</url>
The Chinese URL needs its own <url> entry with the same alternate set. Google’s multilingual Sitemap instructions require an entry for every version, with each entry listing all alternates including itself.
Only emit <lastmod> when the value accurately represents a meaningful last modification. An internal database timestamp is not automatically a public content fact. If the product deliberately withholds real publication and modification dates, omission is more reliable than manufacturing freshness.
Publication and unpublication should invalidate the Sitemap cache together with listings, topics, and details. Otherwise a live page can remain undiscoverable in the Sitemap—or a removed page can continue to be advertised there.
What belongs in JSON-LD, and what should be omitted?
Structured data describes the page; it does not replace it. A concise Article graph might be:
{
"@context": "https://schema.org",
"@type": "Article",
"@id": "https://www.example.com/en/articles/atomic-publishing#article",
"url": "https://www.example.com/en/articles/atomic-publishing",
"headline": "Atomic Bilingual Publishing",
"description": "A practical architecture for bilingual publishing.",
"inLanguage": "en",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://www.example.com/en/articles/atomic-publishing"
},
"publisher": {
"@type": "Organization",
"name": "Example"
},
"citation": [
"https://www.mongodb.com/docs/manual/core/write-operations-atomicity/"
]
}
A BreadcrumbList can be added if its labels and destinations correspond to visible navigation.
Google’s Article documentation lists several recommended properties. “Recommended” is not permission to invent them. If no public author is intended, do not fabricate a person. If genuine dates are intentionally private, do not create a fake datePublished or a dateModified that changes on every render. Missing recommended data may limit certain presentation opportunities; false data damages trust and gives machines facts that readers cannot verify.
Sources should appear in a visible evidence section and may then be mirrored as citations. In this implementation, citation is a schema.org semantic addition; Google’s Article rich-result documentation does not list it as a presentation signal, so it must not be treated as a promise of extra visibility. JSON-LD is not a place for hidden keywords, invisible claims, or an AI-only version of the article.
How does SSR support SEO, AEO, and GEO at the same time?
There is no magic tag that guarantees an AI citation. SSR helps because it creates a stable, crawlable representation in which useful information is easy to locate:
- the opening paragraph answers the central question;
- natural-language H2 and H3 headings organize complete answers;
- tables express comparisons and failure states;
- code samples include scope and limitations;
- claims link to visible primary sources;
- Chinese and English are written naturally for their audiences;
- the page remains usable without JavaScript and with keyboard or small-screen navigation.
Google’s generative-search guidance still starts with crawlability, unique value, and people-first content. For public-web search and citation, a system needs access through a supported crawler, index, data provider, or retrieval path. Technical SEO is therefore the discovery layer; original information and transparent evidence make the page worth retrieving. See GEO and AEO in 2026 for the broader official guidance.
Do caching and failure responses affect crawling?
Yes, because the origin must represent state honestly.
- Normal HTML can use
Cache-Control: public, no-cache, allowing storage but requiring validation before reuse. - Hash the complete HTML into an ETag and answer matching
If-None-Matchwith304. - If Redis fails, fall through to MongoDB rather than declaring every page absent.
- If MongoDB also fails and no cache exists, return a locale-matched HTML
503,Retry-After, andnoindex. - A genuinely missing or unpublished slug returns an HTML
404withnoindex. - GET and HEAD share the same status and material headers; HEAD omits the body.
Do not turn a temporary database outage into a 200 empty page, and do not rewrite every exception as a permanent 404. Search systems interpret status codes; reassuring text inside the page shell cannot repair incorrect HTTP semantics.
What should be checked before launch?
Content
- Raw HTML fetched with
curlalready contains the complete body. - The Chinese URL is substantially Chinese and the English URL substantially English.
- Titles, descriptions, visible evidence, and internal links fit each audience.
- The page does not expose unapproved author or date fields.
URLs and head signals
- Both locale URLs return
200without automatic language redirects. - Each page self-canonicalizes.
- Both output the identical
zh-Hans,en, andx-defaultset. - HTML
lang,Content-Language, and visible prose agree. - Open Graph URL matches the canonical.
Sitemap and structured data
- Only published snapshots enter the Sitemap.
- Chinese and English each have a
<url>entry with complete alternates. - JSON-LD URL, language, headline, and description match the current page.
- JSON-LD contains no unverifiable author, date, or image.
- Sources are visible in HTML and represented consistently in structured data.
Failure and cache behavior
- An ETag revalidation can return
304. - HEAD and GET agree on status.
- Redis failure falls back to the database.
- Database failure produces a temporary
503, not an empty200or permanent404. - Publish and unpublish rotate cache state for both locales, listings, topics, and Sitemap.
This checklist covers the result visible to search systems. For retry behavior when MongoDB commits but Redis invalidation fails, see the state-revision design in the atomic publishing article instead of creating a second publication state machine here.
Frequently asked questions
Must a dynamic SSR page be pre-generated before it can be indexed?
No. If the initial response contains complete crawlable HTML and the status, links, and robots rules permit crawling, no physical file is required. Pre-generation is a deployment strategy, not an SEO identity.
Can hreflang compensate for a poor translation?
No. Google determines language primarily from visible content. Translating only navigation while leaving the main body unchanged does not create a useful localized edition.
Should Chinese and English canonicalize to one another?
No. Fully localized editions should self-canonicalize and use reciprocal hreflang. Canonicalizing English to Chinese risks consolidating away the English URL.
Must hreflang appear in both HTML and the Sitemap?
No. Google treats HTML, HTTP headers, and Sitemap implementations as equivalent. If you maintain more than one for system-level reasons, generate them from the same mapping and test their consistency.
Is Article JSON-LD invalid without an author or dates?
No. Google’s current Article documentation defines no required properties; author and date fields are recommended when applicable. They can be omitted, but must not be invented. Missing recommended data may limit some presentation opportunities, which is preferable to publishing false facts.
Does dynamic SSR guarantee indexing or an AI citation?
No. Complete HTML, correct status codes, and consistent search signals establish eligibility and clarity; no platform guarantees crawling, indexing, ranking, or citation. The page still needs query-relevant, non-commodity, verifiable information, and its real outcome must be observed in platform reports and referral data.