Are you a developer looking to showcase your work, attract clients, or simply practice modern web development? Building a personal portfolio site with Python Flask is an excellent way to combine clean code, flexible routing, and a lightweight framework that scales from a simple static page to a dynamic showcase of projects. In this guide, we’ll walk through every step of creating a polished Flask portfolio website—from project setup and template design to adding a contact form and deploying to the cloud. By the end, you’ll have a fully functional, SEO‑friendly portfolio that you can customize and share with the world.
Why Choose Flask for Your Portfolio?
- Lightweight and modular: Flask gives you just the essentials, letting you add only the features you need.
- Python ecosystem: Leverage powerful libraries for databases, forms, and email without learning a new language.
- Easy to deploy: Works seamlessly with platforms like Heroku, Render, and Railway.
- SEO control: Full access to HTML meta tags, sitemap generation, and structured data.
Project Structure Overview
A well‑organized file layout makes your code maintainable and ready for future enhancements. Below is a recommended structure for a Flask portfolio project:
portfolio/
├── app/
│ ├── __init__.py
│ ├── routes.py
│ ├── models.py # optional, if you use a DB
│ ├── forms.py # contact form
│ └── static/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── main.js
│ └── images/
│ └── profile.jpg
│ └── templates/
│ ├── base.html
│ ├── index.html
│ ├── project.html
│ └── contact.html
├── migrations/ # if using Flask‑Migrate
├── venv/
├── requirements.txt
└── run.py
Step 1: Setting Up the Development Environment
1.1 Create a virtual environment
Isolation prevents package conflicts and keeps your project tidy.
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
1.2 Install Flask and essential extensions
pip install Flask Flask-WTF Flask-Mail python-dotenv
Save the dependencies for later deployment:
pip freeze > requirements.txt
Step 2: Initializing the Flask Application
Create app/__init__.py to configure the app, load environment variables, and register blueprints if you decide to split routes later.
from flask import Flask
from flask_wtf import CSRFProtect
from flask_mail import Mail
import os
csrf = CSRFProtect()
mail = Mail()
def create_app():
app = Flask(__name__, static_folder='static', template_folder='templates')
# Load configuration from .env or a config object
app.config.from_mapping(
SECRET_KEY=os.getenv('SECRET_KEY', 'dev-secret-key'),
MAIL_SERVER=os.getenv('MAIL_SERVER', 'smtp.gmail.com'),
MAIL_PORT=int(os.getenv('MAIL_PORT', 587)),
MAIL_USE_TLS=True,
MAIL_USERNAME=os.getenv('MAIL_USERNAME'),
MAIL_PASSWORD=os.getenv('MAIL_PASSWORD')
)
csrf.init_app(app)
mail.init_app(app)
# Import and register routes
from . import routes
app.register_blueprint(routes.bp)
return app
Step 3: Defining Routes and Views
In app/routes.py we’ll create a blueprint that handles the main pages of the portfolio.
from flask import Blueprint, render_template, request, flash, redirect, url_for
from .forms import ContactForm
from . import mail
from flask_mail import Message
bp = Blueprint('main', __name__)
@bp.route('/')
def index():
# Sample data – replace with your own projects
projects = [
{
'title': 'Flask Blog',
'slug': 'flask-blog',
'description': 'A full‑featured blog with markdown support.',
'image': 'blog.png'
},
{
'title': 'Data Visualizer',
'slug': 'data-visualizer',
'description': 'Interactive charts using Plotly and Flask.',
'image': 'visualizer.png'
}
]
return render_template('index.html', projects=projects)
@bp.route('/project/')
def project_detail(slug):
# In a real app, fetch from a database
project = {
'title': slug.replace('-', ' ').title(),
'description': 'Detailed description of the project.',
'image': f'{slug}.png',
'tech': ['Python', 'Flask', 'Bootstrap']
}
return render_template('project.html', project=project)
@bp.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm()
if form.validate_on_submit():
msg = Message(
subject=f'Portfolio Contact: {form.subject.data}',
sender=form.email.data,
recipients=[os.getenv('MAIL_USERNAME')],
body=form.message.data
)
mail.send(msg)
flash('Your message has been sent!', 'success')
return redirect(url_for('main.contact'))
return render_template('contact.html', form=form)
Step 4: Building the Contact Form
Flask‑WTF simplifies form handling and CSRF protection.
# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length
class ContactForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
email = StringField('Email', validators=[DataRequired(), Email()])
subject = StringField('Subject', validators=[DataRequired(), Length(max=100)])
message = TextAreaField('Message', validators=[DataRequired(), Length(max=1000)])
submit = SubmitField('Send')
Step 5: Crafting SEO‑Friendly Templates
5.1 Base layout (base.html)
All pages inherit from this file, ensuring consistent meta tags and navigation.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title if title else "My Portfolio" }}</title>
<meta name="description" content="{{ meta_description|default('Python developer portfolio built with Flask') }}">
<link rel="canonical" href="{{ request.url }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- Open Graph for social sharing -->
<meta property="og:title" content="{{ title|default('My Portfolio') }}">
<meta property="og:description" content="{{ meta_description|default('Showcasing my Python and Flask projects') }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ request.url }}">
<meta property="og:image" content="{{ url_for('static', filename='images/profile.jpg', _external=True) }}">
</head>
<body>
<header>
<nav>
<a href="{{ url_for('main.index') }}">Home</a>
<a href="{{ url_for('main.contact') }}">Contact</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© {{ current_year }} My Name. All rights reserved.</p>
</footer>
</body>
</html>
5.2 Home page (index.html)
{% extends "base.html" %}
{% block content %}
<section class="hero">
<h2>Hello, I’m {{ your_name }} – Python Developer</h2>
<p>I build web applications, data pipelines, and automation scripts. Explore my latest projects below.</p>
</section>
<section class="projects">
<h3>Featured Projects</h3>
<ul class="project-list">
{% for project in projects %}
<li>
<a href="{{ url_for('main.project_detail', slug=project.slug) }}">
<img src="{{ url_for('static', filename='images/' + project.image) }}" alt="{{ project.title }} thumbnail">
<h4>{{ project.title }}</h4>
<p>{{ project.description }}</p>
</a>
</li>
{% endfor %}
</ul>
</section>
{% endblock %}
5.3 Project detail page (project.html)
{% extends "base.html" %}
{% block content %}
<article class="project-detail">
<h2>{{ project.title }}</h2>
<img src="{{ url_for('static', filename='images/' + project.image) }}" alt="{{
Leave a Reply