Skip to content

Commit d2b827f

Browse files
author
mark pancake
committed
feat: made Tutorial page display cards with tutorials
1 parent c6dfc4d commit d2b827f

File tree

8 files changed

+219
-351
lines changed

8 files changed

+219
-351
lines changed

src/components/EngineeringChallenges.astro

Lines changed: 0 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -29,89 +29,7 @@ const challenges: Challenge[] = [
2929
];
3030
---
3131

32-
<section class="py-16 bg-gradient-to-b from-white to-slate-blue-50">
33-
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
34-
<div class="text-center mb-12">
35-
<h2 class="text-3xl font-bold text-navy-800 mb-4">
36-
Engineering Challenges Solved
37-
</h2>
38-
<p class="text-xl text-slate-blue-600 max-w-3xl mx-auto">
39-
NetDriver addresses the core challenges that we faced when automating infrastructure at scale.Challenges that you won't have to face by using NetDriver you don't have to.
40-
</p>
41-
</div>
4232

43-
<div class="space-y-4">
44-
{challenges.map((challenge, index) => (
45-
<article
46-
class="bg-white rounded-xl shadow-lg border-2 border-slate-blue-200 overflow-hidden hover:shadow-xl transition-all duration-300"
47-
x-data={`{ open: ${index === 0 ? 'true' : 'false'} }`}
48-
>
49-
<button
50-
class="w-full px-6 py-5 text-left flex items-center justify-between hover:bg-slate-blue-50 transition-colors focus:outline-none"
51-
x-on:click="open = !open"
52-
aria-expanded="false"
53-
aria-controls={`challenge-${index}`}
54-
>
55-
<div class="flex-1">
56-
<div class="flex items-start gap-4">
57-
<div class="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-sky-400 to-sky-600 rounded-lg flex items-center justify-center text-white font-bold text-lg">
58-
{index + 1}
59-
</div>
60-
<div class="flex-1">
61-
<div class="flex items-center gap-3 mb-2">
62-
<span class="px-3 py-1 bg-red-100 text-red-700 rounded-full text-sm font-semibold">
63-
Problem
64-
</span>
65-
<h3 class="text-lg font-semibold text-navy-800">
66-
{challenge.problem}
67-
</h3>
68-
</div>
69-
<div class="flex items-center gap-3">
70-
<span class="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm font-semibold">
71-
Solution
72-
</span>
73-
<p class="text-lg font-semibold text-sky-600">
74-
{challenge.solution}
75-
</p>
76-
</div>
77-
</div>
78-
</div>
79-
</div>
80-
<svg
81-
class="flex-shrink-0 w-6 h-6 text-slate-blue-500 transition-transform duration-200"
82-
x-bind:class="{ 'rotate-180': open }"
83-
fill="none"
84-
stroke="currentColor"
85-
viewBox="0 0 24 24"
86-
>
87-
<path
88-
stroke-linecap="round"
89-
stroke-linejoin="round"
90-
stroke-width="2"
91-
d="M19 9l-7 7-7-7"
92-
/>
93-
</svg>
94-
</button>
95-
<div
96-
id={`challenge-${index}`}
97-
class="px-6 pb-5 pl-20"
98-
x-show="open"
99-
x-transition:enter="transition ease-out duration-200"
100-
x-transition:enter-start="opacity-0 transform -translate-y-2"
101-
x-transition:enter-end="opacity-100 transform translate-y-0"
102-
x-transition:leave="transition ease-in duration-150"
103-
x-transition:leave-start="opacity-100 transform translate-y-0"
104-
x-transition:leave-end="opacity-0 transform -translate-y-2"
105-
>
106-
<p class="text-slate-blue-700 leading-relaxed">
107-
{challenge.solutionDetails}
108-
</p>
109-
</div>
110-
</article>
111-
))}
112-
</div>
113-
</div>
114-
</section>
11533

11634
<script>
11735
// Alpine.js is not included, so we'll use vanilla JavaScript for accordion functionality

src/components/FeaturesGrid.astro

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ const { features } = Astro.props;
1919
Netdriver Framework
2020
</h2>
2121
<p class="text-xl text-white max-w-2xl mx-auto font-bold">
22-
Netdriver is the flagship project of OpenSecFlow community.It's an free open-source NetDevOps framework based on Netmiko but with extra quality-of-life features that sets Netdriver apart.It can be seperated in to Netdriver Agent
23-
- Netdriver Simunet both of them have their own use cases.
22+
Netdriver is the flagship project of OpenSecFlow community.It's an free open-source NetDevOps framework based on Netmiko but with extra quality-of-life features that sets Netdriver apart.
2423
</p>
2524
</div>
2625

src/components/Hero.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const { title, subtitle, ctaText = 'Learn More', ctaLink = '#features' } = Astro
122122
}
123123
}
124124
</style>
125-
<p class="text-2xl text-end text-slate-blue-500 font-medium">Network Automation</p>
125+
<p class="text-2xl text-end text-slate-blue-500 font-medium">Open Source Cybersecurity and Automation</p>
126126
</div>
127127
</div>
128128
</div>

src/components/Navigation.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
const currentPath = Astro.url.pathname;
33
const navItems = [
4-
{ href: '/videos', label: 'Content' },
5-
{ href: '/', label: 'Home' },
4+
{ href: '/', label: 'Home' },
65
{ href: '/blog', label: 'Blog' },
6+
{ href: '/videos', label: 'Content' },
77
];
88
99
const tutorialSections = [

src/content.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ const videos = defineCollection({
1818
}),
1919
});
2020

21+
const tutorials = defineCollection({
22+
type: 'content',
23+
schema: z.object({
24+
// No frontmatter required - metadata parsed from content
25+
}),
26+
});
27+
2128
export const collections = {
2229
videos,
30+
tutorials,
2331
};
2432

src/pages/blog/index.astro

Lines changed: 101 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ import SchemaLD from '../../components/SchemaLD.astro';
66
77
const 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+
933
const 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

Comments
 (0)