diff --git a/packages/cli/src/lib/atproto.ts b/packages/cli/src/lib/atproto.ts index aa8b777..10860fb 100644 --- a/packages/cli/src/lib/atproto.ts +++ b/packages/cli/src/lib/atproto.ts @@ -263,9 +263,12 @@ export async function createDocument( path: postPath, textContent: textContent.slice(0, 10000), publishedAt: publishDate.toISOString(), - canonicalUrl: `${config.siteUrl}${postPath}`, }; + if (!config.canonicalUrlBuilder) { + record.canonicalUrl = `${config.siteUrl}${postPath}`; + } + if (post.frontmatter.description) { record.description = post.frontmatter.description; } @@ -284,7 +287,22 @@ export async function createDocument( record, }); - return response.data.uri; + const atUri = response.data.uri; + + if (config.canonicalUrlBuilder) { + const parsed = parseAtUri(atUri); + if (parsed) { + record.canonicalUrl = config.canonicalUrlBuilder(atUri, post); + await agent.com.atproto.repo.putRecord({ + repo: agent.did!, + collection: parsed.collection, + rkey: parsed.rkey, + record, + }); + } + } + + return atUri; } export async function updateDocument( @@ -330,7 +348,9 @@ export async function updateDocument( path: postPath, textContent: textContent.slice(0, 10000), publishedAt: publishDate.toISOString(), - canonicalUrl: `${config.siteUrl}${postPath}`, + canonicalUrl: config.canonicalUrlBuilder + ? config.canonicalUrlBuilder(atUri, post) + : `${config.siteUrl}${postPath}`, }; if (post.frontmatter.description) { diff --git a/packages/cli/src/lib/markdown.ts b/packages/cli/src/lib/markdown.ts index 6a6338f..560f074 100644 --- a/packages/cli/src/lib/markdown.ts +++ b/packages/cli/src/lib/markdown.ts @@ -182,6 +182,13 @@ export function getSlugFromOptions( return slug; } +export function slugifyTitle(title: string): string { + return (title || "") + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w-]/g, ""); +} + export function resolvePathTemplate(template: string, post: BlogPost): string { const publishDate = new Date(post.frontmatter.publishDate); const year = String(publishDate.getFullYear()); @@ -191,10 +198,7 @@ export function resolvePathTemplate(template: string, post: BlogPost): string { const day = String(publishDate.getDate()).padStart(2, "0"); const dayUTC = String(publishDate.getUTCDate()).padStart(2, "0"); - const slugifiedTitle = (post.frontmatter.title || "") - .toLowerCase() - .replace(/\s+/g, "-") - .replace(/[^\w-]/g, ""); + const slugifiedTitle = slugifyTitle(post.frontmatter.title); // Replace known tokens let result = template diff --git a/packages/cli/src/lib/types.ts b/packages/cli/src/lib/types.ts index 802f16f..46e205e 100644 --- a/packages/cli/src/lib/types.ts +++ b/packages/cli/src/lib/types.ts @@ -41,6 +41,7 @@ export interface PublisherConfig { stripDatePrefix?: boolean; // Remove YYYY-MM-DD- prefix from filenames (Jekyll-style, default: false) pathTemplate?: string; // URL path template with tokens like {year}/{month}/{day}/{slug} (overrides pathPrefix + slug) textContentField?: string; // Frontmatter field to use for textContent instead of markdown body + canonicalUrlBuilder?: (atUri: string, post: BlogPost) => string; bluesky?: BlueskyConfig; // Optional Bluesky posting configuration ui?: UIConfig; // Optional UI components configuration } diff --git a/packages/remanso/src/commands/publish.ts b/packages/remanso/src/commands/publish.ts index fa79b4a..536ec1c 100644 --- a/packages/remanso/src/commands/publish.ts +++ b/packages/remanso/src/commands/publish.ts @@ -16,12 +16,14 @@ import { resolveImagePath, deleteRecord, listDocuments, + parseAtUri, } from "../../../cli/src/lib/atproto"; import { scanContentDirectory, getContentHash, updateFrontmatterWithAtUri, resolvePostPath, + slugifyTitle, } from "../../../cli/src/lib/markdown"; import type { BlogPost, @@ -338,6 +340,12 @@ export const publishCommand = command({ imagesDir, pdsUrl: config.pdsUrl, pathPrefix: "", + canonicalUrlBuilder: (atUri: string, post: BlogPost) => { + const parsed = parseAtUri(atUri); + if (!parsed) throw new Error(`Invalid atUri: ${atUri}`); + const slug = slugifyTitle(post.frontmatter.title); + return `${siteUrl}/${parsed.rkey}/${slug}`; + }, }; // Publish posts