רקע
לפני כחצי שנה, רציתי ליצור בלוג פשוט המבוסס על תגיות, מקום שבו אוכל לדבר על כל נושא ולאפשר לקוראים לסנן לפי תחומי העניין שלהם. אבל במהרה ויתרתי על הרעיון הזה, כי תחזוקת בלוג פעיל נראתה כמו עבודה ידנית מסובכת באותה תקופה.
זה השתנה כמה חודשים לאחר מכן, כאשר התחלתי את מסע יצירת הפתקים שלי ב-Obsidian. נהניתי מתהליך הכתיבה, ואהבתי לחפש באופן פעיל מידע נוסף ללמידה. כל פתק שכתבתי השאיר בי תחושה שיש עוד מה ללמוד. כדי להבין למה אני מתכוון, יש לי פוסט שמתאר למה יש לי “מוח שני” (second brain) ב-Obsidian.
הבנתי שהדרך הטובה ביותר לנהל בלוג היא לא להשתמש בפלטפורמה נפרדת, אלא להפוך את Obsidian, הכלי שאני כבר מכיר, למנוע הפרסום שלי.
התוצאה הסופית היא שרשרת פעולות שאיפשרה לי לכתוב את הפוסט הזה ב-Obsidian, ובלחיצת כפתור אחת להריץ סקריפט שמפרסם את הפוסט באתר שלי.
שרשרת הפעולות הזו הושפעה מאוד מ-NetworkChuck ו-4rkal.
מה אנחנו הולכים לבנות
המטרה הסופית היא ליצור אתר סטטי בעל ביצועים גבוהים, בלוג מקצועי שמתארח באינטרנט אבל מנוהל כולו מתוך Obsidian שלכם.
נעבור על איך לגרום לאתר להיראות בדיוק כמו הבלוג שלי, או איך ליצור עיצוב משלכם. החלק הטוב ביותר הוא שאתם לא צריכים ידע קודם בתכנות, מכיוון שכבר עשיתי את העבודה הכבדה עם הסקריפטים, כך שתוכלו להתמקד בכתיבה.
למי שמכיר פיתוח אתרים בסיסי, המערכת ניתנת להתאמה אישית מלאה (לדוגמה, יצרתי את הנושא שלי עבור הבלוג הזה). אבל אל תדאגו אם אתם לא מתכנתים. יש מאות תבניות יפות מוכנות לשימוש (נעבור עליהן בקרוב).
נכיר את השחקנים
Obsidian (העורך): כאן קורה כל הקסם. זהו סביבת הכתיבה העיקרית שלנו, שבה אנחנו יוצרים ומנהלים את קבצי ה-
.md(Markdown) שלנו.Hugo (המנוע): Hugo הוא מחולל אתרים סטטי חזק. הוא לוקח את הפתקים שלכם מ-Obsidian והופך אותם ל-HTML נקי שדפדפנים יכולים להבין. הוא גם מטפל ב"לוגיקה" של האתר שלכם, כלומר הוא מארגן באופן אוטומטי את התגיות, הפוסטים ואת הניתוב של העמודים שלכם בהתאם להגדרות.
GitHub (המאגר): אנו משתמשים ב-GitHub כדי לאחסן את קוד ההתאמה האישית ואת הגדרות האתר שלכם בענן ולבצע תיקון גרסאות.
הערה לאנשים שאינם מתכנתים: אמנם שימוש ב-GitHub עשוי להיראות כמו “דבר למפתחים”, אבל זהו מערכת בקרת גרסאות מתקדמת. חשבו על זה כעל גיבוי מאובטח שמאפשר לכם לראות כל שינוי שביצעתם. אני ממליץ מאוד להירשם לחשבון. זהו הסטנדרט בתעשייה מסיבה טובה.
שרשרת הפעולות
הנה מה שקורה בדיוק כשאתם מסימים לכתוב ורוצים ללחוץ על “פרסם”.
אנו משתמשים בפלאגין של Obsidian כמו Shell command או Script Launcher כדי להפעיל את הסקריפט המותאם אישית שלנו.
זהו הכל. בלחיצה אחת, הסקריפט מפעיל את “רצף הפעולות האוטומטי” הבא:
- מסנכרנים את תיקיית ה-
postsשל Obsidian עם תיקיית ה-contentשל הבלוג (של Hugo). - מסנכרנים כל התמונות שמשתמשים בהן ב-Obsidian עם תיקיית ה-
static/imagesשל הבלוג. - בונים את האתר (מומרים את כל קבצי ה-md ל-HTML ויוצרים את האתר).
- מעלים את האתר הבנוי ל-מאגר ה-GitHub שלכם לתוך ענף ייעודי בשם
deploy.
זה מבטיח שהמאגר של Obsidian שלכם נשאר פרטי ומאורגן, בעוד שהאתר שלכם נשאר מעודכן ומקצועי.
טיפ: אמנם באופן טכני אפשר להתקין את Hugo ישירות בתוך המאגר של Obsidian שלכם, אבל אני ממליץ בחום לשמור אותם בנפרד, כמו שאני עושה. זה שומר על הפתקים האישיים שלכם נקיים ומונע מקבצי הטכנולוגיה של הבלוג שלכם מלסבך את מרחב הכתיבה שלכם.
עיצוב ב-Obsidian
הפוסטים שלכם ב-Obsidian צריכים להשתמש ב-frontmatter. Hugo ישתמש ב-frontmatter הזה כדי לדעת מידע נוסף על הפוסט שלכם.
מסיבה זו, יש לי תבנית “פוסט בסיסי” ב-Obsidian שאני אשתף כאן:
---
title: Untitled
date: 2025-12-17
tags:
- mytag
draft: true
description: "A brief summary of the post"
---
אם אתם לא משתמשים בזה קודם, אל דאגה. ברגע שתדביקו את זה ב-Obsidian, הוא יזהה את הטקסט כ-“תצוגת תכונות”, מה שמקל מאוד למלא את הפרטים.
הערה חשובה לגבי תמונות ממוזערות: שימו לב שהתבנית שלי לא כוללת את השדה
thumbnail:. הסיבה לכך היא שהסקריפט שבניתי יבדוק אוטומטית את תיקיית הצירופים של Obsidian שלכם ויחפש תמונה עם אותו השם בדיוק כמו הפוסט, ואז יגדיר אותה כתמונת ממוזערת.
הורדה והגדרת Hugo
כדי לגרום למנוע הליבה להתחיל לעבוד, עקבתי אחר המדריך המצוין של NetworkChuck. הוא מסביר כיצד להתקין את הדרישות הנדרשות (Git, Go ו-Python). והוא מראה לכם איך ליצור את הפרויקט Hugo הראשון שלכם.
מכיוון שההסבר שלו כל כך ברור וויזואלי, אני ממליץ לצפות בחלק ההתקנה של הסרטון שלו: I started a blog… in 2024
עקבו אחרי השלבים שלו עד שיהיה לכם אתר בסיסי שנוצר. עם זאת, לפני שאתם עוברים לשלבי הפריסה שלו, בואו נדבר על השיפורים הספציפיים שעשיתי בשרשרת הפעולות שלי. אמנם השיטה של Chuck מדהימה, אבל שיניתי מעט את הלוגיקה כדי להתאים לצרכים שלי.
הסקריפט לתמונות
הנה ה-“עבודה הכבדה” שהבטחתי. אמנם הסקריפט במדריך של NetworkChuck הוא התחלה טובה, אבל יש לו כמה מגבלות שרציתי לפתור:
- נפח תמונות מוגזם: הסקריפט של Chuck מעתיק תמונות, אבל אם תמחקו תמונה מהפוסט שלכם מאוחר יותר, הקובץ יישאר בתיקיית הבלוג לנצח. הגרסה שלי כוללת שגרה לניקוי שמסירה כל תמונה מהאתר שלכם שאינה בשימוש פעיל בפוסט.
- תמונות ממוזערות אוטומטיות: הסקריפט הזה בודק את הכותרת של הפוסט, ובודק בתיקיית הצירופים האם קיימת תמונה תואמת. אם הוא מוצא אחת, הוא מגדיר אותה באופן אוטומטי כתמונת ממוזערת.
שימו לב שהסקריפט הזה נועד לעבוד ב-Windows. לא בדקתי אותו על Mac או Linux.
import os
import re
import shutil
from urllib.parse import unquote
# Paths (using raw strings to handle Windows backslashes correctly)
posts_dir = r"C:\Users\livou\Documents\Programming\Personal\manablog\content\posts"
attachments_dir = r"G:\My Drive\Obsidian\Rioru\attachments"
static_images_dir = r"C:\Users\livou\Documents\Programming\Personal\manablog\static\images"
# Ensure static/images directory exists
os.makedirs(static_images_dir, exist_ok=True)
# Track all referenced images across all posts
referenced_images = set()
def slugify(text):
"""Convert text to URL-friendly slug (similar to Hugo's urlize)"""
# Convert to lowercase
text = text.lower()
# Replace spaces and special chars with hyphens
text = re.sub(r'[^\w\s-]', '', text)
text = re.sub(r'[-\s]+', '-', text)
# Remove leading/trailing hyphens
text = text.strip('-')
return text
def extract_title_from_frontmatter(content):
"""Extract title from YAML frontmatter"""
# Try YAML frontmatter (--- or +++ delimiters)
frontmatter_pattern = r'^(?:---|\+\+\+)\s*\n(.*?)\n(?:---|\+\+\+)\s*\n'
match = re.match(frontmatter_pattern, content, re.DOTALL)
if match:
frontmatter = match.group(1)
# Look for title field
title_match = re.search(r'^title:\s*["\']?(.+?)["\']?\s*$', frontmatter, re.MULTILINE)
if title_match:
return title_match.group(1).strip()
return None
def find_title_based_image(title_slug, attachments_dir):
"""Find image in attachments folder that matches the title slug"""
if not os.path.exists(attachments_dir):
return None
image_extensions = ['.png', '.jpg', '.jpeg', '.webp']
# Check for exact match with slug
for ext in image_extensions:
potential_image = f"{title_slug}{ext}"
image_path = os.path.join(attachments_dir, potential_image)
if os.path.exists(image_path):
return potential_image
# Check for case-insensitive match
for filename in os.listdir(attachments_dir):
if not any(filename.lower().endswith(ext) for ext in image_extensions):
continue
# Get filename without extension
name_without_ext = os.path.splitext(filename)[0]
name_slug = slugify(name_without_ext)
if name_slug == title_slug:
return filename
return None
# Step 1: Process each markdown file in the posts directory
for filename in os.listdir(posts_dir):
if filename.endswith(".md"):
filepath = os.path.join(posts_dir, filename)
with open(filepath, "r", encoding="utf-8") as file:
content = file.read()
# Extract title and find title-based image
post_title = extract_title_from_frontmatter(content)
title_image_dest = None
if post_title:
title_slug = slugify(post_title)
title_image = find_title_based_image(title_slug, attachments_dir)
if title_image:
# Copy title-based image to static/images with slugged name
source_path = os.path.join(attachments_dir, title_image)
# Use the extension from the found image
_, ext = os.path.splitext(title_image)
dest_filename = f"{title_slug}{ext}"
dest_path = os.path.join(static_images_dir, dest_filename)
if os.path.exists(source_path):
shutil.copy(source_path, dest_path)
title_image_dest = dest_filename
referenced_images.add(dest_filename)
print(f"Copied title-based image: {title_image} -> {dest_filename}")
# Step 2: Find all image links in Obsidian format:  or 
# Match the entire pattern including optional ! prefix
# Support multiple image formats
image_pattern = r'!?\[\[([^]]*\.(?:png|jpg|jpeg|webp))\]\]'
images = re.findall(image_pattern, content)
# Also check for already-processed images in Hugo format: 
processed_image_pattern = r'!\[[^\]]*\]\(/images/([^)]+)\)'
processed_images = re.findall(processed_image_pattern, content)
# Add all referenced images to the set
for image in images:
referenced_images.add(image)
for processed_image in processed_images:
# Decode URL-encoded image names (e.g., %20 -> space)
decoded_image = unquote(processed_image)
referenced_images.add(decoded_image)
# Step 3: Replace image links and ensure URLs are correctly formatted
for image in images:
# Prepare the Markdown-compatible link with %20 replacing spaces
markdown_image = f"})"
# Replace both ![[image]] and [[image]] formats
content = re.sub(rf'!?\[\[{re.escape(image)}\]\]', markdown_image, content)
