GenerAI / worldmonitor /blog-site /src /layouts /BlogPost.astro
amogaddy's picture
Fix build: ripristinate blog-site/tests/e2e/pro-test/convex (referenziate dagli script di build)
56838f4 verified
Raw
History Blame Contribute Delete
10.6 kB
---
import Base from './Base.astro';
import TrackedProductLink from '../components/TrackedProductLink.astro';
import { coverWebpSources } from '../lib/cover-webp';
interface Props {
title: string;
description: string;
metaTitle?: string;
keywords?: string;
pubDate: Date;
modifiedDate?: Date;
author?: string;
authorUrl?: string;
authorBio?: string;
audience?: string;
heroImage?: string;
slug?: string;
rawBody?: string;
relatedPosts?: { title: string; description: string; href: string }[];
}
const { title, description, metaTitle, keywords, pubDate, modifiedDate, author, authorUrl, authorBio, audience, heroImage, slug, rawBody, relatedPosts = [] } = Astro.props;
function toAbsoluteWorldMonitorUrl(pathOrUrl: string | undefined): string | undefined {
if (!pathOrUrl) return undefined;
if (/^https?:\/\//.test(pathOrUrl)) return pathOrUrl;
return new URL(pathOrUrl, 'https://www.worldmonitor.app').href;
}
const ogImage = toAbsoluteWorldMonitorUrl(heroImage || (slug ? `/blog/images/blog/${slug}.jpg` : undefined));
const canonicalUrl = slug
? `https://www.worldmonitor.app/blog/posts/${slug}/`
: new URL(Astro.url.pathname, 'https://www.worldmonitor.app').href;
const formattedDate = pubDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
const isoDate = pubDate.toISOString();
const isoModifiedDate = modifiedDate ? modifiedDate.toISOString() : isoDate;
const DEFAULT_AUTHOR = 'Elie Habib';
const DEFAULT_AUTHOR_URL = 'https://www.worldmonitor.app/blog/authors/elie-habib/';
const DEFAULT_AUTHOR_ID = `${DEFAULT_AUTHOR_URL}#person`;
const DEFAULT_AUTHOR_BIO = 'Founder of World Monitor. Previously co-founder & CEO of Anghami (NASDAQ: ANGH). Building open-source global intelligence infrastructure.';
const DEFAULT_AUTHOR_GITHUB = 'koala73';
const DEFAULT_SECTION = 'Global Intelligence';
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';
// Committed covers have generated .webp / -640.webp siblings (guarded by
// tests/blog-cover-webp.test.mjs); coverWebpSources() returns null when the
// siblings don't exist (e.g. deploy-generated /blog/og/*.png covers).
const heroWebp = coverWebpSources(heroImage);
const showUpdated = modifiedDate && modifiedDate.getTime() !== pubDate.getTime();
const formattedModifiedDate = modifiedDate ? modifiedDate.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '';
const wordCount = rawBody ? rawBody.trim().split(/\s+/).filter(Boolean).length : undefined;
const readingMinutes = wordCount ? Math.max(1, Math.ceil(wordCount / 220)) : undefined;
function extractCitations(body: string | undefined): { "@type": string; name: string; url: string }[] {
if (!body) return [];
const citations: { "@type": string; name: string; url: string }[] = [];
const seen = new Set<string>();
const linkRegex = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)(?:\s+"[^"]*")?\)/g;
let match: RegExpExecArray | null;
while ((match = linkRegex.exec(body)) !== null) {
const url = match[2];
const host = new URL(url).hostname.replace(/^www\./, '');
if (host === 'worldmonitor.app' || host.endsWith('.worldmonitor.app') || seen.has(url)) continue;
seen.add(url);
citations.push({
"@type": "CreativeWork",
"name": match[1].replace(/[*_`]/g, '').trim(),
"url": url
});
}
return citations;
}
const citations = extractCitations(rawBody);
const breadcrumbLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Blog",
"item": "https://www.worldmonitor.app/blog/"
},
{
"@type": "ListItem",
"position": 2,
"name": title,
"item": canonicalUrl
}
]
};
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"@id": `${canonicalUrl}#article`,
"headline": title,
...(metaTitle && metaTitle !== title ? { "alternativeHeadline": metaTitle } : {}),
"description": description,
"datePublished": isoDate,
"dateModified": isoModifiedDate,
"inLanguage": "en-US",
"isAccessibleForFree": true,
"articleSection": DEFAULT_SECTION,
...(audience ? {
"audience": {
"@type": "Audience",
"audienceType": audience
}
} : {}),
...(wordCount ? { "wordCount": wordCount } : {}),
...(readingMinutes ? { "timeRequired": `PT${readingMinutes}M` } : {}),
"author": {
"@type": "Person",
"name": authorName,
...(resolvedAuthorUrl ? { "url": resolvedAuthorUrl } : {}),
...(authorName === DEFAULT_AUTHOR ? {
"@id": DEFAULT_AUTHOR_ID,
"sameAs": [
"https://x.com/eliehabib",
`https://github.com/${DEFAULT_AUTHOR_GITHUB}`
]
} : {})
},
"publisher": {
"@type": "Organization",
"@id": "https://www.worldmonitor.app/#organization",
"name": "World Monitor",
"url": "https://www.worldmonitor.app",
"sameAs": [
"https://github.com/koala73/worldmonitor",
"https://x.com/worldmonitorai"
],
"logo": {
"@type": "ImageObject",
"url": "https://www.worldmonitor.app/favico/apple-touch-icon.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": canonicalUrl
},
"image": ogImage || "https://www.worldmonitor.app/favico/og-image.png",
"url": canonicalUrl,
"isPartOf": {
"@type": "Blog",
"@id": "https://www.worldmonitor.app/blog/#blog",
"name": "World Monitor Blog",
"url": "https://www.worldmonitor.app/blog/"
},
...(citations.length > 0 ? { "citation": citations } : {}),
...(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);
// The FAQ section ends at the next H2 OR at a horizontal rule — nearly
// every post closes its FAQ with `---` followed by a bold takeaway/CTA
// paragraph, which must not be extracted as a Question.
const nextHeading = afterFaq.search(/\n##\s|\n---\s*\n/);
const faqSection = nextHeading === -1 ? afterFaq : afterFaq.slice(0, nextHeading);
// \n+ (not \n): every post writes a blank line between the bolded question
// and its answer — with a single \n the regex matched nothing and NO post
// ever emitted FAQPage JSON-LD (#5001). Guarded against the real post
// corpus by tests/blog-faq-ld.test.mjs.
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
title={title}
description={description}
metaTitle={metaTitle}
keywords={keywords}
ogType="article"
ogImage={ogImage}
ogImageAlt={`${title} - World Monitor Blog`}
canonicalUrl={canonicalUrl}
publishedTime={isoDate}
modifiedTime={isoModifiedDate}
author={authorName}
section={DEFAULT_SECTION}
jsonLd={jsonLd}
breadcrumbLd={breadcrumbLd}
faqLd={faqLd ?? undefined}
attributionCampaign={slug}
>
<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>
{heroImage && (
<picture>
{heroWebp && (
<source
type="image/webp"
srcset={`${heroWebp.webp640} 640w, ${heroWebp.webp} 1200w`}
sizes="(max-width: 812px) calc(100vw - 2rem), 780px"
/>
)}
<img src={heroImage} alt={title} class="hero-image" width="1200" height="630" fetchpriority="high" decoding="async" />
</picture>
)}
<div class="article-content prose">
<h1>{title}</h1>
<p class="article-dek">{description}</p>
<slot />
</div>
{relatedPosts.length > 0 && (
<nav class="related-posts" aria-labelledby="related-posts-title">
<h2 id="related-posts-title">Related Intelligence Guides</h2>
<div class="related-post-grid">
{relatedPosts.map((post) => (
<a href={post.href} class="related-post-card">
<span>Read Next</span>
<h3>{post.title}</h3>
<p>{post.description}</p>
</a>
))}
</div>
</nav>
)}
<div class="cta-banner">
<h3>Put the Intelligence to Work</h3>
<p>Open the live dashboard free, or explore Pro for deeper analyst workflows, alerts, and AI research.</p>
<div class="cta-actions">
<TrackedProductLink
destination="dashboard"
placement="article-cta-dashboard"
campaign={slug}
class="btn"
>
Open Dashboard
</TrackedProductLink>
<TrackedProductLink
destination="pro"
placement="article-cta-pro"
campaign={slug}
class="btn btn-secondary"
>
Explore Pro
</TrackedProductLink>
</div>
</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} rel="author">{authorName}</a>
: <span>{authorName}</span>
}
</div>
{resolvedAuthorBio && <div class="author-desc" set:html={resolvedAuthorBio} />}
</div>
</div>
</article>
</Base>