feat(seo): BlogPosting schema, FAQPage JSON-LD, extensible author system (#2284)

* feat(seo): BlogPosting schema, FAQPage JSON-LD, author system, AI crawler welcome

Blog structured data:
- Change @type Article to BlogPosting for all blog posts
- Author: Organization to Person with extensible default (Elie Habib)
- Add per-post author/authorUrl/authorBio/modifiedDate frontmatter fields
- Auto-extract FAQPage JSON-LD from FAQ sections in all 17 posts
- Show Updated date when modifiedDate differs from pubDate
- Add author bio section with GitHub avatar and fallback

Main app:
- Add commodity variant to middleware VARIANT_HOST_MAP and VARIANT_OG
- Add commodity.worldmonitor.app to sitemap.xml
- Shorten index.html meta description to 136 chars (was 161)
- Remove worksFor block from index.html author JSON-LD
- Welcome all bots in robots.txt (removed per-bot blocks, global allows)
- Update llms.txt: five variants listed, all 17 blog post URLs added

* fix(seo): scope FAQ regex to section boundary, use author-aware avatar

- extractFaqLd now slices only to the next ## heading (was: to end of body)
  preventing bold text in post-FAQ sections from being mistakenly extracted
- Avatar src now derived from DEFAULT_AUTHOR_GITHUB constant (koala73)
  only when using the default author; custom authors fall back to favicon
  so multi-author posts show a correct image instead of the wrong profile
This commit is contained in:
Elie Habib
2026-03-26 12:48:56 +04:00
committed by GitHub
parent 5b08ce3788
commit 0169245f45
10 changed files with 158 additions and 37 deletions

View File

@@ -10,6 +10,10 @@ const blog = defineCollection({
keywords: z.string(),
audience: z.string(),
pubDate: z.coerce.date(),
modifiedDate: z.coerce.date().optional(),
author: z.string().optional(),
authorUrl: z.string().url().optional(),
authorBio: z.string().optional(),
heroImage: z.string().optional(),
}),
});

View File

@@ -12,9 +12,10 @@ interface Props {
section?: string;
jsonLd?: Record<string, unknown>;
breadcrumbLd?: Record<string, unknown>;
faqLd?: Record<string, unknown>;
}
const { title, description, metaTitle, keywords, ogType, ogImage, publishedTime, modifiedTime, author, section, jsonLd, breadcrumbLd } = Astro.props;
const { title, description, metaTitle, keywords, ogType, ogImage, publishedTime, modifiedTime, author, section, jsonLd, breadcrumbLd, faqLd } = Astro.props;
const pageTitle = metaTitle || title;
const resolvedOgImage = ogImage || 'https://www.worldmonitor.app/favico/og-image.png';
const resolvedOgType = ogType || 'website';
@@ -68,6 +69,9 @@ const keywordTags = keywords ? keywords.split(',').map(k => k.trim()).slice(0, 6
{breadcrumbLd && (
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbLd)} />
)}
{faqLd && (
<script type="application/ld+json" set:html={JSON.stringify(faqLd)} />
)}
<style is:global>
@import '../styles/global.css';

View File

@@ -7,12 +7,17 @@ interface Props {
metaTitle?: string;
keywords?: string;
pubDate: Date;
modifiedDate?: Date;
author?: string;
authorUrl?: string;
authorBio?: string;
audience?: string;
heroImage?: string;
slug?: string;
rawBody?: string;
}
const { title, description, metaTitle, keywords, pubDate, audience, heroImage, slug } = Astro.props;
const { title, description, metaTitle, keywords, pubDate, modifiedDate, author, authorUrl, authorBio, audience, heroImage, slug, rawBody } = Astro.props;
const ogImage = heroImage || (slug ? `https://www.worldmonitor.app/blog/images/blog/${slug}.jpg` : undefined);
const formattedDate = pubDate.toLocaleDateString('en-US', {
year: 'numeric',
@@ -20,6 +25,19 @@ const formattedDate = pubDate.toLocaleDateString('en-US', {
day: 'numeric',
});
const isoDate = pubDate.toISOString();
const isoModifiedDate = modifiedDate ? modifiedDate.toISOString() : isoDate;
const DEFAULT_AUTHOR = 'Elie Habib';
const DEFAULT_AUTHOR_URL = 'https://x.com/eliehabib';
const DEFAULT_AUTHOR_BIO = 'Founder of World Monitor. Previously co-founder &amp; CEO of Anghami (NASDAQ: ANGH). Building open-source global intelligence infrastructure.';
const DEFAULT_AUTHOR_GITHUB = 'koala73';
const authorName = author || DEFAULT_AUTHOR;
const resolvedAuthorUrl = authorUrl || (authorName === DEFAULT_AUTHOR ? DEFAULT_AUTHOR_URL : undefined);
const resolvedAuthorBio = authorBio || (authorName === DEFAULT_AUTHOR ? DEFAULT_AUTHOR_BIO : undefined);
const avatarSrc = authorName === DEFAULT_AUTHOR
? `https://unavatar.io/github/${DEFAULT_AUTHOR_GITHUB}`
: '/favico/apple-touch-icon.png';
const showUpdated = modifiedDate && modifiedDate.getTime() !== pubDate.getTime();
const formattedModifiedDate = modifiedDate ? modifiedDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '';
const breadcrumbLd = {
"@context": "https://schema.org",
@@ -41,19 +59,15 @@ const breadcrumbLd = {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
"@type": "BlogPosting",
"headline": metaTitle || title,
"description": description,
"datePublished": isoDate,
"dateModified": isoDate,
"dateModified": isoModifiedDate,
"author": {
"@type": "Organization",
"name": "World Monitor",
"url": "https://worldmonitor.app",
"logo": {
"@type": "ImageObject",
"url": "https://www.worldmonitor.app/favico/apple-touch-icon.png"
}
"@type": "Person",
"name": authorName,
...(resolvedAuthorUrl ? { "url": resolvedAuthorUrl } : {})
},
"publisher": {
"@type": "Organization",
@@ -72,6 +86,27 @@ const jsonLd = {
"url": Astro.url.href,
...(keywords ? { "keywords": keywords } : {})
};
function extractFaqLd(body: string | undefined): object | null {
if (!body) return null;
const faqIdx = body.indexOf('## Frequently Asked Questions');
if (faqIdx === -1) return null;
const afterFaq = body.slice(faqIdx + '## Frequently Asked Questions'.length);
const nextHeading = afterFaq.search(/\n##\s/);
const faqSection = nextHeading === -1 ? afterFaq : afterFaq.slice(0, nextHeading);
const qaRegex = /\*\*(.+?)\*\*\n([^\n*]+(?:\n[^\n*#]+)*)/g;
const items: { "@type": string; name: string; acceptedAnswer: { "@type": string; text: string } }[] = [];
let match: RegExpExecArray | null;
while ((match = qaRegex.exec(faqSection)) !== null) {
const q = match[1].trim();
const a = match[2].replace(/\[([^\]]+)\]\([^)]+\)/g, '$1').trim();
if (q && a) items.push({ "@type": "Question", name: q, acceptedAnswer: { "@type": "Answer", text: a } });
}
if (items.length === 0) return null;
return { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": items };
}
const faqLd = extractFaqLd(rawBody);
---
<Base
@@ -82,16 +117,19 @@ const jsonLd = {
ogType="article"
ogImage={ogImage}
publishedTime={isoDate}
modifiedTime={isoDate}
modifiedTime={isoModifiedDate}
author={authorName}
section={audience}
jsonLd={jsonLd}
breadcrumbLd={breadcrumbLd}
faqLd={faqLd ?? undefined}
>
<article>
<div class="article-header">
<a href="/blog/" class="back">&larr; All articles</a>
<div class="meta">
<time datetime={isoDate}>{formattedDate}</time>
{showUpdated && <span> &middot; Updated <time datetime={isoModifiedDate}>{formattedModifiedDate}</time></span>}
{audience && <span> &middot; {audience}</span>}
</div>
</div>
@@ -107,5 +145,17 @@ const jsonLd = {
<p>Markets, geopolitics, conflicts, infrastructure. One dashboard. No login required.</p>
<a href="https://worldmonitor.app" class="btn" target="_blank" rel="noopener noreferrer">Open Dashboard</a>
</div>
<div class="author-bio">
<img src={avatarSrc} alt={authorName} width="48" height="48" class="author-avatar" loading="lazy" onerror="this.src='/favico/apple-touch-icon.png'" />
<div class="author-info">
<div class="author-name">
{resolvedAuthorUrl
? <a href={resolvedAuthorUrl} target="_blank" rel="noopener noreferrer">{authorName}</a>
: <span>{authorName}</span>
}
</div>
{resolvedAuthorBio && <div class="author-desc" set:html={resolvedAuthorBio} />}
</div>
</div>
</article>
</Base>

View File

@@ -20,9 +20,14 @@ const { Content } = await render(post);
metaTitle={post.data.metaTitle}
keywords={post.data.keywords}
pubDate={post.data.pubDate}
modifiedDate={post.data.modifiedDate}
author={post.data.author}
authorUrl={post.data.authorUrl}
authorBio={post.data.authorBio}
audience={post.data.audience}
heroImage={post.data.heroImage}
slug={post.id}
rawBody={post.body}
>
<Content />
</BlogPost>

View File

@@ -573,6 +573,49 @@ img { max-width: 100%; height: auto; border-radius: var(--radius); }
color: var(--wm-bg);
}
/* ─── Author Bio ─── */
.author-bio {
display: flex;
align-items: flex-start;
gap: 1rem;
margin: 2.5rem auto 0;
max-width: 720px;
padding: 1.25rem 1.5rem;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
}
.author-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
}
.author-info { flex: 1; }
.author-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.3rem;
}
.author-name a {
color: var(--wm-green);
text-decoration: none;
}
.author-name a:hover { text-decoration: underline; }
.author-desc {
font-size: 0.82rem;
color: var(--text-dim);
line-height: 1.5;
}
/* ─── Responsive ─── */
@media (max-width: 768px) {
.blog-hero h1 { font-size: 2rem; }

View File

@@ -9,7 +9,7 @@
<!-- Primary Meta Tags -->
<title>World Monitor - Real-Time Global Intelligence Dashboard</title>
<meta name="title" content="World Monitor - Real-Time Global Intelligence Dashboard" />
<meta name="description" content="AI-powered real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data. OSINT in one view." />
<meta name="description" content="AI-powered real-time global intelligence dashboard with live news, markets, military tracking, and geopolitical data. OSINT in one view." />
<meta name="keywords" content="AI intelligence, AI-powered dashboard, global intelligence, geopolitical dashboard, world news, market data, military bases, nuclear facilities, undersea cables, conflict zones, real-time monitoring, situation awareness, OSINT, flight tracking, AIS ships, earthquake monitor, protest tracker, power outages, oil prices, government spending, polymarket predictions" />
<meta name="author" content="Elie Habib" />
<meta name="theme-color" content="#0a0f0a" />
@@ -75,7 +75,7 @@
"name": "World Monitor",
"alternateName": "World Monitor",
"url": "https://www.worldmonitor.app/",
"description": "Open-source real-time OSINT dashboard for geopolitical monitoring, conflict tracking, military flight tracking, maritime AIS, and global threat intelligence. Used by 2M+ people across 190+ countries.",
"description": "Open-source real-time global intelligence dashboard aggregating conflicts, military movements, markets, infrastructure, and geopolitical data. Used by 2M+ people across 190+ countries. Featured in WIRED.",
"applicationCategory": "SecurityApplication",
"operatingSystem": "Web, Windows, macOS, Linux",
"offers": [
@@ -87,7 +87,6 @@
"name": "Elie Habib",
"url": "https://x.com/eliehabib",
"jobTitle": "CEO",
"worksFor": { "@type": "Organization", "name": "Anghami", "url": "https://anghami.com" },
"sameAs": ["https://x.com/eliehabib", "https://github.com/koala73"]
},
"featureList": [

View File

@@ -14,6 +14,7 @@ const SOCIAL_IMAGE_UA =
const VARIANT_HOST_MAP: Record<string, string> = {
'tech.worldmonitor.app': 'tech',
'finance.worldmonitor.app': 'finance',
'commodity.worldmonitor.app': 'commodity',
'happy.worldmonitor.app': 'happy',
};
@@ -31,6 +32,12 @@ const VARIANT_OG: Record<string, { title: string; description: string; image: st
image: 'https://finance.worldmonitor.app/favico/finance/og-image.png',
url: 'https://finance.worldmonitor.app/',
},
commodity: {
title: 'Commodity Monitor - Real-Time Commodity Markets & Supply Chain Dashboard',
description: 'Real-time commodity markets dashboard tracking mining sites, processing plants, commodity ports, supply chains, and global commodity trade flows.',
image: 'https://commodity.worldmonitor.app/favico/commodity/og-image.png',
url: 'https://commodity.worldmonitor.app/',
},
happy: {
title: 'Happy Monitor - Good News & Global Progress',
description: 'Curated positive news, progress data, and uplifting stories from around the world.',

View File

@@ -4,7 +4,7 @@
World Monitor is an open-source (AGPL-3.0) intelligence platform that aggregates 435+ news feeds, 45+ interactive map layers, and multiple AI models into a single dashboard. Used by 2M+ people across 190+ countries, as featured in WIRED. It runs as a web app, installable PWA, and native desktop application (Tauri) for macOS, Windows, and Linux.
A single codebase produces three specialized variants — geopolitical, technology, and finance — each with distinct feeds, panels, map layers, and branding. The tri-variant architecture uses build-time selection via the VITE_VARIANT environment variable, with runtime switching available via the header bar.
A single codebase produces five specialized variants — geopolitical (World Monitor), technology (Tech Monitor), finance (Finance Monitor), commodities (Commodity Monitor), and positive news (Happy Monitor) — each with distinct feeds, panels, map layers, and branding. The multi-variant architecture uses build-time selection via the VITE_VARIANT environment variable, with runtime switching available via the header bar.
The project is built with TypeScript, Vite, MapLibre GL JS, deck.gl, D3.js, and Tauri. All intelligence analysis (clustering, instability scoring, surge detection) runs client-side in the browser — no backend compute dependency for core intelligence. A browser-side ML pipeline (Transformers.js) provides NER and sentiment analysis without server dependency.
@@ -13,6 +13,8 @@ The project is built with TypeScript, Vite, MapLibre GL JS, deck.gl, D3.js, and
- [World Monitor](https://worldmonitor.app): Geopolitics, military, conflicts, infrastructure
- [Tech Monitor](https://tech.worldmonitor.app): Startups, AI/ML, cloud, cybersecurity
- [Finance Monitor](https://finance.worldmonitor.app): Global markets, trading, central banks, Gulf FDI
- [Commodity Monitor](https://commodity.worldmonitor.app): Mining, metals, energy, supply chains, commodity markets
- [Happy Monitor](https://happy.worldmonitor.app): Positive news, breakthroughs, conservation, renewable energy
- [World Monitor Pro](https://worldmonitor.app/pro): AI-powered equity research, geopolitical analysis, macro intelligence
- [Blog](https://www.worldmonitor.app/blog/): Analysis, OSINT guides, geopolitics, and market intelligence articles
@@ -57,6 +59,26 @@ The project is built with TypeScript, Vite, MapLibre GL JS, deck.gl, D3.js, and
- **AI**: Groq (Llama 3.1), OpenRouter, browser-side T5 fallback
- **Monitoring**: Sentry error tracking, data freshness tracker across 22 sources
## Blog Posts
- [What Is World Monitor?](https://www.worldmonitor.app/blog/posts/what-is-worldmonitor-real-time-global-intelligence/): Overview of the platform, features, and use cases
- [Five Dashboards, One Platform](https://www.worldmonitor.app/blog/posts/five-dashboards-one-platform-worldmonitor-variants/): Guide to all five dashboard variants
- [Track Global Conflicts in Real Time](https://www.worldmonitor.app/blog/posts/track-global-conflicts-in-real-time/): Conflict monitoring using ACLED, UCDP, and Telegram OSINT
- [OSINT for Everyone](https://www.worldmonitor.app/blog/posts/osint-for-everyone-open-source-intelligence-democratized/): How open-source intelligence is democratized
- [Natural Disaster Monitoring](https://www.worldmonitor.app/blog/posts/natural-disaster-monitoring-earthquakes-fires-volcanoes/): USGS earthquakes, NASA FIRMS fires, volcanic activity
- [Real-Time Market Intelligence](https://www.worldmonitor.app/blog/posts/real-time-market-intelligence-for-traders-and-analysts/): Financial markets, Fear & Greed, ETF flows
- [Cyber Threat Intelligence](https://www.worldmonitor.app/blog/posts/cyber-threat-intelligence-for-security-teams/): Feodo Tracker, URLhaus, botnet C2 monitoring
- [Global Supply Chain Monitoring](https://www.worldmonitor.app/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/): Ports, chokepoints, commodity disruptions
- [Satellite Imagery and Orbital Surveillance](https://www.worldmonitor.app/blog/posts/satellite-imagery-orbital-surveillance/): SAR, EO, and OSINT satellite data
- [Live Webcams from Geopolitical Hotspots](https://www.worldmonitor.app/blog/posts/live-webcams-from-geopolitical-hotspots/): Real-time visual feeds from conflict zones
- [Prediction Markets and AI Forecasting](https://www.worldmonitor.app/blog/posts/prediction-markets-ai-forecasting-geopolitics/): Polymarket integration and geopolitical forecasting
- [World Monitor in 21 Languages](https://www.worldmonitor.app/blog/posts/worldmonitor-in-21-languages-global-intelligence-for-everyone/): Multilingual global intelligence platform
- [Command Palette Search](https://www.worldmonitor.app/blog/posts/command-palette-search-everything-instantly/): Cmd+K fuzzy search across all data layers
- [AI-Powered Intelligence Without the Cloud](https://www.worldmonitor.app/blog/posts/ai-powered-intelligence-without-the-cloud/): Local LLMs, Ollama, offline-first AI analysis
- [Build on World Monitor](https://www.worldmonitor.app/blog/posts/build-on-worldmonitor-developer-api-open-source/): Developer API, proto definitions, open-source contribution
- [World Monitor vs Traditional Intelligence Tools](https://www.worldmonitor.app/blog/posts/worldmonitor-vs-traditional-intelligence-tools/): Comparison with Bloomberg, Palantir, Dataminr, Recorded Future
- [Tracking Global Trade Routes](https://www.worldmonitor.app/blog/posts/tracking-global-trade-routes-chokepoints-freight-costs/): Maritime chokepoints, freight costs, trade flow analysis
## Optional
- [Source Code](https://github.com/koala73/worldmonitor): GitHub repository (AGPL-3.0)

View File

@@ -1,29 +1,10 @@
# World Monitor - protect API routes from crawlers
User-agent: *
Allow: /
Allow: /api/story
Allow: /api/og-story
Disallow: /api/
Disallow: /tests/
Sitemap: https://www.worldmonitor.app/sitemap.xml
Sitemap: https://www.worldmonitor.app/blog/sitemap-index.xml
# Allow social media bots for OG previews
User-agent: Twitterbot
Allow: /api/story
Allow: /api/og-story
User-agent: facebookexternalhit
Allow: /api/story
Allow: /api/og-story
User-agent: LinkedInBot
Allow: /api/story
Allow: /api/og-story
User-agent: Slackbot
Allow: /api/story
Allow: /api/og-story
User-agent: Discordbot
Allow: /api/story
Allow: /api/og-story

View File

@@ -24,6 +24,12 @@
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://commodity.worldmonitor.app/</loc>
<lastmod>2026-03-26</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://happy.worldmonitor.app/</loc>
<lastmod>2026-03-19</lastmod>