דלג לתוכן הראשי

        המדריך המלא ליצירת תהליך אוטומטי (pipeline) הממיר הערות שנשמרו ב-Obsidian לאתר אינטרנט.

צינור הפוסטים האוטומטי לבלוג.

המדריך המלא ליצירת תהליך אוטומטי (pipeline) הממיר הערות שנשמרו ב-Obsidian לאתר אינטרנט.

רקע

לפני כחצי שנה, רציתי ליצור בלוג פשוט המבוסס על תגיות, מקום שבו אוכל לדבר על כל נושא ולאפשר לקוראים לסנן לפי תחומי העניין שלהם. אבל במהרה ויתרתי על הרעיון הזה, כי תחזוקת בלוג פעיל נראתה כמו עבודה ידנית מסובכת באותה תקופה.

זה השתנה כמה חודשים לאחר מכן, כאשר התחלתי את מסע יצירת הפתקים שלי ב-Obsidian. נהניתי מתהליך הכתיבה, ואהבתי לחפש באופן פעיל מידע נוסף ללמידה. כל פתק שכתבתי השאיר בי תחושה שיש עוד מה ללמוד. כדי להבין למה אני מתכוון, יש לי פוסט שמתאר למה יש לי “מוח שני” (second brain) ב-Obsidian.

הבנתי שהדרך הטובה ביותר לנהל בלוג היא לא להשתמש בפלטפורמה נפרדת, אלא להפוך את Obsidian, הכלי שאני כבר מכיר, למנוע הפרסום שלי.

התוצאה הסופית היא שרשרת פעולות שאיפשרה לי לכתוב את הפוסט הזה ב-Obsidian, ובלחיצת כפתור אחת להריץ סקריפט שמפרסם את הפוסט באתר שלי.

שרשרת הפעולות הזו הושפעה מאוד מ-NetworkChuck ו-4rkal.

מה אנחנו הולכים לבנות

המטרה הסופית היא ליצור אתר סטטי בעל ביצועים גבוהים, בלוג מקצועי שמתארח באינטרנט אבל מנוהל כולו מתוך Obsidian שלכם.

נעבור על איך לגרום לאתר להיראות בדיוק כמו הבלוג שלי, או איך ליצור עיצוב משלכם. החלק הטוב ביותר הוא שאתם לא צריכים ידע קודם בתכנות, מכיוון שכבר עשיתי את העבודה הכבדה עם הסקריפטים, כך שתוכלו להתמקד בכתיבה.

למי שמכיר פיתוח אתרים בסיסי, המערכת ניתנת להתאמה אישית מלאה (לדוגמה, יצרתי את הנושא שלי עבור הבלוג הזה). אבל אל תדאגו אם אתם לא מתכנתים. יש מאות תבניות יפות מוכנות לשימוש (נעבור עליהן בקרוב).

נכיר את השחקנים

  • Obsidian (העורך): כאן קורה כל הקסם. זהו סביבת הכתיבה העיקרית שלנו, שבה אנחנו יוצרים ומנהלים את קבצי ה-.md (Markdown) שלנו.

  • Hugo (המנוע): Hugo הוא מחולל אתרים סטטי חזק. הוא לוקח את הפתקים שלכם מ-Obsidian והופך אותם ל-HTML נקי שדפדפנים יכולים להבין. הוא גם מטפל ב"לוגיקה" של האתר שלכם, כלומר הוא מארגן באופן אוטומטי את התגיות, הפוסטים ואת הניתוב של העמודים שלכם בהתאם להגדרות.

  • GitHub (המאגר): אנו משתמשים ב-GitHub כדי לאחסן את קוד ההתאמה האישית ואת הגדרות האתר שלכם בענן ולבצע תיקון גרסאות.

הערה לאנשים שאינם מתכנתים: אמנם שימוש ב-GitHub עשוי להיראות כמו “דבר למפתחים”, אבל זהו מערכת בקרת גרסאות מתקדמת. חשבו על זה כעל גיבוי מאובטח שמאפשר לכם לראות כל שינוי שביצעתם. אני ממליץ מאוד להירשם לחשבון. זהו הסטנדרט בתעשייה מסיבה טובה.

שרשרת הפעולות

הנה מה שקורה בדיוק כשאתם מסימים לכתוב ורוצים ללחוץ על “פרסם”.

אנו משתמשים בפלאגין של Obsidian כמו Shell command או Script Launcher כדי להפעיל את הסקריפט המותאם אישית שלנו.

זהו הכל. בלחיצה אחת, הסקריפט מפעיל את “רצף הפעולות האוטומטי” הבא:

  1. מסנכרנים את תיקיית ה-posts של Obsidian עם תיקיית ה-content של הבלוג (של Hugo).
  2. מסנכרנים כל התמונות שמשתמשים בהן ב-Obsidian עם תיקיית ה-static/images של הבלוג.
  3. בונים את האתר (מומרים את כל קבצי ה-md ל-HTML ויוצרים את האתר).
  4. מעלים את האתר הבנוי ל-מאגר ה-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 הוא התחלה טובה, אבל יש לו כמה מגבלות שרציתי לפתור:

  1. נפח תמונות מוגזם: הסקריפט של Chuck מעתיק תמונות, אבל אם תמחקו תמונה מהפוסט שלכם מאוחר יותר, הקובץ יישאר בתיקיית הבלוג לנצח. הגרסה שלי כוללת שגרה לניקוי שמסירה כל תמונה מהאתר שלכם שאינה בשימוש פעיל בפוסט.
  2. תמונות ממוזערות אוטומטיות: הסקריפט הזה בודק את הכותרת של הפוסט, ובודק בתיקיית הצירופים האם קיימת תמונה תואמת. אם הוא מוצא אחת, הוא מגדיר אותה באופן אוטומטי כתמונת ממוזערת.

שימו לב שהסקריפט הזה נועד לעבוד ב-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: ![Image Description](/images/image.png) or ![Image Description](/images/image.png)

            # 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: ![...](/images/...)

        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"![Image Description](/images/{image.replace(' ', '%20')})"

          # Replace both ![[image]] and [[image]] formats

            content = re.sub(rf'!?\[\[{re.escape(image)}\]\]', markdown_image, content)