From 18f61c5fd91b3be3c7a08e77e13ba7613efa314e Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Sat, 20 Jun 2026 20:07:32 -0700 Subject: [PATCH] Remove canonicalUrl from site.standard.document Fixes #34 --- .../cli/src/components/sequoia-comments.js | 2 +- packages/cli/src/lib/atproto.ts | 7 +- packages/cli/test/atproto.test.ts | 182 ++++++++++++++++++ 3 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 packages/cli/test/atproto.test.ts diff --git a/packages/cli/src/components/sequoia-comments.js b/packages/cli/src/components/sequoia-comments.js index c1490e7..fc6ae7b 100644 --- a/packages/cli/src/components/sequoia-comments.js +++ b/packages/cli/src/components/sequoia-comments.js @@ -542,7 +542,7 @@ async function getRecord(did, collection, rkey) { /** * Fetch a document record from its AT URI * @param {string} atUri - AT Protocol URI for the document - * @returns {Promise<{$type: string, title: string, site: string, path: string, textContent: string, publishedAt: string, canonicalUrl?: string, description?: string, tags?: string[], bskyPostRef?: {uri: string, cid: string}}>} Document record + * @returns {Promise<{$type: string, title: string, site: string, path: string, textContent: string, publishedAt: string, description?: string, tags?: string[], bskyPostRef?: {uri: string, cid: string}}>} Document record */ async function getDocument(atUri) { const parsed = parseAtUri(atUri); diff --git a/packages/cli/src/lib/atproto.ts b/packages/cli/src/lib/atproto.ts index 179042d..a3881d3 100644 --- a/packages/cli/src/lib/atproto.ts +++ b/packages/cli/src/lib/atproto.ts @@ -282,7 +282,6 @@ export async function createDocument( path: postPath, textContent: textContent, publishedAt: publishDate.toISOString(), - canonicalUrl: `${config.siteUrl}${postPath}`, }; if (updatedAt) { @@ -360,16 +359,17 @@ export async function updateDocument( rkey: rkey!, }); const existingRecord = existingResponse.data.value as Record; + const recordBase = { ...existingRecord }; + delete recordBase.canonicalUrl; const record: Record = { - ...existingRecord, + ...recordBase, $type: "site.standard.document", title: post.frontmatter.title, site: config.publicationUri, path: postPath, textContent: textContent, publishedAt: publishDate.toISOString(), - canonicalUrl: `${config.siteUrl}${postPath}`, }; if (updatedAt) { @@ -421,7 +421,6 @@ export interface DocumentRecord { textContent?: string; publishedAt: string; updatedAt?: string; - canonicalUrl?: string; description?: string; coverImage?: BlobObject; tags?: string[]; diff --git a/packages/cli/test/atproto.test.ts b/packages/cli/test/atproto.test.ts new file mode 100644 index 0000000..77df70e --- /dev/null +++ b/packages/cli/test/atproto.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, it } from "bun:test"; +import type { Agent } from "@atproto/api"; +import { + createBlueskyPost, + createDocument, + updateDocument, +} from "../src/lib/atproto"; +import type { BlogPost, PublisherConfig } from "../src/lib/types"; + +function createPost(overrides: Partial = {}): BlogPost { + return { + filePath: "/tmp/hello-world.md", + slug: "hello-world", + frontmatter: { + title: "Hello World", + description: "A short description", + publishDate: "2025-01-02T03:04:05.000Z", + }, + content: "Body content", + rawContent: "---\ntitle: Hello World\n---\nBody content", + rawFrontmatter: {}, + ...overrides, + }; +} + +function createConfig( + overrides: Partial = {}, +): PublisherConfig { + return { + siteUrl: "https://example.com", + contentDir: "./content", + publicationUri: "at://did:plc:publisher/site.standard.publication/main", + publishContent: false, + ...overrides, + }; +} + +describe("ATProto document publishing", () => { + it("does not persist canonicalUrl on newly created document records", async () => { + let capturedRecord: Record | undefined; + + const agent = { + did: "did:plc:publisher", + com: { + atproto: { + repo: { + createRecord: async (input: { + repo: string; + collection: string; + record: Record; + }) => { + capturedRecord = input.record; + return { + data: { + cid: "new-cid", + uri: "at://did:plc:publisher/site.standard.document/new", + }, + }; + }, + }, + }, + }, + } as Agent; + + await createDocument(agent, createPost(), createConfig()); + + expect(capturedRecord).toBeDefined(); + expect(capturedRecord?.canonicalUrl).toBeUndefined(); + expect(capturedRecord?.path).toBe("/posts/hello-world"); + }); + + it("removes canonicalUrl from updated document records while preserving other fields", async () => { + let capturedRecord: Record | undefined; + + const agent = { + did: "did:plc:publisher", + com: { + atproto: { + repo: { + getRecord: async () => ({ + data: { + value: { + $type: "site.standard.document", + title: "Old title", + site: "at://did:plc:publisher/site.standard.publication/main", + path: "/posts/old", + publishedAt: "2024-01-01T00:00:00.000Z", + canonicalUrl: "https://example.com/posts/old", + bskyPostRef: { + uri: "at://did:plc:publisher/app.bsky.feed.post/abc", + cid: "bsky-cid", + }, + location: "keep-me", + }, + }, + }), + putRecord: async (input: { + repo: string; + collection: string; + rkey: string; + record: Record; + }) => { + capturedRecord = input.record; + return { + data: { + cid: "updated-cid", + uri: "at://did:plc:publisher/site.standard.document/existing", + }, + }; + }, + }, + }, + }, + } as unknown as Agent; + + await updateDocument( + agent, + createPost(), + "at://did:plc:publisher/site.standard.document/existing", + createConfig(), + ); + + expect(capturedRecord).toBeDefined(); + expect(capturedRecord?.canonicalUrl).toBeUndefined(); + expect(capturedRecord?.path).toBe("/posts/hello-world"); + expect(capturedRecord?.location).toBe("keep-me"); + expect(capturedRecord?.bskyPostRef).toEqual({ + uri: "at://did:plc:publisher/app.bsky.feed.post/abc", + cid: "bsky-cid", + }); + }); + + it("still uses canonicalUrl for the Bluesky external embed URL", async () => { + let capturedRecord: Record | undefined; + + const agent = { + did: "did:plc:publisher", + com: { + atproto: { + repo: { + createRecord: async (input: { + repo: string; + collection: string; + record: Record; + }) => { + capturedRecord = input.record; + return { + data: { + cid: "bsky-cid", + uri: "at://did:plc:publisher/app.bsky.feed.post/new", + }, + }; + }, + }, + }, + }, + } as Agent; + + await createBlueskyPost(agent, { + title: "Hello World", + description: "A short description", + canonicalUrl: "https://example.com/posts/hello-world", + documentRef: { + uri: "at://did:plc:publisher/site.standard.document/hello-world", + cid: "doc-cid", + }, + publicationRef: { + uri: "at://did:plc:publisher/site.standard.publication/main", + cid: "pub-cid", + }, + publishedAt: "2025-01-02T03:04:05.000Z", + }); + + expect(capturedRecord).toBeDefined(); + expect(capturedRecord?.embed).toMatchObject({ + $type: "app.bsky.embed.external", + external: { + uri: "https://example.com/posts/hello-world", + }, + }); + }); +}); -- 2.51.2