diff --git a/.changeset/article-ispartof-array.md b/.changeset/article-ispartof-array.md new file mode 100644 index 0000000..0715cf6 --- /dev/null +++ b/.changeset/article-ispartof-array.md @@ -0,0 +1,14 @@ +--- +'@jdevalk/seo-graph-core': minor +--- + +**`buildArticle` now accepts an array for `isPartOf`.** + +`ArticleInput.isPartOf` was typed as a single `Reference`, but the shipped +"Personal blog" recipe in `AGENTS.md` links a posting to both its `WebPage` +and the `Blog` via `isPartOf: [{ '@id': webPage }, { '@id': blog }]`. The +builder already emitted the value verbatim at runtime, so the array worked — +but the type rejected it, forcing callers to add an `as` cast. + +The input type is now `Reference | Reference[]`. No runtime change; existing +single-reference callers are unaffected. diff --git a/packages/seo-graph-core/src/pieces/article.ts b/packages/seo-graph-core/src/pieces/article.ts index e60820e..1a1f5ff 100644 --- a/packages/seo-graph-core/src/pieces/article.ts +++ b/packages/seo-graph-core/src/pieces/article.ts @@ -25,8 +25,13 @@ export type ArticleType = interface ArticleCoreFields extends CreativeWorkFields { /** Canonical URL of the article's page. The @id is `${url}#article`. */ url: string; - /** Reference to the enclosing WebPage (usually ids.webPage(url)). */ - isPartOf: Reference; + /** + * Reference to the enclosing entity, usually the WebPage + * (`ids.webPage(url)`). Pass an array to link the Article to more than + * one parent — e.g. both its WebPage and a Blog + * (`[{ '@id': ids.webPage(url) }, { '@id': blogId }]`). + */ + isPartOf: Reference | Reference[]; /** Author reference. May include a `name` alongside the `@id`. */ author: Reference; /** Publisher reference. Usually the same as the author for personal blogs. */ diff --git a/packages/seo-graph-core/test/pieces.test.ts b/packages/seo-graph-core/test/pieces.test.ts index cd2727c..158fd45 100644 --- a/packages/seo-graph-core/test/pieces.test.ts +++ b/packages/seo-graph-core/test/pieces.test.ts @@ -398,6 +398,24 @@ describe('buildArticle', () => { expect(article.wordCount).toBe(100); expect(article.articleBody).toBe('Hello world'); }); + + it('accepts an array of isPartOf references and emits them verbatim', () => { + const blogId = `${siteUrl}#blog`; + const isPartOf = [{ '@id': ids.webPage(postUrl) }, { '@id': blogId }]; + const article = buildArticle( + { + url: postUrl, + isPartOf, + author: { '@id': ids.person }, + publisher: { '@id': ids.person }, + headline: 'Hello', + description: 'World', + datePublished: new Date('2026-01-01T00:00:00.000Z'), + }, + ids, + ); + expect(article.isPartOf).toEqual(isPartOf); + }); }); describe('buildBreadcrumbList', () => {