Replies: 3 comments
|
Status check on 0.13.0 — partially addressed. { "@context":"https://schema.org", "@type":"BlogPosting", "headline":"…", "description":"…",
"image":"…", "url":"…", "datePublished":"…", "dateModified":"…",
"author":{"@type":"Person","name":"…"},
"publisher":{"@type":"Organization","name":"…"},
"mainEntityOfPage":{"@type":"WebPage","@id":"…"} }Still missing from the original request (so keeping this open):
We still add a manual BlogPosting block to fill these. The |
|
Tip: you can avoid the duplicate-block problem without waiting on this feature. Instead of adding your own For example, // src/plugins/enrich-blogposting/index.ts
//
// Descriptor: tells EmDash this plugin exists and how to load it.
// This file is what you import into astro.config.mjs.
import type { PluginDescriptor } from "emdash";
export function enrichBlogPostingPlugin(): PluginDescriptor {
return {
id: "enrich-blogposting",
version: "1.0.0",
format: "standard",
entrypoint: new URL("./runtime.ts", import.meta.url).pathname,
options: {},
};
}// src/plugins/enrich-blogposting/runtime.ts
//
// Hook logic: loaded via the descriptor's `entrypoint` above.
//
// The trick: core's own BlogPosting block is contributed with id: "primary".
// EmDash keeps only the FIRST metadata block it sees for a given id, and
// plugin contributions are checked before core's own defaults. Returning
// our block with the same id: "primary" replaces core's block instead of
// adding a second <script> tag next to it.
import type { PageMetadataContribution, PageMetadataEvent } from "emdash";
function cleanJsonLd(obj: Record<string, unknown>): Record<string, unknown> {
const cleaned: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (value === undefined || value === null) continue;
cleaned[key] =
typeof value === "object" && !Array.isArray(value)
? cleanJsonLd(value as Record<string, unknown>)
: value;
}
return cleaned;
}
export default {
hooks: {
"page:metadata": {
handler: (event: PageMetadataEvent): PageMetadataContribution | null => {
const { page } = event;
if (page.pageType !== "article" || !page.canonical) return null;
const ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;
const description = page.seo?.ogDescription || page.description;
const ogImage = page.seo?.ogImage || page.image || null;
const { publishedTime, modifiedTime, author } = page.articleMeta ?? {};
const siteName = page.siteName;
// Swap these for your own site's real values.
const authorUrl = "https://example.com/authors/jane-doe";
const publisherLogoUrl = "https://example.com/logo.png";
const category = "Tutorials";
return {
kind: "jsonld",
id: "primary", // must match core's id to replace, not duplicate
graph: cleanJsonLd({
"@context": "https://schema.org",
"@type": "BlogPosting",
"@id": page.canonical,
headline: ogTitle,
description,
image: ogImage || undefined,
url: page.canonical,
inLanguage: page.locale || "en",
articleSection: category,
datePublished: publishedTime || undefined,
dateModified: modifiedTime || publishedTime || undefined,
author: author
? { "@type": "Person", name: author, url: authorUrl }
: undefined,
publisher: siteName
? {
"@type": "Organization",
name: siteName,
logo: { "@type": "ImageObject", url: publisherLogoUrl },
}
: undefined,
mainEntityOfPage: { "@type": "WebPage", "@id": page.canonical },
}),
};
},
},
},
};// astro.config.mjs — register the plugin
import { enrichBlogPostingPlugin } from "./src/plugins/enrich-blogposting/index.ts";
export default defineConfig({
integrations: [
emdash({
plugins: [
// ...your other plugins
enrichBlogPostingPlugin(),
],
}),
],
}); |
|
Converted this to a discussion as it is a feature request. |
Uh oh!
There was an error while loading. Please reload this page.
Feature Request
EmDash auto-generates BlogPosting JSON-LD via
buildBlogPostingJsonLd()insrc/page/jsonld.ts. This is great — it prevents XSS bugs from hand-rolled serialization and ensures every article page has structured data.However, the generated schema is missing several fields that Google recommends for Article/BlogPosting rich results. This forces developers to add a second, manual BlogPosting block with the missing fields — creating duplicate JSON-LD blocks on every article page.
Currently generated
{ "@type": "BlogPosting", "headline": "...", "description": "...", "image": "...", "url": "...", "datePublished": "...", "dateModified": "...", "author": { "@type": "Person", "name": "..." }, "publisher": { "@type": "Organization", "name": "..." }, "mainEntityOfPage": { "@type": "WebPage", "@id": "..." } }Missing fields (recommended by Google)
@idhttps://example.com/blog/post-slug)inLanguagearticleSectionauthor.@idauthor.urlpublisher.@idpublisher.logoImageObjectwithurl)Proposal
Extend
buildBlogPostingJsonLd()andPublicPageContextto support these fields. Possible approach:@id— derive frompage.canonical(already available)inLanguage— derive frompage.locale(already available)articleSection— new optional field inarticleMetaorPublicPageContextauthor— allowarticleMeta.authorto be an object{ name, url, id }instead of just a stringpublisher— allowsiteNameto include logo URL, or add apublisherLogofieldThis way the auto-generated schema would be rich enough that developers don't need to add their own BlogPosting block, eliminating duplicates.
Current workaround
We add a manual
<script type="application/ld+json">with a complete BlogPosting in our[slug].astrotemplate. This works but creates two BlogPosting blocks per page — the EmDash one (id: "primary") and ours (raw script tag). Google handles it but it's noisy.Related
buildWebSiteJsonLd()with@id,description,inLanguage,publisher, andpotentialAction(SearchAction) — currently it only hasnameandurl.All reactions