Generate a structured, illustrated article from a single keyword. Pick a topic, pick the sections you want, and the app writes the introduction, each section, the conclusion, and a cover image using the OpenAI API.
Stack: Django 5.2 + Django REST Framework · React 19 + Vite 7 + Tailwind CSS 3
The application is stateless — it defines no models and stores no user content. Generated articles are returned in the HTTP response and never persisted.
Requires Python 3.12+ and Node 22 (see frontend/.nvmrc). No database server is needed: the default
configuration uses SQLite, which Django creates on first run.
cd backend
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then set OPENAI_API_KEY and DEBUG=True
python manage.py migrate
python manage.py runserverThe API is now on http://localhost:8000/api/.
cd frontend
npm ci
cp .env.example .env # defaults already point at localhost:8000
npm run devThe UI is now on http://localhost:5173.
cp backend/.env.example backend/.env
# set OPENAI_API_KEY and DJANGO_SECRET_KEY in that file
docker compose up --buildFrontend on http://localhost:5173, backend on http://localhost:8000. Both containers run production servers (gunicorn and nginx) as non-root users.
| Variable | Default | Description |
|---|---|---|
DJANGO_SECRET_KEY |
(dev fallback) | Django signing key. Required when DEBUG=False — startup fails if unset. |
DEBUG |
False |
Enables the insecure dev fallback for the secret key. Never enable in production. |
ALLOWED_HOSTS |
localhost,127.0.0.1,[::1] |
Comma-separated. * is rejected when DEBUG=False. |
CSRF_TRUSTED_ORIGINS |
(empty) | Comma-separated absolute origins. |
CORS_ALLOWED_ORIGINS |
http://localhost:5173,http://127.0.0.1:5173 |
Origins allowed to call the API from a browser. Never a wildcard. |
LOG_LEVEL |
INFO |
Root logger level. |
SECURE_SSL_REDIRECT |
True |
Applied only when DEBUG=False. |
SECURE_HSTS_SECONDS |
31536000 |
Applied only when DEBUG=False. 0 disables HSTS. |
OPENAI_API_KEY |
(empty) | Required for generation. Without it the API returns 503. |
OPENAI_CHAT_MODEL |
gpt-4o-mini |
Model used for all text generation. |
OPENAI_IMAGE_MODEL |
dall-e-2 |
Model used for the cover image. |
OPENAI_IMAGE_SIZE |
512x512 |
Cover image dimensions. |
OPENAI_TIMEOUT_SECONDS |
60 |
Per-request timeout for the OpenAI client. |
DB_ENGINE |
django.db.backends.sqlite3 |
Set to django.db.backends.mysql or ...postgresql to use a server. |
DB_NAME |
<BASE_DIR>/db.sqlite3 |
Database name, or SQLite file path. |
DB_USER / DB_PASSWORD / DB_HOST / DB_PORT |
(empty) | Ignored by SQLite. |
Non-SQLite engines need their driver installed separately —
pip install mysqlclient or pip install "psycopg[binary]".
| Variable | Default | Description |
|---|---|---|
VITE_API_URL |
http://localhost:8000/api |
Base URL of the API, including the /api prefix. Inlined at build time, so it is public — never put a secret in a VITE_ variable. |
All endpoints accept and return JSON. There is no authentication; deploy behind your own gateway if you need it.
| Method | Endpoint | Description |
|---|---|---|
GET |
/healthz/ |
Liveness probe. Returns {"status": "ok"}. |
POST |
/api/search_topics/ |
Suggest article topics for a keyword. |
POST |
/api/select_topics/ |
List the sections available for a chosen topic. |
POST |
/api/generate_article/ |
Generate the full article. |
| — | /admin/ |
Django admin. No app models are registered. |
The keyword itself is always appended as the final topic so it can be used verbatim.
Send either topic_id (with the topics map from the previous call) or
custom_topic — never both.
// request
{ "topic_id": 1, "topics": { "1": "Kubernetes For Beginners" } }
// 200
{
"selected_topics": { "1": "Kubernetes For Beginners" },
"topic_sections": {
"1": { "sections": { "1": "What Is A Pod", "2": "Services And Ingress" }, "type": "predefined" }
}
}For a custom topic the key is custom_1 and type is "custom".
// request
{
"selected_sections": [{ "section_name": "What Is A Pod" }],
"selected_topics": { "1": "Kubernetes For Beginners" },
"topic_sections": { "1": { "sections": ["What Is A Pod", "Services And Ingress"] } },
"keyword": "kubernetes"
}
// 200
{
"keyword": "kubernetes",
"main_topic": "kubernetes",
"selected_topic": { "1": "Kubernetes For Beginners" },
"section_headers": ["What Is A Pod"],
"introduction": "...",
"conclusion": "...",
"image_url": "https://...",
"section_content": { "What Is A Pod": "..." }
}Sections that the chosen topic did not offer are dropped; if none remain the request is rejected with 400.
| Status | Meaning |
|---|---|
400 |
Validation failed, or the selection was inconsistent. Body carries the field errors. |
502 |
The OpenAI API failed or returned unusable content. |
503 |
OPENAI_API_KEY is not configured on the server. |
A failed generation is never returned as a 200 with empty content.
- No authentication and no rate limiting. Every request spends OpenAI credits, so do not expose this to the open internet without a gateway that enforces both.
- Cost and latency scale with section count. One article is 3 + n chat completions plus one image; a 6-section article takes roughly 30–60 seconds.
- Article state travels in the URL between pages. Very long topic lists can approach browser query-string limits.
image_urlpoints at OpenAI's CDN and expires (typically within an hour). Download it if you need to keep it.- Generated content is unverified. The model can and does state things that are wrong. Treat every article as a first draft.
- No streaming. The full article is returned in one response.
- English only — prompts are hardcoded in English.
Setup, the exact commands CI runs, and the project layout are documented in CONTRIBUTING.md. Security reporting and a pre-deployment checklist are in SECURITY.md.
Copyright (C) 2026 Article Generator contributors.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file for the full text.