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

- Name
- Huashan
- @herohuashan
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
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.
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.
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
| Solution | Advantages | Disadvantages |
|---|---|---|
| Shell Scripts | Lightweight, fast | Requires memorizing commands, complex args, inflexible |
| Python Scripts | Powerful, easily extensible | Still requires command-line operation, poor interactivity |
| Obsidian Plugins | Deep integration with editor | Complex development, requires TypeScript |
| Claude Skills | Natural language interaction, intelligent decision-making | Requires Claude Code |
Unique Advantages of Agent Skills:
- Natural Interaction: No need to memorize commands; just describe your needs in plain language.
- Intelligent Decision-Making: Can automatically handle edge cases based on context.
- Flexible Adaptation: Can handle different phrasings like "publish this article" or "publish the latest article in the ready directory."
- Error Handling: Automatically detects problems and provides suggestions.
- 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.
2. Image Link Differences
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:


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
| Feature | Obsidian | Hugo | Conversion Needed |
|---|---|---|---|
| Front Matter | YAML | TOML | ✅ Yes |
| Image Links | ![[...]] | ![]() | ✅ Yes |
| Image Paths | Relative | Absolute | ✅ Yes |
| Filename Spaces | Supported | Not recommended | ✅ Yes |
| Wiki Links | [[...]] | []() | ❌ No (Hugo supports it) |
| Markdown Syntax | Standard + Ext | Standard | ❌ 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
Draft Scanning and Selection
- Automatically scan the
ready/queue directory. - Display all available drafts and their modification times.
- Support interactive selection.
- Automatically scan the
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.
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.
- Extract all
Link Conversion
- Replace
![[image.png]]with. - Keep Wiki links
[[article]]unchanged (Hugo supports them). - Handle edge cases (special characters in filenames).
- Replace
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'sTrue/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: 
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.png→mac-mini.pngPasted image 20230320223703.png→pasted-image-20230320223703.pngTest File.JPG→test-file.jpg
3. Link Replacement
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''
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''
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)
| Metric | Manual Process | Automation Tool | Improvement |
|---|---|---|---|
| Publish Time | 10-15 minutes | 1-2 minutes | 83% |
| Image Handling | Manual find, copy, rename | Automatic | 100% |
| Front Matter | Manual writing | Auto-generated | 100% |
| Link Conversion | Manual find & replace | Auto bulk replace | 100% |
| 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:
- 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.
- 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.
- Chaotic Management: Published articles were not organized by category, making them difficult to manage.
- 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
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.
- Obsidian's
Intelligent Sync Detection
- Automatically detect which articles need syncing.
- Determine the sync direction based on file modification times.
- Avoid unnecessary file copying.
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)
| Metric | Data |
|---|---|
| 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
- Detect if the article contains any images.
- If not, generate search keywords based on the title/tags.
- Call the Unsplash API to search for relevant images.
- Display 10 images for the user to choose from.
- Download the selected image to
static/{category}/cover.jpg. - Add an
imagefield to the Front Matter. - 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:
- English words in tags → e.g.,
Python,Docker - English words in the title → e.g.,
Guide,Tutorial - 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-xxxxx→sk-***[REDACTED]*** - Company Name:
BASF Shanghai R&D Center→A chemical company's R&D center - Server:
192.168.1.100→192.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
lastmodfield (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:
- Local preview (hugo server -D)
- Build and deploy (hugo && git push)
- 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:
- Replace the API Key with: sk-[REDACTED]
- 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
Publishing Related
# 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/"
Sync Related
# 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
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.
Sync immediately after publishing
python publish.py # Publish python sync.py # Sync (crucial step)Use descriptive image filenames
- ✅
docker-setup-diagram.png - ❌
IMG_1234.png
- ✅
Carefully write SEO metadata
- Title: Concise, includes keywords, 50-60 characters.
- Description: 100-160 characters, summarizes core content.
- Tags: 3-5 relevant tags.
Keep the
ready/queue emptyready/is a temporary queue, not for long-term storage.- Publish it once it's written.
❌ Don'ts
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.shto check.
Don't run
sync.pyafter publishingpublished/will not be updated automatically.- Must be synced via
sync.py.
Don't store articles in
ready/for a long time- Modify in
draft/. - Move to
ready/when complete.
- Modify in
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
| Feature | Manual Operation | Automation Tool | Improvement |
|---|---|---|---|
| Publish Single Article | 10-15 minutes | 1-2 minutes | 83% |
| Sync 37 Articles | N/A (manually impractical) | ~2 seconds | ∞ |
| Image Handling | Manual copy one by one | Auto bulk | 100% |
| Front Matter | Manual writing | Auto-generated | 100% |
| 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
| Solution | Time | Steps | Learning Curve | Error Rate |
|---|---|---|---|---|
| Manual Operation | 10-15 min | 10+ steps | Medium | ~20% |
| Python Script | 2-3 min | 5 commands | High | ~5% |
| Agent Skills | 1-2 min | 1 sentence | Zero | <3% |
Key Insight: The True Value of AI Agents
It's not just automation, but a revolution in interaction:
From Command to Conversation
- Traditional: Need to memorize commands, parameters, order.
- Agent: Just state your needs in plain language.
From Passive to Proactive
- Traditional: The tool waits for instructions.
- Agent: Understands intent, proactively asks questions, makes intelligent decisions.
From Tool to Partner
- Traditional: You need to understand the tool's language.
- Agent: The tool understands your language.
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:
Development Workflows
- "Help me create a new React component."
- "Run the tests and fix all failing cases."
- "Refactor this function to improve readability."
Data Processing
- "Analyze this CSV file and generate a visual report."
- "Extract error messages from these logs."
- "Merge the data from these two databases."
Content Management
- "Organize my notes by topic."
- "Generate a work summary for this week."
- "Translate this article into English."
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
- Clear Responsibility Boundaries: A Skill should focus on a specific task domain.
- Complete Error Handling: Foresee and handle all possible exceptions.
- Intelligent Defaults: Reduce user input and improve the experience.
- Friendly Feedback: Let the user know what the Agent is doing.
- Composability: A Skill can call other Skills or tools.
- 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
Method 1: Using Agent Skills (★Recommended★)
Steps:
Install Claude Code
- Visit https://claude.ai/claude-code
- Download and install Claude Code.
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/ directoryConfigure
publish_config.yamlpaths: 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: trueStart 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:
Install Dependencies
pip install pyyaml requests pillowConfigure Edit
.claude/skills/obsidian-hugo-publisher/publish_config.yaml.Run the Script
python .claude/skills/obsidian-hugo-publisher/publish.py
Porting to Other Projects
Just 3 steps:
Copy the entire skill folder
cp -r .claude/skills/obsidian-hugo-publisher /path/to/new/project/.claude/skills/Modify the configuration file Update the paths in
publish_config.yaml.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
Claude Agent Skills Related
- Claude Code Official Documentation
- Agent Skills Development Guide
- How to Design Effective Agent Prompts
Automation Tool Development
Technical Documentation
- Hugo Official Docs: Front Matter
- Obsidian Official Docs: Formatting Syntax
- Python pathlib Official Docs
- Python shutil Official Docs
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.

Keywords: Claude Agent Skills, AI Agent, Automated Workflow, Obsidian, Hugo, Natural Language Interaction, Blog Publishing
Photo by Juan Encalada on Unsplash
Related Posts
SEO Optimization in the AI Era - From Search Engines to AI Agents
AI is changing how information is accessed. This article introduces how to optimize blogs for AI agents (ChatGPT, Perplexity), including Schema.org structured data, FAQ markup, and robots.txt configuration.
Bidirectional Links System Demo - Backlinks
Demonstrating the bidirectional links feature implemented in Hugo blog, inspired by knowledge management approaches in Obsidian and Roam Research.
Love Heart Watering System - AI-Powered Eco-Friendly Innovation
An eco-friendly watering device designed together with Xixi, combining AI technology with waste reuse to solve the problem of unwatered plants during business trips.