@@ -95,6 +95,32 @@ async function putRecord(
9595 return res . json ( ) as Promise < { uri : string } > ;
9696}
9797
98+ type AtprotoBlob = {
99+ $type : 'blob' ;
100+ ref : { $link : string } ;
101+ mimeType : string ;
102+ size : number ;
103+ } ;
104+
105+ async function uploadBlob (
106+ session : Session ,
107+ data : Buffer ,
108+ mimeType : string
109+ ) : Promise < AtprotoBlob > {
110+ const res = await fetch ( `${ PDS_URL } /xrpc/com.atproto.repo.uploadBlob` , {
111+ method : 'POST' ,
112+ headers : {
113+ 'Content-Type' : mimeType ,
114+ Authorization : `Bearer ${ session . accessJwt } ` ,
115+ } ,
116+ // Node.js fetch accepts Buffer at runtime; cast needed due to strict lib types
117+ body : data as unknown as BodyInit ,
118+ } ) ;
119+ if ( ! res . ok ) throw new Error ( `uploadBlob failed: ${ await res . text ( ) } ` ) ;
120+ const json = ( await res . json ( ) ) as { blob : AtprotoBlob } ;
121+ return json . blob ;
122+ }
123+
98124// ─── Local data helpers ──────────────────────────────────────────────────────
99125
100126type AtprotoData = {
@@ -141,7 +167,11 @@ function readContentDirectory(dir: string): ContentFrontmatter[] {
141167 . map ( ( file ) => {
142168 const raw = fs . readFileSync ( path . join ( dirPath , file ) , 'utf-8' ) ;
143169 const { data } = matter ( raw ) ;
144- return data as ContentFrontmatter ;
170+ const fm = data as ContentFrontmatter ;
171+ // Always use filename-derived slug to match Next.js routing, which uses
172+ // filenames not frontmatter slugs when building static paths.
173+ fm . slug = path . basename ( file , path . extname ( file ) ) ;
174+ return fm ;
145175 } )
146176 . filter (
147177 ( fm ) : fm is ContentFrontmatter =>
@@ -161,15 +191,27 @@ const CREATE_ONLY = process.argv.includes('--create-only');
161191// When present, only those files are synced instead of the full directory scan.
162192const TARGET_PATHS = process . argv . slice ( 2 ) . filter ( ( a ) => ! a . startsWith ( '--' ) ) ;
163193
194+ const ICON_PATH = path . join ( process . cwd ( ) , 'public/images/publication-icon.png' ) ;
195+
164196async function syncPublication (
165197 session : Session ,
166198 data : AtprotoData
167199) : Promise < string > {
200+ let icon : AtprotoBlob | undefined ;
201+ if ( fs . existsSync ( ICON_PATH ) ) {
202+ const imgData = fs . readFileSync ( ICON_PATH ) ;
203+ icon = await uploadBlob ( session , imgData , 'image/png' ) ;
204+ console . log ( ' Uploaded publication icon blob' ) ;
205+ } else {
206+ console . log ( ` No icon found at ${ ICON_PATH } , skipping` ) ;
207+ }
208+
168209 const record = buildPublicationRecord ( {
169210 url : BASE_SITE_URL ,
170- name : 'Mike Bifulco ' ,
211+ name : '💌 Tiny Improvements - for builders, by @MikeBifulco.com ' ,
171212 description :
172213 'Resources for modern software designers and developers. Tips and walkthroughs on React, node, and javascript.' ,
214+ icon,
173215 } ) ;
174216
175217 if ( data . publicationUri ) {
@@ -244,13 +286,11 @@ async function main() {
244286 const raw = fs . readFileSync ( path . join ( process . cwd ( ) , filePath ) , 'utf-8' ) ;
245287 const { data : fm } = matter ( raw ) ;
246288 const frontmatter = fm as ContentFrontmatter ;
289+ // Use filename as slug, consistent with Next.js static path generation.
290+ frontmatter . slug = path . basename ( filePath , path . extname ( filePath ) ) ;
247291
248- if (
249- ! frontmatter . slug ||
250- ! frontmatter . title ||
251- frontmatter . published === false
252- ) {
253- console . log ( ` Skipped (unpublished or missing slug): ${ filePath } ` ) ;
292+ if ( ! frontmatter . title || frontmatter . published === false ) {
293+ console . log ( ` Skipped (unpublished or missing title): ${ filePath } ` ) ;
254294 continue ;
255295 }
256296
0 commit comments