From e2a8c664ae9af8f64a920ccdfd531687041446a9 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:33:27 +0200 Subject: [PATCH] remove standard site publication --- apps/web/src/lib/atproto/methods.ts | 17 +- .../web/src/lib/atproto/server/repo.remote.ts | 3 +- .../lib/cards/special/UpdatedBlentos/index.ts | 6 +- apps/web/src/lib/contrail.config.ts | 3 +- apps/web/src/lib/helpers/save.test.ts | 84 +++++++++ apps/web/src/lib/helpers/save.ts | 39 ++-- apps/web/src/lib/website/data/load.ts | 46 ++--- .../lib/website/edit/EditableWebsite.svelte | 2 +- .../website/settings/SettingsOverlay.svelte | 5 +- .../sections/CustomDomainSection.svelte | 167 ++++++++++++++---- .../(pages)/p/[[page]]/copy/+page.svelte | 19 +- .../[[actor=actor]]/blog/+layout.server.ts | 11 +- .../blog/layout.server.test.ts | 66 +++++++ .../src/routes/api/activate-domain/+server.ts | 36 +++- 14 files changed, 395 insertions(+), 109 deletions(-) create mode 100644 apps/web/src/lib/helpers/save.test.ts create mode 100644 apps/web/src/routes/[[actor=actor]]/blog/layout.server.test.ts diff --git a/apps/web/src/lib/atproto/methods.ts b/apps/web/src/lib/atproto/methods.ts index 93e9ba6..e268939 100644 --- a/apps/web/src/lib/atproto/methods.ts +++ b/apps/web/src/lib/atproto/methods.ts @@ -103,13 +103,22 @@ export async function getBlentoOrBskyProfile(data: { did: Did; client?: Client } let blentoProfile; try { blentoProfile = await getRecord({ - collection: 'site.standard.publication', - did: data?.did, + collection: 'app.blento.page', + did: data.did, rkey: 'blento.self', - client: data?.client + client: data.client }); } catch { - // User doesn't have a blento publication — expected for most users + try { + blentoProfile = await getRecord({ + collection: 'site.standard.publication', + did: data.did, + rkey: 'blento.self', + client: data.client + }); + } catch { + // No Blento page yet. + } } let response; diff --git a/apps/web/src/lib/atproto/server/repo.remote.ts b/apps/web/src/lib/atproto/server/repo.remote.ts index facff98..a765b9b 100644 --- a/apps/web/src/lib/atproto/server/repo.remote.ts +++ b/apps/web/src/lib/atproto/server/repo.remote.ts @@ -48,7 +48,8 @@ export const putRecord = command( status: response.status, data: response.data }); - error(500, 'Failed to put record'); + const message = (response.data as { message?: string } | undefined)?.message; + error(response.status >= 400 ? response.status : 500, message || 'Failed to put record'); } // Immediately index in contrail diff --git a/apps/web/src/lib/cards/special/UpdatedBlentos/index.ts b/apps/web/src/lib/cards/special/UpdatedBlentos/index.ts index b0a5e53..23270b6 100644 --- a/apps/web/src/lib/cards/special/UpdatedBlentos/index.ts +++ b/apps/web/src/lib/cards/special/UpdatedBlentos/index.ts @@ -48,7 +48,11 @@ function extractProfiles( } } - if (p.collection === 'site.standard.publication' && value) { + if ( + (p.collection === 'app.blento.page' || p.collection === 'site.standard.publication') && + value && + (p.collection === 'app.blento.page' || !existing.hasBlento) + ) { existing.hasBlento = true; existing.displayName = (value.name as string) ?? existing.displayName; existing.url = value.url as string | undefined; diff --git a/apps/web/src/lib/contrail.config.ts b/apps/web/src/lib/contrail.config.ts index cb6a245..50ef24a 100644 --- a/apps/web/src/lib/contrail.config.ts +++ b/apps/web/src/lib/contrail.config.ts @@ -30,7 +30,8 @@ export const config: ContrailConfig = { }, profiles: [ 'app.bsky.actor.profile', - { collection: 'site.standard.publication', rkey: 'blento.self' }, + { collection: 'site.standard.publication', rkey: 'blento.self' }, // legacy + { collection: 'app.blento.page', rkey: 'blento.self' }, { collection: 'app.nearhorizon.actor.pronouns', rkey: 'self' } ] }; diff --git a/apps/web/src/lib/helpers/save.test.ts b/apps/web/src/lib/helpers/save.test.ts new file mode 100644 index 0000000..7886f5d --- /dev/null +++ b/apps/web/src/lib/helpers/save.test.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { WebsiteData } from '../types'; + +const { putRecord, deleteRecord } = vi.hoisted(() => ({ + putRecord: vi.fn().mockResolvedValue({}), + deleteRecord: vi.fn().mockResolvedValue(true) +})); + +vi.mock('$lib/atproto', () => ({ putRecord, deleteRecord })); +vi.mock('../cards', () => ({ CardDefinitionsByType: {} })); +vi.mock('$env/dynamic/public', () => ({ env: { PUBLIC_ENABLE_NODE_MIGRATION: 'false' } })); + +import { savePage } from './save'; + +function page(page: string): WebsiteData { + return { + page, + did: 'did:plc:example', + handle: 'example.test', + cards: [], + sections: [], + publication: { name: 'Example', description: 'Bio', preferences: { accentColor: 'red' } }, + profile: { did: 'did:plc:example', handle: 'example.test' } as WebsiteData['profile'], + additionalData: {}, + updatedAt: 0 + }; +} + +beforeEach(() => { + putRecord.mockClear(); + deleteRecord.mockClear(); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({})); +}); + +describe('savePage metadata', () => { + it('writes the home page to app.blento.page even if metadata has not changed', async () => { + const data = page('blento.self'); + await savePage(data, [], [], JSON.stringify(data.publication)); + + expect(putRecord).toHaveBeenCalledWith({ + collection: 'app.blento.page', + rkey: 'blento.self', + record: expect.objectContaining({ name: 'Example', url: 'https://blento.app/example.test' }) + }); + expect(putRecord).not.toHaveBeenCalledWith( + expect.objectContaining({ collection: 'site.standard.publication' }) + ); + }); + + it('deletes only the legacy blento.self publication record after writing the home page', async () => { + const data = page('blento.self'); + await savePage(data, [], [], JSON.stringify(data.publication)); + + expect(deleteRecord).toHaveBeenCalledTimes(1); + expect(deleteRecord).toHaveBeenCalledWith({ + collection: 'site.standard.publication', + rkey: 'blento.self' + }); + expect(putRecord.mock.invocationCallOrder[0]).toBeLessThan( + deleteRecord.mock.invocationCallOrder[0] + ); + }); + + it('keeps the legacy record if writing the home page fails', async () => { + putRecord.mockRejectedValueOnce(new Error('PDS down')); + const data = page('blento.self'); + await expect(savePage(data, [], [], JSON.stringify(data.publication))).rejects.toThrow(); + expect(deleteRecord).not.toHaveBeenCalled(); + }); + + it('does not touch site.standard.publication when saving a sub-page', async () => { + const data = page('blento.about'); + await savePage(data, [], [], ''); + expect(deleteRecord).not.toHaveBeenCalled(); + }); + + it('writes changed sub-page metadata to app.blento.page', async () => { + const data = page('blento.about'); + await savePage(data, [], [], ''); + expect(putRecord).toHaveBeenCalledWith( + expect.objectContaining({ collection: 'app.blento.page', rkey: 'blento.about' }) + ); + }); +}); diff --git a/apps/web/src/lib/helpers/save.ts b/apps/web/src/lib/helpers/save.ts index 43060c2..45cdc8e 100644 --- a/apps/web/src/lib/helpers/save.ts +++ b/apps/web/src/lib/helpers/save.ts @@ -106,14 +106,13 @@ export async function savePage( data.publication.preferences.hideProfileSection = data.publication?.preferences?.hideProfile; } - // With nodes on, the root record moves to app.blento.page for all pages and the legacy - // site.standard.publication store is retired on first migration. Legacy path keeps v1's split - // (app.blento.page for sub-pages, site.standard.publication for the main page). - const needsRootMigration = useNodes && data.page === 'blento.self' && !data.migratedStorage; + // The home page belongs in app.blento.page, too. site.standard.publication requires a + // TID rkey, so blento.self cannot be written there (even when using legacy cards/sections). + // Always write the page record on save to migrate older home pages as well. if ( !originalPublication || originalPublication !== JSON.stringify(data.publication) || - needsRootMigration + data.page === 'blento.self' ) { data.publication ??= { name: getName(data), @@ -131,22 +130,22 @@ export async function savePage( } } - if (useNodes || data.page !== 'blento.self') { - promises.push( - putRecord({ collection: 'app.blento.page', rkey: data.page, record: data.publication }) - ); - } else { - promises.push( - putRecord({ - collection: 'site.standard.publication', - rkey: data.page, - record: data.publication - }) - ); - } - if (needsRootMigration) { + const pagePut = putRecord({ + collection: 'app.blento.page', + rkey: data.page, + record: data.publication + }); + promises.push(pagePut); + + // Retire the legacy home record once its replacement is written. Only Blento's own + // blento.self rkey: other apps keep their site.standard.publication records. + if (data.page === 'blento.self') { promises.push( - deleteRecord({ collection: 'site.standard.publication', rkey: data.page }).catch(() => {}) + pagePut.then(() => + deleteRecord({ collection: 'site.standard.publication', rkey: 'blento.self' }).catch( + () => {} + ) + ) ); } diff --git a/apps/web/src/lib/website/data/load.ts b/apps/web/src/lib/website/data/load.ts index 2776672..b2870aa 100644 --- a/apps/web/src/lib/website/data/load.ts +++ b/apps/web/src/lib/website/data/load.ts @@ -75,7 +75,10 @@ function extractProfileData( if (p.collection === 'app.bsky.actor.profile' && value) { bskyRecord = value; } - if (p.collection === 'site.standard.publication' && value) { + if (p.collection === 'site.standard.publication' && value && !pubRecord) { + pubRecord = value; + } + if (p.collection === 'app.blento.page' && p.rkey === 'blento.self' && value) { pubRecord = value; } if (p.collection === 'app.nearhorizon.actor.pronouns' && value) { @@ -227,12 +230,18 @@ function getPronounsFromPDS(did: Did) { }).catch(() => undefined) as Promise; } -function getSelfPublicationFromPDS(did: Did) { - return getRecord({ - did, - collection: 'site.standard.publication', - rkey: 'blento.self' - }).catch(() => undefined); +async function getSelfPublicationFromPDS(did: Did) { + const page = await getRecord({ did, collection: 'app.blento.page', rkey: 'blento.self' }).catch( + () => undefined + ); + return ( + page ?? + (await getRecord({ + did, + collection: 'site.standard.publication', + rkey: 'blento.self' + }).catch(() => undefined)) + ); } export async function loadData( @@ -334,11 +343,9 @@ export async function loadData( pronounsRecord = pronouns; } - // If no publication found from contrail profiles, check page records - if (!publication) { - const pubFromPages = pageRecords.find((v) => parseUri(v.uri)?.rkey === 'blento.' + page); - publication = pubFromPages?.value as WebsiteData['publication'] | undefined; - } + // Page records take precedence over the legacy publication profile, including on the home page. + const pubFromPages = pageRecords.find((v) => parseUri(v.uri)?.rkey === fullPage); + if (pubFromPages?.value) publication = pubFromPages.value as WebsiteData['publication']; publication ??= defaultPublication(profile); @@ -472,15 +479,12 @@ export async function loadCardData( const card = migrateCard(structuredClone(cardValue)); const page = card.page ?? 'blento.self'; - // For non-self pages, publication comes from app.blento.page (not in contrail profiles). - if (!publication || page !== 'blento.self') { - const pubRecord = await getRecord({ - did, - collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page', - rkey: page - }).catch(() => undefined); - if (pubRecord?.value) publication = pubRecord.value as WebsiteData['publication']; - } + // Page metadata lives in app.blento.page; retain a fallback for old home pages. + const pubRecord = + page === 'blento.self' + ? await getSelfPublicationFromPDS(did) + : await getRecord({ did, collection: 'app.blento.page', rkey: page }).catch(() => undefined); + if (pubRecord?.value) publication = pubRecord.value as WebsiteData['publication']; const cards = [card]; const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did); diff --git a/apps/web/src/lib/website/edit/EditableWebsite.svelte b/apps/web/src/lib/website/edit/EditableWebsite.svelte index 37afeb4..138a1cd 100644 --- a/apps/web/src/lib/website/edit/EditableWebsite.svelte +++ b/apps/web/src/lib/website/edit/EditableWebsite.svelte @@ -553,7 +553,7 @@ {/if} - + diff --git a/apps/web/src/lib/website/settings/SettingsOverlay.svelte b/apps/web/src/lib/website/settings/SettingsOverlay.svelte index 67023a2..ea96b70 100644 --- a/apps/web/src/lib/website/settings/SettingsOverlay.svelte +++ b/apps/web/src/lib/website/settings/SettingsOverlay.svelte @@ -15,8 +15,7 @@ import AccountSection from './sections/AccountSection.svelte'; import AnalyticsSection from './sections/AnalyticsSection.svelte'; - let { data = $bindable(), publicationUrl }: { data: WebsiteData; publicationUrl?: string } = - $props(); + let { data = $bindable() }: { data: WebsiteData } = $props(); $effect(() => { if (settingsOverlayState.visible) { @@ -85,7 +84,7 @@ {:else if settingsOverlayState.activeSection === 'layout'} {:else if settingsOverlayState.activeSection === 'domain'} - + {:else if settingsOverlayState.activeSection === 'analytics'} {:else if settingsOverlayState.activeSection === 'account'} diff --git a/apps/web/src/lib/website/settings/sections/CustomDomainSection.svelte b/apps/web/src/lib/website/settings/sections/CustomDomainSection.svelte index d785391..6e5173c 100644 --- a/apps/web/src/lib/website/settings/sections/CustomDomainSection.svelte +++ b/apps/web/src/lib/website/settings/sections/CustomDomainSection.svelte @@ -1,48 +1,116 @@