Supercharge Your Hugo Blog with Claude: Automated Content Enhancement using Anthropic's API

A practical exploration of using Anthropic's Claude API to automatically enhance existing Hugo blog posts. The article covers the technical implementation of content parsing and API integration, while focusing on the benefits and limitations of AI-assisted content optimization. Includes workflow descriptions and best practices for maintaining content authenticity.

8 Minutes reading time

Behold the masterpiece that AI hallucinated while reading this post:

"How Little Hugo's Blog Got Super Powers"

(after I fed it way too many marketing blogs and memes)

Created using DALL-E 3

AI-Generated: How Little Hugo's Blog Got Super Powers

Introduction

Maintaining a technical blog requires constant attention to quality, readability, and SEO optimization. While creating fresh content is important, enhancing existing posts can be equally valuable. This guide explores how to leverage Anthropic’s Claude API to automatically improve your Hugo blog posts while preserving their core message and authenticity. In fact, this blog was target of such an optimization.

The Power of AI-Enhanced Content

Traditional content updates are time-consuming and often overlooked. By automating the enhancement process with Claude, you can systematically improve your entire blog archive without manual rewrites. The AI assists in refining titles for better click-through rates, crafting compelling descriptions, and generating comprehensive abstracts that accurately represent your content.

Understanding the Technical Flow

The enhancement process follows a straightforward path:

  1. Parse existing AsciiDoc files and extract TOML frontmatter

  2. Analyze the content structure and key messages

  3. Formulate targeted prompts for Claude

  4. Process API responses and update frontmatter

  5. Preserve original content while enhancing metadata

Crafting Effective Prompts

The key to successful enhancement lies in prompt engineering. Your prompts should guide Claude to:

  • Maintain your writing style and technical accuracy

  • Focus on clarity and engagement

  • Optimize for search intent

  • Preserve the original message while improving presentation

Implementation Considerations

Content Parsing

Hugo’s content structure makes it relatively simple to extract both frontmatter and main content. Using standard parsing libraries, we can separate TOML metadata from AsciiDoc content for targeted enhancement. It is also quite easy to use the YAML or JSON notation for the frontmatter in Hugo. All of the formats are machine-readable.

API Integration

Anthropic’s API offers straightforward integration options. The key is managing rate limits and handling responses appropriately to ensure reliable updates across your blog archive.

Example Implementation using Node.js

The overall optimitation process can be seen in the following Node.js/JavaScript code. Please pay close attention to the convertToMessage function, as this is the place where the Claude prompt to improve the blog content is crafted. One detail is that we explicitly instruct Claude to return the optimization in a machine readable JSON notation, as this makes it easy to feedback the optimized content to the original blog posting. In this example, the optimized frontmatter is written to a separate file, so the results can be reviewed before updating the original content.

import Anthropic from '@anthropic-ai/sdk';
import { sleep } from '@anthropic-ai/sdk/core';
import { Console } from 'console';

import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';

dotenv.config();

const client = new Anthropic({
    apiKey: process.env['ANTHROPIC_API_KEY'], // This is the default and can be omitted
});

function loadFilesWithExtension(dirPath, extension) {
    const results = [];

    // Read directory contents
    function readDirectory(currentPath) {
        console.info('Scanning directory ' + currentPath);
        const files = fs.readdirSync(currentPath);

        for (const file of files) {
            const filePath = path.join(currentPath, file);
            const stat = fs.statSync(filePath);

            if (stat.isDirectory()) {
                // Recursively process subdirectories
                readDirectory(filePath);
            } else if (path.extname(file).toLowerCase() === extension.toLowerCase()) {
                // Load file content if extension matches
                try {
                    const content = fs.readFileSync(filePath, 'utf8');
                    results.push({
                        path: filePath,
                        content: content
                    });
                } catch (error) {
                    console.error(`Error reading file ${filePath}:`, error);
                }
            }
        }
    }

    // Start recursive processing
    readDirectory(dirPath);
    return results;
}

/**
 * Extracts and parses TOML front matter from an AsciiDoctor file
 * @param {string} content - The content of the AsciiDoctor file
 * @returns {Object} Object containing parsed frontMatter and remaining content
 */
function parseAsciidocFrontMatter(content) {
    const frontMatterRegex = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n([\s\S]*)$/;
    const match = content.match(frontMatterRegex);

    if (!match) {
        return {
            frontMatter: {},
            content: content.trim()
        };
    }

    const [, frontMatterStr, remainingContent] = match;

    try {
        const frontMatter = parseTOML(frontMatterStr);
        return {
            frontMatter,
            content: remainingContent.trim()
        };
    } catch (error) {
        throw new Error(`Error parsing TOML front matter: ${error.message}`);
    }
}

/**
 * Parses TOML-formatted string into JavaScript object
 * @param {string} toml - TOML string to parse
 * @returns {Object} Parsed TOML as JavaScript object
 */
function parseTOML(toml) {
    const result = {};
    let currentTable = result;
    let currentTablePath = [];

    const lines = toml.split('\n');

    for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();

        // Skip empty lines and comments
        if (!line || line.startsWith('#')) continue;

        // Handle table headers
        if (line.startsWith('[') && line.endsWith(']')) {
            const tableName = line.slice(1, -1).trim();
            currentTablePath = tableName.split('.');

            // Create nested objects for the table path
            let current = result;
            currentTablePath.forEach((key, index) => {
                if (index === currentTablePath.length - 1) {
                    current[key] = current[key] || {};
                    currentTable = current[key];
                } else {
                    current[key] = current[key] || {};
                    current = current[key];
                }
            });
            continue;
        }

        // Handle key-value pairs
        const equalIndex = line.indexOf('=');
        if (equalIndex === -1) continue;

        const key = line.slice(0, equalIndex).trim();
        let value = line.slice(equalIndex + 1).trim();

        // Parse the value
        try {
            value = parseTOMLValue(value, lines, i);
            // If parseTOMLValue returns an object with nextIndex, update the line counter
            if (value && typeof value === 'object' && 'nextIndex' in value) {
                i = value.nextIndex;
                value = value.value;
            }
        } catch (error) {
            throw new Error(`Error parsing value at line ${i + 1}: ${error.message}`);
        }

        currentTable[key] = value;
    }

    return result;
}

/**
 * Parses a TOML value, handling various data types
 * @param {string} value - The value string to parse
 * @param {string[]} lines - All lines of the TOML content
 * @param {number} currentIndex - Current line index
 * @returns {any} Parsed value
 */
function parseTOMLValue(value, lines, currentIndex) {
    // Handle strings
    if (value.startsWith('"') && value.endsWith('"')) {
        return value.slice(1, -1).replace(/\\"/g, '"');
    }

    // Handle multi-line strings
    if (value.startsWith('"""')) {
        return parseMultilineString(lines, currentIndex);
    }

    // Handle arrays
    if (value.startsWith('[')) {
        return parseArray(value);
    }

    // Handle booleans
    if (value.toLowerCase() === 'true') return true;
    if (value.toLowerCase() === 'false') return false;

    // Handle dates
    if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
        const date = new Date(value);
        if (!isNaN(date.getTime())) return date;
    }

    // Handle numbers
    if (!isNaN(value)) {
        // Check if it's an integer or float
        return value.includes('.') ? parseFloat(value) : parseInt(value, 10);
    }

    return value;
}

/**
 * Parses a TOML array
 * @param {string} arrayStr - The array string to parse
 * @returns {Array} Parsed array
 */
function parseArray(arrayStr) {
    // Remove brackets and split by commas
    const items = arrayStr.slice(1, -1).split(',');
    return items.map(item => {
        const trimmed = item.trim();
        if (!trimmed) return undefined;
        return parseTOMLValue(trimmed);
    }).filter(item => item !== undefined);
}

/**
 * Parses a multi-line string
 * @param {string[]} lines - All lines of the TOML content
 * @param {number} startIndex - Starting line index
 * @returns {Object} Object containing the parsed value and next index
 */
function parseMultilineString(lines, startIndex) {
    let result = '';
    let currentIndex = startIndex;
    let foundEnd = false;

    // Skip the first line with """
    currentIndex++;

    while (currentIndex < lines.length) {
        const line = lines[currentIndex].trim();
        if (line.endsWith('"""')) {
            foundEnd = true;
            result += line.slice(0, -3);
            break;
        }
        result += lines[currentIndex] + '\n';
        currentIndex++;
    }

    if (!foundEnd) {
        throw new Error('Unterminated multi-line string');
    }

    return {
        value: result.trim(),
        nextIndex: currentIndex
    };
}

function convertToMessage(frontMatter, content) {
    const prompt = `<instructions>
Please summarize the following text stored in the content tags taken from a blog in a catchy way.
The content is formatted as AsciiDoc, so please handle headlines and code formattings accordingly.
The summary must not contain the phrases "the autor" or "the writer" or something like that and
must not be longer than three sentences. The summary should be in the witty, but not too technical
style. Try to use and adapt to the original writing style.

Please format the answer as a machine readable json containing the attributes "title" and "summary".
The title attribute of the response should contain an alternative title, based on the title tag of the
content and the content itself. Please also try to generate keywords for the content and return them as
the "keywords" attribute so they can be used as HTML meta data.

Finally please try to generate an abstract for the content and return it as the "abstract" attribute
of the response.
</instructions>

<content>
    <title>${frontMatter.title}</title>
    <body>
        ${content}
    </body>
</content>`;

    const messageObj = {
        system: `You are a technical writer with strong computer science background and try to
                 optimize Blog content in terms of quality, readability, joy and search engine optimization.`,
        max_tokens: 1024,
        messages: [
            { role: 'user', content: prompt }
        ],
        model: 'claude-3-5-sonnet-latest'
    };
    return messageObj;
}

async function handleSingleFile(entry) {
    const filename = entry.path;

    const {frontMatter, content} = parseAsciidocFrontMatter(entry.content);
    const message = convertToMessage(frontMatter, content);

    const response = await client.messages.create(message);

    const aicontent = response.content;
    for (var i = 0; i < aicontent.length; i++) {
        const elem = aicontent[i];
        if ('text' === elem.type) {
            const js = JSON.parse(elem.text);
            if (js.title) {
                frontMatter["ai_title"] = js.title;
            }
            if (js.summary) {
                frontMatter["ai_summary"] = js.summary;
            }
            if (js.summary) {
                frontMatter["ai_abstract"] = js.abstract;
            }
        }
    }

    fs.writeFileSync(filename + '.aifrontmatter.json', JSON.stringify(frontMatter, null, 2));
    console.info('Finished file ' + filename);
}

async function main() {
    const files = loadFilesWithExtension('./content/post/', '.adoc');

    console.info('Found ' + files.length + ' files');
    for (var i = 0; i < files.length; i++) {
        handleSingleFile(files[i]);
        if (i % 5 == 0) {
            console.log('Sleeping due to rate limit!');
            await sleep(30000);
        }
    }
    console.log('Finished!');
}

main();

Benefits and Opportunities

  1. AI-driven enhancement ensures consistent quality across all posts, regardless of when they were written.

  2. Automated enhancement frees up time for creating new content while improving existing posts.

  3. Claude can help optimize titles and descriptions for better search engine visibility without sacrificing readability.

Limitations and Considerations

  1. While AI can enhance presentation, it’s crucial to maintain your authentic voice and technical accuracy.

  2. API usage costs should be factored into your enhancement strategy, especially for larger blogs. For smaller blogs, it might cost just a few dollars.

  3. Regular review of AI-enhanced content is recommended to ensure changes align with your blog’s goals.

Best Practices

  1. Start with a small subset of posts to fine-tune your approach

  2. Maintain version control of original content

  3. Review and adjust enhancement parameters based on results

  4. Monitor SEO and engagement metrics to measure impact

Conclusion

Leveraging Claude for blog enhancement offers a powerful way to improve content quality while respecting your original work. By automating the enhancement process, you can maintain a fresh, engaging blog without the traditional overhead of manual updates.

Remember that AI enhancement should complement, not replace, your expertise and unique perspective. Used thoughtfully, it’s a valuable tool in your content management arsenal.

Git revision: 32d6bce