ci: migrate GitHub actions to Gitea actions
Deploy CV Site (andreknie.de) / deploy (push) Canceled after 0s
Deploy CV Site (andreknie.de) / deploy (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
POSTS_DIR = "content/posts"
|
||||
files_to_update = [
|
||||
"2025-11-19-001-systemprompt-prompting.md",
|
||||
"2025-12-03-002-llms-wahrheit.md",
|
||||
"2025-12-17-003-kometen-ki-browser.md",
|
||||
"2025-12-31-004-deepfakes-ki-pornos.md",
|
||||
"2026-01-14-005-reasoning-komplexere-recherchen.md",
|
||||
"2026-01-28-006-kinder-ki.md",
|
||||
"2026-02-11-007-agenten-teil-1-mcp-a2a.md",
|
||||
"2026-02-25-008-agenten-teil-2-openclaw-moltbook.md",
|
||||
"2026-03-11-009-rags-vektordatenbanken.md",
|
||||
"2026-04-04-010-ki-kirche.md",
|
||||
"2026-04-22-011-datensouver-nit-t-teil-1-abh-ngigkeiten.md",
|
||||
"2026-05-06-012-datensouver-nit-t-teil-2-f-derierung.md",
|
||||
"2025-11-21-eigentlich-muesste-ich-heute-etwas-ueber-den-omnibus-und-die-drohende-erosion-de.md"
|
||||
]
|
||||
|
||||
for filename in files_to_update:
|
||||
filepath = os.path.join(POSTS_DIR, filename)
|
||||
if not os.path.exists(filepath):
|
||||
print(f"File not found: {filename}")
|
||||
continue
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if url already exists
|
||||
if re.search(r'^url:\s*', content, re.M):
|
||||
# Update existing url
|
||||
content = re.sub(r'^url:\s*.*$', 'url: /podcast', content, flags=re.M)
|
||||
print(f"Updated existing url in {filename}")
|
||||
else:
|
||||
# Add url to frontmatter
|
||||
content = re.sub(r'^---\n', '---\nurl: /podcast\n', content, count=1)
|
||||
print(f"Added url to frontmatter in {filename}")
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content', 'posts')
|
||||
const TITLES_FILE = path.join(process.cwd(), '..', '..', '..', '..', '..', '.gemini', 'antigravity', 'brain', '12f7ab79-1833-4303-aace-1f9957dc5f67', 'artikel_titles.md')
|
||||
|
||||
const rawTitles = fs.readFileSync(TITLES_FILE, 'utf8')
|
||||
const yamlBlockMatch = rawTitles.match(/```yaml\n([\s\S]*?)```/)
|
||||
if (!yamlBlockMatch) {
|
||||
console.error("Could not find yaml block")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const lines = yamlBlockMatch[1].split('\n')
|
||||
const titleMap = {}
|
||||
|
||||
lines.forEach(line => {
|
||||
// match filename: 'title'
|
||||
// using split to handle titles that contain colons
|
||||
const idx = line.indexOf(':')
|
||||
if (idx > -1) {
|
||||
const filename = line.slice(0, idx).trim()
|
||||
let title = line.slice(idx + 1).trim()
|
||||
|
||||
// strip quotes if present
|
||||
if (title.startsWith("'") && title.endsWith("'")) {
|
||||
title = title.slice(1, -1)
|
||||
} else if (title.startsWith('"') && title.endsWith('"')) {
|
||||
title = title.slice(1, -1)
|
||||
}
|
||||
|
||||
if (filename.endsWith('.md')) {
|
||||
titleMap[filename] = title
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md'))
|
||||
|
||||
mdFiles.forEach(file => {
|
||||
if (titleMap[file]) {
|
||||
const p = path.join(CONTENT_DIR, file)
|
||||
let data = matter(fs.readFileSync(p, 'utf8'))
|
||||
|
||||
data.data.title = titleMap[file].replace(/''/g, "'") // restore escaped single quotes
|
||||
|
||||
fs.writeFileSync(p, matter.stringify(data.content, data.data))
|
||||
console.log(`Updated ${file} -> ${data.data.title}`)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
|
||||
const TITLES_FILE = path.join(process.cwd(), '..', '..', '..', '..', '..', '.gemini', 'antigravity', 'brain', '12f7ab79-1833-4303-aace-1f9957dc5f67', 'kniepunkt_titles.md')
|
||||
|
||||
const rawTitles = fs.readFileSync(TITLES_FILE, 'utf8')
|
||||
const yamlBlockMatch = rawTitles.match(/```yaml\n([\s\S]*?)```/)
|
||||
if (!yamlBlockMatch) {
|
||||
console.error("Could not find yaml block")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Very simple parse since it's just `001: 'Title'`
|
||||
const titleMap = {}
|
||||
yamlBlockMatch[1].split('\n').forEach(line => {
|
||||
const match = line.match(/^(\d{3}):\s*'(.*)'$/)
|
||||
if (match) {
|
||||
titleMap[match[1]] = match[2]
|
||||
}
|
||||
})
|
||||
|
||||
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md'))
|
||||
|
||||
mdFiles.forEach(file => {
|
||||
const numMatch = file.match(/^(\d{3})-/)
|
||||
if (!numMatch) return
|
||||
const numStr = numMatch[1]
|
||||
|
||||
if (titleMap[numStr]) {
|
||||
const p = path.join(CONTENT_DIR, file)
|
||||
let data = matter(fs.readFileSync(p, 'utf8'))
|
||||
|
||||
// Set the new title! (Prepend the number to match the user's style preference, wait, they stripped it in the file. Let's prepend it so it matches standard!)
|
||||
// Wait, the user provided them without the prefix in the list (e.g. 001: 'Die biblische KI'). So I should set title to: `001: Die biblische KI`.
|
||||
const newTitle = `${numStr}: ${titleMap[numStr]}`
|
||||
data.data.title = newTitle
|
||||
|
||||
// If it's 011, make sure it has the image because it was missed due to duplicate files
|
||||
if (numStr === '011') {
|
||||
data.data.image = `/images/kniepunkt/011-cover.png`
|
||||
}
|
||||
|
||||
fs.writeFileSync(p, matter.stringify(data.content, data.data))
|
||||
console.log(`Updated ${numStr} -> ${newTitle}`)
|
||||
}
|
||||
})
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR="./server/data/backups"
|
||||
DB_CONTAINER="andrekniede-umami-db-1"
|
||||
DB_USER="postgres"
|
||||
DB_NAME="postgres" # Defaults for the postgres image unless otherwise specified
|
||||
DATE=$(date +"%Y-%m-%d")
|
||||
BACKUP_FILE="$BACKUP_DIR/umami-backup-$DATE.sql"
|
||||
|
||||
# Create backup directory if it doesn't exist
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Dump the database
|
||||
echo "Dumping Umami database..."
|
||||
docker exec -t $DB_CONTAINER pg_dump -c -U $DB_USER > "$BACKUP_FILE"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Database dump successful."
|
||||
|
||||
# Git operations
|
||||
git add "$BACKUP_FILE"
|
||||
git commit -m "chore: automated umami database backup $DATE"
|
||||
git push
|
||||
|
||||
echo "Backup pushed to repository."
|
||||
else
|
||||
echo "Error during database dump."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,39 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import yaml from 'js-yaml'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const PODCAST_DIR = path.join(process.cwd(), 'content', 'podcast')
|
||||
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
|
||||
|
||||
const podFiles = fs.readdirSync(PODCAST_DIR).filter(f => f.startsWith('ep-') && f.endsWith('.yaml')).sort()
|
||||
const postFiles = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith('.md')).sort()
|
||||
|
||||
const podcasts = podFiles.map(f => {
|
||||
const content = fs.readFileSync(path.join(PODCAST_DIR, f), 'utf8')
|
||||
const doc = yaml.load(content)
|
||||
return { filename: f, ...doc }
|
||||
})
|
||||
|
||||
const posts = postFiles.map(f => {
|
||||
const content = fs.readFileSync(path.join(POSTS_DIR, f), 'utf8')
|
||||
const data = matter(content)
|
||||
return { filename: f, title: data.data.title, date: data.data.date, body: data.content }
|
||||
})
|
||||
|
||||
console.log("=== PODCASTS ===")
|
||||
podcasts.forEach(p => {
|
||||
console.log(`\nID: ${p.id} | Date: ${p.date} | Title: ${p.title}`)
|
||||
// Find matching post by title similarity or keywords
|
||||
const cleanTitle = p.title.replace(/^\d{3}:\s*/, '').toLowerCase()
|
||||
const matches = posts.filter(post => {
|
||||
const pTitle = (post.title || '').toLowerCase()
|
||||
const pBody = (post.body || '').toLowerCase()
|
||||
return pTitle.includes(cleanTitle) || pBody.includes(cleanTitle) || (post.title && cleanTitle.includes(post.title.toLowerCase()))
|
||||
})
|
||||
if (matches.length > 0) {
|
||||
matches.forEach(m => console.log(` -> MATCHED POST: [${m.filename}] Date: ${m.date} | Title: ${m.title}`))
|
||||
} else {
|
||||
console.log(` -> NO MATCHING POST FOUND`)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
PODCAST_DIR = "content/podcast"
|
||||
POSTS_DIR = "content/posts"
|
||||
|
||||
pod_files = sorted([f for f in os.listdir(PODCAST_DIR) if f.startswith("ep-") and f.endswith(".yaml")])
|
||||
post_files = sorted([f for f in os.listdir(POSTS_DIR) if f.endswith(".md")])
|
||||
|
||||
podcasts = []
|
||||
for pf in pod_files:
|
||||
with open(os.path.join(PODCAST_DIR, pf), 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
title_m = re.search(r'^title:\s*"?(.+?)"?$', content, re.M)
|
||||
date_m = re.search(r'^date:\s*(.+)$', content, re.M)
|
||||
summary_m = re.search(r'^summary:\s*"?(.+?)"?$', content, re.M)
|
||||
|
||||
title = title_m.group(1) if title_m else ""
|
||||
date = date_m.group(1) if date_m else ""
|
||||
summary = summary_m.group(1) if summary_m else ""
|
||||
podcasts.append({"file": pf, "title": title, "date": date, "summary": summary})
|
||||
|
||||
posts = []
|
||||
for pf in post_files:
|
||||
with open(os.path.join(POSTS_DIR, pf), 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
title_m = re.search(r'^title:\s*"?(.+?)"?$', content, re.M)
|
||||
date_m = re.search(r'^date:\s*(.+)$', content, re.M)
|
||||
|
||||
title = title_m.group(1) if title_m else ""
|
||||
date = date_m.group(1) if date_m else ""
|
||||
posts.append({"file": pf, "title": title, "date": date, "body": content})
|
||||
|
||||
print(f"Total podcasts: {len(podcasts)}")
|
||||
print(f"Total posts: {len(posts)}")
|
||||
|
||||
for p in podcasts:
|
||||
print(f"\n==========================================")
|
||||
print(f"PODCAST: {p['file']} | Date: {p['date']} | Title: {p['title']}")
|
||||
|
||||
clean_title = re.sub(r'^\d{3}:\s*', '', p['title']).lower().strip()
|
||||
keywords = [w for w in clean_title.split() if len(w) > 3]
|
||||
|
||||
matches = []
|
||||
for post in posts:
|
||||
post_title = post['title'].lower()
|
||||
post_body = post['body'].lower()
|
||||
|
||||
# Check title similarity or date proximity or keywords
|
||||
if clean_title in post_title or clean_title in post_body:
|
||||
matches.append((post, "direct_text_match"))
|
||||
elif any(kw in post_title for kw in keywords if len(kw) > 4):
|
||||
matches.append((post, "title_keyword_match"))
|
||||
|
||||
if matches:
|
||||
for m, reason in matches:
|
||||
print(f" [MATCH ({reason})] Post: {m['file']} | Date: {m['date']} | Title: {m['title']}")
|
||||
else:
|
||||
print(" ❌ NO MATCH FOUND IN POSTS")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
|
||||
|
||||
allFiles.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
let raw = fs.readFileSync(file, 'utf8')
|
||||
let changed = false;
|
||||
|
||||
// Replace the specific block of quotes that causes issues
|
||||
if (raw.includes('"\n"\n"')) {
|
||||
raw = raw.replace(/"\n"\n"/g, ' ');
|
||||
changed = true;
|
||||
}
|
||||
if (raw.includes('" \n" \n"')) {
|
||||
raw = raw.replace(/" \n" \n"/g, ' ');
|
||||
changed = true;
|
||||
}
|
||||
// Also fix the smashed text by looking for common lower-Upper boundaries that are NOT typical camel case
|
||||
// Wait, the smashed text ONLY happened where ` "\n"\n"` or similar was removed completely!
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(file, raw)
|
||||
console.log(`Fixed quotes in ${path.basename(file)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
|
||||
|
||||
allFiles.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
let raw = fs.readFileSync(file, 'utf8')
|
||||
let data = matter(raw)
|
||||
let content = data.content
|
||||
|
||||
// Clean up quotes
|
||||
let newContent = content
|
||||
.replace(/ "\n/g, '\n')
|
||||
.replace(/\n"\n/g, '\n')
|
||||
.replace(/\n"/g, '\n')
|
||||
.replace(/^"/g, '')
|
||||
.replace(/"$/g, '')
|
||||
.replace(/ "\r\n/g, '\r\n')
|
||||
.replace(/\r\n"\r\n/g, '\r\n')
|
||||
.replace(/\r\n"/g, '\r\n')
|
||||
.replace(/ +/g, ' ') // Optional: remove double spaces if any
|
||||
|
||||
// Some files have `KI-Quiz, Teil 1: einfache Fragen"` at the very top
|
||||
newContent = newContent.replace(/"\n/g, '\n');
|
||||
|
||||
if (newContent !== data.content) {
|
||||
data.content = newContent
|
||||
fs.writeFileSync(file, matter.stringify(data.content, data.data))
|
||||
console.log(`Cleaned up quotes in ${path.basename(file)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
|
||||
|
||||
allFiles.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
const raw = fs.readFileSync(file, 'utf8')
|
||||
// Fast check for floating quotes
|
||||
if (raw.includes(' " \n" \n"') || raw.includes(' "\n"\n"')) {
|
||||
const data = matter(raw)
|
||||
|
||||
let newContent = data.content.replace(/ "\n"\n"/g, '').replace(/ " \n" \n"/g, '').replace(/^"\n"\n/g, '').replace(/\n"\n/g, '\n').replace(/"\n/g, '\n')
|
||||
|
||||
if (newContent !== data.content) {
|
||||
data.content = newContent
|
||||
fs.writeFileSync(file, matter.stringify(data.content, data.data))
|
||||
console.log(`🧹 Cleaned up quotes in ${path.basename(file)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
|
||||
const KNIEPUNKT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
|
||||
|
||||
// 1. Rename Article #5 (Gaia-X)
|
||||
const gaiaXFile = path.join(POSTS_DIR, '2026-01-16-gaia-x-ist-keine-cloud-und-warum-das-gut-ist.md')
|
||||
if (fs.existsSync(gaiaXFile)) {
|
||||
const data = matter(fs.readFileSync(gaiaXFile, 'utf8'))
|
||||
data.data.title = 'Digitale Souveränität Teil 2: Gaia-X ist keine Cloud und warum das gut ist'
|
||||
fs.writeFileSync(gaiaXFile, matter.stringify(data.content, data.data))
|
||||
console.log('Renamed Article 5 (Gaia-X)')
|
||||
}
|
||||
|
||||
// 2. Move Article #8 (Kniepunkt 011) to kniepunkt directory
|
||||
const ethikFile = path.join(POSTS_DIR, '2025-11-09-011-ethik-weggeklickt.md')
|
||||
const ethikDest = path.join(KNIEPUNKT_DIR, '011-ethik-weggeklickt.md')
|
||||
if (fs.existsSync(ethikFile)) {
|
||||
fs.renameSync(ethikFile, ethikDest)
|
||||
console.log('Moved Kniepunkt 011 to content/kniepunkt/')
|
||||
}
|
||||
|
||||
// 3. Delete Post #2 and #3 (KI-Quiz)
|
||||
const quiz1 = path.join(POSTS_DIR, '2026-03-06-ki-quiz-teil-1-einfache-fragen.md')
|
||||
const quiz2 = path.join(POSTS_DIR, '2026-03-13-ki-quiz-teil-2-jetzt-wird-s-schwerer.md')
|
||||
if (fs.existsSync(quiz1)) { fs.unlinkSync(quiz1); console.log('Deleted Quiz 1') }
|
||||
if (fs.existsSync(quiz2)) { fs.unlinkSync(quiz2); console.log('Deleted Quiz 2') }
|
||||
|
||||
// 4. Delete Post 7-11 (Digitale Souveränität Intros)
|
||||
const p7 = path.join(POSTS_DIR, '2026-02-06-digitale-souveraenitaet-teil-5-das-finale-wer-hat-eigentlich-die-macht-ueber-dei.md')
|
||||
const p8 = path.join(POSTS_DIR, '2026-01-30-rechnen-im-dunkeln-warum-du-deine-daten-nicht-mehr-teilen-musst.md')
|
||||
const p9 = path.join(POSTS_DIR, '2026-01-23-ki-souveraenitaet-bist-du-pilot-oder-nur-passagier.md')
|
||||
const p10 = path.join(POSTS_DIR, '2026-01-16-digitale-souveraenitaet-teil-2-warum-wir-kein-europaeisches-aws-brauchen.md')
|
||||
const p11 = path.join(POSTS_DIR, '2026-01-09-die-frankfurt-falle-warum-server-in-deutschland-nicht-gleich-digitale-souverae.md')
|
||||
|
||||
;[p7, p8, p9, p10, p11].forEach(p => {
|
||||
if (fs.existsSync(p)) { fs.unlinkSync(p); console.log('Deleted intro post:', path.basename(p)) }
|
||||
})
|
||||
|
||||
// 5. Delete all Kniepunkt posts/dupes from content/posts/
|
||||
const allPosts = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith('.md'))
|
||||
allPosts.forEach(f => {
|
||||
const filepath = path.join(POSTS_DIR, f)
|
||||
const raw = fs.readFileSync(filepath, 'utf8')
|
||||
const data = matter(raw)
|
||||
const title = (data.data.title || '').toLowerCase()
|
||||
|
||||
if (title.includes('kniepunkt') || f.toLowerCase().includes('kniepunkt')) {
|
||||
fs.unlinkSync(filepath)
|
||||
console.log('Deleted Kniepunkt dupe from posts:', f)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
// 1. Clean up stubs (less than 150 chars of body)
|
||||
const allPosts = getFiles(POSTS_DIR)
|
||||
allPosts.forEach(file => {
|
||||
const content = fs.readFileSync(file, 'utf-8')
|
||||
const { content: body } = matter(content)
|
||||
if (body.trim().length < 150) {
|
||||
console.log(`🗑️ Deleting stub: ${path.basename(file)} (length: ${body.trim().length})`)
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Deduplicate Kniepunkte
|
||||
const allKniepunktFiles = getFiles(KNIEPUNKT_DIR)
|
||||
const issues = {}
|
||||
|
||||
allKniepunktFiles.forEach(file => {
|
||||
const content = fs.readFileSync(file, 'utf-8')
|
||||
const { data, content: body } = matter(content)
|
||||
|
||||
if (body.trim().length < 150) {
|
||||
console.log(`🗑️ Deleting stub: ${path.basename(file)}`)
|
||||
fs.unlinkSync(file)
|
||||
return
|
||||
}
|
||||
|
||||
const filename = path.basename(file)
|
||||
|
||||
// Extract issue number (1 to 3 digits)
|
||||
const match = filename.match(/(?:kniepunkt|kp|issue|ausgabe)[^\d]*(\d{1,3})/i) || data.title.match(/(?:kniepunkt|kp|issue|ausgabe)[^\d]*(\d{1,3})/i)
|
||||
|
||||
let num = -1
|
||||
if (match) {
|
||||
num = parseInt(match[1], 10)
|
||||
} else {
|
||||
// try to find any standalone 2-3 digit number
|
||||
const m2 = filename.match(/\b(\d{2,3})\b/)
|
||||
if (m2) num = parseInt(m2[1], 10)
|
||||
}
|
||||
|
||||
if (num !== -1) {
|
||||
if (!issues[num]) issues[num] = []
|
||||
issues[num].push({ file, filename, data, body, length: body.length })
|
||||
} else {
|
||||
console.log(`⚠️ Could not parse issue number for: ${filename}`)
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(issues).forEach(num => {
|
||||
const files = issues[num]
|
||||
if (files.length > 1) {
|
||||
console.log(`\n🔍 Found ${files.length} duplicates for Issue ${num}:`)
|
||||
|
||||
// Pick the best one
|
||||
// Priority: 1. Real date (not 2026-07-18) 2. Longest content 3. Starts with YYYY-MM-DD
|
||||
files.sort((a, b) => {
|
||||
const aRealDate = a.data.date !== '2026-07-18'
|
||||
const bRealDate = b.data.date !== '2026-07-18'
|
||||
if (aRealDate && !bRealDate) return -1
|
||||
if (!aRealDate && bRealDate) return 1
|
||||
|
||||
const aDatePrefix = /^\d{4}-\d{2}-\d{2}/.test(a.filename)
|
||||
const bDatePrefix = /^\d{4}-\d{2}-\d{2}/.test(b.filename)
|
||||
if (aDatePrefix && !bDatePrefix) return -1
|
||||
if (!aDatePrefix && bDatePrefix) return 1
|
||||
|
||||
return b.length - a.length // Longer is better
|
||||
})
|
||||
|
||||
const best = files[0]
|
||||
console.log(`✅ Keeping best: ${best.filename} (Date: ${best.data.date}, Len: ${best.length})`)
|
||||
|
||||
// Delete the rest
|
||||
for (let i = 1; i < files.length; i++) {
|
||||
console.log(`🗑️ Deleting duplicate: ${files[i].filename}`)
|
||||
fs.unlinkSync(files[i].file)
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log('✅ Deduplication complete!')
|
||||
@@ -0,0 +1,30 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allPosts = getFiles(POSTS_DIR)
|
||||
allPosts.forEach(post => {
|
||||
if (fs.existsSync(post)) {
|
||||
const pData = matter(fs.readFileSync(post, 'utf8'))
|
||||
let dateStr = ''
|
||||
if (pData.data.date instanceof Date) {
|
||||
dateStr = pData.data.date.toISOString()
|
||||
} else if (typeof pData.data.date === 'string') {
|
||||
dateStr = pData.data.date
|
||||
}
|
||||
|
||||
// If date is July 18, 2026 or July 17, 2026
|
||||
if (dateStr.includes('2026-07-18') || dateStr.includes('2026-07-17')) {
|
||||
// It's a duplicate of a properly dated file. Delete it.
|
||||
fs.unlinkSync(post)
|
||||
console.log(`🗑️ Deleted wrongly dated file: ${path.basename(post)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allPosts = getFiles(POSTS_DIR)
|
||||
const allKniepunkte = getFiles(KNIEPUNKT_DIR)
|
||||
|
||||
// 1. Clean up Kniepunkt Residues in posts/
|
||||
allPosts.forEach(post => {
|
||||
const filename = path.basename(post)
|
||||
if (filename.match(/^0[1-9][0-9]/)) {
|
||||
const issueMatch = filename.match(/^0([1-9][0-9])/)
|
||||
if (issueMatch) {
|
||||
const issueNum = issueMatch[1]
|
||||
const matchingKniepunkt = allKniepunkte.find(k => path.basename(k).includes(`0${issueNum}`) || path.basename(k).includes(issueNum))
|
||||
if (matchingKniepunkt) {
|
||||
console.log(`🗑️ Deleting residue in posts/: ${filename}`)
|
||||
fs.unlinkSync(post)
|
||||
} else {
|
||||
// move to kniepunkt
|
||||
const newPath = path.join(KNIEPUNKT_DIR, filename)
|
||||
fs.renameSync(post, newPath)
|
||||
console.log(`➡️ Moved residue to kniepunkt/: ${filename}`)
|
||||
allKniepunkte.push(newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Merge intro posts in kniepunkt/
|
||||
const introPosts = allKniepunkte.filter(k => path.basename(k).match(/^202[0-9]-/))
|
||||
introPosts.forEach(intro => {
|
||||
const introData = matter(fs.readFileSync(intro, 'utf8'))
|
||||
const titleMatch = introData.data.title.match(/kniepunkt\s*0?(\d+)/i) || path.basename(intro).match(/kniepunkt-0?(\d+)/i)
|
||||
|
||||
if (titleMatch) {
|
||||
const issueNum = titleMatch[1]
|
||||
const article = allKniepunkte.find(k => k !== intro && (path.basename(k).includes(`0${issueNum}`) || path.basename(k).includes(`-${issueNum}-`)) && !path.basename(k).match(/^202[0-9]-/))
|
||||
|
||||
if (article) {
|
||||
const articleData = matter(fs.readFileSync(article, 'utf8'))
|
||||
const mergedContent = introData.content.trim() + '\n\n---\n\n' + articleData.content.trim()
|
||||
articleData.data.date = introData.data.date
|
||||
fs.writeFileSync(article, matter.stringify(mergedContent, articleData.data))
|
||||
fs.unlinkSync(intro)
|
||||
console.log(`✅ Merged Intro ${path.basename(intro)} -> ${path.basename(article)}`)
|
||||
} else {
|
||||
console.log(`⚠️ Could not find article for intro: ${path.basename(intro)} (Issue ${issueNum})`)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,144 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
|
||||
const KNIEPUNKT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
|
||||
const CONSULTING_DIR = path.join(process.cwd(), 'content', 'consulting')
|
||||
const TALKS_DIR = path.join(process.cwd(), 'content', 'talks')
|
||||
|
||||
function deleteIfExists(file) {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file)
|
||||
console.log('Deleted:', path.basename(file))
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Delete requested posts
|
||||
const toDelete = [
|
||||
'2026-04-06-puenktlich-zu-ostern-haben-sich-lasse-und-ich-ueber-ki-und-kirche-unterhalten-es.md',
|
||||
'2026-01-02-ai-almost-intelligent-geht-in-die-vierte-runde.md',
|
||||
'2025-12-26-zweiter-weihnachtsfeiertag.md',
|
||||
'2025-08-29-glueckwunsch-zu-dem-artikel.md',
|
||||
'2023-07-19-jobsharing-ist-eine-tolle-moeglichkeit-fuehrung-zu-leben-im-direkten-sparring-ka.md',
|
||||
'2022-12-23-mindestens-drei-marketingprofessionals-melden-sich-hier-pro-woche-bei-mir-was-s.md',
|
||||
'2022-11-22-jobsharing-fuer-maenner-geht-das.md',
|
||||
'2022-09-22-sonnenaufgang-zwischen-goettingen-und-hildesheim-der-tag-hat-frueh-begonnen-aber.md',
|
||||
'2022-07-23-wer-beschwert-sich-gerne-ueber-die-bahn-fast-jeder-den-ich-kenne-aber-die-meist.md',
|
||||
'2021-12-17-eine-echte-chance-den-zugang-zur-welt-der-bahn-einfacher-schneller-und-attrakti.md',
|
||||
'2020-04-01-die-krise-bietet-auch-chancen-digitalisierung-und-homeoffice-wird-ploetzlich-nor.md',
|
||||
// Post 12
|
||||
'2025-11-15-heute-geht-es-um-musik-immerhin-ist-ein-virtueller-ki-cowboy-auf-platz-1-der-co.md'
|
||||
]
|
||||
toDelete.forEach(f => deleteIfExists(path.join(POSTS_DIR, f)))
|
||||
|
||||
// 3. Post 16 -> Workshop section
|
||||
const post16Path = path.join(POSTS_DIR, '2025-10-16-unsere-ki-fraitage-an-der-universitaet-kassel-haben-auch-grosses-interesse-bei-a.md')
|
||||
if (fs.existsSync(post16Path)) {
|
||||
const p16 = matter(fs.readFileSync(post16Path, 'utf8'))
|
||||
const ws16 = `id: ki-experimentierraeume-verwaltung
|
||||
title: "KI über #Experimentierräume erfolgreich in der Verwaltung etablieren"
|
||||
description: "${p16.content.trim().replace(/\n/g, ' ')}"
|
||||
format: workshop
|
||||
target_audience: [public-admin, universities]
|
||||
tags: [ki, verwaltung, experimentierraeume]
|
||||
visibility: primary
|
||||
`
|
||||
fs.writeFileSync(path.join(CONSULTING_DIR, 'ki-experimentierraeume-verwaltung.yaml'), ws16)
|
||||
deleteIfExists(post16Path)
|
||||
console.log('Moved Post 16 to consulting')
|
||||
}
|
||||
|
||||
// 4. Post 17 -> Merge into Zwischen Gigafactories und DSGVO
|
||||
const post17Path = path.join(POSTS_DIR, '2025-10-09-diese-woche-kamen-gleich-zwei-grosse-europaeische-ki-initiativen-auf-die-buehne-.md')
|
||||
const gigaPath = path.join(POSTS_DIR, '2025-10-10-zwischen-gigafactories-und-dsgvo-europas-ki-dilemma.md')
|
||||
if (fs.existsSync(post17Path) && fs.existsSync(gigaPath)) {
|
||||
const p17 = matter(fs.readFileSync(post17Path, 'utf8'))
|
||||
let giga = matter(fs.readFileSync(gigaPath, 'utf8'))
|
||||
if (!giga.content.includes(p17.content.trim())) {
|
||||
giga.content = `> ${p17.content.trim()}\n\n---\n\n${giga.content}`
|
||||
fs.writeFileSync(gigaPath, matter.stringify(giga.content, giga.data))
|
||||
console.log('Merged Post 17 into article')
|
||||
}
|
||||
deleteIfExists(post17Path)
|
||||
}
|
||||
|
||||
// 5. Post 18 is Kniepunkt 006
|
||||
const post18Path = path.join(POSTS_DIR, '2025-10-03-ki-wunderkind-oder-klassenclown.md')
|
||||
const kp006Path = path.join(KNIEPUNKT_DIR, '006-ki-wunderkind-oder-klassenclown.md')
|
||||
if (fs.existsSync(post18Path)) {
|
||||
fs.renameSync(post18Path, kp006Path)
|
||||
console.log('Moved Post 18 to Kniepunkt 006')
|
||||
}
|
||||
|
||||
// 6 & 7. Posts 21 and 23 are already in Kniepunkt, delete from posts
|
||||
deleteIfExists(path.join(POSTS_DIR, '2025-09-20-die-ki-wetterkarte-zeigt-ein-chaotisches-bild-ueber-london-und-washington-regnet.md'))
|
||||
deleteIfExists(path.join(POSTS_DIR, '2025-09-06-ki-als-fitnessstudio-abo-jeder-bezahlt-es-aber-kaum-einer-nutzt-es.md'))
|
||||
|
||||
// 8. Rename Post 30 title
|
||||
const post30Path = path.join(POSTS_DIR, '2025-07-25-untitled-post.md')
|
||||
if (fs.existsSync(post30Path)) {
|
||||
let p30 = matter(fs.readFileSync(post30Path, 'utf8'))
|
||||
p30.data.title = 'Braucht es wirklich einen Chief AI Officer?'
|
||||
fs.writeFileSync(post30Path, matter.stringify(p30.content, p30.data))
|
||||
console.log('Renamed Post 30')
|
||||
}
|
||||
|
||||
// 9. Post 36 -> Workshop
|
||||
const post36Path = path.join(POSTS_DIR, '2024-04-03-wir-freuen-uns-auch-darauf-zeigen-zu-koennen-dass-jeder-produzierende-betrieb-am.md')
|
||||
if (fs.existsSync(post36Path)) {
|
||||
const p36 = matter(fs.readFileSync(post36Path, 'utf8'))
|
||||
const ws36 = `id: ki-in-der-praxis
|
||||
title: "KI in der Praxis – von der Datenerfassung bis zum Produktiveinsatz"
|
||||
description: "${p36.content.trim().replace(/\n/g, ' ')}"
|
||||
format: workshop
|
||||
target_audience: [mittelstand, executives]
|
||||
tags: [ki, datenerfassung, produktiveinsatz, praxis]
|
||||
visibility: primary
|
||||
`
|
||||
fs.writeFileSync(path.join(CONSULTING_DIR, 'ki-in-der-praxis.yaml'), ws36)
|
||||
deleteIfExists(post36Path)
|
||||
console.log('Moved Post 36 to consulting')
|
||||
}
|
||||
|
||||
// 10. Post 38 -> Retitle
|
||||
const post38Path = path.join(POSTS_DIR, '2023-12-19-.md')
|
||||
if (fs.existsSync(post38Path)) {
|
||||
let p38 = matter(fs.readFileSync(post38Path, 'utf8'))
|
||||
p38.data.title = 'Lehrer sind Führungskräfte!'
|
||||
fs.writeFileSync(post38Path, matter.stringify(p38.content, p38.data))
|
||||
console.log('Renamed Post 38')
|
||||
}
|
||||
|
||||
// 11. Post 50 -> Experience as mentor
|
||||
const post50Path = path.join(POSTS_DIR, '2021-12-16-es-hat-mir-viel-freude-bereitet-mit-start-ups-zu-diskutieren-welche-rolle-newwo.md')
|
||||
if (fs.existsSync(post50Path)) {
|
||||
const t50 = `id: mentor-pwc-scale-2021
|
||||
title: "PwC #Scale Mentor"
|
||||
description: "Startschuss des diesjährigen PwC #Scale. Mentoring zu Investor Readiness, Corporate Sales und Geschäftsmodellentwicklung."
|
||||
format: mentoring
|
||||
target_audience: [startups]
|
||||
tags: [newwork, culture, makechangework, pwc, lemin]
|
||||
visibility: primary
|
||||
`
|
||||
fs.writeFileSync(path.join(TALKS_DIR, 'mentor-pwc-scale-2021.yaml'), t50)
|
||||
deleteIfExists(post50Path)
|
||||
console.log('Moved Post 50 to talks as mentor')
|
||||
}
|
||||
|
||||
// 12. Post 53 -> Experience as speaker
|
||||
const post53Path = path.join(POSTS_DIR, '2020-07-29-ich-freue-mich-auf-digitaler-live-talk-agiles-arbeiten.md')
|
||||
if (fs.existsSync(post53Path)) {
|
||||
const t53 = `id: einfachbahn-agiles-arbeiten-2020
|
||||
title: "Agiles Arbeiten bei der DB Netz AG: Haltung, Methoden und Selbstorganisation bei der #Einfachbahn"
|
||||
description: "Digitaler Live-Talk: Agiles Arbeiten. Praxisbeispiele für neue Arbeitsweisen, servant leadership und Selbstorganisation."
|
||||
format: keynote
|
||||
target_audience: [executives, public-admin, mittelstand]
|
||||
tags: [newwork, einfachbahn, leadership, agile]
|
||||
visibility: primary
|
||||
`
|
||||
fs.writeFileSync(path.join(TALKS_DIR, 'einfachbahn-agiles-arbeiten-2020.yaml'), t53)
|
||||
deleteIfExists(post53Path)
|
||||
console.log('Moved Post 53 to talks as speaker')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content', 'posts')
|
||||
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md')).sort()
|
||||
|
||||
let yamlOutput = '```yaml\n'
|
||||
|
||||
mdFiles.forEach(file => {
|
||||
const p = path.join(CONTENT_DIR, file)
|
||||
const data = matter(fs.readFileSync(p, 'utf8'))
|
||||
let title = data.data.title || ''
|
||||
|
||||
// Escape quotes
|
||||
title = title.replace(/'/g, "''")
|
||||
|
||||
yamlOutput += `${file}: '${title}'\n`
|
||||
})
|
||||
|
||||
yamlOutput += '```\n'
|
||||
console.log(yamlOutput)
|
||||
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
|
||||
|
||||
allFiles.forEach(file => {
|
||||
const content = fs.readFileSync(file, 'utf-8')
|
||||
const { data, content: body } = matter(content)
|
||||
|
||||
// Delete empty or almost empty files (like the bad PDF parses)
|
||||
if (body.trim().length < 50) {
|
||||
console.log(`🗑️ Deleting empty file: ${path.basename(file)}`)
|
||||
fs.unlinkSync(file)
|
||||
return
|
||||
}
|
||||
|
||||
const filename = path.basename(file).toLowerCase()
|
||||
const title = (data.title || '').toLowerCase()
|
||||
|
||||
const isKniepunkt = filename.includes('kniepunkt') || title.includes('kniepunkt')
|
||||
const targetDir = isKniepunkt ? KNIEPUNKT_DIR : POSTS_DIR
|
||||
const targetPath = path.join(targetDir, path.basename(file))
|
||||
|
||||
// Adjust date for Kniepunkte (must be Sunday)
|
||||
if (isKniepunkt && data.date) {
|
||||
const d = new Date(data.date)
|
||||
const day = d.getDay()
|
||||
if (day !== 0) {
|
||||
// Shift to nearest Sunday (if Saturday, +1. If Monday, -1)
|
||||
const diff = day === 6 ? 1 : (day === 1 ? -1 : (day > 3 ? 7 - day : -day))
|
||||
d.setDate(d.getDate() + diff)
|
||||
data.date = d.toISOString().split('T')[0]
|
||||
}
|
||||
}
|
||||
|
||||
// If it's the wrong directory or we need to update the date, rewrite it
|
||||
if (file !== targetPath || isKniepunkt) {
|
||||
console.log(`📦 Moving/Updating: ${path.basename(file)} -> ${isKniepunkt ? 'kniepunkt' : 'posts'}`)
|
||||
const newContent = matter.stringify(body, data)
|
||||
fs.writeFileSync(targetPath, newContent)
|
||||
if (file !== targetPath) {
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log('✅ Cleanup complete!')
|
||||
@@ -0,0 +1,36 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allPosts = getFiles(POSTS_DIR)
|
||||
allPosts.forEach(post => {
|
||||
if (fs.existsSync(post)) {
|
||||
const pData = matter(fs.readFileSync(post, 'utf8'))
|
||||
let dateStr = ''
|
||||
if (pData.data.date instanceof Date) {
|
||||
dateStr = pData.data.date.toISOString()
|
||||
} else if (typeof pData.data.date === 'string') {
|
||||
dateStr = pData.data.date
|
||||
}
|
||||
|
||||
// If date is July 18, 2026 or July 17, 2026
|
||||
if (dateStr.includes('2026-07-18') || dateStr.includes('2026-07-17')) {
|
||||
// Try to get original date from filename
|
||||
const match = path.basename(post).match(/^(\d{4}-\d{2}-\d{2})/)
|
||||
if (match && !match[1].includes('2026-07-18') && !match[1].includes('2026-07-17')) {
|
||||
pData.data.date = match[1] + 'T00:00:00.000Z'
|
||||
fs.writeFileSync(post, matter.stringify(pData.content, pData.data))
|
||||
console.log(`📅 Fixed date for ${path.basename(post)} to ${match[1]}`)
|
||||
} else {
|
||||
// Find it from the text if possible or leave it
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allFiles = [...getFiles(KNIEPUNKT_DIR), ...getFiles(POSTS_DIR)]
|
||||
|
||||
function decodeEntities(str) {
|
||||
if (!str) return str
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/'/g, "'")
|
||||
.replace(/ /g, ' ')
|
||||
}
|
||||
|
||||
allFiles.forEach(file => {
|
||||
if (fs.existsSync(file)) {
|
||||
const raw = fs.readFileSync(file, 'utf8')
|
||||
// Fast check
|
||||
if (raw.includes('&#') || raw.includes('"') || raw.includes('&')) {
|
||||
const data = matter(raw)
|
||||
let changed = false
|
||||
|
||||
if (data.data.title && (data.data.title.includes('&#') || data.data.title.includes('"'))) {
|
||||
data.data.title = decodeEntities(data.data.title)
|
||||
changed = true
|
||||
}
|
||||
|
||||
const newContent = decodeEntities(data.content)
|
||||
if (newContent !== data.content) {
|
||||
data.content = newContent
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(file, matter.stringify(data.content, data.data))
|
||||
console.log(`📝 Fixed entities in ${path.basename(file)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
CONTENT_DIR = "content/podcast"
|
||||
updates = [
|
||||
{"old": "ep-010-datensouveraenitaet-teil-1.yaml", "new": "ep-011-datensouveraenitaet-teil-1.yaml", "id": "ep-011-datensouveraenitaet-teil-1", "title": "Datensouveränität (Teil 1) & Abhängigkeiten", "date": None},
|
||||
{"old": "ep-011-datensouveraenitaet-teil-2.yaml", "new": "ep-012-datensouveraenitaet-teil-2.yaml", "id": "ep-012-datensouveraenitaet-teil-2", "title": "Datensouveränität (Teil 2) & Föderierung", "date": None},
|
||||
]
|
||||
|
||||
for up in updates:
|
||||
old_path = os.path.join(CONTENT_DIR, up['old'])
|
||||
if not os.path.exists(old_path):
|
||||
print(f"Skipping {old_path}, not found.")
|
||||
continue
|
||||
|
||||
with open(old_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update id
|
||||
content = re.sub(r'^id:\s*.*$', f'id: {up["id"]}', content, flags=re.MULTILINE)
|
||||
|
||||
# Update title
|
||||
content = re.sub(r'^title:\s*.*$', f'title: "{up["title"]}"', content, flags=re.MULTILINE)
|
||||
|
||||
new_filename = up['new']
|
||||
new_path = os.path.join(CONTENT_DIR, new_filename)
|
||||
|
||||
with open(new_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Updated {new_path}")
|
||||
|
||||
# Remove old file if renamed
|
||||
if new_filename != up['old']:
|
||||
os.remove(old_path)
|
||||
print(f"Removed {old_path}")
|
||||
@@ -0,0 +1,121 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
let allPosts = getFiles(POSTS_DIR)
|
||||
let allKniepunkte = getFiles(KNIEPUNKT_DIR)
|
||||
|
||||
// 1. Undo false attributions
|
||||
const unassign = [
|
||||
{ match: '003-wird-ki-wirklich-alle-jobs-uebernehmen', newName: 'wird-ki-wirklich-alle-jobs-uebernehmen.md' },
|
||||
{ match: '005-enough-lets-move-on', newName: 'enough-lets-move-on.md' },
|
||||
{ match: '006-zwischen-gigafactories', newName: 'zwischen-gigafactories-und-dsgvo.md' }
|
||||
]
|
||||
|
||||
unassign.forEach(u => {
|
||||
const f = allKniepunkte.find(x => x.includes(u.match))
|
||||
if (f && fs.existsSync(f)) {
|
||||
const m = matter(fs.readFileSync(f, 'utf8'))
|
||||
m.data.title = m.data.title.replace(/^Kniepunkt 00[356]: /, '')
|
||||
delete m.data.issue
|
||||
const out = path.join(POSTS_DIR, u.newName)
|
||||
fs.writeFileSync(out, matter.stringify(m.content, m.data))
|
||||
fs.unlinkSync(f)
|
||||
console.log(`↩️ Undid false attribution for ${u.match}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Merge intro posts from POSTS_DIR into KNIEPUNKT_DIR and delete them
|
||||
allPosts = getFiles(POSTS_DIR) // Refresh
|
||||
allKniepunkte = getFiles(KNIEPUNKT_DIR) // Refresh
|
||||
|
||||
const introPosts = allPosts.filter(p => fs.readFileSync(p, 'utf8').toLowerCase().includes('kniepunkt'))
|
||||
introPosts.forEach(intro => {
|
||||
const content = fs.readFileSync(intro, 'utf8')
|
||||
const introData = matter(content)
|
||||
const isHeader = introData.data.title.toLowerCase().includes('kniepunkt') || content.toLowerCase().includes('kniepunkt')
|
||||
if (isHeader) {
|
||||
// Try to find issue number
|
||||
const titleMatch = introData.data.title.match(/kniepunkt\s*0?(\d+)/i) || path.basename(intro).match(/kniepunkt-0?(\d+)/i)
|
||||
|
||||
// Also handle specific ones like 040, 029, 16.05.2026 (037)
|
||||
let issueNum = titleMatch ? titleMatch[1] : null
|
||||
|
||||
// Manual overrides based on dates/content
|
||||
if (intro.includes('05-30-der-papst-scheint')) issueNum = '40'
|
||||
if (intro.includes('03-14-der-neue-kniepunkt')) issueNum = '29'
|
||||
if (intro.includes('05-16-guten-morgen')) issueNum = '37' // check if 37
|
||||
if (intro.includes('05-02-der-neue-kniepunkt')) issueNum = '36'
|
||||
if (intro.includes('05-09-der-neue-kniepunkt')) issueNum = '38' // Example
|
||||
if (intro.includes('05-23-der-neue-kniepunkt')) issueNum = '39' // Example
|
||||
|
||||
if (issueNum) {
|
||||
const paddedNum = String(issueNum).padStart(3, '0')
|
||||
const article = allKniepunkte.find(k => k !== intro && (path.basename(k).startsWith(paddedNum)))
|
||||
|
||||
if (article && fs.existsSync(article)) {
|
||||
const articleData = matter(fs.readFileSync(article, 'utf8'))
|
||||
// Only merge if not already merged
|
||||
if (!articleData.content.includes(introData.content.trim().substring(0, 50))) {
|
||||
const mergedContent = introData.content.trim() + '\n\n---\n\n' + articleData.content.trim()
|
||||
articleData.data.date = introData.data.date
|
||||
fs.writeFileSync(article, matter.stringify(mergedContent, articleData.data))
|
||||
console.log(`✅ Merged Intro ${path.basename(intro)} -> ${path.basename(article)}`)
|
||||
} else {
|
||||
console.log(`⚠️ Already merged: ${path.basename(intro)} into ${path.basename(article)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete the intro post from POSTS_DIR to remove it from Artikel
|
||||
fs.unlinkSync(intro)
|
||||
console.log(`🗑️ Deleted intro post from posts/: ${path.basename(intro)}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 3. Fix 012 dupe in Kniepunkt
|
||||
const dupe12 = allKniepunkte.filter(k => path.basename(k).includes('012'))
|
||||
if (dupe12.length > 1) {
|
||||
// Keep the one with better content or specific name
|
||||
const toDelete = dupe12.find(k => k.includes('KI-Soundtrack---LinkedIn'))
|
||||
if (toDelete && fs.existsSync(toDelete)) {
|
||||
fs.unlinkSync(toDelete)
|
||||
console.log(`🗑️ Deleted 012 dupe: ${path.basename(toDelete)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fix 040 dupe in Kniepunkt
|
||||
const dupe40 = allKniepunkte.filter(k => path.basename(k).includes('040'))
|
||||
if (dupe40.length > 1) {
|
||||
const toDelete = dupe40.find(k => !k.includes('paepstliche'))
|
||||
if (toDelete && fs.existsSync(toDelete)) {
|
||||
fs.unlinkSync(toDelete)
|
||||
console.log(`🗑️ Deleted 040 dupe: ${path.basename(toDelete)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Fix dates for articles in POSTS_DIR that are wrongly dated 18.07.2026 (or 17.07.2026)
|
||||
allPosts = getFiles(POSTS_DIR) // Refresh
|
||||
allPosts.forEach(post => {
|
||||
if (fs.existsSync(post)) {
|
||||
const pData = matter(fs.readFileSync(post, 'utf8'))
|
||||
// If date is July 18, 2026 or July 17, 2026 (the import date)
|
||||
if (pData.data.date && pData.data.date.includes('2026-07-18') || pData.data.date && pData.data.date.includes('2026-07-17')) {
|
||||
// Try to get original date from filename
|
||||
const match = path.basename(post).match(/^(\d{4}-\d{2}-\d{2})/)
|
||||
if (match) {
|
||||
pData.data.date = match[1] + 'T00:00:00.000Z'
|
||||
fs.writeFileSync(post, matter.stringify(pData.content, pData.data))
|
||||
console.log(`📅 Fixed date for ${path.basename(post)} to ${match[1]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content')
|
||||
const KNIEPUNKT_DIR = path.join(CONTENT_DIR, 'kniepunkt')
|
||||
const POSTS_DIR = path.join(CONTENT_DIR, 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allPosts = getFiles(POSTS_DIR)
|
||||
const allKniepunkte = getFiles(KNIEPUNKT_DIR)
|
||||
const all = [...allPosts, ...allKniepunkte]
|
||||
|
||||
function mergeFiles(headerPath, articlePath, outputPath, issueNum, issueDate) {
|
||||
if (fs.existsSync(headerPath) && fs.existsSync(articlePath)) {
|
||||
const headerData = matter(fs.readFileSync(headerPath, 'utf8'))
|
||||
const articleData = matter(fs.readFileSync(articlePath, 'utf8'))
|
||||
|
||||
// Combine
|
||||
const mergedContent = headerData.content.trim() + '\n\n---\n\n' + articleData.content.trim()
|
||||
|
||||
// Update frontmatter
|
||||
articleData.data.title = `Kniepunkt 0${issueNum}: ` + articleData.data.title.replace(/^Kniepunkt \d+: /, '')
|
||||
if (issueDate) articleData.data.date = issueDate
|
||||
|
||||
fs.writeFileSync(outputPath, matter.stringify(mergedContent, articleData.data))
|
||||
|
||||
// Delete old
|
||||
if (headerPath !== outputPath && fs.existsSync(headerPath)) fs.unlinkSync(headerPath)
|
||||
if (articlePath !== outputPath && fs.existsSync(articlePath)) fs.unlinkSync(articlePath)
|
||||
console.log(`Merged 0${issueNum} to ${path.basename(outputPath)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Merge 029
|
||||
const article029 = allKniepunkte.find(f => f.includes('029-ki-nudelsuppe'))
|
||||
const h29 = allPosts.find(f => fs.existsSync(f) && fs.readFileSync(f, 'utf8').includes('Nudelsuppe')) || allKniepunkte.find(f => fs.existsSync(f) && path.basename(f).includes('29.md'))
|
||||
if (h29 && article029) mergeFiles(h29, article029, path.join(KNIEPUNKT_DIR, '029-ki-nudelsuppe.md'), 29)
|
||||
|
||||
// 4. Merge 031
|
||||
const header031 = all.find(f => f.includes('031-ist-da'))
|
||||
const article031 = all.find(f => f.includes('031-ki-verkaeufer-statt-ki-spielberg')) || path.join(KNIEPUNKT_DIR, '031-ki-verkaeufer-statt-ki-spielberg.md')
|
||||
if (fs.existsSync(header031) && fs.existsSync(article031)) {
|
||||
mergeFiles(header031, article031, path.join(KNIEPUNKT_DIR, '031-ki-verkaeufer-statt-ki-spielberg.md'), 31)
|
||||
} else {
|
||||
console.log(`Could not find 031 files`)
|
||||
}
|
||||
|
||||
// 5. Rename 002-006 and move to kniepunkt
|
||||
const renames = [
|
||||
{ match: '2025-09-06-ki-als-fitnessstudio', newName: '002-ki-als-fitnessstudio-abo.md', issue: 2 },
|
||||
{ match: '2025-09-12-wird-ki-wirklich-alle-jobs-uebernehmen', newName: '003-wird-ki-wirklich-alle-jobs-uebernehmen.md', issue: 3 },
|
||||
{ match: '2025-09-20-die-ki-wetterkarte', newName: '004-die-ki-wetterkarte.md', issue: 4 },
|
||||
{ match: '2025-09-26-enough-let-s-move-on', newName: '005-enough-lets-move-on.md', issue: 5 },
|
||||
{ match: '2025-10-10-zwischen-gigafactories', newName: '006-zwischen-gigafactories.md', issue: 6 }
|
||||
]
|
||||
|
||||
renames.forEach(r => {
|
||||
const f = allPosts.find(x => fs.existsSync(x) && x.includes(r.match))
|
||||
if (f) {
|
||||
const m = matter(fs.readFileSync(f, 'utf8'))
|
||||
m.data.title = `Kniepunkt 00${r.issue}: ` + m.data.title
|
||||
m.data.issue = r.issue
|
||||
const out = path.join(KNIEPUNKT_DIR, r.newName)
|
||||
fs.writeFileSync(out, matter.stringify(m.content, m.data))
|
||||
fs.unlinkSync(f)
|
||||
console.log(`Moved ${r.issue} to kniepunkt: ${r.newName}`)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const files = [
|
||||
path.join(process.cwd(), 'content', 'consulting', 'ki-experimentierraeume-verwaltung.yaml'),
|
||||
path.join(process.cwd(), 'content', 'consulting', 'ki-in-der-praxis.yaml'),
|
||||
path.join(process.cwd(), 'content', 'talks', 'mentor-pwc-scale-2021.yaml'),
|
||||
path.join(process.cwd(), 'content', 'talks', 'einfachbahn-agiles-arbeiten-2020.yaml')
|
||||
]
|
||||
|
||||
files.forEach(f => {
|
||||
if (fs.existsSync(f)) {
|
||||
let raw = fs.readFileSync(f, 'utf8')
|
||||
// We want to fix the description line which is description: "..."
|
||||
// Specifically, any " inside the outer " "
|
||||
raw = raw.replace(/^description: "(.*)"$/gm, (match, p1) => {
|
||||
// replace " with ' inside p1
|
||||
return 'description: "' + p1.replace(/"/g, "'") + '"'
|
||||
})
|
||||
fs.writeFileSync(f, raw)
|
||||
console.log('Fixed:', path.basename(f))
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const KNIEPUNKT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
|
||||
|
||||
// 1. Delete the intro for 040
|
||||
const papstIntro = path.join(KNIEPUNKT_DIR, '2026-05-30-der-papst-scheint-den-kniepunkt-auch-zu-lesen-darum-geht-es-heute-und-darum-da.md')
|
||||
if (fs.existsSync(papstIntro)) {
|
||||
fs.unlinkSync(papstIntro)
|
||||
console.log('Deleted Papst intro')
|
||||
}
|
||||
|
||||
// 2. Merge "Der neue Kniepunkt ist da" into 029
|
||||
const kniepunktIntro = path.join(KNIEPUNKT_DIR, '2026-03-14-der-neue-kniepunkt-ist-da.md')
|
||||
const k29Path = path.join(KNIEPUNKT_DIR, '029-ki-nudelsuppe.md')
|
||||
if (fs.existsSync(kniepunktIntro) && fs.existsSync(k29Path)) {
|
||||
const intro = matter(fs.readFileSync(kniepunktIntro, 'utf8'))
|
||||
let k29 = matter(fs.readFileSync(k29Path, 'utf8'))
|
||||
if (!k29.content.includes(intro.content.trim())) {
|
||||
k29.content = `> ${intro.content.trim()}\n\n---\n\n${k29.content}`
|
||||
fs.writeFileSync(k29Path, matter.stringify(k29.content, k29.data))
|
||||
console.log('Merged intro into 029')
|
||||
}
|
||||
fs.unlinkSync(kniepunktIntro)
|
||||
console.log('Deleted Kniepunkt 029 intro')
|
||||
}
|
||||
|
||||
// 3. Rename all Kniepunkt titles to "0[XX]: [Title]"
|
||||
const allFiles = fs.readdirSync(KNIEPUNKT_DIR).filter(f => f.match(/^\d{3}-.*\.md$/))
|
||||
|
||||
allFiles.forEach(file => {
|
||||
const numMatch = file.match(/^(\d{3})-/)
|
||||
if (!numMatch) return
|
||||
const numStr = numMatch[1] // e.g. "012"
|
||||
|
||||
const p = path.join(KNIEPUNKT_DIR, file)
|
||||
let data = matter(fs.readFileSync(p, 'utf8'))
|
||||
|
||||
let currentTitle = data.data.title || ''
|
||||
|
||||
// Clean up current title, remove existing Kniepunkt prefix if any
|
||||
currentTitle = currentTitle.replace(/^Kniepunkt\s*\d+\s*[:-]?\s*/i, '')
|
||||
currentTitle = currentTitle.replace(/^0\d{2}\s*[:-]?\s*/, '')
|
||||
currentTitle = currentTitle.trim()
|
||||
|
||||
const newTitle = `${numStr}: ${currentTitle}`
|
||||
|
||||
if (data.data.title !== newTitle) {
|
||||
data.data.title = newTitle
|
||||
fs.writeFileSync(p, matter.stringify(data.content, data.data))
|
||||
console.log('Renamed title for', file, 'to', newTitle)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
|
||||
|
||||
function getFiles(dir) {
|
||||
return fs.readdirSync(dir).map(f => path.join(dir, f)).filter(f => f.endsWith('.md'))
|
||||
}
|
||||
|
||||
const allPosts = getFiles(POSTS_DIR)
|
||||
let items = []
|
||||
|
||||
allPosts.forEach((file) => {
|
||||
const data = matter(fs.readFileSync(file, 'utf8'))
|
||||
items.push({
|
||||
file: path.basename(file),
|
||||
title: data.data.title || 'Untitled',
|
||||
date: data.data.date || 'Unknown',
|
||||
source: data.data.source || 'unknown',
|
||||
tags: data.data.tags ? data.data.tags.join(', ') : '',
|
||||
})
|
||||
})
|
||||
|
||||
// Sort by date descending
|
||||
items.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
|
||||
const articles = items.filter(i => i.source === 'linkedin-article')
|
||||
const posts = items.filter(i => i.source === 'linkedin-post' || i.source === 'unknown')
|
||||
|
||||
let md = `# Post Inventory
|
||||
|
||||
Here are all the items currently in your \`Artikel\` subpage (\`content/posts/\`), clustered by Articles and Posts.
|
||||
You can reply with the IDs you want me to delete (e.g., "Delete Post #4, Post #12, Article #2").
|
||||
|
||||
## 📄 Articles
|
||||
|
||||
| ID | Date | Title | Filename |
|
||||
|---|---|---|---|
|
||||
`
|
||||
articles.forEach((a, i) => {
|
||||
md += `| Article #${i+1} | ${a.date.substring?.(0,10) || a.date} | ${a.title} | \`${a.file}\` |\n`
|
||||
})
|
||||
|
||||
md += `
|
||||
|
||||
## 💬 Posts (Shares / Stubs)
|
||||
|
||||
| ID | Date | Title | Filename |
|
||||
|---|---|---|---|
|
||||
`
|
||||
posts.forEach((p, i) => {
|
||||
md += `| Post #${i+1} | ${p.date.substring?.(0,10) || p.date} | ${p.title} | \`${p.file}\` |\n`
|
||||
})
|
||||
|
||||
fs.writeFileSync('/home/andre/.gemini/antigravity/brain/12f7ab79-1833-4303-aace-1f9957dc5f67/post_inventory.md', md)
|
||||
console.log('Artifact created.')
|
||||
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const PODCAST_DIR = path.join(process.cwd(), 'content', 'podcast')
|
||||
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
|
||||
|
||||
const podFiles = fs.readdirSync(PODCAST_DIR).filter(f => f.startsWith('ep-') && f.endsWith('.yaml')).sort()
|
||||
|
||||
// Define known commercial posts that already exist for specific episodes
|
||||
const commercialPostsMap = {
|
||||
'ep-001-systemprompt-und-prompting.yaml': '2025-11-21-eigentlich-muesste-ich-heute-etwas-ueber-den-omnibus-und-die-drohende-erosion-de.md'
|
||||
}
|
||||
|
||||
podFiles.forEach(pf => {
|
||||
const filepath = path.join(PODCAST_DIR, pf)
|
||||
const content = fs.readFileSync(filepath, 'utf8')
|
||||
|
||||
const titleMatch = content.match(/^title:\s*"?(.+?)"?$/m)
|
||||
const dateMatch = content.match(/^date:\s*(.+)$/m)
|
||||
|
||||
if (!titleMatch || !dateMatch) return
|
||||
|
||||
const title = titleMatch[1]
|
||||
const date = dateMatch[1]
|
||||
|
||||
// If we have a known commercial post, skip
|
||||
if (commercialPostsMap[pf]) {
|
||||
console.log(`Skipping ${pf} (has commercial post)`)
|
||||
return
|
||||
}
|
||||
|
||||
// Create filename for new post
|
||||
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
||||
const postFilename = `${date}-${slug}.md`
|
||||
const postFilepath = path.join(POSTS_DIR, postFilename)
|
||||
|
||||
if (!fs.existsSync(postFilepath)) {
|
||||
const postContent = `---
|
||||
title: '${title.replace(/'/g, "''")}'
|
||||
date: ${date}
|
||||
---
|
||||
|
||||
Eine neue Episode des **Almost Intelligent** Podcasts ist online!
|
||||
|
||||
🎙️ **Thema:** ${title.replace(/^\d{3}:\s*/, '')}
|
||||
|
||||
Hört euch die komplette Folge auf unserer [Podcast-Seite](/podcast) an!
|
||||
`
|
||||
fs.writeFileSync(postFilepath, postContent)
|
||||
console.log(`Created article for ${pf}: ${postFilename}`)
|
||||
} else {
|
||||
console.log(`Article already exists for ${pf}`)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
POSTS_DIR = "content/posts"
|
||||
post_files = sorted([f for f in os.listdir(POSTS_DIR) if f.endswith(".md")])
|
||||
|
||||
for pf in post_files:
|
||||
with open(os.path.join(POSTS_DIR, pf), 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
title_m = re.search(r'^title:\s*"?(.+?)"?$', content, re.M)
|
||||
date_m = re.search(r'^date:\s*(.+)$', content, re.M)
|
||||
|
||||
title = title_m.group(1) if title_m else ""
|
||||
date = date_m.group(1) if date_m else ""
|
||||
print(f"[{pf}] Date: {date} | Title: {title}")
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
CONTENT_DIR = "content/podcast"
|
||||
for filename in sorted(os.listdir(CONTENT_DIR)):
|
||||
if filename.startswith("ep-") and filename.endswith(".yaml"):
|
||||
# extract number
|
||||
match = re.search(r'ep-(\d{3})-', filename)
|
||||
if match:
|
||||
num = match.group(1)
|
||||
|
||||
filepath = os.path.join(CONTENT_DIR, filename)
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# extract current title
|
||||
title_match = re.search(r'^title:\s*"?(.+?)"?$', content, flags=re.MULTILINE)
|
||||
if title_match:
|
||||
current_title = title_match.group(1)
|
||||
# strip existing number if any
|
||||
current_title = re.sub(r'^\d{3}:\s*', '', current_title)
|
||||
new_title = f"{num}: {current_title}"
|
||||
|
||||
# update title
|
||||
content = re.sub(r'^title:\s*.*$', f'title: "{new_title}"', content, flags=re.MULTILINE)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"Updated {filename} -> {new_title}")
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const SRC_DIR = '/home/andre/coden/Kniepunkt/Kniepunkt-Visuals'
|
||||
const DEST_DIR = path.join(process.cwd(), 'public', 'images', 'kniepunkt')
|
||||
const CONTENT_DIR = path.join(process.cwd(), 'content', 'kniepunkt')
|
||||
|
||||
if (!fs.existsSync(DEST_DIR)) {
|
||||
fs.mkdirSync(DEST_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
const images = fs.readdirSync(SRC_DIR).filter(f => f.endsWith('.png'))
|
||||
const mdFiles = fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.md'))
|
||||
|
||||
images.forEach(img => {
|
||||
// Extract number from image name
|
||||
const match = img.match(/(\d{1,3})/)
|
||||
if (!match) return
|
||||
|
||||
// Normalize number string to 3 digits (e.g., '1' -> '001', '12' -> '012')
|
||||
const numStr = match[1].padStart(3, '0')
|
||||
|
||||
// Find matching markdown file
|
||||
const mdFile = mdFiles.find(f => f.startsWith(numStr + '-'))
|
||||
if (mdFile) {
|
||||
// Copy image
|
||||
const safeName = `${numStr}-cover.png`
|
||||
fs.copyFileSync(path.join(SRC_DIR, img), path.join(DEST_DIR, safeName))
|
||||
|
||||
// Update frontmatter
|
||||
const mdPath = path.join(CONTENT_DIR, mdFile)
|
||||
const data = matter(fs.readFileSync(mdPath, 'utf8'))
|
||||
data.data.image = `/images/kniepunkt/${safeName}`
|
||||
|
||||
fs.writeFileSync(mdPath, matter.stringify(data.content, data.data))
|
||||
console.log(`Matched and updated: ${numStr}`)
|
||||
} else {
|
||||
console.log(`No markdown file found for image: ${img} (expected prefix ${numStr}-)`)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,181 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const urls = [
|
||||
'https://andreknie.de',
|
||||
'https://api.andreknie.de',
|
||||
'https://notes.andreknie.de',
|
||||
'https://unikat.andreknie.de',
|
||||
'https://projekt-kiq.d-hive.de',
|
||||
'https://d-hive.de'
|
||||
];
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function checkUrl(url) {
|
||||
try {
|
||||
const res = await fetch(url, { method: 'GET', redirect: 'follow' });
|
||||
if (res.ok) {
|
||||
return { url, status: 'UP', code: res.status };
|
||||
} else {
|
||||
return { url, status: 'DOWN', code: res.status };
|
||||
}
|
||||
} catch (err) {
|
||||
return { url, status: 'DOWN', error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function sendAlert(failedUrls) {
|
||||
const { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, STAKEHOLDER_EMAIL } = process.env;
|
||||
|
||||
if (!SMTP_HOST || !SMTP_USER || !SMTP_PASS || !STAKEHOLDER_EMAIL) {
|
||||
console.error('SMTP credentials missing. Cannot send alert email.');
|
||||
return;
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: parseInt(SMTP_PORT || '587', 10),
|
||||
secure: parseInt(SMTP_PORT || '587', 10) === 465,
|
||||
auth: {
|
||||
user: SMTP_USER,
|
||||
pass: SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
const urlList = failedUrls.map(f => `- ${f.url} (Code: ${f.code || f.error})`).join('\n');
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: `"Smoketest Alert" <${SMTP_USER}>`,
|
||||
to: STAKEHOLDER_EMAIL,
|
||||
subject: '🚨 Smoketest Alert: Endpoints Down',
|
||||
text: `The following endpoints failed after two attempts:\n\n${urlList}`,
|
||||
});
|
||||
console.log('Alert email sent.');
|
||||
} catch (err) {
|
||||
console.error('Failed to send alert email:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateDashboard(results) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Smoketest Dashboard</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
|
||||
.container { max-width: 800px; margin: 0 auto; background: #1e293b; padding: 2.5rem; border-radius: 12px; box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.3); }
|
||||
h1 { margin-top: 0; color: #e2e8f0; font-size: 2rem; border-bottom: 2px solid #334155; padding-bottom: 1rem; }
|
||||
.status-up { color: #4ade80; font-weight: bold; background: rgba(74, 222, 128, 0.1); padding: 0.25rem 0.5rem; border-radius: 4px; }
|
||||
.status-down { color: #f87171; font-weight: bold; background: rgba(248, 113, 113, 0.1); padding: 0.25rem 0.5rem; border-radius: 4px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 2rem; }
|
||||
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid #334155; }
|
||||
th { background: #0f172a; font-weight: 600; color: #cbd5e1; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
a { color: #38bdf8; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.footer { margin-top: 3rem; font-size: 0.875rem; color: #64748b; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Smoketest Dashboard</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<th>Status</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.map(r => `
|
||||
<tr>
|
||||
<td><a href="${r.url}" target="_blank">${r.url}</a></td>
|
||||
<td><span class="${r.status === 'UP' ? 'status-up' : 'status-down'}">${r.status}</span></td>
|
||||
<td>${r.status === 'UP' ? `HTTP ${r.code}` : (r.code ? `HTTP ${r.code}` : r.error)}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer">
|
||||
Last updated: ${new Date().toUTCString()}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Write to a separate directory to avoid interfering with Vite's public dir if we run locally
|
||||
const outDir = path.join(__dirname, '..', 'public-dashboard');
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(outDir, 'index.html'), html);
|
||||
console.log('Dashboard generated at public-dashboard/index.html');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting smoketest...');
|
||||
|
||||
const results = [];
|
||||
const failedFirstTry = [];
|
||||
|
||||
for (const url of urls) {
|
||||
const result = await checkUrl(url);
|
||||
if (result.status === 'DOWN') {
|
||||
console.log(`❌ ${url} is DOWN (First Try)`);
|
||||
failedFirstTry.push(result);
|
||||
} else {
|
||||
console.log(`✅ ${url} is UP`);
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
const finalFailures = [];
|
||||
|
||||
if (failedFirstTry.length > 0) {
|
||||
console.log(`\nWaiting 5 minutes before retrying ${failedFirstTry.length} failed endpoints...`);
|
||||
// Wait 5 minutes
|
||||
await sleep(5 * 60 * 1000);
|
||||
|
||||
console.log('\nRetrying failed endpoints...');
|
||||
for (const f of failedFirstTry) {
|
||||
const result = await checkUrl(f.url);
|
||||
if (result.status === 'DOWN') {
|
||||
console.log(`❌ ${f.url} is DOWN (Second Try)`);
|
||||
finalFailures.push(result);
|
||||
results.push(result);
|
||||
} else {
|
||||
console.log(`✅ ${f.url} is UP (Second Try)`);
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Dashboard
|
||||
await generateDashboard(results);
|
||||
|
||||
// Alert if needed
|
||||
if (finalFailures.length > 0) {
|
||||
await sendAlert(finalFailures);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('All endpoints are UP.');
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Unexpected error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
CONTENT_DIR = "content/podcast"
|
||||
updates = [
|
||||
{"old": "ep-001-systemprompt-und-prompting.yaml", "id": "ep-001-systemprompt-und-prompting", "title": "Systemprompt & Prompting", "date": "2025-11-19"},
|
||||
{"old": "ep-002-llms-und-wahrheit.yaml", "id": "ep-002-llms-und-wahrheit", "title": "LLMs & Wahrheit", "date": "2025-12-03"},
|
||||
{"old": "ep-003-kometen-und-ki-browser.yaml", "id": "ep-003-kometen-und-ki-browser", "title": "Kometen & KI-Browser", "date": "2025-12-17"},
|
||||
{"old": "ep-004-deepfakes-und-ki-pornos.yaml", "id": "ep-004-deepfakes-und-ki-pornos", "title": "Deepfakes & KI-Pornos", "date": "2025-12-31"},
|
||||
{"old": "ep-005-reasoning-und-recherchen.yaml", "id": "ep-005-reasoning-und-recherchen", "title": "Reasoning & komplexere Recherchen", "date": None},
|
||||
{"old": "ep-006-kinder-und-ki.yaml", "id": "ep-006-kinder-und-ki", "title": "Kinder & KI", "date": None},
|
||||
{"old": "ep-007-agenten-mcp-a2a.yaml", "id": "ep-007-agenten-mcp-a2a", "title": "Agenten (Teil 1) MCP & A2A", "date": None},
|
||||
{"old": "ep-008-agenten-openclaw-moltbook.yaml", "id": "ep-008-agenten-openclaw-moltbook", "title": "Agenten (Teil 2) OpenClaw & Moltbook", "date": None},
|
||||
{"old": "ep-009-rags-und-vektordatenbanken.yaml", "id": "ep-009-rags-und-vektordatenbanken", "title": "RAGs & Vektordatenbanken", "date": None},
|
||||
# Skip ep-010-ki-und-kirche.yaml, delete it later
|
||||
{"old": "ep-011-datensouveraenitaet-teil-1.yaml", "new": "ep-010-datensouveraenitaet-teil-1.yaml", "id": "ep-010-datensouveraenitaet-teil-1", "title": "Datensouveränität (Teil 1) & Abhängigkeiten", "date": None},
|
||||
{"old": "ep-012-datensouveraenitaet-teil-2.yaml", "new": "ep-011-datensouveraenitaet-teil-2.yaml", "id": "ep-011-datensouveraenitaet-teil-2", "title": "Datensouveränität (Teil 2) & Föderierung", "date": None},
|
||||
]
|
||||
|
||||
for up in updates:
|
||||
old_path = os.path.join(CONTENT_DIR, up['old'])
|
||||
if not os.path.exists(old_path):
|
||||
print(f"Skipping {old_path}, not found.")
|
||||
continue
|
||||
|
||||
with open(old_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Update id
|
||||
content = re.sub(r'^id:\s*.*$', f'id: {up["id"]}', content, flags=re.MULTILINE)
|
||||
|
||||
# Update title
|
||||
content = re.sub(r'^title:\s*.*$', f'title: "{up["title"]}"', content, flags=re.MULTILINE)
|
||||
|
||||
# Update date if specified
|
||||
if up['date']:
|
||||
content = re.sub(r'^date:\s*.*$', f'date: {up["date"]}', content, flags=re.MULTILINE)
|
||||
|
||||
new_filename = up.get('new', up['old'])
|
||||
new_path = os.path.join(CONTENT_DIR, new_filename)
|
||||
|
||||
with open(new_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Updated {new_path}")
|
||||
|
||||
# Remove old file if renamed
|
||||
if new_filename != up['old']:
|
||||
os.remove(old_path)
|
||||
print(f"Removed {old_path}")
|
||||
|
||||
# Delete ki-und-kirche
|
||||
kirche_path = os.path.join(CONTENT_DIR, 'ep-010-ki-und-kirche.yaml')
|
||||
if os.path.exists(kirche_path):
|
||||
os.remove(kirche_path)
|
||||
print(f"Deleted {kirche_path}")
|
||||
|
||||
Reference in New Issue
Block a user