simplify knowledgebase articles
This commit is contained in:
parent
a9c15eb9c6
commit
4cc28bceb7
6
.astro/content.d.ts
vendored
6
.astro/content.d.ts
vendored
@ -164,11 +164,9 @@ declare module 'astro:content' {
|
||||
type DataEntryMap = {
|
||||
"knowledgebase": Record<string, {
|
||||
id: string;
|
||||
render(): Render[".md"];
|
||||
slug: string;
|
||||
body: string;
|
||||
body?: string;
|
||||
collection: "knowledgebase";
|
||||
data: InferEntrySchema<"knowledgebase">;
|
||||
data: any;
|
||||
rendered?: RenderedContent;
|
||||
filePath?: string;
|
||||
}>;
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1752478949435
|
||||
"lastUpdateCheck": 1753528124767
|
||||
}
|
||||
}
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -81,3 +81,6 @@ src/_data/config.local.yaml
|
||||
tmp/
|
||||
temp/
|
||||
.astro/data-store.json
|
||||
.astro/settings.json
|
||||
.astro/data-store.json
|
||||
.astro/content.d.ts
|
||||
|
31
README.md
31
README.md
@ -37,7 +37,7 @@ Ein kuratiertes Verzeichnis für Digital Forensics und Incident Response (DFIR)
|
||||
|
||||
## 🛠 Technische Grundlage
|
||||
|
||||
- **Framework:** Astro 5.x mit TypeScript
|
||||
- **Framework:** Astro 4.x mit TypeScript
|
||||
- **Styling:** CSS Custom Properties mit Dark/Light Mode
|
||||
- **API:** Node.js Backend mit Astro API Routes
|
||||
- **Datenbank:** YAML-basierte Konfiguration (tools.yaml)
|
||||
@ -117,8 +117,8 @@ sudo systemctl enable nginx
|
||||
|
||||
```bash
|
||||
# Klonen des Repositorys
|
||||
sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /var/www/cc24-hub
|
||||
cd /var/www/cc24-hub
|
||||
sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /opt/cc24-hub
|
||||
cd /opt/cc24-hub
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
sudo npm install
|
||||
@ -127,12 +127,12 @@ sudo npm install
|
||||
sudo npm run build
|
||||
|
||||
# Berechtigungen setzen
|
||||
sudo chown -R www-data:www-data /var/www/cc24-hub
|
||||
sudo chown -R www-data:www-data /opt/cc24-hub
|
||||
```
|
||||
|
||||
#### 3. Umgebungsvariablen konfigurieren
|
||||
|
||||
Erstelle `/var/www/cc24-hub/.env`:
|
||||
Erstelle `/opt/cc24-hub/.env`:
|
||||
|
||||
```bash
|
||||
# === GRUNDKONFIGURATION ===
|
||||
@ -166,13 +166,13 @@ GIT_API_TOKEN=your_git_api_token
|
||||
GIT_REPO_URL=https://ihr-git-server.de/user/cc24-hub
|
||||
|
||||
# === UPLOAD-KONFIGURATION ===
|
||||
LOCAL_UPLOAD_PATH=/var/www/cc24-hub/public/uploads
|
||||
LOCAL_UPLOAD_PATH=/opt/cc24-hub/public/uploads
|
||||
```
|
||||
|
||||
```bash
|
||||
# Berechtigungen sichern
|
||||
sudo chmod 600 /var/www/cc24-hub/.env
|
||||
sudo chown www-data:www-data /var/www/cc24-hub/.env
|
||||
sudo chmod 600 /opt/cc24-hub/.env
|
||||
sudo chown www-data:www-data /opt/cc24-hub/.env
|
||||
```
|
||||
|
||||
#### 4. Nginx konfigurieren
|
||||
@ -205,7 +205,7 @@ server {
|
||||
# Static Files
|
||||
location / {
|
||||
try_files $uri $uri/ @nodejs;
|
||||
root /var/www/cc24-hub/dist;
|
||||
root /opt/cc24-hub/dist;
|
||||
index index.html;
|
||||
|
||||
# Cache static assets
|
||||
@ -256,7 +256,7 @@ Wants=nginx.service
|
||||
Type=exec
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/var/www/cc24-hub
|
||||
WorkingDirectory=/opt/cc24-hub
|
||||
Environment=NODE_ENV=production
|
||||
ExecStart=/usr/bin/node ./dist/server/entry.mjs
|
||||
Restart=always
|
||||
@ -269,7 +269,7 @@ NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/var/www/cc24-hub
|
||||
ReadWritePaths=/opt/cc24-hub
|
||||
CapabilityBoundingSet=
|
||||
|
||||
# Resource Limits
|
||||
@ -431,7 +431,7 @@ domain-agnostic-software:
|
||||
|
||||
```bash
|
||||
# Repository aktualisieren
|
||||
cd /var/www/cc24-hub
|
||||
cd /opt/cc24-hub
|
||||
sudo git pull
|
||||
|
||||
# Dependencies aktualisieren
|
||||
@ -449,8 +449,8 @@ sudo systemctl restart cc24-hub
|
||||
Wichtige Dateien für Backup:
|
||||
|
||||
```bash
|
||||
/var/www/cc24-hub/src/data/tools.yaml
|
||||
/var/www/cc24-hub/.env
|
||||
/opt/cc24-hub/src/data/tools.yaml
|
||||
/opt/cc24-hub/.env
|
||||
/etc/nginx/sites-available/cc24-hub
|
||||
/etc/systemd/system/cc24-hub.service
|
||||
```
|
||||
@ -472,4 +472,5 @@ Bei Problemen oder Fragen:
|
||||
- **Dokumentation:** Siehe `/knowledgebase` auf der Website
|
||||
|
||||
## 📄 Lizenz
|
||||
Dieses Projekt steht unter der BSD-3-Clause Lizenz.
|
||||
|
||||
Dieses Projekt steht unter der **BSD-3-Clause** Lizenz.
|
File diff suppressed because it is too large
Load Diff
@ -4,26 +4,18 @@ const knowledgebaseCollection = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
tool_name: z.string(),
|
||||
description: z.string(),
|
||||
last_updated: z.date(),
|
||||
author: z.string().default('CC24-Team'),
|
||||
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']),
|
||||
|
||||
tool_name: z.string().optional(),
|
||||
related_tools: z.array(z.string()).default([]),
|
||||
|
||||
author: z.string().default('Anon'),
|
||||
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']).optional(),
|
||||
categories: z.array(z.string()).default([]),
|
||||
tags: z.array(z.string()).default([]),
|
||||
sections: z.object({
|
||||
overview: z.boolean().default(true),
|
||||
installation: z.boolean().default(false),
|
||||
configuration: z.boolean().default(false),
|
||||
usage_examples: z.boolean().default(true),
|
||||
best_practices: z.boolean().default(true),
|
||||
troubleshooting: z.boolean().default(false),
|
||||
advanced_topics: z.boolean().default(false),
|
||||
}).default({}),
|
||||
review_status: z.enum(['draft', 'review', 'published']).default('published'),
|
||||
|
||||
published: z.boolean().default(true),
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
knowledgebase: knowledgebaseCollection,
|
||||
};
|
@ -3,7 +3,7 @@ title: "Kali Linux - Die Hacker-Distribution für Forensik & Penetration Testing
|
||||
tool_name: "Kali Linux"
|
||||
description: "Leitfaden zur Installation, Nutzung und Best Practices für Kali Linux – die All-in-One-Plattform für Security-Profis."
|
||||
last_updated: 2025-07-20
|
||||
author: "CC24-Team"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "intermediate"
|
||||
categories: ["incident-response", "forensics", "penetration-testing"]
|
||||
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"]
|
||||
|
@ -3,7 +3,7 @@ title: "MISP - Plattform für Threat Intelligence Sharing"
|
||||
tool_name: "MISP"
|
||||
description: "Das Rückgrat des modernen Threat-Intelligence-Sharings mit über 40.000 aktiven Instanzen weltweit."
|
||||
last_updated: 2025-07-20
|
||||
author: "CC24-Team"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "intermediate"
|
||||
categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"]
|
||||
tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"]
|
||||
|
@ -3,7 +3,7 @@ title: "Nextcloud - Sichere Kollaborationsplattform"
|
||||
tool_name: "Nextcloud"
|
||||
description: "Detaillierte Anleitung und Best Practices für Nextcloud in forensischen Einsatzszenarien"
|
||||
last_updated: 2025-07-20
|
||||
author: "CC24-Team"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "novice"
|
||||
categories: ["collaboration-general"]
|
||||
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"]
|
||||
|
@ -3,7 +3,7 @@ title: "Regular Expressions (Regex) – Musterbasierte Textanalyse"
|
||||
tool_name: "Regular Expressions (Regex)"
|
||||
description: "Pattern matching language für Suche, Extraktion und Manipulation von Text in forensischen Analysen."
|
||||
last_updated: 2025-07-20
|
||||
author: "CC24-Team"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "intermediate"
|
||||
categories: ["incident-response", "malware-analysis", "network-forensics", "fraud-investigation"]
|
||||
tags: ["pattern-matching", "text-processing", "log-analysis", "string-manipulation", "search-algorithms"]
|
||||
|
@ -3,7 +3,7 @@ title: "Velociraptor – Skalierbare Endpoint-Forensik mit VQL"
|
||||
tool_name: "Velociraptor"
|
||||
description: "Detaillierte Anleitung und Best Practices für Velociraptor – Remote-Forensik der nächsten Generation"
|
||||
last_updated: 2025-07-20
|
||||
author: "CC24-Team"
|
||||
author: "Claude 4 Sonnet"
|
||||
difficulty: "advanced"
|
||||
categories: ["incident-response", "malware-analysis", "network-forensics"]
|
||||
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
|
||||
|
@ -1,16 +1,51 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getToolsData } from '../utils/dataService.js';
|
||||
import ContributionButton from '../components/ContributionButton.astro';
|
||||
|
||||
// Load tools data
|
||||
// Load tools data and knowledgebase articles
|
||||
const data = await getToolsData();
|
||||
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
|
||||
// Only include published articles
|
||||
return entry.data.published !== false;
|
||||
});
|
||||
|
||||
// Filter tools that have knowledgebase flag set to true
|
||||
const knowledgebaseTools = data.tools.filter((tool: any) => tool.knowledgebase === true);
|
||||
// Create unified knowledgebase entries with optional tool association
|
||||
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
||||
const associatedTool = entry.data.tool_name
|
||||
? data.tools.find((tool: any) => tool.name === entry.data.tool_name)
|
||||
: null;
|
||||
|
||||
// Sort alphabetically by name
|
||||
knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
return {
|
||||
// Article metadata
|
||||
slug: entry.slug,
|
||||
title: entry.data.title,
|
||||
description: entry.data.description,
|
||||
author: entry.data.author,
|
||||
last_updated: entry.data.last_updated,
|
||||
difficulty: entry.data.difficulty,
|
||||
categories: entry.data.categories || [],
|
||||
tags: entry.data.tags || [],
|
||||
|
||||
// Tool association (optional)
|
||||
tool_name: entry.data.tool_name,
|
||||
related_tools: entry.data.related_tools || [],
|
||||
associatedTool,
|
||||
|
||||
// Derived properties for consistency with existing UI
|
||||
name: entry.data.title, // For search compatibility
|
||||
type: associatedTool?.type || 'article',
|
||||
icon: associatedTool?.icon || '📖',
|
||||
platforms: associatedTool?.platforms || [],
|
||||
skillLevel: entry.data.difficulty || associatedTool?.skillLevel || 'intermediate',
|
||||
phases: associatedTool?.phases || [],
|
||||
license: associatedTool?.license
|
||||
};
|
||||
});
|
||||
|
||||
// Sort alphabetically by title
|
||||
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
||||
---
|
||||
|
||||
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
|
||||
@ -48,16 +83,16 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tools Count -->
|
||||
<!-- Articles Count -->
|
||||
<div style="text-align: center; margin-bottom: 2rem;">
|
||||
<p class="text-muted" style="font-size: 0.875rem;">
|
||||
<span id="visible-count">{knowledgebaseTools.length}</span> von {knowledgebaseTools.length} Einträgen
|
||||
<span id="visible-count">{knowledgebaseEntries.length}</span> von {knowledgebaseEntries.length} Einträgen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Knowledgebase Entries -->
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
{knowledgebaseTools.length === 0 ? (
|
||||
{knowledgebaseEntries.length === 0 ? (
|
||||
<div class="card" style="text-align: center; padding: 3rem;">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" style="margin: 0 auto 1rem;">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
@ -68,54 +103,61 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
</svg>
|
||||
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Noch keine Knowledgebase-Einträge</h3>
|
||||
<p class="text-muted">
|
||||
Knowledgebase-Einträge werden automatisch angezeigt, sobald Datenbankeinträge das Attribut "knowledgebase: true" haben.
|
||||
Knowledgebase-Einträge werden automatisch angezeigt, sobald Artikel veröffentlicht werden.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div id="kb-entries">
|
||||
{knowledgebaseTools.map((tool: any, index: number) => {
|
||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
{knowledgebaseEntries.map((entry: any, index: number) => {
|
||||
const hasAssociatedTool = !!entry.associatedTool;
|
||||
const hasValidProjectUrl = hasAssociatedTool &&
|
||||
entry.associatedTool.projectUrl !== undefined &&
|
||||
entry.associatedTool.projectUrl !== null &&
|
||||
entry.associatedTool.projectUrl !== "" &&
|
||||
entry.associatedTool.projectUrl.trim() !== "";
|
||||
|
||||
const toolSlug = tool.name.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Remove duplicate hyphens
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
const isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
|
||||
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
|
||||
const isStandalone = !hasAssociatedTool;
|
||||
|
||||
return (
|
||||
<article
|
||||
class="kb-entry card"
|
||||
id={`kb-${toolSlug}`}
|
||||
data-tool-name={tool.name.toLowerCase()}
|
||||
id={`kb-${entry.slug}`}
|
||||
data-tool-name={entry.title.toLowerCase()}
|
||||
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<h3 style="margin: 0; color: var(--color-primary);">
|
||||
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
|
||||
{tool.name}
|
||||
<span style="margin-right: 0.5rem;">{entry.icon}</span>
|
||||
{entry.title}
|
||||
</h3>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<!-- Type indicator badges -->
|
||||
{tool.type === 'concept' && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
||||
{tool.type === 'method' && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
||||
{tool.type === 'software' && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
||||
{isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
|
||||
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
||||
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
||||
{hasAssociatedTool && !isMethod && !isConcept && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
||||
|
||||
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
||||
{tool.license !== 'Proprietary' && tool.type !== 'concept' && tool.type !== 'method' && <span class="badge badge-success">Open Source</span>}
|
||||
{hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
|
||||
|
||||
<!-- Difficulty indicator -->
|
||||
{entry.difficulty && (
|
||||
<span class="badge" style="background-color: var(--color-text-secondary); color: white; font-size: 0.75rem;">
|
||||
{tool.skillLevel || 'intermediate'}
|
||||
{entry.difficulty}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<!-- Knowledge Base indicator -->
|
||||
<span class="badge badge-error">📖</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0;">
|
||||
<a href={`/knowledgebase/${toolSlug}`} class="btn btn-primary" style="font-size: 0.8125rem;">
|
||||
<a href={`/knowledgebase/${entry.slug}`} class="btn btn-primary" style="font-size: 0.8125rem;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
@ -124,38 +166,58 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
Artikel öffnen
|
||||
</a>
|
||||
|
||||
<!-- NEW: Edit button for existing knowledgebase articles -->
|
||||
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
|
||||
<!-- Edit button for knowledgebase articles -->
|
||||
<ContributionButton type="edit" toolName={entry.tool_name || entry.title} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rest of existing article content remains unchanged -->
|
||||
<!-- Description -->
|
||||
<p style="margin: 1rem 0; color: var(--color-text-secondary); line-height: 1.5;">
|
||||
{tool.description}
|
||||
{entry.description}
|
||||
</p>
|
||||
|
||||
<!-- Tags and Metadata -->
|
||||
<!-- Metadata and Tags -->
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-top: 1rem;">
|
||||
{tool.tags && tool.tags.length > 0 && (
|
||||
<!-- Tags -->
|
||||
{entry.tags && entry.tags.length > 0 && (
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
||||
{tool.tags.map((tag: string) => (
|
||||
{entry.tags.map((tag: string) => (
|
||||
<span class="tag" style="font-size: 0.75rem;">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tool.phases && tool.phases.length > 0 && (
|
||||
<!-- Categories -->
|
||||
{entry.categories && entry.categories.length > 0 && (
|
||||
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||
<strong>Phasen:</strong> {tool.phases.join(', ')}
|
||||
<strong>Kategorien:</strong> {entry.categories.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tool.platforms && tool.platforms.length > 0 && tool.type !== 'concept' && tool.type !== 'method' && (
|
||||
<!-- Tool-specific metadata (only if associated with tool) -->
|
||||
{hasAssociatedTool && entry.phases && entry.phases.length > 0 && (
|
||||
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||
<strong>Plattformen:</strong> {tool.platforms.join(', ')}
|
||||
<strong>Phasen:</strong> {entry.phases.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasAssociatedTool && entry.platforms && entry.platforms.length > 0 && !isMethod && !isConcept && (
|
||||
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||
<strong>Plattformen:</strong> {entry.platforms.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Related tools for standalone articles -->
|
||||
{isStandalone && entry.related_tools && entry.related_tools.length > 0 && (
|
||||
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
|
||||
<strong>Verwandte Tools:</strong> {entry.related_tools.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Author and date -->
|
||||
<div style="font-size: 0.8125rem; color: var(--color-text-secondary); margin-left: auto;">
|
||||
<strong>Autor:</strong> {entry.author} • <strong>Aktualisiert:</strong> {entry.last_updated.toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
@ -170,6 +232,8 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
<p class="text-muted">Versuchen Sie es mit anderen Suchbegriffen.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Floating Action Button -->
|
||||
<div id="fab-container" style="position: fixed; bottom: 2rem; right: 2rem; z-index: 100; display: none;">
|
||||
<ContributionButton
|
||||
type="write"
|
||||
@ -178,10 +242,9 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
style="border-radius: 50%; width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-lg); font-size: 1.5rem; padding: 0;"
|
||||
className="fab-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
|
||||
<script>
|
||||
// Enhanced knowledgebase functionality with search
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@ -228,5 +291,22 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
filterEntries(target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Show floating action button on scroll (optional enhancement)
|
||||
let lastScrollY = window.scrollY;
|
||||
const fabContainer = document.getElementById('fab-container');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (fabContainer) {
|
||||
if (window.scrollY > 200 && window.scrollY < lastScrollY) {
|
||||
// Scrolling up and past threshold
|
||||
fabContainer.style.display = 'block';
|
||||
} else {
|
||||
// Scrolling down or at top
|
||||
fabContainer.style.display = 'none';
|
||||
}
|
||||
lastScrollY = window.scrollY;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
@ -26,20 +26,35 @@ const { Content } = await entry.render();
|
||||
|
||||
// Load tools data to get the tool details
|
||||
const data = await getToolsData();
|
||||
const tool = data.tools.find((t: any) => t.name === entry.data.tool_name);
|
||||
|
||||
if (!tool) {
|
||||
console.warn(`Tool not found for knowledgebase entry: ${entry.data.tool_name}`);
|
||||
return Astro.redirect('/knowledgebase');
|
||||
// UPGRADED: Handle optional tool association
|
||||
const primaryTool = entry.data.tool_name
|
||||
? data.tools.find((t: any) => t.name === entry.data.tool_name)
|
||||
: null;
|
||||
|
||||
// UPGRADED: Handle multiple related tools
|
||||
const relatedTools = entry.data.related_tools
|
||||
? entry.data.related_tools.map((toolName: string) =>
|
||||
data.tools.find((t: any) => t.name === toolName)
|
||||
).filter(Boolean)
|
||||
: [];
|
||||
|
||||
// UPGRADED: Use primary tool or first related tool for styling, fallback to generic
|
||||
const displayTool = primaryTool || relatedTools[0];
|
||||
|
||||
// UPGRADED: Don't redirect - show article even without tool association
|
||||
if (!displayTool && !entry.data.tool_name && relatedTools.length === 0) {
|
||||
console.log(`Standalone knowledgebase article: ${entry.slug}`);
|
||||
}
|
||||
|
||||
// Determine tool type for styling
|
||||
const isMethod = tool.type === 'method';
|
||||
const isConcept = tool.type === 'concept';
|
||||
const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
// Determine styling based on tool type or fallback to generic
|
||||
const isMethod = displayTool?.type === 'method';
|
||||
const isConcept = displayTool?.type === 'concept';
|
||||
const isStandalone = !displayTool;
|
||||
const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &&
|
||||
displayTool.projectUrl !== null &&
|
||||
displayTool.projectUrl !== "" &&
|
||||
displayTool.projectUrl.trim() !== "";
|
||||
---
|
||||
|
||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
||||
@ -49,7 +64,7 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
||||
<div style="flex: 1;">
|
||||
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);">
|
||||
{tool.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{tool.icon}</span>}
|
||||
{displayTool?.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{displayTool.icon}</span>}
|
||||
{entry.data.title}
|
||||
</h1>
|
||||
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
|
||||
@ -58,32 +73,59 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<!-- UPGRADED: Conditional badges based on tool type or standalone -->
|
||||
{isStandalone ? (
|
||||
<span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>
|
||||
) : (
|
||||
<>
|
||||
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
|
||||
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
|
||||
{!isMethod && !isConcept && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
||||
{!isMethod && !isConcept && !isStandalone && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
|
||||
{!isMethod && !isConcept && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
|
||||
{!isMethod && !isConcept && tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
||||
{!isMethod && !isConcept && !isStandalone && displayTool?.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
|
||||
</>
|
||||
)}
|
||||
<span class="badge badge-error">📖</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<!-- UPGRADED: Flexible metadata section -->
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
|
||||
<!-- Difficulty (always shown if present) -->
|
||||
{entry.data.difficulty && (
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Last Updated (always shown) -->
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Letztes Update</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.last_updated.toLocaleDateString('de-DE')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Author (always shown) -->
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Autor</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.author}</p>
|
||||
</div>
|
||||
{entry.data.categories && entry.data.categories.length > 0 && (
|
||||
|
||||
<!-- UPGRADED: Show article type -->
|
||||
<div>
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Typ</strong>
|
||||
<p style="margin: 0; font-size: 0.9375rem;">
|
||||
{isStandalone ? 'Allgemeiner Artikel' :
|
||||
isConcept ? 'Konzept-Artikel' :
|
||||
isMethod ? 'Methoden-Artikel' :
|
||||
'Software-Artikel'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- UPGRADED: Categories (if present) -->
|
||||
{entry.data.categories && entry.data.categories.length > 0 && (
|
||||
<div style="grid-column: 1 / -1;">
|
||||
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Kategorien</strong>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
|
||||
{entry.data.categories.map((cat: string) => (
|
||||
@ -112,12 +154,30 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tool Actions -->
|
||||
<!-- UPGRADED: Flexible Tool Actions Section -->
|
||||
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary);">
|
||||
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">Tool-Aktionen</h3>
|
||||
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">
|
||||
{isStandalone ? 'Verwandte Aktionen' : 'Tool-Aktionen'}
|
||||
</h3>
|
||||
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
{isStandalone ? (
|
||||
<!-- UPGRADED: Standalone article actions -->
|
||||
<a href="/knowledgebase" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
Weitere Artikel
|
||||
</a>
|
||||
) : (
|
||||
<!-- UPGRADED: Tool-specific actions (existing logic) -->
|
||||
<>
|
||||
{isConcept ? (
|
||||
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
|
||||
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
@ -126,7 +186,7 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
Mehr erfahren
|
||||
</a>
|
||||
) : isMethod ? (
|
||||
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method);">
|
||||
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method);">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
@ -136,7 +196,7 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
@ -145,7 +205,7 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
Software-Homepage
|
||||
</a>
|
||||
{hasValidProjectUrl && (
|
||||
<a href={tool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||
<a href={displayTool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 16l4-4-4-4"/>
|
||||
@ -156,6 +216,38 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<!-- UPGRADED: Show related tools if present -->
|
||||
{relatedTools.length > 0 && relatedTools.length > (primaryTool ? 1 : 0) && (
|
||||
<div style="margin-left: auto;">
|
||||
<details style="position: relative;">
|
||||
<summary class="btn btn-secondary" style="cursor: pointer; list-style: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="8.5" cy="7" r="4"/>
|
||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||
</svg>
|
||||
Verwandte Tools ({relatedTools.length})
|
||||
</summary>
|
||||
<div style="position: absolute; top: 100%; left: 0; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 200px; z-index: 100; box-shadow: var(--shadow-lg);">
|
||||
{relatedTools.map((tool: any) => (
|
||||
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer"
|
||||
style="display: block; padding: 0.5rem; border-radius: 0.25rem; text-decoration: none; color: var(--color-text); margin-bottom: 0.25rem;"
|
||||
onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'"
|
||||
onmouseout="this.style.backgroundColor='transparent'">
|
||||
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
|
||||
{tool.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Always show return to main page -->
|
||||
<a href="/" class="btn btn-secondary">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user