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:
![]() |
![]() |
| Homepage | Dashboard |
![]() |
![]() |
| Edit Page | New Post |
- 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):
Note: The
.envfile is not tracked in Git. See.env.examplefor all available variables.
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.
Q: How do I add a new tag? A: Run the command in the "Adding Tags" section above, or use the admin dashboard.
Q: Can I change the theme colors?
A: Yes — edit the CSS variables in app/static/style.css.
Q: How do I deploy this to a custom domain?
A: Update the SERVER_NAME in app/__init__.py and configure your DNS.
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. 🚀



