@@ -6,6 +6,30 @@ import SchemaLD from '../../components/SchemaLD.astro';
66
77const blogPosts = await db .select ().from (BlogPosts ).orderBy (desc (BlogPosts .pubDate ));
88
9+ // Static external articles (not stored in database)
10+ const externalArticles = [
11+ {
12+ slug: ' netdriver-the-new-netmiko-medium' ,
13+ title: ' NetDriver, the New Netmiko: A Modern Approach to Network Automation' ,
14+ description: ' Discover how NetDriver revolutionizes network automation with REST API, persistent sessions, command queuing, and async architecture - addressing Netmiko\' s limitations while building on its strengths.' ,
15+ pubDate: new Date (' 2025-11-06' ),
16+ updatedDate: null ,
17+ heroImage: null ,
18+ tags: [' netdriver' , ' netmiko' , ' network-automation' , ' ssh' , ' rest-api' , ' asyncssh' , ' scaling-automation' ],
19+ body: ' ' ,
20+ externalLink: ' https://medium.com/@skycloudinet/netdriver-the-new-netmiko-bbbad90302db' ,
21+ isExternalOnly: true , // Flag to indicate this links directly to external source
22+ },
23+ ];
24+
25+ // Merge database posts with external articles and sort by date
26+ type BlogPost = typeof blogPosts [0 ] | typeof externalArticles [0 ];
27+ const allPosts: BlogPost [] = [... blogPosts , ... externalArticles ].sort ((a , b ) => {
28+ const dateA = a .pubDate instanceof Date ? a .pubDate : new Date (a .pubDate );
29+ const dateB = b .pubDate instanceof Date ? b .pubDate : new Date (b .pubDate );
30+ return dateB .getTime () - dateA .getTime ();
31+ });
32+
933const siteURL = Astro .site || ' https://opensecflow.io' ;
1034
1135// CollectionPage Schema for blog listing
@@ -17,18 +41,20 @@ const collectionPageSchema = {
1741 url: new URL (' /blog' , siteURL ).toString (),
1842 mainEntity: {
1943 ' @type' : ' ItemList' ,
20- numberOfItems: blogPosts .length ,
21- itemListElement: blogPosts .map ((post , index ) => ({
44+ numberOfItems: allPosts .length ,
45+ itemListElement: allPosts .map ((post , index ) => ({
2246 ' @type' : ' ListItem' ,
2347 position: index + 1 ,
2448 item: {
2549 ' @type' : ' TechArticle' ,
26- ' @id' : new URL (` /blog/${post .slug } ` , siteURL ).toString (),
50+ ' @id' : (' isExternalOnly' in post && (post as BlogPost & { isExternalOnly? : boolean }).isExternalOnly && ' externalLink' in post && (post as BlogPost & { externalLink? : string }).externalLink )
51+ ? (post as BlogPost & { externalLink? : string }).externalLink !
52+ : new URL (` /blog/${post .slug } ` , siteURL ).toString (),
2753 headline: post .title ,
2854 description: post .description ,
29- datePublished: post .pubDate .toISOString (),
55+ datePublished: ( post .pubDate instanceof Date ? post . pubDate : new Date ( post . pubDate )) .toISOString (),
3056 ... (post .updatedDate && {
31- dateModified: post .updatedDate .toISOString (),
57+ dateModified: ( post .updatedDate instanceof Date ? post . updatedDate : new Date ( post . updatedDate )) .toISOString (),
3258 }),
3359 ... (post .heroImage && {
3460 image: new URL (post .heroImage , siteURL ).toString (),
@@ -84,69 +110,85 @@ const collectionPageSchema = {
84110 </p >
85111 </div >
86112
87- { blogPosts .length === 0 ? (
113+ { allPosts .length === 0 ? (
88114 <div class = " text-center py-12" >
89115 <p class = " text-slate-blue-600" >
90116 No blog posts yet. Check back soon!
91117 </p >
92118 </div >
93119 ) : (
94120 <div class = " grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" >
95- { blogPosts .map ((post ) => (
96- <a
97- href = { ` /blog/${post .slug } ` }
98- class = " bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 border-2 border-slate-blue-200 hover:border-sky-400 transform hover:-translate-y-1 group"
99- >
100- { post .heroImage && (
101- <div class = " aspect-video bg-slate-blue-100 overflow-hidden" >
102- <img
103- src = { post .heroImage }
104- alt = { post .title }
105- class = " w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
106- loading = " lazy"
107- width = " 400"
108- height = " 225"
109- />
110- </div >
111- )}
112- <div class = " p-6" >
113- <h2 class = " text-xl font-semibold text-navy-800 mb-2 group-hover:text-sky-500 transition-colors" >
114- { post .title }
115- </h2 >
116- <p class = " text-slate-blue-600 text-sm mb-4 line-clamp-2" >
117- { post .description }
118- </p >
119- <div class = " flex items-center justify-between text-sm text-slate-blue-500 mb-3" >
120- <time datetime = { post .pubDate .toISOString ()} >
121- { post .pubDate .toLocaleDateString (' en-US' , {
122- year: ' numeric' ,
123- month: ' long' ,
124- day: ' numeric' ,
125- })}
126- </time >
127- { post .tags && Array .isArray (post .tags ) && post .tags .length > 0 && (
128- <span class = " text-sky-500 font-medium" >
129- { post .tags [0 ]}
130- </span >
121+ { allPosts .map ((post ) => {
122+ const postDate = post .pubDate instanceof Date ? post .pubDate : new Date (post .pubDate );
123+ const postWithExtras = post as BlogPost & { isExternalOnly? : boolean ; externalLink? : string };
124+ const isExternalOnly = postWithExtras .isExternalOnly && postWithExtras .externalLink ;
125+ const cardHref: string = isExternalOnly && postWithExtras .externalLink ? postWithExtras .externalLink : ` /blog/${post .slug } ` ;
126+
127+ return (
128+ <a
129+ href = { cardHref }
130+ target = { isExternalOnly ? ' _blank' : undefined }
131+ rel = { isExternalOnly ? ' noopener noreferrer' : undefined }
132+ class = " bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 border-2 border-slate-blue-200 hover:border-sky-400 transform hover:-translate-y-1 group"
133+ >
134+ { post .heroImage && (
135+ <div class = " aspect-video bg-slate-blue-100 overflow-hidden" >
136+ <img
137+ src = { post .heroImage }
138+ alt = { post .title }
139+ class = " w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
140+ loading = " lazy"
141+ width = " 400"
142+ height = " 225"
143+ />
144+ </div >
145+ )}
146+ <div class = " p-6" >
147+ <h2 class = " text-xl font-semibold text-navy-800 mb-2 group-hover:text-sky-500 transition-colors" >
148+ { post .title }
149+ </h2 >
150+ <p class = " text-slate-blue-600 text-sm mb-4 line-clamp-2" >
151+ { post .description }
152+ </p >
153+ <div class = " flex items-center justify-between text-sm text-slate-blue-500 mb-3" >
154+ <time datetime = { postDate .toISOString ()} >
155+ { postDate .toLocaleDateString (' en-US' , {
156+ year: ' numeric' ,
157+ month: ' long' ,
158+ day: ' numeric' ,
159+ })}
160+ </time >
161+ { post .tags && Array .isArray (post .tags ) && post .tags .length > 0 && (
162+ <span class = " text-sky-500 font-medium" >
163+ { post .tags [0 ]}
164+ </span >
165+ )}
166+ </div >
167+ { isExternalOnly ? (
168+ <div class = " inline-flex items-center gap-1 text-xs text-sky-600 font-medium" >
169+ <svg class = " w-4 h-4" fill = " none" stroke = " currentColor" viewBox = " 0 0 24 24" xmlns = " http://www.w3.org/2000/svg" >
170+ <path stroke-linecap = " round" stroke-linejoin = " round" stroke-width = " 2" d = " M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" ></path >
171+ </svg >
172+ Read on Medium
173+ </div >
174+ ) : postWithExtras .externalLink && (
175+ <a
176+ href = { postWithExtras .externalLink as string }
177+ target = " _blank"
178+ rel = " noopener noreferrer"
179+ class = " inline-flex items-center gap-1 text-xs text-sky-600 hover:text-sky-700 font-medium"
180+ onclick = " event.stopPropagation()"
181+ >
182+ <svg class = " w-4 h-4" fill = " none" stroke = " currentColor" viewBox = " 0 0 24 24" xmlns = " http://www.w3.org/2000/svg" >
183+ <path stroke-linecap = " round" stroke-linejoin = " round" stroke-width = " 2" d = " M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" ></path >
184+ </svg >
185+ Original Article
186+ </a >
131187 )}
132188 </div >
133- { post .externalLink && (
134- <a
135- href = { post .externalLink }
136- target = " _blank"
137- rel = " noopener noreferrer"
138- class = " inline-flex items-center gap-1 text-xs text-sky-600 hover:text-sky-700 font-medium"
139- onclick = " event.stopPropagation()"
140- >
141- <svg class = " w-4 h-4" fill = " none" stroke = " currentColor" viewBox = " 0 0 24 24" xmlns = " http://www.w3.org/2000/svg" >
142- <path stroke-linecap = " round" stroke-linejoin = " round" stroke-width = " 2" d = " M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" ></path >
143- </svg >
144- Original Article
145- </a >
146- )}
147- </div >
148- </a >
149- ))}
189+ </a >
190+ );
191+ })}
150192 </div >
151193 )}
152194 </div >
0 commit comments