
I’ve been putting off writing blog posts since switching over to the new, improved teachweaver site because the process annoys me. Writing is fine, but converting the post to HTML, adding it to a copy of my blog template, updating the meta tags, and modifying posts.json list of all my entries was taking entirely too long.
So, I decided to try Claude to write a Python program to help me streamline the process. I was really impressed with its coding ability—it only took about six prompts and some code details, and now I have a program that meets my needs.
The program automates the entire workflow:
- Prompts me for the blog title, description, and image details.
- Lets me input markdown directly or choose a file.
- Converts markdown to HTML.
- Fills in a template with metadata and formatted content.
- Saves the result in my blog’s post directory.
- Updates posts.json to include the new entry.
This removes all the tedious steps and lets me focus on writing. Now, instead of getting annoyed, I can just run the script and my post is ready to publish. I have considered adding an option to push the update right to Github, but for now I am going to do it manually so I can double check the post before making it live.
It’s not perfect, but it’s a huge improvement. I know there are other more convenient options, but I like having everything for my personal site stay on my coding VM and remain something I fully understand. I didn’t write the code myself, but thanks to Claude, I understand exactly what it does—and I could have written it myself… albeit much, much slower and with a lot of research. For now, it solved a problem that was keeping me from sharing more. It works—that’s what matters.
The program Claude created for me, or at least something similiar as I made a few changes before posting, is below. Check it out!
import os
import re
import json
import markdown
from datetime import datetime
import tkinter as tk
from tkinter import filedialog
import sys
def get_user_input(prompt, multiline=False):
"""Get input from user with the given prompt."""
print(prompt)
if multiline:
print("(Type 'END' on a new line when finished)")
lines = []
while True:
line = input()
if line == "END":
break
lines.append(line)
return "\n".join(lines)
else:
return input()
def slugify(title):
"""Convert title to URL-friendly slug."""
# Convert to lowercase
slug = title.lower()
# Replace non-alphanumeric characters with hyphens
slug = re.sub(r'[^a-z0-9]+', '-', slug)
# Remove leading and trailing hyphens
slug = slug.strip('-')
return slug
def select_markdown_file():
"""Open a file dialog to select a markdown file."""
root = tk.Tk()
root.withdraw() # Hide the main window
file_path = filedialog.askopenfilename(
title="Select Markdown File",
filetypes=[("Markdown files", "*.md"), ("Text files", "*.txt"), ("All files", "*.*")]
)
root.destroy()
return file_path if file_path else None
def read_markdown_file(file_path):
"""Read content from markdown file."""
try:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
except Exception as e:
print(f"Error reading markdown file: {e}")
return None
def markdown_to_html(markdown_content):
"""Convert markdown content to HTML."""
try:
# Create a markdown converter with extensions
html = markdown.markdown(
markdown_content,
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.smarty'
]
)
return html
except Exception as e:
print(f"Error converting markdown to HTML: {e}")
return None
def generate_html(template_path, output_path, replacements):
"""Generate HTML file with the provided replacements."""
try:
with open(template_path, 'r', encoding='utf-8') as file:
template = file.read()
# Apply replacements
for key, value in replacements.items():
template = template.replace(key, value)
# Write to output file
with open(output_path, 'w', encoding='utf-8') as file:
file.write(template)
print(f"\nHTML blog post generated successfully at: {output_path}")
return True
except Exception as e:
print(f"Error generating HTML file: {e}")
return False
def get_text_excerpt(markdown_content, max_chars=250):
"""Extract a plain text excerpt from markdown content."""
# Convert markdown to HTML
html = markdown_to_html(markdown_content)
if not html:
return ""
# Remove HTML tags to get plain text
plain_text = re.sub(r'<[^>]+>', '', html)
# Truncate to max_chars
if len(plain_text) <= max_chars:
return plain_text
# Try to find the last complete sentence within max_chars
truncated = plain_text[:max_chars]
last_sentence_end = max(
truncated.rfind('.'),
truncated.rfind('!'),
truncated.rfind('?')
)
if last_sentence_end > 0:
return plain_text[:last_sentence_end+1]
else:
# If no sentence ending found, truncate at the last space
last_space = truncated.rfind(' ')
if last_space > 0:
return plain_text[:last_space] + '...'
else:
return truncated + '...'
def update_posts_json(post_data, json_path):
"""Update the posts.json file with new blog post data."""
try:
# Read existing posts with proper error handling
posts = []
if os.path.exists(json_path):
with open(json_path, 'r', encoding='utf-8') as file:
content = file.read().strip()
print(f"DEBUG: Read {len(content)} characters from {json_path}")
if content: # Check if file has content
try:
posts = json.loads(content)
print(f"DEBUG: Successfully loaded {len(posts)} posts from JSON")
except json.JSONDecodeError as json_err:
print(f"JSON parsing error: {json_err}")
# Try to recover by creating a backup of the file
backup_path = json_path + ".backup"
with open(backup_path, 'w', encoding='utf-8') as backup_file:
backup_file.write(content)
print(f"Created backup of original file at {backup_path}")
# Initialize as empty list since we couldn't parse the file
posts = []
# Add new post at the beginning
posts.insert(0, post_data)
print(f"DEBUG: Preparing to write {len(posts)} posts to JSON file")
# Write updated posts back to file
with open(json_path, 'w', encoding='utf-8') as file:
json.dump(posts, file, indent=2, ensure_ascii=False)
print(f"\nSuccessfully updated posts.json at: {json_path}")
return True
except Exception as e:
print(f"Error updating posts.json: {e}")
return False
def main():
print("===== Blog Post Generator =====")
print("This program will help you generate an HTML blog post from your template.")
# Get blog post details
blog_title = get_user_input("Enter the blog post title:")
blog_description = get_user_input("Enter a brief description of the blog post (for metadata):")
# Ask for image details
image_filename = get_user_input("Enter the image filename (e.g., blog-image.jpg):")
image_alt = get_user_input("Enter alt text for the image:")
# Get today's date formatted as Month Day, Year
today_date = datetime.now().strftime("%B %d, %Y")
# Create a slug from the title
slug = slugify(blog_title)
# Ask user if they want to input markdown directly or from a file
use_file = get_user_input("Do you want to select a markdown file? (y/n)").lower() == 'y'
markdown_content = ""
if use_file:
print("Please select your markdown file...")
file_path = select_markdown_file()
if file_path:
markdown_content = read_markdown_file(file_path)
if not markdown_content:
print("Failed to read markdown content. Exiting.")
return
else:
print("No file selected. Exiting.")
return
else:
print("Enter your markdown content below:")
markdown_content = get_user_input("", multiline=True)
# Convert markdown to HTML
html_content = markdown_to_html(markdown_content)
if not html_content:
print("Failed to convert markdown to HTML. Exiting.")
return
# Define replacements
replacements = {
"": blog_title,
"": blog_title,
"": blog_description,
"": slug,
"": blog_title,
"": today_date,
"": image_filename,
"": image_alt,
"": html_content
}
# Ask for output location with options
print("\nWhere would you like to save the HTML file?")
print("1. Current directory")
print("2. Your blog posts directory")
print("3. Enter a custom path")
output_choice = get_user_input("Enter your choice (1, 2, or 3):")
if output_choice == "1":
output_directory = os.getcwd()
elif output_choice == "2":
# Default to a 'posts' subdirectory in the current working directory
output_directory = os.path.join(os.getcwd(), "posts")
# Create directory if it doesn't exist
os.makedirs(output_directory, exist_ok=True)
elif output_choice == "3":
output_directory = get_user_input("Enter the custom output directory path:")
# Create directory if it doesn't exist
os.makedirs(output_directory, exist_ok=True)
else:
print("Invalid choice. Using current directory.")
output_directory = os.getcwd()
output_filename = f"{slug}.html"
output_path = os.path.join(output_directory, output_filename)
# Generate HTML file
template_path = os.path.join(os.getcwd(), "blog_template.html")
if not os.path.exists(template_path):
template_path = get_user_input("Template file 'blog_template.html' not found. Enter the path to your template file:")
result = generate_html(template_path, output_path, replacements)
if result:
# Ask if user wants to update posts.json
print("\nDo you want to update a posts.json file with this blog post?")
print("1. Yes")
print("2. No")
update_json_choice = get_user_input("Enter your choice (1 or 2):")
if update_json_choice == "1":
print("\nWhich posts.json file would you like to update?")
print("1. Default location (./posts.json)")
print("2. Enter a custom path")
json_path_choice = get_user_input("Enter your choice (1 or 2):")
if json_path_choice == "1":
json_path = os.path.join(os.getcwd(), "posts.json")
elif json_path_choice == "2":
json_path = get_user_input("Enter the path to your posts.json file:")
else:
print("Invalid choice. Skipping JSON update.")
json_path = None
if json_path:
# Extract description from markdown content
description = get_text_excerpt(markdown_content)
# Create post data object
post_data = {
"title": blog_title,
"image": f"./images/{image_filename}",
"alt": image_alt,
"description": description,
"author": get_user_input("Enter author name:"),
"date": today_date,
"link": f"./posts/{slug}"
}
update_posts_json(post_data, json_path)
# Ask if user wants to open the generated file
open_file = get_user_input("Do you want to open the generated HTML file? (y/n)").lower() == 'y'
if open_file:
try:
if sys.platform == 'win32':
os.startfile(output_path)
elif sys.platform == 'darwin': # macOS
os.system(f"open '{output_path}'")
else: # Linux
os.system(f"xdg-open '{output_path}'")
except Exception as e:
print(f"Error opening file: {e}")
if __name__ == "__main__":
main()