@@ -15,6 +15,13 @@ const LIST_JSON = path.join(
1515 "portfolioList.json" ,
1616) ;
1717
18+ const THUMBNAIL_DIR = path . join (
19+ ROOT ,
20+ "assets" ,
21+ "img" ,
22+ "thumbnails" ,
23+ ) ;
24+
1825function escapeHtml ( str = "" ) {
1926 return String ( str ) . replace ( / [ & < > " ] / g, ( c ) => {
2027 return {
@@ -145,12 +152,17 @@ ${bodyHtml}
145152 fs . mkdirSync ( OUTPUT_DIR , { recursive : true } ) ;
146153 }
147154
155+ if ( ! fs . existsSync ( THUMBNAIL_DIR ) ) {
156+ fs . mkdirSync ( THUMBNAIL_DIR , { recursive : true } ) ;
157+ }
158+
148159 const works = [ ] ;
149160 const files = fs . existsSync ( CONTENT_DIR )
150161 ? fs
151162 . readdirSync ( CONTENT_DIR )
152163 . filter ( ( f ) => f . endsWith ( ".md" ) )
153164 : [ ] ;
165+ const usedThumbnails = new Set ( ) ;
154166
155167 for ( const file of files ) {
156168 const id = path . basename ( file , ".md" ) ; // work_0001 など
@@ -166,7 +178,72 @@ ${bodyHtml}
166178 const category = data . category || "" ;
167179 const role = data . role || "" ;
168180 const tech = data . tech || "" ;
169- const thumbnail = data . thumbnail || "" ;
181+ let thumbnail = data . thumbnail || "" ;
182+
183+ if ( thumbnail ) {
184+ try {
185+ if ( thumbnail . startsWith ( "data:image" ) ) {
186+ const matches = thumbnail . match (
187+ / ^ d a t a : i m a g e \/ ( [ a - z A - Z 0 - 9 ] + ) ; b a s e 6 4 , ( .+ ) $ / ,
188+ ) ;
189+ if ( matches ) {
190+ const ext =
191+ matches [ 1 ] === "jpeg" ? "jpg" : matches [ 1 ] ;
192+ const filename = `${ id } .${ ext } ` ;
193+ const thumbPath = path . join (
194+ THUMBNAIL_DIR ,
195+ filename ,
196+ ) ;
197+ fs . writeFileSync (
198+ thumbPath ,
199+ Buffer . from ( matches [ 2 ] , "base64" ) ,
200+ ) ;
201+ thumbnail = `assets/img/thumbnails/${ filename } ` ;
202+ usedThumbnails . add ( filename ) ;
203+ }
204+ } else if ( thumbnail . startsWith ( "http" ) ) {
205+ const urlObj = new URL ( thumbnail ) ;
206+ let ext = path
207+ . extname ( urlObj . pathname )
208+ . substring ( 1 ) ;
209+ if ( ! ext ) ext = "png" ;
210+
211+ const filename = `${ id } .${ ext } ` ;
212+ const thumbPath = path . join (
213+ THUMBNAIL_DIR ,
214+ filename ,
215+ ) ;
216+
217+ console . log (
218+ `Downloading thumbnail for ${ id } from ${ thumbnail } ` ,
219+ ) ;
220+ const res = await fetch ( thumbnail ) ;
221+ if ( res . ok ) {
222+ const buffer = Buffer . from (
223+ await res . arrayBuffer ( ) ,
224+ ) ;
225+ fs . writeFileSync ( thumbPath , buffer ) ;
226+ thumbnail = `assets/img/thumbnails/${ filename } ` ;
227+ usedThumbnails . add ( filename ) ;
228+ } else {
229+ console . error (
230+ `Failed to fetch thumbnail for ${ id } : ${ res . statusText } ` ,
231+ ) ;
232+ }
233+ } else if (
234+ thumbnail . includes ( "assets/img/thumbnails/" )
235+ ) {
236+ const filename = path . basename ( thumbnail ) ;
237+ usedThumbnails . add ( filename ) ;
238+ }
239+ } catch ( e ) {
240+ console . error (
241+ `Failed to process thumbnail for ${ id } :` ,
242+ e ,
243+ ) ;
244+ }
245+ }
246+
170247 const tagsRaw = data . tags || [ ] ;
171248 const tags = Array . isArray ( tagsRaw )
172249 ? tagsRaw
@@ -226,6 +303,20 @@ ${bodyHtml}
226303 "utf8" ,
227304 ) ;
228305 console . log ( `updated: assets/data/portfolioList.json` ) ;
306+
307+ // Clean up unused portfolio thumbnails
308+ if ( fs . existsSync ( THUMBNAIL_DIR ) ) {
309+ const allThumbnails = fs . readdirSync ( THUMBNAIL_DIR ) ;
310+ for ( const file of allThumbnails ) {
311+ if (
312+ file . startsWith ( "portfolio_" ) &&
313+ ! usedThumbnails . has ( file )
314+ ) {
315+ console . log ( `Removing unused thumbnail: ${ file } ` ) ;
316+ fs . unlinkSync ( path . join ( THUMBNAIL_DIR , file ) ) ;
317+ }
318+ }
319+ }
229320} ) ( ) . catch ( ( err ) => {
230321 console . error ( err ) ;
231322 process . exit ( 1 ) ;
0 commit comments