DEV Community

Cover image for How to Automate Content Generation with Patterns, Prompts, Astro, and GitHub Actions
Damian Soto
Damian Soto

Posted on

How to Automate Content Generation with Patterns, Prompts, Astro, and GitHub Actions

Imagine being able to automatically generate content using predefined patterns and prompts, and then publish it directly by simply committing a new article.

I've been eager to develop this solution for a long time, and I took advantage of the development of BestApologyLetters.com to implement it.

In this tutorial, I'll show you step by step how to create a Node.js script that does exactly that. Note that the code provided is for example purposes only.

We'll use:

  • AstroBuild for static site generation.
  • The OpenAI API to generate content.
  • Unsplash to fetch images.
  • GitHub Actions to schedule content generation.
  • Vercel to automate deployment.

This solution is perfect for static site generators and JAMStack frameworks, making it easy to keep your content fresh without manual intervention.

Let's get started!

Step 1: Set Up Your Project Environment

First, remember to install the necessary dependencies in your project:

npm install openai node-fetch@2 fs/promises path stopword
Enter fullscreen mode Exit fullscreen mode

Note: We're using node-fetch@2 because version 3 requires ES modules and additional configurations.

Step 2: Define Patterns and Prompts

Create a folder src/lib/ and inside it, two files: patterns.js and prompts.js.

patterns.js

Here we'll define the patterns and variables we'll use to generate titles.

// src/lib/patterns.js

// Define the patterns with placeholders for variables
export const patterns = [
  "How to {action} in {language}",
  "Beginner's Guide to {topic}",
  "Top {tools} for {activity}"
];

// Define possible values for each variable
export const variables = {
  action: ["write an apology letter", "express regret", "seek forgiveness"],
  language: ["English", "Spanish", "French"],
  topic: ["Apology Letters", "Sincere Regrets", "Making Amends"],
  tools: ["phrases", "templates", "tips"],
  activity: ["writing apologies", "mending relationships", "expressing sincerity"]
};
Enter fullscreen mode Exit fullscreen mode

prompts.js

Here we'll define the prompts we'll use with the OpenAI API to generate the content.

This part is the most important. You need to dedicate time to writing each one to create clear and as "human" as possible articles. These are just example prompts.

// src/lib/prompts.js

// Define prompts corresponding to each pattern
export const prompts = {
  "How to {action} in {language}": "You are an expert in writing apology letters in {language}. Write an article on how to {action}, including practical examples.",
  "Beginner's Guide to {topic}": "You are a friendly instructor. Write an introductory guide on {topic} aimed at beginners.",
  "Top {tools} for {activity}": "You are a professional advisor. Write an article about the best {tools} for {activity}, including pros and cons."
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement Pattern and Variable Selection

We'll create functions to select patterns and fill in variables.

// index.js

import { patterns, variables } from './src/lib/patterns.js';

// Function to select a random element from an array
function selectRandomElement(array) {
  return array[Math.floor(Math.random() * array.length)];
}

// Function to extract variable names from a pattern
function extractVariables(pattern) {
  // Match all occurrences of {variable} in the pattern
  return (pattern.match(/\{(\w+)\}/g) || []).map(v => v.slice(1, -1));
}

// Function to create a title by replacing variables in the pattern
function createTitle(pattern, selectedVariables) {
  return pattern.replace(/\{(\w+)\}/g, (_, key) => selectedVariables[key]);
}

// Example usage
const selectedPattern = selectRandomElement(patterns); // Select a random pattern
const variableNames = extractVariables(selectedPattern); // Get variable names from the pattern
const selectedVariables = {};

// Select random values for each variable
variableNames.forEach(name => {
  selectedVariables[name] = selectRandomElement(variables[name]);
});

const title = createTitle(selectedPattern, selectedVariables); // Create the title
console.log(`Generated Title: ${title}`);
Enter fullscreen mode Exit fullscreen mode

Step 4: Generate Content with the OpenAI API

Now, we'll use the title and prompt to generate content.

// index.js (continued)

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY, // Your OpenAI API key from environment variables
});

async function generateContent(title, pattern) {
  // Get the prompt template and replace variables with selected values
  const promptTemplate = prompts[pattern];
  const prompt = promptTemplate.replace(/\{(\w+)\}/g, (_, key) => selectedVariables[key]);

  try {
    // Make a request to the OpenAI API
    const response = await openai.chat.completions.create({
      model: "gpt-4", // Specify the model to use
      messages: [
        { role: "system", content: prompt }, // Provide the system prompt
        { role: "user", content: title }     // Provide the user input
      ],
    });

    // Extract and return the generated content
    const content = response.choices[0].message.content.trim();
    return content;
  } catch (error) {
    console.error(`Error generating content: ${error}`);
    return null;
  }
}

// Usage
(async () => {
  const content = await generateContent(title, selectedPattern);
  console.log(`Generated Content:\n${content}`);
})();
Enter fullscreen mode Exit fullscreen mode

Note: Make sure to set the OPENAI_API_KEY environment variable with your OpenAI API key.

Step 5: Fetch Images from Unsplash

To enrich our content, we'll fetch related images.

// index.js (continued)

import fetch from 'node-fetch';

async function getUnsplashImage(query) {
  try {
    // Make a request to the Unsplash API for a random image
    const response = await fetch(`https://api.unsplash.com/photos/random?query=${encodeURIComponent(query)}&orientation=landscape`, {
      headers: {
        'Authorization': `Client-ID ${process.env.UNSPLASH_ACCESS_KEY}` // Your Unsplash access key
      }
    });

    const data = await response.json();

    // Extract necessary information from the response
    return {
      url: data.urls.regular,
      author: data.user.name,
      authorProfile: data.user.links.html
    };
  } catch (error) {
    console.error(`Error fetching image: ${error}`);
    return null;
  }
}

// Usage
(async () => {
  const image = await getUnsplashImage(title);
  console.log(`Fetched Image: ${image.url}`);
})();
Enter fullscreen mode Exit fullscreen mode

Note: You'll need an Unsplash access key in UNSPLASH_ACCESS_KEY.

Step 6: Save the Generated Content

Now, we'll save the content in a Markdown file, including the frontmatter.

// index.js (continued)

import fs from 'fs/promises';
import path from 'path';

// Function to create a URL-friendly slug from the title
function createSlug(title) {
  return title.toLowerCase().split(' ').join('-').replace(/[^\w\-]/g, '');
}

async function saveContent(title, content, image) {
  const slug = createSlug(title); // Generate slug from the title
  const dir = path.join('src', 'content', 'articles', slug); // Define the directory path
  await fs.mkdir(dir, { recursive: true }); // Create the directory if it doesn't exist

  // Prepare the frontmatter with metadata
  const frontmatter = `---
title: "${title}"
image: "${image.url}"
author: "${image.author}"
authorProfile: "${image.authorProfile}"
---`;

  const file = path.join(dir, 'index.md'); // Define the file path
  await fs.writeFile(file, `${frontmatter}\n\n${content}`); // Write the content to the file
  console.log(`Content saved in ${file}`);
}

// Usage
(async () => {
  const image = await getUnsplashImage(title);
  await saveContent(title, content, image);
})();
Enter fullscreen mode Exit fullscreen mode

Step 7: Deploy with Vercel

We use Vercel as our hosting platform, which integrates seamlessly with GitHub. Every time we push new content, Vercel automatically rebuilds and deploys the site.

Deployment Steps:

  1. Push to GitHub: Commit your code to a GitHub repository.

  2. Connect Vercel: Go to Vercel and import your repository.

  3. Set Environment Variables: In Vercel, set the OPENAI_API_KEY and UNSPLASH_ACCESS_KEY environment variables.

  4. Automatic Deployments: Vercel will automatically build and deploy your site every time you push changes.

Step 8: Automate with GitHub Actions

To automate the execution of the script, we'll create a GitHub Actions workflow.

Create the file .github/workflows/generate-content.yml:

name: Generate Content

on:
  schedule:
    - cron: '0 0 * * *' # Every day at midnight UTC

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm install
      - name: Run script
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          UNSPLASH_ACCESS_KEY: ${{ secrets.UNSPLASH_ACCESS_KEY }}
        run: node index.js
      - name: Commit and push
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add .
          git commit -m "Automatically generated content"
          git push
Enter fullscreen mode Exit fullscreen mode

Important: Add your API keys as secrets in your repository (OPENAI_API_KEY and UNSPLASH_ACCESS_KEY).

Step 9: Putting It All Together

With everything set up, here's how the workflow operates:

  1. Content Generation: The GitHub Action runs node index.js, which generates a new article.

  2. Commit Changes: The new article is committed back to the repository.

  3. Trigger Deployment: Vercel detects the new commit and rebuilds the site.

  4. Automatic Publishing: The new content is live on your site without any manual intervention.

This approach is ideal for static site generators and JAMStack frameworks. By automating content creation and deployment, you can keep your site fresh and engaging without lifting a finger.

Personal Experience

When I first set up BestApologyLetters.com, I wanted a way to consistently provide new, valuable content to my readers without spending countless hours writing each article manually.

By combining the power of OpenAI's API, Astro's static site generation, and the automation capabilities of GitHub Actions and Vercel, I was able to create a self-sustaining content pipeline.

This setup not only saved me time but also ensured that my site remained up-to-date with relevant content. The integration with Astro and Vercel made deployment seamless, and leveraging JAMStack principles resulted in a fast, scalable website.

Conclusion

And there you have it! We've created a script that automatically generates content using patterns and prompts, fetches images from Unsplash, and saves everything as Markdown files. By integrating with AstroBuild and deploying with Vercel, we automate the entire process from content generation to publishing.

This solution is perfect for static site generators and JAMStack frameworks, making it easy to keep your content fresh without manual intervention.

I hope this tutorial was helpful! If you have any questions or comments, feel free to share them.

Top comments (0)