|
| 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> |
0 commit comments