Overview
Historia Diaria is an automated story generator that runs daily using GitHub Actions. It combines AI-powered content generation with static HTML output to create a self-updating story archive.
System Architecture
The system consists of four main components:
Python Script Core logic for API calls, content generation, and file management
GitHub Actions Automated scheduler that runs the script daily at 8:00 AM Lima time
HTML Template Reusable layout with dynamic content placeholders
JSON History Persistent storage for story metadata and navigation
Workflow Diagram
The automated workflow follows this sequence:
Automated Scheduling
The GitHub Actions workflow is configured to run automatically:
name : Actualizar Historia Diaria
on :
schedule :
# UTC time: 13:00 = 8:00 AM Lima time
- cron : '0 13 * * *'
workflow_dispatch : # Manual trigger option
jobs :
generar-pagina :
runs-on : ubuntu-latest
permissions :
contents : write
The workflow_dispatch trigger allows you to manually run the workflow from the GitHub Actions tab for testing.
API Integration Flow
The script connects to OpenRouter’s API to generate stories:
# 1. API Connection
api_key = os.environ.get( "OPENROUTER_API_KEY" )
response = requests.post(
url = "https://openrouter.ai/api/v1/chat/completions" ,
headers = {
"Authorization" : f "Bearer { api_key } " ,
"Content-Type" : "application/json"
},
data = json.dumps({
"model" : "stepfun/step-3.5-flash:free" ,
"messages" : [{ "role" : "user" , "content" : prompt}]
})
)
respuesta_ia = response.json()[ 'choices' ][ 0 ][ 'message' ][ 'content' ]
The API key is stored as a GitHub Secret (OPENROUTER_API_KEY) and passed as an environment variable during workflow execution.
File Generation Process
The script generates two HTML files on each run:
Generate Daily Filename
Creates a date-stamped filename using the format historia-YYYY-MM-DD.html fecha_hoy = datetime.now()
año = fecha_hoy.strftime( "%Y" )
mes_num = fecha_hoy.strftime( "%m" )
dia = fecha_hoy.strftime( " %d " )
nombre_archivo_hoy = f "historia- { año } - { mes_num } - { dia } .html"
Fill Template with Content
Replaces placeholder variables with generated content with open ( 'plantilla.html' , 'r' , encoding = 'utf-8' ) as archivo:
contenido_html = archivo.read()
contenido_html = contenido_html.replace( ' {{ TITULO }} ' , nuevo_titulo)
contenido_html = contenido_html.replace( ' {{ HISTORIA }} ' , nueva_historia)
contenido_html = contenido_html.replace( ' {{ IMAGEN_URL }} ' , url_imagen)
contenido_html = contenido_html.replace( ' {{ MENU }} ' , menu_html)
Save Both Files
Writes index.html (latest story) and the dated archive file # Latest story (homepage)
with open ( 'index.html' , 'w' , encoding = 'utf-8' ) as archivo:
archivo.write(contenido_html)
# Archived version
with open (nombre_archivo_hoy, 'w' , encoding = 'utf-8' ) as archivo:
archivo.write(contenido_html)
History Management
The historial.json file maintains a structured archive of all generated stories:
Data Structure
{
"Marzo 2026" : [
{
"titulo" : "El Guardián del Faro de las Sombras" ,
"archivo" : "historia-2026-03-04.html"
},
{
"titulo" : "El Código de las Estrellas" ,
"archivo" : "historia-2026-03-02.html"
}
],
"Febrero 2026" : [
{
"titulo" : "El Eco de los Días Perdidos" ,
"archivo" : "historia-2026-02-26.html"
}
]
}
Update Logic
The script implements smart update logic to handle re-runs on the same day:
# Load existing history
if os.path.exists( 'historial.json' ):
with open ( 'historial.json' , 'r' , encoding = 'utf-8' ) as f:
historial = json.load(f)
else :
historial = {}
# Create month key if new
if llave_mes not in historial:
historial[llave_mes] = []
# Update if exists, insert if new
historia_actualizada = False
for item in historial[llave_mes]:
if item[ 'archivo' ] == nombre_archivo_hoy:
item[ 'titulo' ] = nuevo_titulo # Update existing
historia_actualizada = True
break
if not historia_actualizada:
# Insert at beginning (newest first)
historial[llave_mes].insert( 0 , {
"titulo" : nuevo_titulo,
"archivo" : nombre_archivo_hoy
})
This logic prevents duplicate entries when the workflow runs multiple times on the same day, updating the title instead of creating a new entry.
Error Handling
The script includes fallback mechanisms for API failures:
try :
response = requests.post(
url = "https://openrouter.ai/api/v1/chat/completions" ,
headers = { "Authorization" : f "Bearer { api_key } " , "Content-Type" : "application/json" },
data = json.dumps({ "model" : "stepfun/step-3.5-flash:free" , "messages" : [{ "role" : "user" , "content" : prompt}]})
)
respuesta_ia = response.json()[ 'choices' ][ 0 ][ 'message' ][ 'content' ]
except Exception as e:
respuesta_ia = "<TITULO>Error</TITULO><HISTORIA>Fallo al conectar con la API.</HISTORIA><IMAGEN>error</IMAGEN>"
If the API call fails, a default error message is used to ensure the workflow completes successfully.
Next Steps
Story Generation Learn how AI generates and parses story content
HTML Templates Explore the template structure and styling system