diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index a81eac92..c15e04f8 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -217,18 +217,22 @@ export async function publishToPublication({ } }); + // Determine the rkey early since we need it for the path field + const rkey = existingDocUri ? new AtUri(existingDocUri).rkey : TID.nextStr(); + // Create record based on the document type let record: PubLeafletDocument.Record | SiteStandardDocument.Record; if (documentType === "site.standard.document") { // site.standard.document format - // For standalone docs, use a constructed site URI; for publication docs, use the publication URI + // For standalone docs, use HTTPS URL; for publication docs, use the publication AT-URI const siteUri = publication_uri || `https://leaflet.pub/p/${credentialSession.did}`; record = { $type: "site.standard.document", title: title || "Untitled", site: siteUri, + path: rkey, publishedAt: existingRecord.publishedAt || new Date().toISOString(), ...(description && { description }), ...(tags !== undefined && { tags }), @@ -257,8 +261,6 @@ export async function publishToPublication({ } satisfies PubLeafletDocument.Record; } - // Keep the same rkey if updating an existing document - let rkey = existingDocUri ? new AtUri(existingDocUri).rkey : TID.nextStr(); let { data: result } = await agent.com.atproto.repo.putRecord({ rkey, repo: credentialSession.did!, diff --git a/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts b/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts index 0e1591d1..067f8e3e 100644 --- a/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts +++ b/app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts @@ -63,7 +63,7 @@ export async function getProfilePosts( for (let doc of docs || []) { // Normalize records - filter out unrecognized formats - const normalizedData = normalizeDocumentRecord(doc.data); + const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); if (!normalizedData) continue; let pubFromDoc = doc.documents_in_publications?.[0]?.publications; diff --git a/app/(home-pages)/reader/getReaderFeed.ts b/app/(home-pages)/reader/getReaderFeed.ts index 39def812..f3ada41d 100644 --- a/app/(home-pages)/reader/getReaderFeed.ts +++ b/app/(home-pages)/reader/getReaderFeed.ts @@ -55,7 +55,7 @@ export async function getReaderFeed( let handle = await idResolver.did.resolve(uri.host); // Normalize records - filter out unrecognized formats - const normalizedData = normalizeDocumentRecord(post.data); + const normalizedData = normalizeDocumentRecord(post.data, post.uri); if (!normalizedData) return null; const normalizedPubRecord = normalizePublicationRecord(pub?.record); diff --git a/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts b/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts index b2b1eaa3..b22417e8 100644 --- a/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts +++ b/app/(home-pages)/tag/[tag]/getDocumentsByTag.ts @@ -41,7 +41,7 @@ export async function getDocumentsByTag( } // Normalize the document data - skip unrecognized formats - const normalizedData = normalizeDocumentRecord(doc.data); + const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); if (!normalizedData) { return null; } diff --git a/app/api/inngest/functions/migrate_user_to_standard.ts b/app/api/inngest/functions/migrate_user_to_standard.ts index fc473124..c27b14ec 100644 --- a/app/api/inngest/functions/migrate_user_to_standard.ts +++ b/app/api/inngest/functions/migrate_user_to_standard.ts @@ -171,7 +171,7 @@ export const migrate_user_to_standard = inngest.createFunction( } const rkey = aturi.rkey; - const normalized = normalizeDocumentRecord(doc.data); + const normalized = normalizeDocumentRecord(doc.data, doc.uri); if (!normalized) { stats.errors.push(`Document ${doc.uri}: Failed to normalize document record`); @@ -197,10 +197,10 @@ export const migrate_user_to_standard = inngest.createFunction( $type: "site.standard.document", title: normalized.title || "Untitled", site: siteValue, + path: rkey, publishedAt: normalized.publishedAt || new Date().toISOString(), description: normalized.description, content: normalized.content, - path: normalized.path, tags: normalized.tags, coverImage: normalized.coverImage, bskyPostRef: normalized.bskyPostRef, diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index a9bee7ad..bde8c685 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -70,7 +70,7 @@ export const get_publication_data = makeRoute({ const documents = (publication?.documents_in_publications || []) .map((dip) => { if (!dip.documents) return null; - const normalized = normalizeDocumentRecord(dip.documents.data); + const normalized = normalizeDocumentRecord(dip.documents.data, dip.documents.uri); if (!normalized) return null; return { uri: dip.documents.uri, diff --git a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts index 2306f759..9f7f44a2 100644 --- a/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts +++ b/app/lish/[did]/[publication]/[rkey]/getPostPageData.ts @@ -33,7 +33,7 @@ export async function getPostPageData(did: string, rkey: string) { if (!document) return null; // Normalize the document record - this is the primary way consumers should access document data - const normalizedDocument = normalizeDocumentRecord(document.data); + const normalizedDocument = normalizeDocumentRecord(document.data, document.uri); if (!normalizedDocument) return null; // Normalize the publication record - this is the primary way consumers should access publication data @@ -83,7 +83,7 @@ export async function getPostPageData(did: string, rkey: string) { // Filter and sort documents by publishedAt const sortedDocs = allDocs .map((dip) => { - const normalizedData = normalizeDocumentRecord(dip?.documents?.data); + const normalizedData = normalizeDocumentRecord(dip?.documents?.data, dip?.documents?.uri); return { uri: dip?.documents?.uri, title: normalizedData?.title, diff --git a/app/lish/[did]/[publication]/generateFeed.ts b/app/lish/[did]/[publication]/generateFeed.ts index ef5887fc..59fbed31 100644 --- a/app/lish/[did]/[publication]/generateFeed.ts +++ b/app/lish/[did]/[publication]/generateFeed.ts @@ -54,7 +54,7 @@ export async function generateFeed( await Promise.all( publication.documents_in_publications.map(async (doc) => { if (!doc.documents) return; - const record = normalizeDocumentRecord(doc.documents?.data); + const record = normalizeDocumentRecord(doc.documents?.data, doc.documents?.uri); const uri = new AtUri(doc.documents?.uri); const rkey = uri.rkey; if (!record) return; diff --git a/app/lish/feeds/[...path]/route.ts b/app/lish/feeds/[...path]/route.ts index 953df731..02743e63 100644 --- a/app/lish/feeds/[...path]/route.ts +++ b/app/lish/feeds/[...path]/route.ts @@ -37,7 +37,7 @@ export async function GET( let posts = pub.publications?.documents_in_publications || []; return posts.flatMap((p) => { if (!p.documents?.data) return []; - const normalizedDoc = normalizeDocumentRecord(p.documents.data); + const normalizedDoc = normalizeDocumentRecord(p.documents.data, p.documents.uri); if (!normalizedDoc?.bskyPostRef) return []; return { post: normalizedDoc.bskyPostRef.uri }; }); diff --git a/feeds/index.ts b/feeds/index.ts index 0bedf3d4..564dd576 100644 --- a/feeds/index.ts +++ b/feeds/index.ts @@ -136,7 +136,7 @@ app.get("/xrpc/app.bsky.feed.getFeedSkeleton", async (c) => { cursor: newCursor || cursor, feed: posts.flatMap((p) => { if (!p.data) return []; - const normalizedDoc = normalizeDocumentRecord(p.data); + const normalizedDoc = normalizeDocumentRecord(p.data, p.uri); if (!normalizedDoc?.bskyPostRef) return []; return { post: normalizedDoc.bskyPostRef.uri }; }), diff --git a/lexicons/src/normalize.ts b/lexicons/src/normalize.ts index e74da31c..9b642c3f 100644 --- a/lexicons/src/normalize.ts +++ b/lexicons/src/normalize.ts @@ -21,6 +21,7 @@ import type * as SiteStandardPublication from "../api/types/site/standard/public import type * as SiteStandardThemeBasic from "../api/types/site/standard/theme/basic"; import type * as PubLeafletThemeColor from "../api/types/pub/leaflet/theme/color"; import type { $Typed } from "../api/util"; +import { AtUri } from "@atproto/syntax"; // Normalized document type - uses the generated site.standard.document type // with an additional optional theme field for backwards compatibility @@ -143,9 +144,10 @@ export function leafletThemeToBasicTheme( * Normalizes a document record from either format to the standard format. * * @param record - The document record from the database (either pub.leaflet or site.standard) + * @param uri - Optional document URI, used to extract the rkey for the path field when normalizing pub.leaflet records * @returns A normalized document in site.standard format, or null if invalid/unrecognized */ -export function normalizeDocument(record: unknown): NormalizedDocument | null { +export function normalizeDocument(record: unknown, uri?: string): NormalizedDocument | null { if (!record || typeof record !== "object") return null; // Pass through site.standard records directly (theme is already in correct format if present) @@ -168,6 +170,9 @@ export function normalizeDocument(record: unknown): NormalizedDocument | null { // This matches the pattern used in publishToPublication.ts for new standalone docs const site = record.publication || `https://leaflet.pub/p/${record.author}`; + // Extract path from URI if available + const path = uri ? new AtUri(uri).rkey : undefined; + // Wrap pages in pub.leaflet.content structure const content: $Typed | undefined = record.pages ? { @@ -180,6 +185,7 @@ export function normalizeDocument(record: unknown): NormalizedDocument | null { $type: "site.standard.document", title: record.title, site, + path, publishedAt, description: record.description, tags: record.tags, diff --git a/src/notifications.ts b/src/notifications.ts index e4dd1406..5e0711fd 100644 --- a/src/notifications.ts +++ b/src/notifications.ts @@ -105,7 +105,7 @@ async function hydrateCommentNotifications(notifications: NotificationRow[]) { ? comments?.find((c) => c.uri === notification.data.parent_uri) : undefined, commentData, - normalizedDocument: normalizeDocumentRecord(commentData.documents?.data), + normalizedDocument: normalizeDocumentRecord(commentData.documents?.data, commentData.documents?.uri), normalizedPublication: normalizePublicationRecord( commentData.documents?.documents_in_publications[0]?.publications?.record, ), @@ -198,7 +198,7 @@ async function hydrateQuoteNotifications(notifications: NotificationRow[]) { document_uri: notification.data.document_uri, bskyPost, document, - normalizedDocument: normalizeDocumentRecord(document.data), + normalizedDocument: normalizeDocumentRecord(document.data, document.uri), normalizedPublication: normalizePublicationRecord( document.documents_in_publications[0]?.publications?.record, ), @@ -299,12 +299,12 @@ async function hydrateMentionNotifications(notifications: NotificationRow[]) { documentCreatorHandle, mentionedPublication, mentionedDocument: mentionedDoc, - normalizedDocument: normalizeDocumentRecord(document.data), + normalizedDocument: normalizeDocumentRecord(document.data, document.uri), normalizedPublication: normalizePublicationRecord( document.documents_in_publications[0]?.publications?.record, ), normalizedMentionedPublication: normalizePublicationRecord(mentionedPublication?.record), - normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data), + normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data, mentionedDoc?.uri), }; }) .filter((n) => n !== null); @@ -404,12 +404,12 @@ async function hydrateCommentMentionNotifications(notifications: NotificationRow commenterHandle, mentionedPublication, mentionedDocument: mentionedDoc, - normalizedDocument: normalizeDocumentRecord(commentData.documents?.data), + normalizedDocument: normalizeDocumentRecord(commentData.documents?.data, commentData.documents?.uri), normalizedPublication: normalizePublicationRecord( commentData.documents?.documents_in_publications[0]?.publications?.record, ), normalizedMentionedPublication: normalizePublicationRecord(mentionedPublication?.record), - normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data), + normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data, mentionedDoc?.uri), }; }) .filter((n) => n !== null); diff --git a/src/utils/normalizeRecords.ts b/src/utils/normalizeRecords.ts index 50b419f1..cc4df11d 100644 --- a/src/utils/normalizeRecords.ts +++ b/src/utils/normalizeRecords.ts @@ -17,17 +17,21 @@ import type { Json } from "supabase/database.types"; * Normalizes a document record from a database query result. * Returns the normalized document or null if the record is invalid/unrecognized. * + * @param data - The document record data from the database + * @param uri - Optional document URI, used to extract the rkey for the path field when normalizing pub.leaflet records + * * @example - * const doc = normalizeDocumentRecord(dbResult.data); + * const doc = normalizeDocumentRecord(dbResult.data, dbResult.uri); * if (doc) { * // doc is NormalizedDocument with proper typing * console.log(doc.title, doc.site, doc.publishedAt); * } */ export function normalizeDocumentRecord( - data: Json | unknown + data: Json | unknown, + uri?: string ): NormalizedDocument | null { - return normalizeDocument(data); + return normalizeDocument(data, uri); } /** @@ -69,13 +73,14 @@ export type PublicationRowWithNormalizedRecord< /** * Normalizes a document row in place, returning a properly typed row. + * If the row has a `uri` field, it will be used to extract the path. */ -export function normalizeDocumentRow( +export function normalizeDocumentRow( row: T ): DocumentRowWithNormalizedData { return { ...row, - data: normalizeDocumentRecord(row.data), + data: normalizeDocumentRecord(row.data, row.uri), }; }