From 8569a31321e770ea93a7941a4bc265a97d27a53a Mon Sep 17 00:00:00 2001 From: Bissbert <43237892+Bissbert@users.noreply.github.com> Date: Mon, 11 May 2026 12:31:12 +0700 Subject: [PATCH 1/3] =?UTF-8?q?feat(seo):=20wave=201=20=E2=80=94=20fix=20i?= =?UTF-8?q?ndexing=20gap=20with=20knowledge=20subdomain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six-agent SEO audit (audits/seo-2026-05/) diagnosed why gemmology.dev is indexed worse than knowledge.gemmology.dev despite being the better product: 48% of sitemap URLs were /og/ image templates and high-value surfaces (/tools, /quiz, /playground) were client:load React shells with no server-rendered prose. This wave addresses the structural gap; v3 will layer keyword-clustered content on top. Sitemap & indexability (W1, W2): - astro.config.mjs: filter excludes /og/, /og-image/, /study/review, /study/settings; drop priority/changefreq (Google ignores since 2023) - /og/learn, /og/minerals, /og-image: noindex,nofollow meta added - Result: sitemap 910 → 466 URLs (zero /og/, zero /study/) Tool pages SSG prose + schema (W3+W4): - 7 tool pages now have 581–1326 server-rendered body words each (was 0–28 words); 3 paragraphs covering definition, method, and diagnostic application with named gem examples - New ToolsSchema.astro component emits SoftwareApplication JSON-LD on every tool page (applicationCategory EducationalApplication, free Offer, gemmology.dev creator) - /tools/index gains h1, intro, static category nav before React island Learn schema (W5, W6): - /learn/index: Course (FGA Diploma prep, 8 categories, PT100H workload) + CollectionPage with ItemList of all 138 articles + BreadcrumbList - LearnSchema.astro: BreadcrumbList fragment URL bug fixed (was emitting /learn#fundamentals as a crumb), datePublished now wired through from new publishedAt content-collection field, hasPart Quiz emitted when an article has curated pretest questions - src/content/config.ts: publishedAt field added to learn collection Quiz hub (W7): - 250-word SSG intro with all 8 categories listed statically - Course JSON-LD with 8 hasCourseInstance entries + Quiz JSON-LD - /quiz/ now returns 541 body words (was 30) About page E-E-A-T (W8): - Visible Maintainers & Editors block with credentials list - Person JSON-LD via extended StructuredData component - TODO(author) markers for real name+credential before deploy Verification: - npm run build: 912 pages, clean - Sitemap: 466 URLs, zero junk paths - All key pages emit valid JSON-LD (homepage 2, /learn 3, /quiz 2, /tools 1, /about 3 blocks) - 441 /og/ pages all carry noindex,nofollow Audit corpus: workspace/gemmology.dev/audits/seo-2026-05/ --- astro.config.mjs | 28 ++---- src/components/seo/LearnSchema.astro | 57 +++++++++-- src/components/seo/StructuredData.astro | 50 +++++++++- src/components/seo/ToolsSchema.astro | 49 ++++++++++ src/content/config.ts | 1 + src/pages/about.astro | 35 +++++++ src/pages/learn/[...slug].astro | 6 ++ src/pages/learn/index.astro | 77 +++++++++++++++ src/pages/og-image.astro | 1 + src/pages/og/learn/[...slug].astro | 1 + src/pages/og/minerals/[slug].astro | 1 + src/pages/quiz/index.astro | 121 ++++++++++++++++++++++-- src/pages/tools/advanced.astro | 69 +++++++++++++- src/pages/tools/conversions.astro | 63 +++++++++++- src/pages/tools/identification.astro | 68 ++++++++++++- src/pages/tools/index.astro | 108 ++++++++++++++++++++- src/pages/tools/lab.astro | 73 +++++++++++++- src/pages/tools/measurement.astro | 80 +++++++++++++++- src/pages/tools/optical.astro | 82 +++++++++++++++- 19 files changed, 905 insertions(+), 65 deletions(-) create mode 100644 src/components/seo/ToolsSchema.astro diff --git a/astro.config.mjs b/astro.config.mjs index 44acfbf..e18eec9 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -22,29 +22,13 @@ export default defineConfig({ react(), tailwind(), sitemap({ - filter: (page) => !page.includes('/admin'), - changefreq: 'weekly', - priority: 0.7, + filter: (page) => + !page.includes('/admin') && + !page.includes('/og/') && + !page.includes('/og-image/') && + !page.includes('/study/review') && + !page.includes('/study/settings'), lastmod: new Date(), - serialize(item) { - // Boost priority/lastmod for hub pages and canonical references - if (item.url === 'https://gemmology.dev/' || - item.url === 'https://gemmology.dev/gallery/' || - item.url === 'https://gemmology.dev/learn/') { - item.priority = 1.0; - item.changefreq = 'weekly'; - } else if (/\/minerals\/[^/]+\/$/.test(item.url)) { - item.priority = 0.8; - item.changefreq = 'monthly'; - } else if (/\/learn\//.test(item.url)) { - item.priority = 0.7; - item.changefreq = 'monthly'; - } else if (/\/docs\//.test(item.url)) { - item.priority = 0.5; - item.changefreq = 'monthly'; - } - return item; - }, }), ], vite: { diff --git a/src/components/seo/LearnSchema.astro b/src/components/seo/LearnSchema.astro index 1dddf00..14588fc 100644 --- a/src/components/seo/LearnSchema.astro +++ b/src/components/seo/LearnSchema.astro @@ -4,6 +4,11 @@ * Improves SEO and enables rich results in search engines */ +interface PretestQuestion { + questionText: string; + correctAnswer: string; +} + interface Props { title: string; description: string; @@ -15,9 +20,25 @@ interface Props { datePublished?: string; /** ISO 8601 datetime — when the article was last meaningfully updated. */ dateModified?: string; + /** Number of pretest questions available for this article. */ + pretestQuestionCount?: number; + /** Pretest question data for schema serialisation. */ + pretestQuestions?: PretestQuestion[]; } -const { title, description, category, subcategory, difficulty, slug, datePublished, dateModified } = Astro.props; +const { + title, + description, + category, + subcategory, + difficulty, + slug, + datePublished, + dateModified, + pretestQuestionCount = 0, + pretestQuestions = [], +} = Astro.props; + const url = `https://gemmology.dev/learn/${slug}`; // Helper to format category names @@ -28,6 +49,22 @@ function formatName(name: string): string { .join(' '); } +// Build hasPart Quiz node when pretest questions are available +const quizNode = pretestQuestionCount > 0 + ? { + "@type": "Quiz", + "name": `Pretest: ${title}`, + "educationalLevel": "Intermediate", + "numberOfQuestions": pretestQuestionCount, + "about": { "@type": "Thing", "name": title }, + "hasPart": pretestQuestions.map(q => ({ + "@type": "Question", + "name": q.questionText, + "acceptedAnswer": { "@type": "Answer", "text": q.correctAnswer }, + })), + } + : null; + // Use combined LearningResource + Article so Google can pick either rich-result type const articleSchema = { "@context": "https://schema.org", @@ -64,34 +101,36 @@ const articleSchema = { }, ...(datePublished ? { "datePublished": datePublished } : {}), ...(dateModified ? { "dateModified": dateModified } : {}), + ...(quizNode ? { "hasPart": quizNode } : {}), }; -// Build breadcrumb from category/subcategory +// Build BreadcrumbList — no fragment URLs (Google rejects them). +// Chain: Home → Learn → [subcategory overview if present →] Article const breadcrumbItems = [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://gemmology.dev" }, { "@type": "ListItem", "position": 2, "name": "Learn", "item": "https://gemmology.dev/learn" }, - { "@type": "ListItem", "position": 3, "name": formatName(category), "item": `https://gemmology.dev/learn#${category}` }, ]; if (subcategory) { + // Position 3: article page (subcategory is visible-only; no clean URL to use here) breadcrumbItems.push({ "@type": "ListItem", - "position": 4, + "position": 3, "name": formatName(subcategory), - "item": url + "item": url, }); breadcrumbItems.push({ "@type": "ListItem", - "position": 5, + "position": 4, "name": title, - "item": url + "item": url, }); } else { breadcrumbItems.push({ "@type": "ListItem", - "position": 4, + "position": 3, "name": title, - "item": url + "item": url, }); } diff --git a/src/components/seo/StructuredData.astro b/src/components/seo/StructuredData.astro index 7441339..2249ce0 100644 --- a/src/components/seo/StructuredData.astro +++ b/src/components/seo/StructuredData.astro @@ -1,6 +1,6 @@ --- /** - * StructuredData - emit a BreadcrumbList plus optional WebPage JSON-LD. + * StructuredData - emit a BreadcrumbList, optional WebPage, and optional Person JSON-LD. * Use for non-mineral / non-learn pages that still benefit from breadcrumbs. */ @@ -9,6 +9,13 @@ interface Crumb { url: string; } +interface PersonData { + name: string; + jobTitle?: string; + credentials?: string[]; + sameAs?: string[]; +} + interface Props { breadcrumbs?: Crumb[]; webPage?: { @@ -16,9 +23,15 @@ interface Props { description?: string; url: string; }; + /** + * Optional Person JSON-LD — used on /about to signal E-E-A-T authorship. + * TODO(author): replace with real maintainer name + credentials before deploy + * (placeholder values are defined in src/pages/about.astro) + */ + person?: PersonData; } -const { breadcrumbs = [], webPage } = Astro.props; +const { breadcrumbs = [], webPage, person } = Astro.props; const breadcrumbSchema = breadcrumbs.length > 0 ? { @@ -48,7 +61,40 @@ const webPageSchema = webPage }, } : null; + +const personSchema = person + ? { + "@context": "https://schema.org", + "@type": "Person", + name: person.name, + ...(person.jobTitle ? { jobTitle: person.jobTitle } : {}), + knowsAbout: [ + "Gemmology", + "Mineralogy", + "Crystallography", + "Gemstone identification", + ], + ...(person.credentials && person.credentials.length > 0 + ? { + hasCredential: person.credentials.map((c) => ({ + "@type": "EducationalOccupationalCredential", + credentialCategory: "certificate", + name: c, + })), + } + : {}), + ...(person.sameAs && person.sameAs.length > 0 + ? { sameAs: person.sameAs } + : {}), + worksFor: { + "@type": "Organization", + name: "gemmology.dev", + url: "https://gemmology.dev", + }, + } + : null; --- {breadcrumbSchema &&