← Blog

Don't Let Google Miss Your Posts—SSR Blog's SEO Pitfalls and Lessons Learned

· 7 min read · 317 views
Astro SEO SSR Sitemap JSON-LD
SSR Blog: Hands-On

Don't Let Google Miss Your Articles

SSR Blog's SEO Pitfalls: A Firsthand Account

The reasons behind every SEO decision when building a blog with Astro + PostgreSQL—and the pitfalls that almost made the articles disappear from search results.

Question

Why Is SEO for the SSR Blog Different from That of a Static Site?

1

Dynamic content is not included in the sitemap

SSR pages are generated at runtime; @astrojs/sitemap only includes static pages generated at build time.

The article was published but isn't in the sitemap, so Google has no idea it exists.

2

Tool Schema Is Not the Only Hidden Cost

Just like the MCP token, SEO also has “hidden costs”: missing meta tags, incorrect hreflang attributes, and inconsistent canonical tags.

67% Error in hreflang implementation on multilingual site

3

The Double-Edged Sword of SSR Performance

SSR allows content to be updated in real time, but it also makes TTFB dependent on the time taken for database queries.

No cache policy = A DB query is run for every request.

Technology Stack

The Structure of the SSR Blog

Astro 5
Framework
PostgreSQL
Database
Drizzle
ORM Query Layer
Tailwind
Style
Cloudflare
Tunnel Deployment
SEO Decisions

What have we done?

We added five layers, ranging from the dynamic sitemap to the robots.txt file, with each layer corresponding to a specific user.

Dynamic Sitemap
sitemap-posts.xml retrieves published articles from the database in real time and automatically patches them into the build's sitemap-index.xml
🔍 Google / Bing Web Crawlers
JSON-LD Structured Data
Automatically generate a BlogPosting schema for each article—the title, author, publication date, and language are all mapped from corresponding database fields.
📊 Detailed Summary of Search Results
Canonical URL
SSR pages automatically set the canonical tag to point to themselves. Avoid http://localhost and https://blog.joneshong.com being flagged as duplicates
🔗 Duplicate Removal Mechanism
hreflang Multilingual
zh-TW ↔ en bidirectional alternate link, with an x-default fallback. 67% of websites get this step wrong.
🌐 Multilingual Search
robots.txt
Allow crawlers to access all public pages; block /admin/ and /api/. Points to two sitemaps
🤖 Crawler Control
Efficiency

Core Web Vitals Optimization

0 KB
JS Baseline
Astro defaults to zero JS
On Demand
React Island Hydration
Search, Atlas
Delay
Font Loading Strategies
CJK System Fonts + Latin Lazy Load
SSR
Dynamic Content
Real-time Updates

Astro's Secret Weapon: By default, no JavaScript is included. React components are only hydrated where interaction is required (client:only), and the rest is all static HTML.

A Record of Pitfalls

How did we discover the problem?

It wasn't planned in advance; I had to fix three issues before everything was complete: the sitemap wasn't being linked to, the estimated reading time was calculated as 73 minutes, and the Nginx subpath pages were blank.

The article was published → It doesn't show up in Google search results

sitemap-posts.xml: no one has cited it.

@astrojs/sitemap generates sitemap-index.xml. At this time, it only includes static pages. The dynamic sitemap-posts.xml was ignored. Code fix: After "build," automatically patch "sitemap-index" to include the posts sitemap.

Insight: Automation tools don't necessarily cover all cases; dynamic SSR pages require manual troubleshooting.

12-minute article | 73-minute read

Rich HTML's content.length includes the entire CSS

content.length / 400 calculates the reading time, but the Rich HTML content has 25,000+ characters of CSS + HTML tags. I need to strip the tags first so the calculated reading time is accurate.

Insight: For any logic related to the length of the content, you must first determine the format of the content.

Blank Page Under Nginx Subpath Proxy

Vite dev server + subpath = Dead end

Vite uses absolute paths for ES module imports; nginx's sub_filter — there's no end to the changes. Solution: In dev preview, don't use the Vite dev server; use the built-in SSR server instead.

Insight: Astro Islands' component-url property also needs to be rewritten by `sub_filter`.

Tools

Paste your own `head` here to check

There are six items on the list above, and three of them can be verified without crawling the site—just paste the URL of any one of your articles <head> — the source code is sufficient. Canonical, hreflang, and JSON-LD are all parsed locally in the browser and do not send anything out. The remaining three (sitemap inclusion, robots.txt, and TTFB) require actual requests to be made to your domain, which browser cross-site limitations cannot do. Leave those three for the AI assigned in the prompt above to crawl.

It's checking "the string of text you pasted", not your online page—if what you pasted is the Elements panel from browser DevTools (which is the DOM after JS has run), the result might be different from what a web crawler sees. To match what the crawler sees, please use the "View Page Source" (view-source) version.

Copy this section

Copy this to your AI Agent

You might think that once an article is published, Google doesn't know it exists—SSR blog pages are generated at runtime, and @astrojs/sitemap only collects static pages at build time, so dynamic posts aren't written to the sitemap at all. Missing meta tags at worst leads to poor rankings, but not being indexed means disappearing from search results entirely.

Copy the text below and paste it into your AI. Tell it not to rely on common sense—instead, have it crawl your site, compare it to the list in this article, and identify exactly which items you’ve actually omitted:

I am reading an SSR blog SEO pitfalls post (https://blog.joneshong.com/en/blog/hello-world). The author built their site using Astro + PostgreSQL and listed 5 SEO decisions and 3 SSR-specific pitfalls. Please help me check if my site has fallen into the same pitfalls. Do not answer based on general SEO common sense; directly inspect my actual output: My site: [Insert URL], which is SSR / SSG / Purely Static (help me determine if unsure). Please inspect each item on-site (in the order of the pitfalls mentioned in the post): 1. Fetch my sitemap and verify that "the most recently published dynamic pages" are actually in it— The most common SSR pitfall is that dynamic pages are not indexed, meaning Google doesn't know they exist. 2. Check the head tags of any article: Does the canonical point to itself? Does it leave behind localhost or a preview domain? 3. Do hreflang tags have two-way alternate + x-default? (The post tested that 67% of multilingual sites fail at this step.) 4. Fetch my robots.txt: Does it block non-public paths like /admin/ and /api/? Does it point to the sitemap? 5. Does every article have JSON-LD BlogPosting, and are the fields generated from real data or hardcoded? 6. If I am SSR: Is TTFB slowed down by DB queries? Is there a caching layer? Report back to me: Which items I actually missed or did wrong (with the evidence you found), prioritized by "whether it will make articles disappear from search." If I am a purely static site, points 1 and 6 do not apply; state that they do not apply directly without forcing them.
References

Further Reading

Each of these five articles corresponds to a specific decision, covering topics ranging from the hidden-cost analogy to data on a 67% hreflang error rate.

Article Why It Matters
Apideck: Your MCP Server Is Consuming Your Context Window Inspired the analogy of “hidden costs”
SEO for Astro: How to Make the Fastest Framework Also the Smartest A Complete Guide to Implementing Astro SEO
JSON-LD Blog Post Example Reference Implementation of the BlogPosting Schema
Multilingual SEO and Hreflang Guide Source of the 67% error rate data
How to Improve Core Web Vitals Specific Methods for Optimizing SSR Performance
Conclusion

SEO is an engineering discipline, not a form of mysticism.

Every decision has a reason, and every setback offers a lesson.
SEO for the SSR Blog isn't something that ends once the meta tags are set up; it's an ongoing, iterative process.

// Decision Chain

Question 1: Dynamic articles are not included in the sitemap
  → Manual correction sitemap-posts.xml

Question 2: SSR without caching
  → Static Page prerender + Dynamic Pages SSR

Question 3: Multilingual SEO is prone to errors
  → hreflang Two-way + x-default

Verification: Google Search Console Manual Submission + Monitoring

// Induction
Astro SSR + Automated SEO Pipeline
No omissions, no duplicates, no delays
✦ Copy Prompt