From 76cdcf67c83183c38e8e420b731959d2e050c3c3 Mon Sep 17 00:00:00 2001 From: Bissbert <43237892+Bissbert@users.noreply.github.com> Date: Wed, 13 May 2026 14:48:51 +0700 Subject: [PATCH] feat(seo): ship WA3 keyword-cluster hubs (identification, equipment, treatments, properties) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four content-cluster hub pages claiming high-intent head terms from the v3 keyword landscape: - /learn/identification — "how to identify gemstones" (9-step protocol, 14 child articles linked, LearningResource JSON-LD with hasPart) - /learn/equipment — "how to use a refractometer (and the rest of the gemmological toolkit)" (7-step refractometer workflow table, 13 child articles, LearningResource JSON-LD) - /learn/identification/treatments — "synthetic diamond identification, treatments & composite stones" (11-row detection matrix, LearningResource with about[] covering HPHT/CVD/heat/diffusion/composite stones) - /reference/properties — "gemstone properties chart" (50-row RI/SG/hardness/ birefringence/system/cleavage reference table, Dataset + WebPage mainEntity:Table JSON-LD for table-rich-result eligibility) Each hub renders >=400 body words, emits at least one JSON-LD block, links to >=4 existing child pages, and breadcrumbs back to /learn/. The treatments hub collides with the dynamic [...slug] route from the gemmology-knowledge YAML article of the same path; the static hub takes precedence and the YAML slug is filtered from getStaticPaths so its content remains in the collection (referenced by the hub) without producing a duplicate route. --- src/pages/learn/[...slug].astro | 18 +- src/pages/learn/equipment/index.astro | 277 ++++++++++++++ src/pages/learn/identification/index.astro | 354 +++++++++++++++++ .../identification/treatments/index.astro | 362 ++++++++++++++++++ src/pages/reference/properties.astro | 294 ++++++++++++++ 5 files changed, 1301 insertions(+), 4 deletions(-) create mode 100644 src/pages/learn/equipment/index.astro create mode 100644 src/pages/learn/identification/index.astro create mode 100644 src/pages/learn/identification/treatments/index.astro create mode 100644 src/pages/reference/properties.astro diff --git a/src/pages/learn/[...slug].astro b/src/pages/learn/[...slug].astro index 3f06c00..e8ae66b 100644 --- a/src/pages/learn/[...slug].astro +++ b/src/pages/learn/[...slug].astro @@ -14,11 +14,21 @@ import type { WidgetQuestion } from '../../components/quiz/study/LearnQuizWidget import { buildCitationIndex } from '../../lib/citations/index'; export async function getStaticPaths() { + // Slugs handled by static hub pages under src/pages/learn/. Excluding them here + // prevents a duplicate-route build error; the article content still lives in + // the collection and is referenced by the corresponding hub (e.g. Hub 5). + // Declared inside getStaticPaths because Astro hoists this function into its + // own module scope and outer consts are not accessible. + const STATIC_HUB_SLUGS = new Set([ + 'identification/treatments', + ]); const entries = await getCollection('learn'); - return entries.map(entry => ({ - params: { slug: entry.id }, - props: { entry, allEntries: entries }, - })); + return entries + .filter((entry) => !STATIC_HUB_SLUGS.has(entry.id)) + .map((entry) => ({ + params: { slug: entry.id }, + props: { entry, allEntries: entries }, + })); } interface Props { diff --git a/src/pages/learn/equipment/index.astro b/src/pages/learn/equipment/index.astro new file mode 100644 index 0000000..1bccd0e --- /dev/null +++ b/src/pages/learn/equipment/index.astro @@ -0,0 +1,277 @@ +--- +import { getCollection } from 'astro:content'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { Card, CardTitle, CardDescription } from '../../../components/ui/Card'; +import { IconBox } from '../../../components/ui/IconBox'; +import { Container } from '../../../components/ui/Container'; +import { DifficultyBadge } from '../../../components/ui/DifficultyBadge'; +import { Breadcrumb } from '../../../components/docs'; + +const learnEntries = await getCollection('learn'); + +interface EquipmentTopic { + id: string; + title: string; + description: string; + href: string; + difficulty?: 'beginner' | 'intermediate' | 'advanced'; +} + +const equipmentTopics: EquipmentTopic[] = learnEntries + .filter((e) => e.data.category === 'equipment') + .map((e) => ({ + id: e.id, + title: e.data.title, + description: e.data.description, + href: `/learn/${e.id}`, + difficulty: e.data.difficulty as EquipmentTopic['difficulty'], + })) + .sort((a, b) => { + if (a.id === 'equipment/overview') return -1; + if (b.id === 'equipment/overview') return 1; + if (a.id === 'equipment/refractometer') return -1; + if (b.id === 'equipment/refractometer') return 1; + return a.title.localeCompare(b.title); + }); + +const learningResourceSchema = { + '@context': 'https://schema.org', + '@type': 'LearningResource', + name: 'How to Use a Refractometer and Other Gemstone Testing Equipment', + alternateName: [ + 'Gemmological Equipment Guide', + 'Refractometer Tutorial', + 'FGA Equipment Module', + ], + description: + 'Comprehensive guide to gemmological testing equipment: refractometer, polariscope, dichroscope, spectroscope, microscope, UV lamp, and advanced laboratory instruments.', + url: 'https://gemmology.dev/learn/equipment/', + learningResourceType: 'Guide', + educationalLevel: 'beginner', + inLanguage: 'en', + isAccessibleForFree: true, + audience: { + '@type': 'EducationalAudience', + educationalRole: 'student', + }, + about: [ + 'Refractometer operation', + 'Polariscope use', + 'Dichroscope use', + 'Spectroscope operation', + 'Gemmological microscope technique', + 'UV fluorescence inspection', + 'Specific gravity measurement', + ], + teaches: equipmentTopics.map((t) => t.title), + hasPart: equipmentTopics.map((t) => ({ + '@type': 'LearningResource', + name: t.title, + url: `https://gemmology.dev${t.href}/`, + learningResourceType: 'Article', + })), +}; + +const breadcrumbSchema = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@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: 'Equipment', + item: 'https://gemmology.dev/learn/equipment/', + }, + ], +}; + +const breadcrumbItems = [ + { label: 'Learn', href: '/learn' }, + { label: 'Equipment' }, +]; + +const refractometerRows = [ + { + step: 1, + instruction: 'Clean the prism with the supplied solvent and a soft tissue. Any residue affects the half-shadow boundary.', + }, + { + step: 2, + instruction: 'Apply one small drop of contact fluid (refractive index 1.79–1.81; di-iodomethane saturated with sulphur) directly to the prism.', + }, + { + step: 3, + instruction: 'Place the stone polished-table-side-down so it rests fully in the fluid. The table must lie flat — wedge thin stones with a small fluid pool.', + }, + { + step: 4, + instruction: 'View through the eyepiece under sodium (589 nm) or filtered white light. Adjust the eyepiece focus until the scale and shadow boundary are crisp.', + }, + { + step: 5, + instruction: 'Rotate the stone through 180° in 45° increments. Singly refractive stones show one stationary shadow; doubly refractive stones show two boundaries that move (the birefringence).', + }, + { + step: 6, + instruction: 'Record the highest and lowest readings to ±0.002. The difference is the birefringence; the midpoint is the spot reading.', + }, + { + step: 7, + instruction: 'Cabochons and rough surfaces use the spot method: a tiny droplet on the prism contacts only the dome. Read the point at which the bright field switches to dark.', + }, +]; + +const iconPaths: Record = { + microscope: + 'M9 3h6m-6 0v6m6-6v6m-3 0v3m0 0a6 6 0 100 12 6 6 0 000-12zm0 0V9m-3 12h6', + beaker: + 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z', +}; +--- + + +