Skip to content

Commit 75255f1

Browse files
authored
fix: standard.site fixes (#1901)
1 parent 0020999 commit 75255f1

File tree

10 files changed

+102
-41
lines changed

10 files changed

+102
-41
lines changed

app/components/global/BlogPostWrapper.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { BlogPostFrontmatter } from '#shared/schemas/blog'
3+
import { generateBlogTID } from '#shared/utils/atproto'
34
45
const props = defineProps<{
56
frontmatter: BlogPostFrontmatter
@@ -14,6 +15,15 @@ useSeoMeta({
1415
...(props.frontmatter.draft ? { robots: 'noindex, nofollow' } : {}),
1516
})
1617
18+
useHead({
19+
link: [
20+
{
21+
rel: 'site.standard.document',
22+
href: `at://${NPMX_DEV_DID}/site.standard.document/${generateBlogTID(props.frontmatter.date, props.frontmatter.slug)}`,
23+
},
24+
],
25+
})
26+
1727
defineOgImageComponent('BlogPost', {
1828
title: props.frontmatter.title,
1929
authors: props.frontmatter.authors,

modules/standard-site-sync.ts

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import process from 'node:process'
22
import { createHash } from 'node:crypto'
33
import { defineNuxtModule, useNuxt, createResolver } from 'nuxt/kit'
44
import { safeParse } from 'valibot'
5-
import { BlogPostSchema, type BlogPostFrontmatter } from '../shared/schemas/blog'
6-
import { NPMX_SITE } from '../shared/utils/constants'
5+
import { BlogPostSchema, type BlogPostFrontmatter } from '#shared/schemas/blog'
6+
import { NPMX_DEV_DID, NPMX_SITE } from '#shared/utils/constants'
77
import { read } from 'gray-matter'
8-
import { TID } from '@atproto/common'
98
import { PasswordSession } from '@atproto/lex-password-session'
109
import {
1110
Client,
@@ -15,11 +14,9 @@ import {
1514
} from '@atproto/lex'
1615
import * as com from '../shared/types/lexicons/com'
1716
import * as site from '../shared/types/lexicons/site'
17+
import { generateBlogTID, npmxPublicationRkey } from '#shared/utils/atproto'
1818

1919
const syncedDocuments = new Map<string, string>()
20-
const CLOCK_ID_THREE = 3
21-
const MS_TO_MICROSECONDS = 1000
22-
const ONE_DAY_MILLISECONDS = 86400000
2320

2421
type BlogPostDocument = Pick<
2522
BlogPostFrontmatter,
@@ -89,6 +86,8 @@ export default defineNuxtModule({
8986
rkey: possiblePublication.tid,
9087
},
9188
)
89+
// Wait for the firehose and indexers to catch up if we create a publication
90+
await new Promise(sleepResolve => setTimeout(sleepResolve, 2_000))
9291
}
9392
if (documentsToSync.length > 0) {
9493
await syncsiteStandardDocuments(authenticatedClient, documentsToSync)
@@ -153,23 +152,6 @@ async function syncsiteStandardDocuments(client: Client, documentsToSync: Docume
153152
console.log('[standard-site-sync] synced all new publications')
154153
}
155154

156-
// Parse date from frontmatter, add file-path entropy for same-date collision resolution
157-
function generateTID(dateString: string, filePath: string): string {
158-
let timestamp = new Date(dateString).getTime()
159-
160-
// If date has no time component (exact midnight), add file-based entropy
161-
// This ensures unique TIDs when multiple posts share the same date
162-
if (timestamp % ONE_DAY_MILLISECONDS === 0) {
163-
// Hash the file path to generate deterministic microseconds offset
164-
const pathHash = createHash('md5').update(filePath).digest('hex')
165-
const offset = parseInt(pathHash.slice(0, 8), 16) % 1000000 // 0-999999 microseconds
166-
timestamp += offset
167-
}
168-
169-
// Clock id(3) needs to be the same everytime to get the same TID from a timestamp
170-
return TID.fromTime(timestamp * MS_TO_MICROSECONDS, CLOCK_ID_THREE).str
171-
}
172-
173155
// Schema expects 'path' & frontmatter provides 'slug'
174156
function normalizeBlogFrontmatter(frontmatter: Record<string, unknown>): Record<string, unknown> {
175157
return {
@@ -187,12 +169,13 @@ function createContentHash(data: unknown): string {
187169

188170
function buildATProtoDocument(siteUrl: string, data: BlogPostDocument) {
189171
return site.standard.document.$build({
190-
site: siteUrl as `${string}:${string}`,
172+
site: `at://${NPMX_DEV_DID}/site.standard.publication/${npmxPublicationRkey()}`,
191173
path: data.path,
192174
title: data.title,
193175
description: data.description ?? data.excerpt,
194176
tags: data.tags,
195-
publishedAt: new Date(data.date).toISOString(),
177+
// Publish on the record with the current date
178+
publishedAt: new Date().toISOString(),
196179
})
197180
}
198181

@@ -242,7 +225,7 @@ const syncFile = async (
242225
return
243226
}
244227

245-
const tid = generateTID(data.date, filePath)
228+
const tid = generateBlogTID(data.date, data.slug)
246229

247230
let checkForBlogResult = await pdsPublicClient.xrpcSafe(com.atproto.repo.getRecord, {
248231
params: {
@@ -275,11 +258,7 @@ const syncFile = async (
275258
* @returns
276259
*/
277260
const checkPublication = async (identifier: AtIdentifierString, pdsPublicClient: Client) => {
278-
// Using our release date as the tid for the publication
279-
const publicationTid = TID.fromTime(
280-
new Date('2026-03-03').getTime() * MS_TO_MICROSECONDS,
281-
CLOCK_ID_THREE,
282-
).str
261+
const publicationTid = npmxPublicationRkey()
283262

284263
//Check to see if we have a publication yet
285264
const publicationCheck = await pdsPublicClient.xrpcSafe(com.atproto.repo.getRecord, {
@@ -302,8 +281,11 @@ const checkPublication = async (identifier: AtIdentifierString, pdsPublicClient:
302281
tid: publicationTid,
303282
record: site.standard.publication.$build({
304283
name: 'npmx.dev',
305-
url: 'https://npmx.dev/blog',
284+
url: 'https://npmx.dev',
306285
description: 'a fast, modern browser for the npm registry',
286+
preferences: {
287+
showInDiscover: true,
288+
},
307289
}),
308290
}
309291
}

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ export default defineNuxtConfig({
133133
'/opensearch.xml': { isr: true },
134134
'/oauth-client-metadata.json': { prerender: true },
135135
'/.well-known/jwks.json': { prerender: true },
136+
'/.well-known/site.standard.publication': { prerender: true },
136137
// never cache
137138
'/api/auth/**': { isr: false, cache: false },
138139
'/api/social/**': { isr: false, cache: false },

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
},
5555
"dependencies": {
5656
"@atcute/bluesky-richtext-segmenter": "3.0.0",
57+
"@atcute/tid": "1.1.2",
5758
"@atproto/api": "^0.19.0",
58-
"@atproto/common": "0.5.13",
5959
"@atproto/lex": "0.0.19",
6060
"@atproto/lex-password-session": "0.0.7",
6161
"@atproto/oauth-client-node": "^0.3.15",

pnpm-lock.yaml

Lines changed: 32 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { npmxPublicationRkey } from '#shared/utils/atproto'
2+
3+
export default defineEventHandler(async event => {
4+
setResponseHeader(event, 'content-type', 'text/plain')
5+
return `at://${NPMX_DEV_DID}/site.standard.publication/${npmxPublicationRkey()}`
6+
})

server/utils/atproto/utils/likes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Backlink } from '#shared/utils/constellation'
33
import type * as blue from '#shared/types/lexicons/blue'
44
import * as dev from '#shared/types/lexicons/dev'
55
import { Client } from '@atproto/lex'
6-
import { TID } from '@atproto/common'
6+
import * as TID from '@atcute/tid'
77

88
//Cache keys and helpers
99
const CACHE_PREFIX = 'atproto-likes:'
@@ -26,8 +26,8 @@ export function aggregateBacklinksByDay(
2626
const countsByDay = new Map<string, number>()
2727
for (const backlink of backlinks) {
2828
try {
29-
const tid = TID.fromStr(backlink.rkey)
30-
const timestampMs = tid.timestamp() / 1000
29+
const { timestamp } = TID.parse(backlink.rkey)
30+
const timestampMs = timestamp / 1000
3131
const date = new Date(timestampMs)
3232
const day = date.toISOString().slice(0, 10)
3333
countsByDay.set(day, (countsByDay.get(day) ?? 0) + 1)

shared/utils/atproto.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { TID_CLOCK_ID } from './constants'
2+
import * as TID from '@atcute/tid'
3+
4+
const ONE_DAY_MILLISECONDS = 86400000
5+
const MS_TO_MICROSECONDS = 1000
6+
7+
// A very simple hasher to get an offset for blog posts on the same day
8+
const simpleHash = (str: string): number => {
9+
let h = 0
10+
for (let i = 0; i < str.length; i++) {
11+
h = ((h << 5) - h + str.charCodeAt(i)) >>> 0
12+
}
13+
return h
14+
}
15+
16+
// Parse date from frontmatter, add slug-path entropy for same-date collision resolution
17+
export const generateBlogTID = (dateString: string, slug: string): string => {
18+
let timestamp = new Date(dateString).getTime()
19+
20+
if (timestamp % ONE_DAY_MILLISECONDS === 0) {
21+
const offset = simpleHash(slug) % 1000000
22+
timestamp += offset
23+
}
24+
25+
// Clock id(3) needs to be the same everytime to get the same TID from a timestamp
26+
return TID.create(timestamp * MS_TO_MICROSECONDS, TID_CLOCK_ID)
27+
}
28+
29+
// Using our release date as the tid for the publication
30+
export const npmxPublicationRkey = () =>
31+
TID.create(new Date('2026-03-03').getTime() * MS_TO_MICROSECONDS, TID_CLOCK_ID)

shared/utils/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const PACKAGE_SUBJECT_REF = (packageName: string) =>
5151
// OAuth scopes as we add new ones we need to check these on certain actions. If not redirect the user to login again to upgrade the scopes
5252
export const LIKES_SCOPE = `repo:${dev.npmx.feed.like.$nsid}`
5353
export const PROFILE_SCOPE = `repo:${dev.npmx.actor.profile.$nsid}`
54+
export const NPMX_DEV_DID = 'did:plc:u5zp7npt5kpueado77kuihyz'
55+
export const TID_CLOCK_ID = 3
5456

5557
// Discord
5658
export const DISCORD_COMMUNITY_URL = 'https://chat.npmx.dev'

test/unit/server/utils/likes-evolution.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it, vi, beforeEach, type Mocked } from 'vitest'
2-
import { TID } from '@atproto/common'
2+
import * as TID from '@atcute/tid'
33
import type { ConstellationLike } from '../../../../server/utils/atproto/utils/likes'
44
import type { CacheAdapter } from '../../../../server/utils/cache/shared'
55

@@ -18,7 +18,7 @@ const { aggregateBacklinksByDay, PackageLikesUtils } =
1818

1919
function tidFromDate(date: Date): string {
2020
const microseconds = date.getTime() * 1000
21-
return TID.fromTime(microseconds, 0).toString()
21+
return TID.create(microseconds, 0).toString()
2222
}
2323

2424
function backlink(date: Date): { did: string; collection: string; rkey: string } {

0 commit comments

Comments
 (0)