Skip to content

Commit 90554ef

Browse files
committed
docs(seo): PNG social card, per-page TechArticle JSON-LD, IndexNow ping
- Switched og:image / twitter:image to img/social-card.png (1200x630). Some unfurlers (Slack, LinkedIn legacy) prefer PNG over SVG. - Added apps/docs/plugins/api-jsonld.mjs — postBuild plugin that walks build/docs/api/*.html and injects a TechArticle JSON-LD block per page, templated from the page's own <title> and meta description. Docs frontmatter doesn't allow `head:` so this is the cleanest path to per-page structured data in the static HTML. - Added IndexNow key file at static/<key>.txt + a notify-search-engines job in deploy-docs.yml that POSTs the full sitemap URL list to api.indexnow.org after each Pages deploy. Bing + Yandex pick up changes within minutes instead of waiting for crawl.
1 parent 30df73a commit 90554ef

5 files changed

Lines changed: 131 additions & 1 deletion

File tree

.github/workflows/deploy-docs.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,40 @@ jobs:
5858
- name: Deploy to GitHub Pages
5959
id: deployment
6060
uses: actions/deploy-pages@v4
61+
62+
notify-search-engines:
63+
needs: deploy
64+
runs-on: ubuntu-latest
65+
permissions:
66+
contents: read
67+
steps:
68+
- name: Checkout
69+
uses: actions/checkout@v4
70+
- name: Ping IndexNow (Bing, Yandex)
71+
run: |
72+
set -euo pipefail
73+
KEY=00ead8a088444c033454e7c5d686592d
74+
HOST=awbx.github.io
75+
SITEMAP=https://awbx.github.io/fhir-dsl/sitemap.xml
76+
# Wait briefly for the deploy edge to flush, then extract URLs from sitemap.
77+
sleep 30
78+
URLS=$(curl -sf "$SITEMAP" | grep -oE '<loc>[^<]+</loc>' | sed 's:</?loc>::g')
79+
if [ -z "$URLS" ]; then
80+
echo "No URLs found in sitemap; skipping IndexNow notify."
81+
exit 0
82+
fi
83+
# Build the JSON payload (Bing IndexNow batch endpoint).
84+
URL_LIST=$(echo "$URLS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
85+
PAYLOAD=$(jq -n \
86+
--arg host "$HOST" \
87+
--arg key "$KEY" \
88+
--arg keyLocation "https://$HOST/fhir-dsl/$KEY.txt" \
89+
--argjson urls "$URL_LIST" \
90+
'{host: $host, key: $key, keyLocation: $keyLocation, urlList: $urls}')
91+
echo "Notifying IndexNow with $(echo "$URL_LIST" | jq 'length') URLs."
92+
curl -sS -X POST "https://api.indexnow.org/IndexNow" \
93+
-H "Content-Type: application/json; charset=utf-8" \
94+
-H "Host: api.indexnow.org" \
95+
--data "$PAYLOAD" \
96+
-o /tmp/indexnow.txt -w "HTTP %{http_code}\n" || true
97+
cat /tmp/indexnow.txt 2>/dev/null || true

apps/docs/docusaurus.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const BASE_URL = '/fhir-dsl/';
77
const FULL_SITE_URL = `${SITE_URL}${BASE_URL}`;
88
const SITE_DESCRIPTION =
99
'The TypeScript FHIR toolchain — typed query builder, code generator, FHIRPath, validators, SMART-on-FHIR, terminology, and an MCP bridge.';
10-
const SOCIAL_CARD = 'img/social-card.svg';
10+
const SOCIAL_CARD = 'img/social-card.png';
1111

1212
/** @type {import('@docusaurus/types').Config} */
1313
const config = {
@@ -96,6 +96,7 @@ const config = {
9696
],
9797

9898
plugins: [
99+
'./plugins/api-jsonld.mjs',
99100
[
100101
require.resolve('@easyops-cn/docusaurus-search-local'),
101102
/** @type {import('@easyops-cn/docusaurus-search-local').PluginOptions} */

apps/docs/plugins/api-jsonld.mjs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Docusaurus plugin: inject TechArticle JSON-LD into every built /docs/api/*.html
2+
// page after the build. Docs frontmatter doesn't support `head:`, so this is the
3+
// cleanest way to get per-page structured data into the static HTML for SEO.
4+
//
5+
// Reads each page's <title> (already populated by Docusaurus from the doc's
6+
// frontmatter title) and the og:description meta tag for the description, so
7+
// no per-file maintenance is needed when API ref pages change.
8+
9+
import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
10+
import { join } from "node:path";
11+
12+
const SITE = "https://awbx.github.io/fhir-dsl";
13+
14+
function buildJsonLd({ title, description, url }) {
15+
return {
16+
"@context": "https://schema.org",
17+
"@type": "TechArticle",
18+
headline: title,
19+
name: title,
20+
description,
21+
url,
22+
inLanguage: "en",
23+
isPartOf: {
24+
"@type": "WebSite",
25+
name: "fhir-dsl docs",
26+
url: `${SITE}/`,
27+
},
28+
about: {
29+
"@type": "SoftwareSourceCode",
30+
name: "fhir-dsl",
31+
codeRepository: "https://github.com/awbx/fhir-dsl",
32+
programmingLanguage: "TypeScript",
33+
},
34+
author: { "@type": "Person", name: "Abdelhadi Sabani" },
35+
publisher: { "@type": "Person", name: "Abdelhadi Sabani" },
36+
proficiencyLevel: "Expert",
37+
articleSection: "API Reference",
38+
};
39+
}
40+
41+
function extractMeta(html) {
42+
// Docusaurus emits both <title> and <meta name="description"> in head.
43+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/);
44+
const descMatch =
45+
html.match(/<meta[^>]*\bname=["']description["'][^>]*\bcontent=["']([^"']+)["']/) ||
46+
html.match(/<meta[^>]*\bcontent=["']([^"']+)["'][^>]*\bname=["']description["']/);
47+
if (!titleMatch || !descMatch) return null;
48+
// Strip Docusaurus's " | site title" suffix from <title> for cleaner JSON-LD.
49+
const title = titleMatch[1].replace(/\s*\|\s*fhir-dsl\s*$/, "");
50+
return { title, description: descMatch[1] };
51+
}
52+
53+
function injectJsonLd(html, ld) {
54+
const tag = `<script type="application/ld+json">${JSON.stringify(ld)}</script>`;
55+
// Insert just before </head> so it lands among the other head tags.
56+
return html.replace(/<\/head>/, `${tag}</head>`);
57+
}
58+
59+
export default function apiJsonLdPlugin() {
60+
return {
61+
name: "api-jsonld-plugin",
62+
async postBuild({ outDir }) {
63+
const apiDir = join(outDir, "docs", "api");
64+
let stats;
65+
try {
66+
stats = statSync(apiDir);
67+
} catch {
68+
return;
69+
}
70+
if (!stats.isDirectory()) return;
71+
72+
let injected = 0;
73+
for (const entry of readdirSync(apiDir)) {
74+
if (!entry.endsWith(".html")) continue;
75+
const path = join(apiDir, entry);
76+
const html = readFileSync(path, "utf-8");
77+
const meta = extractMeta(html);
78+
if (!meta) continue;
79+
const slug = entry.replace(/\.html$/, "");
80+
const url = `${SITE}/docs/api/${slug}`;
81+
const ld = buildJsonLd({ ...meta, url });
82+
writeFileSync(path, injectJsonLd(html, ld), "utf-8");
83+
injected++;
84+
}
85+
if (injected > 0) {
86+
// eslint-disable-next-line no-console
87+
console.log(`[api-jsonld-plugin] injected TechArticle JSON-LD into ${injected} API ref pages`);
88+
}
89+
},
90+
};
91+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
00ead8a088444c033454e7c5d686592d
89.6 KB
Loading

0 commit comments

Comments
 (0)