From 96218b0eed591240ffacdf274b0851bc6f35e68e Mon Sep 17 00:00:00 2001 From: Oleksandr Zhuravlov Date: Wed, 8 Jul 2026 21:43:53 +0300 Subject: [PATCH] fix(docs): strengthen crawl + indexing signals for the blog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Google Search Console reported ~107 blog/docs URLs as "Discovered — currently not indexed". There is no technical blocker (pages are statically generated; robots/canonical/sitemap are clean; no noindex in HTML or headers) — it's the young-domain crawl-budget pattern. These changes maximize the signals that help Google prioritize crawling the URLs it has already discovered. - sitemap: emit each blog URL's real frontmatter date as instead of the build timestamp. Stamping every URL with the build time told Google that all ~107 pages changed on every deploy, so it treated lastmod as noise and ignored it — discarding the signal it uses to schedule crawls. Docs/home stay on the build time (no per-page date); the blog index uses the newest post. - blog posts: add per-post BlogPosting JSON-LD (headline, datePublished, author, image, mainEntityOfPage) so crawlers can model each post as an Article instead of guessing — a content signal that aids prioritization and unlocks Article rich results. - blog posts: add a per-post OG image route (/og/blog/[slug]) mirroring the docs one, wired into openGraph/twitter/JSON-LD. Posts previously declared a summary_large_image card with no image, so social cards rendered blank. - home page: add a "From the blog" section linking the 4 latest posts. The home page is the most-crawled URL, so direct links from it are the strongest crawl-priority signal we can give a young blog's posts. Verified: /sitemap.xml serves distinct per-post dates; docs typecheck passes (the new typed OG route resolves); the home page renders the section with 4 crawlable /blog links. Co-Authored-By: Claude Opus 4.8 --- apps/docs/app/(home)/blog/[slug]/page.tsx | 147 +++++++++++------- .../app/(home)/components/from-the-blog.tsx | 63 ++++++++ apps/docs/app/(home)/page.tsx | 2 + apps/docs/app/og/blog/[slug]/route.tsx | 44 ++++++ apps/docs/app/sitemap.ts | 32 +++- 5 files changed, 225 insertions(+), 63 deletions(-) create mode 100644 apps/docs/app/(home)/components/from-the-blog.tsx create mode 100644 apps/docs/app/og/blog/[slug]/route.tsx diff --git a/apps/docs/app/(home)/blog/[slug]/page.tsx b/apps/docs/app/(home)/blog/[slug]/page.tsx index 2bbec26f..3e5564c5 100644 --- a/apps/docs/app/(home)/blog/[slug]/page.tsx +++ b/apps/docs/app/(home)/blog/[slug]/page.tsx @@ -6,6 +6,7 @@ import { getRelatedPosts, postSlug, } from '@/lib/blog'; +import { jsonLdHtml } from '@/lib/json-ld'; import { appName, siteUrl } from '@/lib/shared'; import { DocsBody } from 'fumadocs-ui/layouts/docs/page'; @@ -21,63 +22,96 @@ export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) { const MDX = post.data.body; const related = getRelatedPosts(post); - return ( -
- - ← Back to blog - -
-
-

- {post.data.title} -

-

- {post.data.author} - · - -

-
- - - - -
+ // Per-post structured data. The root layout emits site-wide WebSite + + // SoftwareApplication nodes; this classifies each post as an Article so a + // crawler can model author, publish date, and headline instead of guessing — + // the content-quality signal that helps Google prioritize a young blog's URLs + // for indexing (and unlocks Article rich results). `image`/`url` must resolve, + // so they mirror the OG route and canonical exactly. + const canonical = `${siteUrl}/blog/${slug}`; + const jsonLd = { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: post.data.title, + ...(post.data.description + ? { description: post.data.description } + : {}), + datePublished: post.data.date, + dateModified: post.data.date, + author: { '@type': 'Person', name: post.data.author }, + publisher: { '@type': 'Organization', name: appName, url: siteUrl }, + image: `${siteUrl}/og/blog/${slug}`, + mainEntityOfPage: { '@type': 'WebPage', '@id': canonical }, + url: canonical, + ...(post.data.tags?.length + ? { keywords: post.data.tags.join(', ') } + : {}), + inLanguage: 'en-US', + }; - {related.length > 0 ? ( -