A fully-featured personal blog built with Flask, SQLite, and vanilla CSS. Write posts in Markdown, upload them as ZIP bundles, and publish instantly β no thirdβparty platforms, no lockβin. Just your content, your design, and your server.
- π Live Demo: sandeepkumarkuanar.pythonanywhere.com
- π Notes on the project: Notes on Personal blog
- π Partly inspired by:
- ZIP-based publishing β Write in Markdown, bundle images, upload a ZIP, and the blog parses, renders, and displays your post automatically
- Markdown to HTML β Uses
markdown2with syntax highlighting viapygments - Tag system β Add, filter, and combine tags with OR/AND logic
- Admin dashboard β Create, edit, publish, unpublish, and manage posts
- User authentication β Login, registration, and roleβbased access (admin / regular user)
- Custom error pages β 404, 403, and 500 pages that match your design
- Dark theme β Clean, custom CSS with no external frameworks
- Analytics ready β Builtβin PostHog integration (optional)
- Deployment ready β Designed to run on PythonAnywhere
- Full CRUD β Complete Create, Read, Update, Delete workflow for posts
- Share dropdown β Share posts on X (Twitter) and LinkedIn, or copy the link with UTM tracking
- Open Graph tags β Dynamic meta tags for rich social media previews
- Contact page β Dedicated page with email tagging system and copyβpaste template
| Layer | Technology |
|---|---|
| Backend | Flask 3.1.3 |
| Database | SQLite with SQLAlchemy ORM |
| Authentication | FlaskβLogin + FlaskβBcrypt |
| Forms | FlaskβWTF |
| Markdown | markdown2 + pygments |
| Analytics | PostHog (optional) |
| Frontend | Vanilla CSS + JavaScript |
| Deployment | PythonAnywhere / Gunicorn / WSGI |
| Package Manager | uv |
personal-blog/
βββ app/
β βββ __init__.py # Flask app factory, extensions, PostHog setup
β βββ forms.py # WTForms definitions (Login, Register, Post, Admin, Edit)
β βββ models.py # SQLAlchemy models (User, Post, Tag)
β βββ routes.py # All route handlers (CRUD, share, dashboard)
β βββ utils.py # ZIP extraction, Markdown rendering, image rewriting
β βββ static/ # CSS, JS, and uploaded assets
β β βββ style.css # ~1200 lines of custom dark theme
β β βββ pygments.css # Syntax highlighting theme
β β βββ js/
β β β βββ tags.js # Tag cloud filtering logic
β β β βββ share.js # Share dropdown functionality (Copy, X, LinkedIn)
β β βββ post_images/ # Images extracted from blog ZIPs
β β βββ post_covers/ # Blog cover images
β βββ templates/ # Jinja2 templates
β βββ layout.html # Base template with Open Graph tags
β βββ home.html # Landing page with bio
β βββ blogs.html # Writings listing with tag cloud
β βββ post.html # Individual post view with share dropdown
β βββ dashboard.html # Admin post management (Edit/Delete buttons)
β βββ admin_new_post.html # ZIP upload form
β βββ edit_post.html # Edit post form (Markdown, tags, read time)
β βββ contact.html # Contact page with email tagging system
β βββ login.html / register.html
β βββ errors/ # 404, 403, 500 pages
βββ instance/
β βββ site.db # SQLite database (not tracked in Git)
βββ main.py # Entry point
βββ pyproject.toml # Project metadata and dependencies
βββ .env.example # Environment variables template
βββ README.md
- Python 3.10+
- uv (recommended) or
pip
- Clone the repository
git clone https://github.com/SandeepKumarKuanar/personal-blog.git
cd personal-blog- Create and activate a virtual environment
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate- Install dependencies
uv sync- Set up environment variables
Copy the example environment file and fill in your values:
cp .env.example .envEdit .env with your PostHog credentials (optional β the app will work without them):
POSTHOG_PROJECT_TOKEN=your-posthog-project-token
POSTHOG_HOST=https://us.i.posthog.com
- Initialize the database
uv run python -c "
from app import app, db
with app.app_context():
db.create_all()
print('Database created successfully!')
"- Create an admin user
uv run python -c "
from app import app, db
from app.models import User
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt()
with app.app_context():
hashed = bcrypt.generate_password_hash('your-password').decode('utf-8')
admin = User(
username='Your Name',
email='your@email.com',
password=hashed,
is_admin=True
)
db.session.add(admin)
db.session.commit()
print('Admin user created!')
"- Run the development server
uv run python main.pyVisit http://127.0.0.1:5000 to see your blog.
- Write your post in Markdown (
##for headings,```pythonfor code blocks) - Place any images in the same folder as the
.mdfile - Zip the folder (make sure the
.mdfile is at the root of the ZIP) - Log in as admin and go to Dashboard β + Add
- Upload the ZIP, add a cover image, select tags, and hit Publish
The system will:
- Extract the Markdown and images
- Parse the first
#line as the title - Rewrite image paths to
/static/post_images/<post_id>/ - Render the Markdown to HTML with syntax highlighting
- Store both the raw Markdown and the rendered HTML
- Log in as admin and go to Dashboard
- Click the Edit button next to the post you want to modify
- Update the title, Markdown content, tags, or read time
- Click Update Post β the HTML is automatically reβrendered from the Markdown
Note: Inline images are preserved and don't need to be reβuploaded during edits.
- Log in as admin and go to Dashboard
- Click the Delete button next to the post you want to remove
- Confirm the deletion β the post and its associated images are permanently removed
Each post includes a share dropdown with three options:
- Copy Link β copies the post URL with UTM tracking parameters
- Share on X β opens X (Twitter) with a preβfilled post
- Share on LinkedIn β opens LinkedIn with a rich preview using Open Graph tags
The Open Graph tags are dynamic β each post generates its own title, description, and URL for social sharing.
All styles are in app/static/style.css. The design uses CSS custom properties (variables) for easy theming:
:root {
--bg-primary: #0d0d0d;
--text-primary: #e8e6e3;
--accent: #d4a373;
/* ... */
}Tags are managed manually in the database. To add a new tag:
uv run python -c "
from app import app, db
from app.models import Tag
with app.app_context():
db.session.add(Tag(name='Your Tag'))
db.session.commit()
"The blog includes PostHog integration. To enable it:
- Sign up at PostHog
- Get your project API token, and let the bot run and do changes
- Add it to your
.envfile - Restart the server
- Clone the repository on PythonAnywhere
- Set up a virtual environment and install dependencies
- Configure the WSGI file to point to
main.py - Set environment variables in the Web tab or via a
.envfile - Set up static file mappings:
/static/β/path/to/app/static/ - Reload the web app
There is currently no test suite, but you can verify the system by:
- Creating a test post via the dashboard
- Checking that images render correctly
- Testing tag filtering with OR/AND modes
- Verifying that unpublished posts are hidden from public view
Contributions are welcome! If you find a bug or have an idea for an improvement:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please ensure your code follows the existing style and includes appropriate documentation.
This project is open source and available under the MIT License.
- Corey Schafer β Flask tutorial series that started it all
- Dr. Charles Severance β For inspiring the journey into programming
- The Flask, SQLAlchemy, and PostHog communities
- PostHog β Analytics integration for understanding readers
- roadmap.sh - For inspiring the implementations of this project
- From Blog: Custom Email based contact page
- GitHub: SandeepKumarKuanar
- X: @kuanar_sandeep
- Email:
kuanarsandeepkumar@gmail.com
Built with curiosity, empathy, and intent. π