Skip to content

Commit 4200d56

Browse files
author
mark pancake
committed
feat: Fixed Netdriber tutorial wording and adjusted page
1 parent 915acd9 commit 4200d56

2 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
---
2+
import { getCollection } from 'astro:content';
3+
import MainLayout from '../../../layouts/MainLayout.astro';
4+
import SchemaLD from '../../../components/SchemaLD.astro';
5+
import BreadcrumbList from '../../../components/schemas/BreadcrumbList.astro';
6+
import { marked } from 'marked';
7+
import { extractTitle, extractSummary } from '../../../utils/tutorialParser';
8+
import 'prismjs/themes/prism-tomorrow.css';
9+
10+
export async function getStaticPaths() {
11+
const tutorials = await getCollection('tutorials', ({ data, id }) => {
12+
return id.startsWith('netdriver/');
13+
});
14+
15+
return tutorials.map((tutorial) => {
16+
const slug = tutorial.id.replace('netdriver/', '');
17+
return {
18+
params: { slug },
19+
props: { tutorial },
20+
};
21+
});
22+
}
23+
24+
const { tutorial } = Astro.props;
25+
const htmlContent = marked.parse(tutorial.body);
26+
const title = extractTitle(tutorial.body);
27+
const summary = extractSummary(tutorial.body);
28+
29+
const siteURL = Astro.site || 'https://opensecflow.io';
30+
const tutorialURL = new URL(`/tutorial/netdriver/${tutorial.id.replace('netdriver/', '')}`, siteURL).toString();
31+
32+
// Breadcrumb items
33+
const breadcrumbItems = [
34+
{ name: 'Home', url: '/' },
35+
{ name: 'Tutorial', url: '/tutorial' },
36+
{ name: 'NetDriver', url: '/tutorial/netdriver' },
37+
{ name: title, url: tutorialURL },
38+
];
39+
40+
// HowTo Schema for the tutorial
41+
const howToSchema = {
42+
'@context': 'https://schema.org',
43+
'@type': 'HowTo',
44+
name: title,
45+
description: summary,
46+
image: new URL('/osfLogoFull.svg', siteURL).toString(),
47+
estimatedCost: {
48+
'@type': 'MonetaryAmount',
49+
currency: 'USD',
50+
value: '0',
51+
},
52+
url: tutorialURL,
53+
};
54+
55+
---
56+
57+
<MainLayout
58+
title={`${title} - OpenSecFlow`}
59+
description={summary}
60+
keywords={['netdriver', 'tutorial', 'network automation', 'rest api', 'ssh', 'netdevops']}
61+
>
62+
<BreadcrumbList items={breadcrumbItems} siteURL={siteURL} />
63+
<SchemaLD schema={howToSchema} />
64+
<!-- Animated Background -->
65+
<div class="fixed inset-0 -z-10 overflow-hidden pointer-events-none">
66+
<div class="absolute inset-0 bg-gradient-to-br from-slate-blue-50 via-white to-sky-50"></div>
67+
<!-- Animated SVG shapes -->
68+
<svg class="absolute inset-0 w-full h-full opacity-30" viewBox="0 0 1440 900" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice">
69+
<defs>
70+
<linearGradient id="tutorialGrad1" gradientUnits="userSpaceOnUse" x1="0%" y1="0%" x2="100%" y2="100%">
71+
<stop offset="0%" stop-color="rgb(92,178,214)" stop-opacity="0.3"/>
72+
<stop offset="100%" stop-color="rgb(81,118,151)" stop-opacity="0.2"/>
73+
</linearGradient>
74+
<linearGradient id="tutorialGrad2" gradientUnits="userSpaceOnUse" x1="100%" y1="0%" x2="0%" y2="100%">
75+
<stop offset="0%" stop-color="rgb(18,57,104)" stop-opacity="0.25"/>
76+
<stop offset="100%" stop-color="rgb(65,97,133)" stop-opacity="0.15"/>
77+
</linearGradient>
78+
<linearGradient id="tutorialGrad3" gradientUnits="userSpaceOnUse" x1="50%" y1="0%" x2="50%" y2="100%">
79+
<stop offset="0%" stop-color="rgb(72,104,141)" stop-opacity="0.2"/>
80+
<stop offset="100%" stop-color="rgb(90,171,206)" stop-opacity="0.1"/>
81+
</linearGradient>
82+
</defs>
83+
<!-- Floating shapes -->
84+
<path class="tutorial-shape-1" d="M 0 200 Q 360 150 720 200 T 1440 200 L 1440 0 L 0 0 Z" fill="url(#tutorialGrad1)"/>
85+
<path class="tutorial-shape-2" d="M 0 700 Q 480 650 960 700 T 1920 700 L 1920 900 L 0 900 Z" fill="url(#tutorialGrad2)"/>
86+
<circle class="tutorial-shape-3" cx="1200" cy="300" r="200" fill="url(#tutorialGrad3)"/>
87+
<circle class="tutorial-shape-4" cx="200" cy="600" r="150" fill="url(#tutorialGrad1)"/>
88+
<path class="tutorial-shape-5" d="M 800 0 Q 900 100 1000 0 L 1000 200 Q 900 100 800 200 Z" fill="url(#tutorialGrad2)"/>
89+
</svg>
90+
<!-- Additional gradient overlay -->
91+
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-slate-blue-50/50"></div>
92+
</div>
93+
94+
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
95+
<article class="bg-white rounded-xl shadow-lg border-2 border-slate-blue-200 p-8 md:p-12">
96+
<div class="prose prose-slate prose-lg max-w-none prose-headings:text-navy-800 prose-headings:font-semibold prose-h3:text-2xl prose-h4:text-xl prose-p:text-slate-blue-700 prose-strong:text-navy-800 prose-code:text-sky-600 prose-pre:bg-slate-900 prose-pre:text-slate-100 prose-blockquote:border-sky-500 prose-blockquote:bg-sky-50 prose-blockquote:text-slate-blue-700 prose-a:text-sky-600 prose-a:no-underline hover:prose-a:underline" set:html={htmlContent} />
97+
</article>
98+
</div>
99+
</MainLayout>
100+
101+
<style>
102+
@keyframes float1 {
103+
0%, 100% { transform: translateY(0px) translateX(0px); }
104+
50% { transform: translateY(-30px) translateX(20px); }
105+
}
106+
107+
@keyframes float2 {
108+
0%, 100% { transform: translateY(0px) translateX(0px); }
109+
50% { transform: translateY(20px) translateX(-15px); }
110+
}
111+
112+
@keyframes float3 {
113+
0%, 100% { transform: translateY(0px) scale(1); }
114+
50% { transform: translateY(-25px) scale(1.1); }
115+
}
116+
117+
@keyframes float4 {
118+
0%, 100% { transform: translateY(0px) scale(1); }
119+
50% { transform: translateY(15px) scale(0.9); }
120+
}
121+
122+
@keyframes float5 {
123+
0%, 100% { transform: translateX(0px) rotate(0deg); }
124+
50% { transform: translateX(30px) rotate(5deg); }
125+
}
126+
127+
.tutorial-shape-1 {
128+
animation: float1 20s ease-in-out infinite;
129+
}
130+
131+
.tutorial-shape-2 {
132+
animation: float2 25s ease-in-out infinite;
133+
}
134+
135+
.tutorial-shape-3 {
136+
animation: float3 18s ease-in-out infinite;
137+
}
138+
139+
.tutorial-shape-4 {
140+
animation: float4 22s ease-in-out infinite;
141+
}
142+
143+
.tutorial-shape-5 {
144+
animation: float5 15s ease-in-out infinite;
145+
}
146+
</style>
147+
148+
<script>
149+
import Prism from 'prismjs';
150+
import 'prismjs/components/prism-bash';
151+
import 'prismjs/components/prism-json';
152+
import 'prismjs/components/prism-yaml';
153+
154+
function highlightCode() {
155+
// Handle TextFSM code blocks (treat as text if no language support)
156+
document.querySelectorAll('pre code[class*="language-textfsm"], pre code[class*="language-TextFSM"]').forEach((el) => {
157+
el.className = 'language-text';
158+
});
159+
160+
Prism.highlightAll();
161+
}
162+
163+
// Highlight on DOM ready
164+
if (document.readyState === 'loading') {
165+
document.addEventListener('DOMContentLoaded', highlightCode);
166+
} else {
167+
highlightCode();
168+
}
169+
170+
// Highlight after Astro view transitions
171+
document.addEventListener('astro:page-load', highlightCode);
172+
</script>

src/utils/tutorialParser.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Utility functions for parsing tutorial markdown content
3+
*/
4+
5+
/**
6+
* Extract the title from markdown content (first H1 heading)
7+
*/
8+
export function extractTitle(markdown: string): string {
9+
// Remove frontmatter if present
10+
const contentWithoutFrontmatter = markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
11+
12+
// Find first H1 heading
13+
const h1Match = contentWithoutFrontmatter.match(/^#\s+(.+)$/m);
14+
if (h1Match) {
15+
return h1Match[1].trim();
16+
}
17+
18+
// Fallback: use filename or default
19+
return 'Tutorial';
20+
}
21+
22+
/**
23+
* Extract summary from markdown content (first paragraph after title)
24+
*/
25+
export function extractSummary(markdown: string): string {
26+
// Remove frontmatter if present
27+
const contentWithoutFrontmatter = markdown.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '');
28+
29+
// Remove the first H1 heading
30+
const withoutTitle = contentWithoutFrontmatter.replace(/^#\s+.+$/m, '').trim();
31+
32+
// Find first paragraph (text block before first heading or list)
33+
// Look for blockquote first (common for summaries)
34+
const blockquoteMatch = withoutTitle.match(/^>\s*(.+)$/m);
35+
if (blockquoteMatch) {
36+
return blockquoteMatch[1].trim();
37+
}
38+
39+
// Find first paragraph (non-empty line that's not a heading, list, or code block)
40+
const lines = withoutTitle.split('\n');
41+
for (const line of lines) {
42+
const trimmed = line.trim();
43+
// Skip empty lines, headings, lists, code blocks, horizontal rules
44+
if (
45+
trimmed &&
46+
!trimmed.startsWith('#') &&
47+
!trimmed.startsWith('-') &&
48+
!trimmed.startsWith('*') &&
49+
!trimmed.startsWith('```') &&
50+
!trimmed.startsWith('---') &&
51+
!trimmed.match(/^\d+\./)
52+
) {
53+
// Remove markdown formatting
54+
return trimmed
55+
.replace(/\*\*(.+?)\*\*/g, '$1') // Bold
56+
.replace(/\*(.+?)\*/g, '$1') // Italic
57+
.replace(/\[(.+?)\]\(.+?\)/g, '$1') // Links
58+
.trim();
59+
}
60+
}
61+
62+
// Fallback: return first 150 characters
63+
return withoutTitle.substring(0, 150).trim() + '...';
64+
}
65+
66+
/**
67+
* Generate a URL-friendly slug from a title or filename
68+
*/
69+
export function generateSlug(input: string): string {
70+
return input
71+
.toLowerCase()
72+
.replace(/[^a-z0-9]+/g, '-')
73+
.replace(/^-+|-+$/g, '');
74+
}

0 commit comments

Comments
 (0)