Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/article-ispartof-array.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 7 additions & 2 deletions packages/seo-graph-core/src/pieces/article.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
18 changes: 18 additions & 0 deletions packages/seo-graph-core/test/pieces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down