@@ -16,6 +16,9 @@ interface ContentDocument {
1616 episode ?: number ;
1717 devto ?: string ;
1818 hashnode ?: string ;
19+ awsbuilder ?: string ;
20+ tags ?: Array < { _key ?: string ; label ?: string ; value ?: string } > ;
21+ sponsor ?: Array < { _ref : string } > ;
1922 syndications ?: Array < { _type : "reference" ; _ref : string ; _key ?: string } > ;
2023}
2124
@@ -104,10 +107,25 @@ export const handler = documentEventHandler<ContentDocument>(
104107 } ;
105108
106109 const contentMarkdown = serializeContent ( data . content ) ;
107- const platforms : Array < "devto" | "hashnode" > = [ "devto" , "hashnode" ] ;
110+ const docTags = Array . isArray ( data . tags ) ? data . tags : [ ] ;
111+ const hasAwsTag = docTags . some (
112+ ( t ) =>
113+ t ?. value ?. toLowerCase ( ) === "aws" || t ?. label ?. toLowerCase ( ) === "aws" ,
114+ ) ;
115+ const isSponsored = Array . isArray ( data . sponsor ) && data . sponsor . length > 0 ;
116+ const shouldSyndicateAws =
117+ ( hasAwsTag && ! isSponsored ) ||
118+ Boolean ( data . awsbuilder ) ||
119+ process . env . FORCE_AWS_BUILDER === "true" ;
120+
121+ const platforms : Array < "devto" | "hashnode" | "awsbuilder" > = [
122+ "devto" ,
123+ "hashnode" ,
124+ ...( shouldSyndicateAws ? ( [ "awsbuilder" ] as const ) : [ ] ) ,
125+ ] ;
108126
109127 console . log (
110- `[Syndicate Content] Processing ${ data . _type } (${ data . _id } ) on dataset: ${ dataset } . Production mode: ${ isProduction } ` ,
128+ `[Syndicate Content] Processing ${ data . _type } (${ data . _id } ) on dataset: ${ dataset } . Production mode: ${ isProduction } . Platforms: ${ platforms . join ( ", " ) } ` ,
111129 ) ;
112130
113131 const parentPatch : Record < string , unknown > = { } ;
@@ -122,6 +140,12 @@ export const handler = documentEventHandler<ContentDocument>(
122140 ) ;
123141
124142 try {
143+ const simHost =
144+ platform === "devto"
145+ ? "dev.to"
146+ : platform === "hashnode"
147+ ? "hashnode.com"
148+ : "builder.aws.com" ;
125149 await client . createOrReplace ( {
126150 _id : syndicationDocId ,
127151 _type : "syndication" ,
@@ -132,7 +156,7 @@ export const handler = documentEventHandler<ContentDocument>(
132156 platform,
133157 status : "simulated_complete" ,
134158 canonicalUrl,
135- externalUrl : `https://${ platform === "devto" ? "dev.to" : "hashnode.com" } /preview-simulated/${ slug } ` ,
159+ externalUrl : `https://${ simHost } /preview-simulated/${ slug } ` ,
136160 externalId : `simulated-${ data . _id } -${ platform } ` ,
137161 syndicatedAt : new Date ( ) . toISOString ( ) ,
138162 error : undefined ,
@@ -170,10 +194,22 @@ export const handler = documentEventHandler<ContentDocument>(
170194 continue ;
171195 }
172196
173- const devtoTags = [ "webdev" , "javascript" , "beginners" ] ;
174- if ( data . _type === "podcast" ) {
175- devtoTags . push ( "podcast" ) ;
176- }
197+ const normalizedDocTags = docTags
198+ . map ( ( t ) =>
199+ ( t ?. value || t ?. label || "" )
200+ . toLowerCase ( )
201+ . replace ( / [ ^ a - z 0 - 9 ] / g, "" ) ,
202+ )
203+ . filter ( ( v ) => v . length >= 2 && v . length <= 20 ) ;
204+
205+ const devtoTags = Array . from (
206+ new Set ( [
207+ ...( normalizedDocTags . length > 0
208+ ? normalizedDocTags . slice ( 0 , 3 )
209+ : [ "webdev" , "javascript" , "programming" ] ) ,
210+ ...( data . _type === "podcast" ? [ "podcast" ] : [ ] ) ,
211+ ] ) ,
212+ ) . slice ( 0 , 4 ) ;
177213
178214 const mainImageUrl =
179215 urlForImage ( data . coverImage ) ?. width ( 1280 ) . url ( ) || "" ;
@@ -317,13 +353,30 @@ export const handler = documentEventHandler<ContentDocument>(
317353 ? data . excerpt . substring ( 0 , 247 ) + "..."
318354 : data . excerpt || "" ;
319355
320- const hashnodeTags : Array < { name : string ; slug : string } > = [
321- { name : "JavaScript" , slug : "javascript" } ,
322- { name : "Web Development" , slug : "web-development" } ,
323- { name : "Beginner Developers" , slug : "beginners" } ,
324- ] ;
325-
326- if ( data . _type === "podcast" ) {
356+ const mappedHashnodeTags : Array < { name : string ; slug : string } > =
357+ docTags
358+ . map ( ( t ) => ( {
359+ name : t ?. label || t ?. value || "" ,
360+ slug : ( t ?. value || t ?. label || "" )
361+ . toLowerCase ( )
362+ . replace ( / [ ^ a - z 0 - 9 - ] / g, "-" ) ,
363+ } ) )
364+ . filter ( ( t ) => t . name && t . slug )
365+ . slice ( 0 , 4 ) ;
366+
367+ const hashnodeTags : Array < { name : string ; slug : string } > =
368+ mappedHashnodeTags . length > 0
369+ ? mappedHashnodeTags
370+ : [
371+ { name : "JavaScript" , slug : "javascript" } ,
372+ { name : "Web Development" , slug : "web-development" } ,
373+ { name : "Beginner Developers" , slug : "beginners" } ,
374+ ] ;
375+
376+ if (
377+ data . _type === "podcast" &&
378+ ! hashnodeTags . some ( ( t ) => t . slug === "podcast" )
379+ ) {
327380 hashnodeTags . push ( {
328381 name : "podcast" ,
329382 slug : "podcast" ,
@@ -343,7 +396,9 @@ export const handler = documentEventHandler<ContentDocument>(
343396 publicationId,
344397 slug : `${ data . _type } -${ slug } ` ,
345398 tags : hashnodeTags ,
346- ...( hashnodeCoverImageUrl ? { coverImage : hashnodeCoverImageUrl } : { } ) ,
399+ ...( hashnodeCoverImageUrl
400+ ? { coverImageOptions : { coverImageURL : hashnodeCoverImageUrl } }
401+ : { } ) ,
347402 originalArticleURL : canonicalUrl ,
348403 contentMarkdown : `Original: ${ canonicalUrl } \n\n${ youtubeEmbed } ${ contentMarkdown } ` ,
349404 } ;
@@ -364,7 +419,7 @@ export const handler = documentEventHandler<ContentDocument>(
364419
365420 if ( existingHashnodeSlug && ! existingHashnodeSlug . startsWith ( "simulated-" ) ) {
366421 // Lookup post ID from Hashnode
367- const getPostRes = await fetch ( "https://gql-beta .hashnode.com" , {
422+ const getPostRes = await fetch ( "https://gql.hashnode.com" , {
368423 method : "POST" ,
369424 headers : {
370425 "Content-Type" : "application/json" ,
@@ -373,7 +428,7 @@ export const handler = documentEventHandler<ContentDocument>(
373428 body : JSON . stringify ( {
374429 operationName : "Publication" ,
375430 query : `query Publication {
376- publication(host : "hashnode.codingcat.dev ") {
431+ publication(id : "${ publicationId } ") {
377432 post(slug: "${ existingHashnodeSlug } ") {
378433 id
379434 }
@@ -390,7 +445,7 @@ export const handler = documentEventHandler<ContentDocument>(
390445 const updateInput = { ...articleInput } ;
391446 delete updateInput . slug ; // slug cannot be updated
392447
393- const updateRes = await fetch ( "https://gql-beta .hashnode.com" , {
448+ const updateRes = await fetch ( "https://gql.hashnode.com" , {
394449 method : "POST" ,
395450 headers : {
396451 "Content-Type" : "application/json" ,
@@ -424,7 +479,7 @@ export const handler = documentEventHandler<ContentDocument>(
424479
425480 if ( ! publishedPost ) {
426481 // Create post mutation
427- const publishRes = await fetch ( "https://gql-beta .hashnode.com" , {
482+ const publishRes = await fetch ( "https://gql.hashnode.com" , {
428483 method : "POST" ,
429484 headers : {
430485 "Content-Type" : "application/json" ,
@@ -487,6 +542,205 @@ export const handler = documentEventHandler<ContentDocument>(
487542 `[Syndicate Content] Successfully published to Hashnode: ${ externalUrl } ` ,
488543 ) ;
489544 }
545+
546+ if ( platform === "awsbuilder" ) {
547+ const awsSessionToken = process . env . AWS_BUILDER_SESSION_TOKEN ;
548+ if ( ! awsSessionToken ) {
549+ console . warn (
550+ "[Syndicate Content] AWS_BUILDER_SESSION_TOKEN not configured. Skipping AWS Builder Center dispatch." ,
551+ ) ;
552+ continue ;
553+ }
554+
555+ const awsHeaders = {
556+ "Content-Type" : "application/json" ,
557+ Accept : "application/json" ,
558+ "builder-session-token" : awsSessionToken ,
559+ } ;
560+
561+ const existingSyndication = await client
562+ . getDocument ( syndicationDocId )
563+ . catch ( ( ) => null ) ;
564+
565+ const urlContentIdMatch = data . awsbuilder ?. match (
566+ / b u i l d e r \. a w s \. c o m \/ c o n t e n t \/ ( [ A - Z a - z 0 - 9 ] + ) / ,
567+ ) ;
568+ let contentId =
569+ ( existingSyndication ?. externalId &&
570+ ! existingSyndication . externalId . startsWith ( "simulated-" )
571+ ? existingSyndication . externalId
572+ : null ) ||
573+ ( urlContentIdMatch ? urlContentIdMatch [ 1 ] : null ) ;
574+ let versionId : string | null = null ;
575+
576+ if ( contentId ) {
577+ const editInitRes = await fetch (
578+ "https://api.builder.aws.com/cs/article/InitializeEditArticle" ,
579+ {
580+ method : "POST" ,
581+ headers : awsHeaders ,
582+ body : JSON . stringify ( { contentId } ) ,
583+ } ,
584+ ) ;
585+ if ( editInitRes . ok ) {
586+ const editInitJson = ( await editInitRes . json ( ) ) as any ;
587+ versionId = editInitJson ?. versionId || null ;
588+ }
589+ }
590+
591+ if ( ! contentId || ! versionId ) {
592+ const initRes = await fetch (
593+ "https://api.builder.aws.com/cs/article/InitializeArticle" ,
594+ {
595+ method : "POST" ,
596+ headers : awsHeaders ,
597+ body : JSON . stringify ( { } ) ,
598+ } ,
599+ ) ;
600+ if ( ! initRes . ok ) {
601+ throw new Error (
602+ `AWS Builder InitializeArticle failed (${ initRes . status } ): ${ await initRes . text ( ) } ` ,
603+ ) ;
604+ }
605+ const initJson = ( await initRes . json ( ) ) as any ;
606+ contentId = initJson ?. contentId ;
607+ versionId = initJson ?. versionId ;
608+ }
609+
610+ // Upload cover image if present
611+ let coverImageUploadKey : string | undefined ;
612+ const coverImageUrl =
613+ urlForImage ( data . coverImage ) ?. width ( 1280 ) . format ( "jpg" ) . url ( ) || "" ;
614+ if ( coverImageUrl ) {
615+ const imgRes = await fetch ( coverImageUrl ) ;
616+ if ( imgRes . ok ) {
617+ const imgBuf = await imgRes . arrayBuffer ( ) ;
618+ const presignRes = await fetch (
619+ "https://api.builder.aws.com/cs/upload/generate-presigned-post" ,
620+ {
621+ method : "POST" ,
622+ headers : awsHeaders ,
623+ body : JSON . stringify ( {
624+ contentId,
625+ fileName : `${ slug } -cover.jpg` ,
626+ contentType : "image/jpeg" ,
627+ uploadType : "ARTICLE" ,
628+ } ) ,
629+ } ,
630+ ) ;
631+ if ( presignRes . ok ) {
632+ const presignData = ( await presignRes . json ( ) ) as any ;
633+ const formData = new FormData ( ) ;
634+ for ( const [ k , v ] of Object . entries (
635+ presignData . presignedPostFields || { } ,
636+ ) ) {
637+ formData . append ( k , String ( v ) ) ;
638+ }
639+ formData . append (
640+ "file" ,
641+ new Blob ( [ imgBuf ] , { type : "image/jpeg" } ) ,
642+ `${ slug } -cover.jpg` ,
643+ ) ;
644+ const s3Res = await fetch ( presignData . presignedPostUrl , {
645+ method : "POST" ,
646+ body : formData ,
647+ } ) ;
648+ if ( s3Res . ok || s3Res . status === 204 ) {
649+ coverImageUploadKey = presignData . uploadKey ;
650+ }
651+ }
652+ }
653+ }
654+
655+ const awsTags = Array . from (
656+ new Set (
657+ docTags
658+ . map ( ( t ) => ( t ?. value || t ?. label || "" ) . toLowerCase ( ) )
659+ . filter ( Boolean )
660+ . slice ( 0 , 5 ) ,
661+ ) ,
662+ ) ;
663+ if ( awsTags . length === 0 ) awsTags . push ( "aws" ) ;
664+
665+ const youtubeMd = data . youtube
666+ ? `Watch on YouTube: [${ data . youtube } ](${ data . youtube } )\n\n`
667+ : "" ;
668+
669+ const draftPayload : any = {
670+ contentId,
671+ versionId,
672+ title : data . title || "Untitled" ,
673+ summary : ( data . excerpt || data . title || "" ) . slice ( 0 , 300 ) ,
674+ contentBody : `*Originally published at [${ canonicalUrl } ](${ canonicalUrl } )*\n\n${ youtubeMd } ${ contentMarkdown } ` ,
675+ canonicalUrl,
676+ preferredUrlSlug : slug ,
677+ tags : awsTags ,
678+ autoSave : false ,
679+ ...( coverImageUploadKey
680+ ? {
681+ coverImageUploadKey,
682+ coverImageAlt : data . title || "Cover image" ,
683+ }
684+ : { } ) ,
685+ } ;
686+
687+ const saveRes = await fetch (
688+ "https://api.builder.aws.com/cs/article/SaveArticleDraft" ,
689+ {
690+ method : "POST" ,
691+ headers : awsHeaders ,
692+ body : JSON . stringify ( draftPayload ) ,
693+ } ,
694+ ) ;
695+ if ( ! saveRes . ok ) {
696+ throw new Error (
697+ `AWS Builder SaveArticleDraft failed (${ saveRes . status } ): ${ await saveRes . text ( ) } ` ,
698+ ) ;
699+ }
700+
701+ await fetch ( "https://api.builder.aws.com/cs/article/SubmissionCheck" , {
702+ method : "POST" ,
703+ headers : awsHeaders ,
704+ body : JSON . stringify ( { contentId, versionId } ) ,
705+ } ) ;
706+
707+ const pubRes = await fetch (
708+ "https://api.builder.aws.com/cs/article/PublishArticle" ,
709+ {
710+ method : "POST" ,
711+ headers : awsHeaders ,
712+ body : JSON . stringify ( { contentId, versionId } ) ,
713+ } ,
714+ ) ;
715+ if ( ! pubRes . ok ) {
716+ throw new Error (
717+ `AWS Builder PublishArticle failed (${ pubRes . status } ): ${ await pubRes . text ( ) } ` ,
718+ ) ;
719+ }
720+
721+ const externalUrl = `https://builder.aws.com/content/${ contentId } /${ slug } ` ;
722+
723+ await client . createOrReplace ( {
724+ _id : syndicationDocId ,
725+ _type : "syndication" ,
726+ target : { _type : "reference" , _ref : data . _id } ,
727+ platform : "awsbuilder" ,
728+ status : "published" ,
729+ canonicalUrl,
730+ externalUrl,
731+ externalId : String ( contentId ) ,
732+ syndicatedAt : new Date ( ) . toISOString ( ) ,
733+ error : undefined ,
734+ } ) ;
735+
736+ if ( data . awsbuilder !== externalUrl ) {
737+ parentPatch . awsbuilder = externalUrl ;
738+ }
739+
740+ console . log (
741+ `[Syndicate Content] Successfully published to AWS Builder Center: ${ externalUrl } ` ,
742+ ) ;
743+ }
490744 } catch ( publishErr ) {
491745 const errMsg =
492746 publishErr instanceof Error ? publishErr . message : String ( publishErr ) ;
0 commit comments