Logo

One-Click Blog Publishing with Claude Agent Skills: From Tedious Workflows to Natural Language Interaction

Published on
...
Authors

Introduction: From Multi-Step to a Single Sentence

Imagine these two blog publishing experiences:

The Traditional Way (even with automation scripts):

./check_sensitive.sh

python publish.py \
  -f "ready/article.md" \
  -t "Article Title" \
  -c tech \
  --tags "Tag1,Tag2" \
  -d "Article Description"

python sync.py

# 4. Preview
cd blog && hugo server -D

# 5. Deploy
cd blog && hugo && git add . && git commit -m "..." && git push

Using Claude Agent Skills:

Me: Help me publish this article.

Claude:
Sensitive information checked.
 Article published to Hugo.
 Synced to Obsidian.
 Built and deployed.
🎉 Publishing complete! The article is now live.

This is the magic of AI Agents.

This article will share how I used Claude Agent Skills to encapsulate the complex publishing process from Obsidian to a Hugo Blog into a single-sentence interaction, achieving true "one-click publishing."

More importantly: All related files (Skill definitions, Python scripts, configuration files) are centralized in a single folder. You can transplant this to any project by copying one folder, making it truly "out-of-the-box."


Part 1: Understanding Claude Agent Skills

What are Claude Agent Skills?

Claude Agent Skills is a powerful feature provided by Claude Code that allows users to encapsulate complex workflows into reusable "skills," which can then be triggered and executed through natural language interaction.

Core Concepts

  1. Skills

    • Essentially a Markdown file containing detailed instructions.
    • Describes how Claude should complete a specific task.
    • Can call system tools (file operations, command execution, etc.).
    • Supports complex decision logic and error handling.
  2. Skill Prompt

    • A detailed description of the task's steps and requirements.
    • Defines input parameters and output formats.
    • Includes logic for error handling and edge cases.
  3. Natural Language Trigger

    • The user describes their needs in everyday language.
    • Claude automatically identifies and executes the corresponding Skill.
    • No need to memorize complex command-line arguments.

Technical Architecture

User Request ("Help me publish this article")
Claude Natural Language Understanding
Matching the appropriate Skill
Loading the Skill Prompt
Executing specific steps
    ├─ Reading files
    ├─ Running commands
    ├─ Processing data
    └─ Returning results
Friendly user feedback

Why Choose Agent Skills?

Comparison with Other Automation Solutions

SolutionAdvantagesDisadvantages
Shell ScriptsLightweight, fastRequires memorizing commands, complex args, inflexible
Python ScriptsPowerful, easily extensibleStill requires command-line operation, poor interactivity
Obsidian PluginsDeep integration with editorComplex development, requires TypeScript
Claude SkillsNatural language interaction, intelligent decision-makingRequires Claude Code

Unique Advantages of Agent Skills:

  1. Natural Interaction: No need to memorize commands; just describe your needs in plain language.
  2. Intelligent Decision-Making: Can automatically handle edge cases based on context.
  3. Flexible Adaptation: Can handle different phrasings like "publish this article" or "publish the latest article in the ready directory."
  4. Error Handling: Automatically detects problems and provides suggestions.
  5. Composability: One Skill can call multiple underlying tools.

Part 2: Underlying Technical Implementation

Before encapsulating Agent Skills, we first need to build the underlying automation tools. This part is the technical foundation.

The Content Creator's Dilemma

As a tech blogger, I face typical content management challenges:

  • Obsidian is my primary note-taking tool for daily records, knowledge management, and idea development.
  • Hugo Blog is my external publishing platform for sharing technical articles.

But when I want to publish a note from Obsidian to my blog, I have to go through a tedious process: copying content, adding Front Matter, handling images, converting links, moving files... The whole process takes at least 10-15 minutes.

Core Challenges of a Dual-System Approach

1. Front Matter Format Differences

Obsidian (YAML):

---
Date: 2024-11-13
tags:
  - Tag1
  - Tag2
project: ProjectName
---

Features:

  • Wrapped in ---.
  • Flexible fields, no mandatory requirements.
  • Supports nested structures.
  • Case-insensitive.

Hugo (TOML):

+++
title = "Article Title"
date = "2024-11-13"
lastmod = "2024-11-13"
author = ["geekhuashan"]
draft = false
categories = ["tech"]
tags = ["Tag1", "Tag2"]
description = "Article description"
+++

Features:

  • Wrapped in +++.
  • Must include specific fields (title, date, author, categories).
  • Strict type requirements (arrays, strings, booleans).
  • Must conform to TOML specifications.

Obsidian Wiki-style:

![[image.png]]
![[Pasted image 20230320223703.png]]

Features:

  • Concise Wiki syntax.
  • Automatically looks in the Attachment/ directory.
  • Supports spaces and special characters.

Hugo Standard Markdown:

![Mac Mini physical photo](/images/tech/mac-mini.png)
![Configuration screenshot](/images/tech/pasted-image-20230320223703.png)

Features:

  • Standard Markdown syntax.
  • Requires an explicit path (relative to static/).
  • Requires alt text (for SEO and accessibility).
  • Filenames are recommended to use hyphens, avoiding spaces.

3. File Organization Differences

Obsidian Directory Structure:

Main/
├── 07 blog idea/           # Blog drafts (mixed storage)
│   ├── draft/             # Drafts in progress
│   ├── ready/             # Ready-to-publish queue
│   └── published/         # Published backups
└── Attachment/             # All attachments stored together
    ├── Mac mini.png
    └── ...

Hugo Directory Structure:

blog/
├── content/posts/          # Articles organized by category
│   ├── tech/
│   ├── read/
│   ├── life/
│   └── project/
└── static/                 # Images organized by category
    ├── tech/
    ├── read/
    └── life/

4. Compatibility Matrix

FeatureObsidianHugoConversion Needed
Front MatterYAMLTOML✅ Yes
Image Links![[...]]![]()✅ Yes
Image PathsRelativeAbsolute✅ Yes
Filename SpacesSupportedNot recommended✅ Yes
Wiki Links[[...]][]()❌ No (Hugo supports it)
Markdown SyntaxStandard + ExtStandard❌ No

Step 1: Automated Publishing Tool (v1.0)

Technical Solution Design

Based on the analysis above, I decided to develop a Python script to automate the entire publishing process.

Core Functionality Planning

  1. Draft Scanning and Selection

    • Automatically scan the ready/ queue directory.
    • Display all available drafts and their modification times.
    • Support interactive selection.
  2. Front Matter Conversion

    • Parse YAML Front Matter.
    • Extract existing fields (like Date, tags).
    • Interactively collect missing fields (title, category, description).
    • Generate complete TOML Front Matter.
  3. Image Processing

    • Extract all ![[...]] and ![]() references.
    • Recursively search for images in the Attachment/ directory.
    • Standardize filenames (remove spaces, convert to lowercase).
    • Copy to blog/static/{category}/.
    • Generate an image path mapping table.
  4. Link Conversion

    • Replace ![[image.png]] with ![alt text](/images/{category}/image.png).
    • Keep Wiki links [[article]] unchanged (Hugo supports them).
    • Handle edge cases (special characters in filenames).
  5. File Publishing

    • Combine Front Matter and body content.
    • Generate a filename with a date prefix (2025-11-14_slug.md).
    • Save to blog/content/posts/{category}/.
    • Remove the published file from the ready/ queue.

Technology Stack Selection

Python 3.x

  • Reason: Cross-platform, rich ecosystem, suitable for scripting.
  • Core libraries: pathlib (path operations), re (regex matching), shutil (file copying).
  • Third-party libraries: pyyaml (YAML parsing), questionary (interactive CLI, optional).

YAML Configuration File

  • Reason: Good readability, easy to maintain.
  • Purpose: Path mappings, default values, image naming rules.

Core Implementation Details

1. Front Matter Conversion

Core Challenge: Type mapping from YAML to TOML.

def generate_toml_front_matter(self, metadata: Dict) -> str:
    """Generate TOML Front Matter"""
    toml_lines = ["+++"]

    # String fields - need to be wrapped in quotes
    toml_lines.append(f'title = "{metadata["title"]}"')
    toml_lines.append(f'date = "{metadata["date"]}"')
    toml_lines.append(f'lastmod = "{metadata["lastmod"]}"')

    # Array fields - need brackets and quotes
    toml_lines.append(f'author = ["{metadata["author"]}"]')
    toml_lines.append(f'categories = ["{metadata["category"]}"]')

    # Tags array - dynamically generated
    if metadata['tags']:
        tags_str = ", ".join([f'"{tag}"' for tag in metadata['tags']])
        toml_lines.append(f'tags = [{tags_str}]')

    # Boolean field - lowercase true/false
    toml_lines.append(f'draft = {str(metadata["draft"]).lower()}')

    # Description - needs to escape quotes
    if metadata['description']:
        desc = metadata['description'].replace('"', '\\"')
        toml_lines.append(f'description = "{desc}"')

    toml_lines.append("+++")
    return "\n".join(toml_lines)

Key Points:

  • TOML strings must be wrapped in double quotes.
  • Array syntax: ["item1", "item2"].
  • Boolean values: lowercase true/false (not Python's True/False).
  • Special character escaping: descriptions might contain quotes.

2. Image Extraction and Processing

Core Challenge: Supporting two image reference formats.

def extract_image_references(self, body: str) -> List[str]:
    """Extract all image references from the article"""
    # Match Obsidian Wiki-style: ![[image.png]]
    wiki_pattern = r'!\[\[([^\]]+)\]\]'
    wiki_images = re.findall(wiki_pattern, body)

    # Match standard Markdown local images: ![alt text](image.png)
    md_pattern = r'!\[([^\]]*)\]\((?!http)([^\)]+)\)'
    md_images = [match[1] for match in re.findall(md_pattern, body)]

    return wiki_images + md_images

Image Filename Standardization:

def normalize_image_filename(self, filename: str) -> str:
    """Standardize image filename"""
    # Handle spaces: Mac mini.png → mac-mini.png
    if config['remove_spaces']:
        filename = filename.replace(' ', '-')

    # Convert to lowercase: Test.PNG → test.png
    if config['lowercase']:
        name, ext = os.path.splitext(filename)
        filename = name.lower() + ext.lower()

    return filename

Actual Effect:

  • Mac mini.pngmac-mini.png
  • Pasted image 20230320223703.pngpasted-image-20230320223703.png
  • Test File.JPGtest-file.jpg

Core Challenge: Precise matching and replacement to avoid accidental changes.

def replace_image_links(self, body: str, image_mapping: Dict[str, str], category: str) -> str:
    """Replace image links with Hugo format"""
    result = body

    for original_name, new_name in image_mapping.items():
        # Replace Wiki-style links
        wiki_pattern = rf'!\[\[{re.escape(original_name)}\]\]'
        hugo_link = f'![{new_name}](/images/{category}/{new_name})'
        result = re.sub(wiki_pattern, hugo_link, result)

        # Replace standard Markdown links (preserving original alt text)
        md_pattern = rf'!\[([^\]]*)\]\({re.escape(original_name)}\)'
        hugo_link_with_alt = rf'![\1](/images/{category}/{new_name})'
        result = re.sub(md_pattern, hugo_link_with_alt, result)

    return result

Key Points:

  • Use re.escape() to handle special characters in filenames (like parentheses, spaces).
  • Preserve the alt text of standard Markdown links.
  • Generate an absolute path /images/category/image.png (as required by Hugo).

Usage Demonstration

Publishing Flow:

$ python publish.py

============================================================
  Obsidian → Hugo Blog Automatic Publishing Tool
============================================================

🔍 Scanning for drafts...

📝 Available draft files:

  [1] Lab Automation: From Traditional to Intelligent Transformation.md (Modified: 2024-08-19 09:23)
  [2] tailscale-derp-guide.md (Modified: 2024-06-07 16:52)

Select a draft to publish: 2

✅ Selected: tailscale-derp-guide.md

📋 Please provide publishing information:

Article Title [Tailscale Complete Guide]: Tailscale Complete Guide: From Self-Hosting a DERP Server to Mac Subnet Routing
Select a category:
  ❯ tech
    read
    life
    project

Tags (comma-separated): Tailscale, DERP, Mac, Subnet Routing, Networking, Server Deployment
Article Description (for SEO): A detailed guide on how to self-host a Tailscale DERP server and configure a Mac as a subnet router.

🖼️  Processing images...
Found 3 images.
✅ Copied image: Mac mini.png -> mac-mini.png
✅ Copied image: Pasted image 20230320223703.png -> pasted-image-20230320223703.png
✅ Copied image: tailscale-diagram.png -> tailscale-diagram.png

📝 Generating article...

============================================================
✅ Publishing complete!
============================================================

📍 Article location: blog/content/posts/tech/2025-11-14_tailscale-derp-guide.md
📍 Category: tech
📍 Tags: Tailscale, DERP, Mac, Subnet Routing, Networking, Server Deployment

💡 Next steps:
   1. Sync: python sync.py
   2. Preview: cd blog && hugo server -D
   3. Deploy: cd blog && git push

Performance Data (v1.0)

MetricManual ProcessAutomation ToolImprovement
Publish Time10-15 minutes1-2 minutes83%
Image HandlingManual find, copy, renameAutomatic100%
Front MatterManual writingAuto-generated100%
Link ConversionManual find & replaceAuto bulk replace100%
Error Rate~20% (missing images, wrong paths)<5%75%

Step 2: Bidirectional Sync (v2.0)

Limitations of One-Way Publishing

Version 1.0 of the automation tool greatly improved publishing efficiency, but I soon discovered new problems:

Content management dilemma caused by one-way publishing:

  1. Unsynced Modifications: Sometimes I would directly modify articles in my Hugo Blog (e.g., quick fixes via GitHub Web), but these changes wouldn't sync back to Obsidian.
  2. Incomplete Backups: Obsidian's backup was only updated at the time of publishing. If I modified the article in Hugo, the version in Obsidian became outdated.
  3. Chaotic Management: Published articles were not organized by category, making them difficult to manage.
  4. Dual Maintenance: I had to maintain the same article in two places, which was error-prone.

What I needed was not one-way publishing, but bidirectional synchronization.

Bidirectional Sync System Design

Requirements Analysis

  1. Organization by Category

    • Obsidian's published/ directory should be organized by tech/read/life/project.
    • It should mirror the structure of Hugo Blog's content/posts/ directory.
  2. Intelligent Sync Detection

    • Automatically detect which articles need syncing.
    • Determine the sync direction based on file modification times.
    • Avoid unnecessary file copying.
  3. Flexible Sync Direction

    • Support bidirectional sync (Obsidian ↔ Hugo).
    • Support unidirectional sync (Obsidian → Hugo or Hugo → Obsidian only).
    • Allow the user to choose based on the situation.

Sync Strategy

Core Principle: The latest modification time wins.

# Pseudocode
for each file in all_files:
    if file exists only in Obsidian:
        action = "Add to Hugo"
    elif file exists only in Hugo:
        action = "Add to Obsidian"
    elif obsidian_mtime > hugo_mtime:
        action = "Update Hugo"
    elif hugo_mtime > obsidian_mtime:
        action = "Update Obsidian"
    else:
        action = "Synced, skip"

Time Comparison Precision:

  • Use second-level precision (stat().st_mtime).
  • A time difference greater than 1 second is considered a modification (to avoid filesystem precision issues).

Directory Structure Refactoring

Before (v1.0):

Main/07 blog idea/
├── draft/
├── ready/
├── published/              # Flat structure
│   ├── article-1.md
│   ├── article-2.md
│   └── ...
└── *.md

After (v2.0):

Main/07 blog idea/
├── draft/
├── ready/
├── published/              # Organized by category
│   ├── tech/              # Tech
│   ├── read/              # Reading
│   ├── life/              # Life
│   └── project/           # Projects
└── *.md

Core Implementation

1. File Scanning and Comparison

def scan_files(self, directory: Path, category: str) -> Dict[str, Path]:
    """Scan for article files in the specified directory"""
    files = {}
    category_dir = directory / category
    if category_dir.exists():
        for file in category_dir.glob("*.md"):
            if file.is_file():
                files[file.name] = file
    return files

def compare_files(self, obsidian_files: Dict[str, Path],
                  hugo_files: Dict[str, Path]) -> Tuple[List, List, List]:
    """
    Compare files between Obsidian and Hugo.
    Returns: (to_hugo, to_obsidian, conflicts)
    """
    to_hugo = []      # Obsidian updated → Hugo
    to_obsidian = []  # Hugo updated → Obsidian
    conflicts = []    # Files modified on both sides

    # In Obsidian but not Hugo, or Obsidian is newer
    for filename, obs_path in obsidian_files.items():
        if filename not in hugo_files:
            to_hugo.append((filename, obs_path, None, 'new'))
        else:
            hugo_path = hugo_files[filename]
            obs_mtime = self.get_file_mtime(obs_path)
            hugo_mtime = self.get_file_mtime(hugo_path)

            # Only consider modified if time diff > 1 second
            time_diff = abs(obs_mtime - hugo_mtime)
            if time_diff > 1:
                if obs_mtime > hugo_mtime:
                    to_hugo.append((filename, obs_path, hugo_path, 'update'))
                else:
                    to_obsidian.append((filename, hugo_path, obs_path, 'update'))

    # In Hugo but not Obsidian
    for filename, hugo_path in hugo_files.items():
        if filename not in obsidian_files:
            to_obsidian.append((filename, hugo_path, None, 'new'))

    return to_hugo, to_obsidian, conflicts

2. Sync Execution

def sync_file(self, source: Path, target: Path, action: str):
    """Sync a single file"""
    try:
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source, target)  # Preserves file metadata
        return True
    except Exception as e:
        print(f"❌ Sync failed for {source.name}: {e}")
        return False

def sync_category(self, category: str, direction: str = 'both') -> Dict:
    """
    Sync articles for a specific category.
    direction: 'to_hugo', 'to_obsidian', 'both'
    """
    print(f"\n📂 Syncing category: {category}")
    print("-" * 60)

    # Scan files
    obsidian_files = self.scan_files(self.published_folder, category)
    hugo_files = self.scan_files(self.hugo_content, category)

    print(f"Obsidian: {len(obsidian_files)} articles")
    print(f"Hugo Blog: {len(hugo_files)} articles")

    # Compare files
    to_hugo, to_obsidian, conflicts = self.compare_files(obsidian_files, hugo_files)

    stats = {
        'to_hugo': 0,
        'to_obsidian': 0,
        'skipped': 0,
        'failed': 0
    }

    # Sync to Hugo
    if direction in ['to_hugo', 'both'] and to_hugo:
        print(f"\n📤 Needs sync to Hugo: {len(to_hugo)} articles")
        for filename, source, target, action in to_hugo:
            action_text = "Adding" if action == 'new' else "Updating"
            target_path = self.hugo_content / category / filename
            if self.sync_file(source, target_path, action):
                print(f"  ✅ {action_text}: {filename}")
                stats['to_hugo'] += 1
            else:
                stats['failed'] += 1

    # Sync to Obsidian
    if direction in ['to_obsidian', 'both'] and to_obsidian:
        print(f"\n📥 Needs sync to Obsidian: {len(to_obsidian)} articles")
        for filename, source, target, action in to_obsidian:
            action_text = "Adding" if action == 'new' else "Updating"
            target_path = self.published_folder / category / filename
            if self.sync_file(source, target_path, action):
                print(f"  ✅ {action_text}: {filename}")
                stats['to_obsidian'] += 1
            else:
                stats['failed'] += 1

    return stats

Command-Line Interface

# Check sync status
python sync.py --status

# Bidirectional sync
python sync.py
python sync.py --direction both

# Unidirectional sync
python sync.py --direction to_hugo
python sync.py --direction to_obsidian

# Dry run mode
python sync.py --dry-run

# Interactive mode
python sync.py -i

Usage Effect

Initial Sync: From Hugo to Obsidian

My Hugo Blog already had 37 articles. Running the sync script for the first time:

$ python sync.py --direction to_obsidian

============================================================
  Obsidian ↔ Hugo Blog Bidirectional Sync Tool
============================================================
Sync direction: Hugo → Obsidian

📂 Syncing category: tech
------------------------------------------------------------
Obsidian: 0 articles
Hugo Blog: 20 articles

📥 Needs sync to Obsidian: 20 articles
  ✅ Adding: ai-agent-seo-optimization.md
  ✅ Adding: tailscale-derp-guide.md
  ✅ Adding: github-to-cloudflare-pages.md
  ... (20 articles total)

📂 Syncing category: read
------------------------------------------------------------
Obsidian: 0 articles
Hugo Blog: 5 articles

📥 Needs sync to Obsidian: 5 articles
  ✅ Adding: Getting-things-done.md
  ... (5 articles total)

============================================================
✅ Sync complete!
============================================================

📊 Sync Statistics:
  📤 Obsidian → Hugo: 0 articles
  📥 Hugo → Obsidian: 37 articles

Total: 37 articles synced.

Daily Use: Bidirectional Sync

Scenario 1: Quick fix in Hugo

Found a typo in an article and fixed it quickly via GitHub Web:

# Sync back to Obsidian
$ python sync.py --direction to_obsidian

📂 Syncing category: tech
📥 Needs sync to Obsidian: 1 article
  ✅ Updating: obsidian-to-hugo-automation.md

Scenario 2: Regular bidirectional sync

Run once a week to ensure both sides are always consistent:

# Bidirectional sync
$ python sync.py

📂 Syncing category: tech
Obsidian: 20 articles
Hugo Blog: 21 articles

📥 Needs sync to Obsidian: 1 article
  ✅ Adding: new-article.md

📂 Syncing category: read
Obsidian: 6 articles
Hugo Blog: 5 articles

📤 Needs sync to Hugo: 1 article
  ✅ Adding: new-book-review.md

Performance Data (v2.0)

MetricData
Sync 37 articles~2 seconds
Status check~0.5 seconds
Single file sync~10ms
Memory usage~15MB

Step 3: Feature Enhancements (v3.0)

After perfecting the basic functions, I added several important features based on actual usage needs.

1. Unsplash Cover Image Integration

Problem: Many technical articles lack images, making them look monotonous.

Solution: Integrate the Unsplash API to automatically add high-quality cover images to articles without pictures.

Implementation Idea

  1. Detect if the article contains any images.
  2. If not, generate search keywords based on the title/tags.
  3. Call the Unsplash API to search for relevant images.
  4. Display 10 images for the user to choose from.
  5. Download the selected image to static/{category}/cover.jpg.
  6. Add an image field to the Front Matter.
  7. Add photographer attribution at the end of the article (to comply with Unsplash terms).

Configuration

Edit publish_config.yaml:

unsplash:
  access_key: "YOUR_ACCESS_KEY_HERE"
  enabled: true

Usage Effect

$ python publish.py

💡 No images found in the article. You can add an Unsplash cover image.
🔍 Searching Unsplash for cover images: "programming"...
✅ Found 10 images.

📸 Available images:
  [1] Modern workspace with laptop - by John Doe
  [2] Abstract technology background - by Jane Smith
  [3] Code on screen - by Developer X
  ...

Select an image (1-10, 0=skip): 1

✅ Cover image downloaded: blog/static/tech/cover.jpg
📝 Photographer attribution added.

Search Keyword Strategy

Intelligently generated with priority:

  1. English words in tags → e.g., Python, Docker
  2. English words in the title → e.g., Guide, Tutorial
  3. Category default words → tech"technology programming"

2. Sensitive Information Security Check

Problem: Technical articles may contain sensitive information (API Keys, company names, IP addresses, etc.).

Solution: Provide an automated script to check for sensitive information.

Checklist

  • ✓ Search for "key", "token", "password" - check for API keys.
  • ✓ Search for "BASF", "巴斯夫" - check for company names.
  • ✓ Search for "license" - check for license information.
  • ✓ Check for IP addresses (192.168.x.x).
  • ✓ Check configuration files in code blocks.
  • ✓ Check for sensitive information in screenshots.

Usage

# Automatically check before publishing
./check_sensitive.sh

Redaction Methods

  • API Key: sk-xxxxxsk-***[REDACTED]***
  • Company Name: BASF Shanghai R&D CenterA chemical company's R&D center
  • Server: 192.168.1.100192.168.x.x

3. Non-Interactive Publishing Support

Problem: Interactive publishing is not suitable for batch operations and script integration.

Solution: Support full command-line arguments.

Usage

python publish.py \
  -f "ready/article.md" \
  -t "Article Title" \
  -c tech \
  --tags "Tag1,Tag2,Tag3" \
  -s "article-slug" \
  -d "Article description (100-160 characters)"

Available Parameters:

  • -f, --file: Article path (required).
  • -t, --title: Title.
  • -c, --category: Category (tech/read/life/project).
  • --tags: Tags (comma-separated).
  • -s, --slug: English slug (optional, auto-generated).
  • -d, --description: SEO description.
  • -y, --non-interactive: Non-interactive mode (uses default values).

Auto-generation Features:

  • Automatically generate a filename with a date prefix (2025-11-14_slug.md).
  • Automatically generate an English slug (intelligently converted from the title).
  • Automatically generate a lastmod field (current date).

Part 3: Agent Skills Encapsulation (★Core Innovation★)

With the underlying Python scripts in place, the most important step is to encapsulate them into a Claude Agent Skill to achieve true "one-click publishing."

Design Idea: From Command Line to Natural Language

Problems with the Traditional Way

  • Need to memorize multiple commands and parameters.
  • Need to execute multiple steps in order.
  • Easy to miss a step (like forgetting to run sync.py).
  • Requires manual diagnosis and fixing when errors occur.

Goals of Agent Skills

  • The user only needs to say, "Help me publish this article."
  • Claude automatically understands the user's intent.
  • Automatically executes all necessary steps.
  • Intelligently handles errors and edge cases.
  • Provides clear feedback.

Skill Core Capabilities

A complete publishing Skill needs to have:

1. Intent Understanding

  • "Publish this article" → Publish the currently open file.
  • "Publish the latest article in the ready directory" → Scan and select the latest article.
  • "Publish the article about XX" → Search for a match by title.

2. Automated Workflow

  • Security check (sensitive information scan).
  • Article publishing (calling publish.py).
  • Bidirectional sync (calling sync.py).
  • Preview/Deploy (optional).

3. Intelligent Decision-Making

  • Automatically extract metadata from the article content.
  • Intelligently generate title, description, and tags.
  • Automatically select the appropriate category.
  • Handle image references.

4. Error Handling

  • Detect and fix common problems.
  • Provide clear error messages.
  • Offer solution suggestions.

Skill File Structure

My obsidian-hugo-publisher Skill file structure (all files are centralized for easy portability):

~/.claude/
└── skills/
    └── obsidian-hugo-publisher/
        ├── SKILL.md                 # Main Skill definition
        ├── WORKFLOW.md              # Publishing workflow
        ├── COMMANDS.md              # Command reference
        ├── publish.py               # Publishing script
        ├── sync.py                  # Sync script
        ├── unsplash_cover.py        # Unsplash cover image module
        ├── publish_config.yaml      # Configuration file
        └── check_sensitive.sh       # Sensitive info check script

Important Note:

  • 📍 Skills should be placed in the user's home directory ~/.claude/skills/, not the project directory.
  • 📍 This is Claude Code's global Skills directory.
  • 📍 All projects can share and use these Skills.

Design Advantages:

  • ✅ All related files are in one directory.
  • ✅ Can be transplanted to other projects with a single copy.
  • ✅ Easy for version control and sharing.
  • ✅ Globally available, can be called from any project.

Core Content of the Skill Prompt

Complete Publishing Workflow Definition

## When the user requests to publish an article

### Step 1: Understand User Intent
- If the user specifies a file, use that file.
- If the user says "this article," use the currently open file.
- If not specified, scan the ready/ directory and let the user choose.

### Step 2: Security Check (Mandatory)
- Run: `.claude/skills/obsidian-hugo-publisher/check_sensitive.sh`
- Check for sensitive information:
  - API keys, tokens, passwords
  - Company names (BASF)
  - IP addresses, server information
- If sensitive information is found, **stop publishing** and notify the user.

### Step 3: Extract or Ask for Metadata
- Try to automatically extract from the article content:
  - Title (from YAML Front Matter or the first # heading)
  - Existing tags
  - Category (inferred from file path or project field)
- If missing, intelligently ask the user:
  - Title (required)
  - Category: tech/read/life/project
  - Tags (comma-separated)
  - SEO description

### Step 4: Execute Publishing
Run the publishing command (using the script in the skill directory):
```bash
python .claude/skills/obsidian-hugo-publisher/publish.py \
  -f "ready/article.md" \
  -t "Article Title" \
  -c tech \
  --tags "Tag1,Tag2,Tag3" \
  -d "Article Description"

Step 5: Sync to Obsidian (Crucial)

After publishing, must run sync:

python .claude/skills/obsidian-hugo-publisher/sync.py

Step 6: Ask User for Next Step

  • Need a local preview? → cd blog && hugo server -D
  • Need to deploy? → cd blog && hugo && git add . && git commit && git push

### User Experience Demonstration

#### Scenario 1: Publishing a Specific Article

Me: Help me publish the article about Obsidian Hugo in the ready directory.

Claude: 🔍 Scanning the ready directory... Found article: 2025-11-14_obsidian-hugo-complete-publishing-system.md

📖 Reading article content... Extracted title: One-Click Blog Publishing with Claude Agent Skills Identified category: tech Extracted tags: Claude, Agent Skills, AI, Automation

🔒 Checking for sensitive information... ✅ No sensitive information found.

Please provide an SEO description (100-160 characters):

Leveraging Claude Agent Skills to encapsulate the complex publishing process into a single-sentence interaction.

📤 Publishing article... ✅ Article published to: blog/content/posts/tech/2025-11-14_obsidian-hugo-claude-agent-skills.md

🔄 Syncing to Obsidian... ✅ Synced to: Main/07 blog idea/published/tech/

🎉 Publishing complete!

Next step options:

  1. Local preview (hugo server -D)
  2. Build and deploy (hugo && git push)
  3. Finish

Please choose:


#### Scenario 2: One-Sentence Full Publish

Me: Help me publish the article and deploy it.

Claude: [Executing full workflow...] ✅ Sensitive information checked. ✅ Article published. ✅ Synced to Obsidian. ✅ Site built. ✅ Deployed to GitHub.

🎉 The article is now live!


#### Scenario 3: Intelligent Error Handling

Me: Help me publish the article.

Claude: 🔍 Scanning the ready directory... Found article: sensitive-article.md

🔒 Checking for sensitive information... ⚠️ Sensitive information found!

Issue 1: Line 42 contains a possible API Key. sk-proj-xxxxxxxxxxxxx

Issue 2: Line 156 contains a company name. BASF Shanghai R&D Center

❌ Publishing has been stopped.

Suggested fixes:

  1. Replace the API Key with: sk-[REDACTED]
  2. Replace the company name with: A chemical company's R&D center

Rerun publishing after fixing.


### Comparison: Traditional Way vs. Agent Skills

| Dimension        | Traditional Python Script         | Agent Skills            |
| ---------------- | --------------------------------- | ----------------------- |
| **Trigger**      | `python publish.py -f ... -t ...` | "Help me publish the article" |
| **Parameter Memory**| Need to remember all parameters   | No memory needed, natural conversation |
| **Metadata Input**| Must be specified in command line | Intelligent extraction + interactive supplement |
| **Error Handling**| Manual review of error messages and fixing | Automatic detection + fix suggestions |
| **Process Omission**| Easy to forget a step (like sync) | Automatically executes the full workflow |
| **Flexibility**  | Fixed parameters                  | Understands various phrasings |
| **Learning Curve**| Need to learn command-line args   | **Zero learning curve** |
| **Experience**   | Tool-like, mechanical             | Conversational, natural |

### Technical Implementation Details

#### How Skills Call Python Scripts

Claude Agent executes system commands via the `Bash` tool:
*   Uses the Bash tool to run `python .claude/skills/obsidian-hugo-publisher/publish.py` with arguments.
*   Parses the command output to extract success/failure information.
*   If it fails, analyzes the error message and provides suggestions.

**Path Design:** All scripts are placed in the skill directory to ensure:
*Easy portability (just copy one folder).
*Easy version control.
*Avoids path confusion.

#### How Skills Intelligently Extract Metadata

1.  Use the Read tool to read the article content.
2.  Use regular expressions to parse YAML Front Matter.
3.  Extract the first # heading as a fallback title.
4.  Analyze content keywords to recommend relevant tags.
5.  Extract the first paragraph as a description candidate.

#### How Skills Handle User Input

Claude's natural language understanding capabilities:
*   "Publish this article"Use the currently open file.
*   "Publish the XXX article"Search for a file with a matching title in the `ready/` directory.
*   "Publish the latest article"Sort by modification time and select the newest.
*   "Publish the article about AI"Search for articles with "AI" in the title/content.

---

## Complete User Guide

### Method 1: Using Agent Skills (★Recommended★)

This is the easiest way. You just need to talk to Claude to complete the entire publishing process.

#### The Simplest Publish

Me: Help me publish the article.


Claude will:
1.  Scan the `ready` directory.
2.  Let you choose which article to publish.
3.  Automatically check for sensitive information.
4.  Intelligently extract metadata (title, tags, etc.).
5.  Ask for missing information (category, description).
6.  Execute publishing and syncing.
7.  Ask if you need to preview or deploy.

**All you need to do is answer a few simple questions; everything else is automated!**

#### Publishing a Specific Article

Me: Help me publish the article about AI in the ready directory.


Claude will automatically search for an article with a matching title and publish it.

#### Full Publish and Deploy

Me: Help me publish the article and deploy it online.


Claude will execute the full workflow: publish → sync → build → deploy, fully automated.

**Comparison:**
*   **Agent Skills Method**: 1 sentence, 3-5 interactions, zero learning curve.
*   **Traditional Method**: 5 commands, need to memorize all parameters.

---

### Method 2: Using Python Scripts (Traditional Way)

If you don't want to use Agent Skills, you can also run the Python scripts directly:

#### 1️⃣ Security Check

```bash
./check_sensitive.sh

2️⃣ Publish Article

python publish.py \
  -f "ready/article.md" \
  -t "Article Title" \
  -c tech \
  --tags "Tag1,Tag2,Tag3" \
  -d "Article Description"

3️⃣ Sync

After publishing, you must run sync to sync the article from blog/content/posts/ to published/:

python sync.py

Sync Relationship:

  • blog/content/posts/published/ (primary direction)
  • published/ is a backup synced from the blog.

5️⃣ Local Preview

cd blog
hugo server -D

Visit http://localhost:1313 to check the article's appearance.

6️⃣ Build and Deploy

cd blog
hugo
git add .
git commit -m "Publish new article: [Article Title]"
git push origin main

Command Cheat Sheet

# Publish a single article (interactive)
python publish.py

# View the ready-to-publish queue
ls -lh "Main/07 blog idea/ready/"

# View published articles
ls -lh "Main/07 blog idea/published/"
# Bidirectional sync (recommended, must run after publishing)
python sync.py

# Check sync status (does not execute sync)
python sync.py --status

# Unidirectional sync: Hugo → Obsidian
python sync.py --direction to_obsidian

# Unidirectional sync: Obsidian → Hugo
python sync.py --direction to_hugo

# Interactive sync
python sync.py --interactive

# Dry run mode (see which files would be synced)
python sync.py --dry-run

Preview and Deploy

# Local preview (including drafts)
cd blog && hugo server -D

# Build static site
cd blog && hugo

# Commit and push
cd blog && git add . && git commit -m "Publish new article" && git push

Workflow Best Practices

✅ Do's

  1. Check for sensitive information before publishing (Important)

    ./check_sensitive.sh  # Automatically checks for sensitive info
    
    • Search for and hide API keys, license keys.
    • Replace company names (BASF → a certain chemical company).
    • Check for sensitive info in screenshots.
  2. Sync immediately after publishing

    python publish.py   # Publish
    python sync.py      # Sync (crucial step)
    
  3. Use descriptive image filenames

    • docker-setup-diagram.png
    • IMG_1234.png
  4. Carefully write SEO metadata

    • Title: Concise, includes keywords, 50-60 characters.
    • Description: 100-160 characters, summarizes core content.
    • Tags: 3-5 relevant tags.
  5. Keep the ready/ queue empty

    • ready/ is a temporary queue, not for long-term storage.
    • Publish it once it's written.

❌ Don'ts

  1. Publish without checking for sensitive information (Dangerous)

    • ⚠️ Could leak API keys, passwords, licenses.
    • ⚠️ Could expose internal company information (BASF, etc.).
    • ⚠️ Could violate confidentiality agreements.
    • Always run ./check_sensitive.sh to check.
  2. Don't run sync.py after publishing

    • published/ will not be updated automatically.
    • Must be synced via sync.py.
  3. Don't store articles in ready/ for a long time

    • Modify in draft/.
    • Move to ready/ when complete.
  4. Don't use Chinese characters in image filenames

    • May cause URL encoding issues.

Complete Workflow Example

Scenario 1: Publishing a New Article

# 1. Move the article from draft/ to ready/
mv "Main/07 blog idea/draft/my-article.md" "Main/07 blog idea/ready/"

# 2. Security check (mandatory)
./check_sensitive.sh

# 3. Publish the article
python publish.py
# Select the article, fill in metadata...

# 4. Sync to Obsidian (important)
python sync.py

# 5. Preview
cd blog && hugo server -D

# 6. Deploy
cd blog
hugo
git add .
git commit -m "Publish new article: My Article Title"
git push

Scenario 2: Modified an article in Hugo

If you edited an article directly in blog/content/posts/, sync it back to Obsidian:

python sync.py --direction to_obsidian

Scenario 3: Checking Publishing Status

# View the ready-to-publish queue
ls -lh "Main/07 blog idea/ready/"

# Check sync status
python sync.py --status

# View Hugo articles
ls -lh "blog/content/posts/tech/"

Technical Summary

Technical Highlights

1. Intelligent Front Matter Conversion

Challenge: Differences in type systems between YAML and TOML. Solution: Strictly follow TOML specifications, paying special attention to quotes, arrays, and boolean formats.

2. Image Filename Handling

Challenge: Spaces and special characters in filenames. Solution: Use re.escape() to escape special characters, ensuring accurate regex matching.

3. Timestamp Precision Handling

Challenge: Different filesystems have different timestamp precisions. Solution: Use a 1-second tolerance to avoid false positives.

time_diff = abs(obs_mtime - hugo_mtime)
if time_diff > 1:  # Only consider modified if difference > 1 second
    # Perform sync

4. Automatic Directory Creation

Challenge: The target directory may not exist. Solution: Use mkdir(parents=True, exist_ok=True).

target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)

5. Metadata Preservation

Challenge: Keeping file modification times consistent. Solution: Use shutil.copy2() instead of shutil.copy().

shutil.copy2(source, target)  # Preserves modification time and other metadata

Complete Performance Data

FeatureManual OperationAutomation ToolImprovement
Publish Single Article10-15 minutes1-2 minutes83%
Sync 37 ArticlesN/A (manually impractical)~2 seconds
Image HandlingManual copy one by oneAuto bulk100%
Front MatterManual writingAuto-generated100%
Error Rate~20%<5%75%

Return on Investment

Total Development Time: Approx. 12 hours

  • v1.0 Automated Publishing: 6 hours
  • v2.0 Bidirectional Sync: 4 hours
  • v3.0 Feature Enhancements: 2 hours

Return Calculation:

  • Assume publishing 4 articles per month.
  • Save 10 minutes per article.
  • Save 40 minutes per month.
  • Breakeven in 18 months (12 hours / 40 minutes/month).

Intangible Benefits:

  • Reduced psychological burden, increased motivation to publish.
  • Publishing frequency increased from 2/month to 4-5/month.
  • More standardized content management, with both sides always consistent.
  • Reduced error rate, improved content quality.

Future Outlook

Potential Improvements

1. Conflict Detection and Resolution

The current version simply defaults to the latest modification time. Future improvements could include:

  • Three-way merge: Similar to Git's merge strategy.
  • Manual selection: When a conflict is detected, let the user manually choose which version to keep.
  • Diff display: Show the differences between the two versions to aid decision-making.

2. Incremental Sync

Currently, it scans all files every time. This can be optimized:

  • Record sync history: Use an SQLite database to record the last sync time.
  • Only check modified files: Reduce filesystem operations.
  • Hash validation: Use file hashes instead of timestamps to determine if a file has been modified.

3. Real-time Sync

Use file monitoring to achieve real-time synchronization:

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class SyncHandler(FileSystemEventHandler):
    def on_modified(self, event):
        # Automatically trigger sync
        pass

4. Obsidian Plugin Integration

Use the Obsidian Shell Commands plugin to set up a hotkey for one-click publishing:

# Command configuration
cd /Users/huashan/Documents/Obsidian && python publish.py

# Bind hotkey
Cmd+Shift+P

Effect: While editing an article in Obsidian, press the hotkey to trigger the publishing process.

5. Image Compression

Integrate the Pillow library to automatically compress images:

from PIL import Image

def compress_image(source_path: Path, target_path: Path, quality: int = 85):
    """Compress an image"""
    with Image.open(source_path) as img:
        # Convert RGBA to RGB (PNG → JPG)
        if img.mode == 'RGBA':
            img = img.convert('RGB')

        # Compress and save
        img.save(target_path, optimize=True, quality=quality)

Benefit: Reduce image size by 50-70%, improving blog loading speed.

6. AI-Assisted Metadata Generation

Use AI to automatically generate:

  • SEO-friendly article descriptions.
  • Relevant tag recommendations.
  • Intelligent English slug generation.
  • Image alt text generation (improving SEO and accessibility).

Conclusion

Three-Layer Architecture: From Underlying to Interactive

This project demonstrates a complete three-layer architecture for an automated publishing system:

Layer 1: Underlying Tools (Python Scripts)

✅ Front Matter Conversion (YAML → TOML) ✅ Image Extraction and Processing ✅ Filename Standardization ✅ Link Conversion ✅ Bidirectional Sync ✅ Security Check ✅ Cover Image Integration

Value: Automates manual processes.

Layer 2: Command-Line Interface

✅ Parameterized publishing command ✅ Flexible configuration system ✅ Friendly interactive CLI ✅ Detailed progress feedback

Value: Provides a programmable interface.

Layer 3: Agent Skills (★Core Innovation★)

✅ Trigger the entire workflow with a single sentence ✅ Intelligently understand user intent ✅ Automatically extract and infer metadata ✅ Intelligent error handling and suggestions ✅ Zero learning curve

Value: Turns a tool into a conversational partner.

Efficiency Improvement Comparison

SolutionTimeStepsLearning CurveError Rate
Manual Operation10-15 min10+ stepsMedium~20%
Python Script2-3 min5 commandsHigh~5%
Agent Skills1-2 min1 sentenceZero<3%

Key Insight: The True Value of AI Agents

It's not just automation, but a revolution in interaction:

  1. From Command to Conversation

    • Traditional: Need to memorize commands, parameters, order.
    • Agent: Just state your needs in plain language.
  2. From Passive to Proactive

    • Traditional: The tool waits for instructions.
    • Agent: Understands intent, proactively asks questions, makes intelligent decisions.
  3. From Tool to Partner

    • Traditional: You need to understand the tool's language.
    • Agent: The tool understands your language.
  4. From Lowering the Barrier to Eliminating It

    • Traditional: Simplifies the operational process.
    • Agent: Just say what you want.

Final Results

Efficiency Gains:

  • Publishing time: 10-15 minutes → 1 sentence (90%+ improvement)
  • Syncing 37 articles: Manually impractical → 2 seconds (infinite improvement)
  • Error rate: 20% → <3% (85% reduction)
  • Learning curve: Need to learn command line → Zero learning curve

Experience Improvement:

  • Psychological burden is almost zero; publish whenever you want.
  • Publishing frequency significantly increased.
  • More standardized content management.
  • The creative process is completely uninterrupted.

This is the true value of AI Agents: not just saving time, but making human-computer interaction natural again, letting technology truly serve people.

Further Thoughts

Application Scenarios for Claude Agent Skills

This publishing system is just one example. Agent Skills can be applied to many more scenarios:

  1. Development Workflows

    • "Help me create a new React component."
    • "Run the tests and fix all failing cases."
    • "Refactor this function to improve readability."
  2. Data Processing

    • "Analyze this CSV file and generate a visual report."
    • "Extract error messages from these logs."
    • "Merge the data from these two databases."
  3. Content Management

    • "Organize my notes by topic."
    • "Generate a work summary for this week."
    • "Translate this article into English."
  4. Ops Automation

    • "Check the server status and generate a report."
    • "Back up all databases."
    • "Deploy the latest version to production."

Best Practices for Designing Agent Skills

  1. Clear Responsibility Boundaries: A Skill should focus on a specific task domain.
  2. Complete Error Handling: Foresee and handle all possible exceptions.
  3. Intelligent Defaults: Reduce user input and improve the experience.
  4. Friendly Feedback: Let the user know what the Agent is doing.
  5. Composability: A Skill can call other Skills or tools.
  6. Portability: Centralize all related files (scripts, configs, docs) in one directory.
    • ✅ Copy and use instantly.
    • ✅ Easy for version control and sharing.
    • ✅ Lowers the barrier to entry.

The Future of AI Workflows

From this project, we can see:

  • AI doesn't replace tools; it enhances them.
  • The best AI Agent is invisible: The user doesn't feel like they're "using a tool," but "getting a task done."
  • Natural language is the future API: Describing needs is more natural than calling an interface.

Project Files

Complete File Structure

All files are centralized in one skill directory for easy portability and sharing:

~/.claude/
└── skills/
    └── obsidian-hugo-publisher/
        ├── SKILL.md                 # Main Skill definition
        ├── WORKFLOW.md              # Publishing workflow
        ├── COMMANDS.md              # Command reference
        ├── publish.py               # Publishing script
        ├── sync.py                  # Sync script
        ├── unsplash_cover.py        # Unsplash cover image module
        ├── publish_config.yaml      # Configuration file
        └── check_sensitive.sh       # Sensitive info check script

File Descriptions:

Skill Definition Files:

  • SKILL.md - The core instruction file for the Claude Agent, defining how to perform the publishing task.
  • WORKFLOW.md - Detailed explanation of the publishing workflow.
  • COMMANDS.md - Command reference and user guide.

Executable Scripts:

  • publish.py - Python publishing script, handles Front Matter conversion, image processing, etc.
  • sync.py - Python sync script, implements bidirectional sync between Obsidian and Hugo.
  • unsplash_cover.py - Unsplash cover image module, automatically fetches and downloads high-quality cover images.
  • check_sensitive.sh - Bash script, checks for sensitive information.

Configuration File:

  • publish_config.yaml - Configuration file, contains paths, default values, Unsplash API key, etc.

Quick Start

Steps:

  1. Install Claude Code

  2. Copy the Skill Folder

    # Copy the entire skill folder to your Obsidian root directory
    cp -r obsidian-hugo-publisher ~/.claude/skills/
    # Or just git clone it into the .claude/skills/ directory
    
  3. Configure publish_config.yaml

    paths:
      obsidian_root: "/Users/your_username/Documents/Obsidian"
      blog_root: "/Users/your_username/blog"
      ready_folder: "Main/07 blog idea/ready"
      published_folder: "Main/07 blog idea/published"
    
    unsplash:
      access_key: "YOUR_UNSPLASH_ACCESS_KEY"
      enabled: true
    
  4. Start Using

    • Open your Obsidian directory in Claude Code.
    • Just say: "Help me publish the article."
    • It's that simple! ✨

Advantages:

  • ✅ Copy once, use immediately.
  • ✅ All dependencies are together.
  • ✅ Easy to update and maintain.
  • ✅ Zero learning curve.

Method 2: Using Python Scripts (Traditional Way)

If you don't want to use Agent Skills, you can also run the Python scripts directly:

  1. Install Dependencies

    pip install pyyaml requests pillow
    
  2. Configure Edit .claude/skills/obsidian-hugo-publisher/publish_config.yaml.

  3. Run the Script

    python .claude/skills/obsidian-hugo-publisher/publish.py
    

Porting to Other Projects

Just 3 steps:

  1. Copy the entire skill folder

    cp -r .claude/skills/obsidian-hugo-publisher /path/to/new/project/.claude/skills/
    
  2. Modify the configuration file Update the paths in publish_config.yaml.

  3. Start using In the new project, say to Claude, "Help me publish the article."

It's that simple! All related files are in one folder, truly achieving "configure once, use everywhere."

Further Reading

Automation Tool Development

Technical Documentation


Update Log:

  • 2025-11-14: v1.0 Completed Python automated publishing tool development.
  • 2025-11-14: v2.0 Completed bidirectional sync feature development.
  • 2025-11-14: v3.0 Added Unsplash cover images, sensitive info check, non-interactive publishing.
  • 2025-11-14: v4.0 Encapsulated as a Claude Agent Skill, achieving one-click publishing.
  • 2025-11-14: Published the full technical sharing article.
obsidian-hugo-claude-agent-skills-1.png

Keywords: Claude Agent Skills, AI Agent, Automated Workflow, Obsidian, Hugo, Natural Language Interaction, Blog Publishing


Photo by Juan Encalada on Unsplash

One-Click Blog Publishing with Claude Agent Skills: From Tedious Workflows to Natural Language Interaction | 原子比特之间