Skip to main content

How to build a modern landing page with Flask and Bootstrap? (Part 2)


In the previous tutorial, we created the foundation of our modern landing page using Flask-Frozen and Bootstrap 5. We built a responsive navigation bar, an engaging hero section, and an informative About Us section to introduce the pickleball club. In this tutorial, we will continue enhancing the landing page by adding more essential sections that improve the user experience and provide visitors with important information about the club. By the end of this tutorial, the landing page will become more complete and ready for real-world deployment.

Prerequisite:

This tutorial is part of the Flask Landing Page and Reservation System Series.

📚 View the Complete Flask Landing Page and Reservation System Series

Continues from Part 1                                                                                                    ➡ Next Part

Preliminary:
Before I begin, please activate the virtual environment and install the required dependencies.
python -m venv venv
venv\Scripts\activate
pip install flask frozen-flask
Then, we need to set up the file and folder structure, as below:

Since this tutorial is related to the previous tutorial, every file and folder remains the same, except this file will be amended.
(a) Under the static folder, the style.css, and
(b) Under the templates folder, the index.html. 


Step 1: Connect to Bootstrap
As I have mentioned in the previous tutorial, before I start to code, the HTML template should be connected to Bootstrap's CSS and JavaScript as follows:
(a) CSS
<link rel="stylesheet"
     href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
(b) JavaScript
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3
/dist/js/bootstrap.bundle.min.js">
</script>
(c) Custom CSS
This will be connected to my static folder and style.css file.
<link rel="stylesheet"
          href="{{ url_for('static', filename='style.css') }}">

Step 2: Create the facilities, contact me and footer section
In the previous tutorial, I created the hero and about me sections, and in this tutorial, I will continue to create the rest of the sections for this landing page. 

(1) Facilities section
This section introduces the facilities provided by the Jersey City Pickleball Club. What I show here is a Bootstrap card with an image for every card display. These images are stored in the static and assets folder; I need to indicate where the app can retrieve the images from. Besides, I have created a row with 4 columns, and each column is equally sized. Since I used Bootstrap, it will facilitate the responsive display on both the desktop and mobile devices, and I don't need to rewrite code for small mobile devices.   


(a) style.css
The style description of each element is shown below:
html,
body {
    overflow-x: hidden;
    width: 100%;
    background: #3f496a;
}

h2 { color: white; 
    font-size: 48px; 
    font-weight: bold; 
    text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
    text-align: center;
    margin: 0;} 

 p {
    text-align: center;
    margin-top: 20px;
}

.facilities-subtitle {
    color: white;
    font-size: 18px;
    text-align: center;
    text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
}

.card-body { 
    background: #f8f9fa; 
    border-radius: 10px; 
    padding: 20px; 
    box-shadow: 0 4px 8px rgba(0,0,0,0.1); 
    color: black;
}

.card-description {
    color: #212529;
    font-size: 14px;
    text-shadow: none;
}

.section-divider {
    height: 2px;
    background: #dee2e6;
    margin: 40px 0;
}
(b) index.html 
  • There is a heading and subheading on the top of this section. 
  • Every card contains an image, and below an image are a title and a brief description of what the club offers to its members. 
  • The image is sourced from the static/assets folder.
<hr class="section-divider">

<!---facilities section--->
<section id="facilities" class="section-padding">   
    <div class="text-center mb-5"> 
        <h2>Our Facilities</h2> 
        <p class="facilities-subtitle">Everything you need for 
        	an exceptional game.</p> 
    </div> 
    
    <div class="row g-4">
        <div class="col-md-3">
            <div class="card h-60 shadow-sm">
                <img
                    src="{{ url_for('static', 
                    	filename='assets/court.png') }}"
                    class="card-img-top facility-img"
                    alt="Professional Courts">

                <div class="card-body text-center">
                    <h5>Professional Courts</h5>
                    <p class="card-description">
                        Tournament-quality courts designed for
                        competitive and recreational play whether 
                        you're a beginner or an experienced player.
                    </p>
                </div>
            </div>
        </div>

  
         <div class="col-md-3">
            <div class="card h-60 shadow-sm">
                <img
                    src="{{ url_for('static', 
                    	filename='assets/night_lighting.png') }}"
                    class="card-img-top facility-img"
                    alt="Night Lighting">

                <div class="card-body text-center">
                    <h5>Night Lighting</h5>
                    <p class="card-description">
                       State-of-the-art LED lighting ensures a bright and 
                       enjoyable playing experience, 
                       allowing members to play comfortably day or night.
                    </p>
                </div>
            </div>
        </div>

        <div class="col-md-3">
            <div class="card h-60 shadow-sm">
                <img
                    src="{{ url_for('static', 
                    	filename='assets/changing_rooms.png') }}"
                    class="card-img-top facility-img"
                    alt="Changing Rooms">

                <div class="card-body text-center">
                    <h5>Changing Rooms</h5>
                    <p class="card-description">
                        Modern changing facilities provide a comfortable space 
                        to prepare for your game and unwind after 
                        an exciting match.
                    </p>
                </div>
            </div>
        </div>

         <div class="col-md-3">
            <div class="card h-60 shadow-sm">
                <img
                    src="{{ url_for('static', 
                    	filename='assets/free_parking.png') }}"
                    class="card-img-top facility-img"
                    alt="Free Parking">

                <div class="card-body text-center">
                    <h5>Free Parking</h5>
                    <p class="card-description">
                        Convenient complimentary and free parking is available 
                        for all members and guests, offering easy access 
                        to our facilities.
                    </p>
                </div>
            </div>
        </div>
    </div>
</section>
<hr class="section-divider">

(i) Horizontal Divider (<hr>)

Bootstrap/custom class used here is:

section-divider is a custom CSS class that styles the horizontal rule (<hr>), typically by adjusting its width, colour, thickness, and spacing to visually separate different sections of the webpage.

(ii) Facilities Section (<section>)

Classes used here are:

section-padding is a custom CSS class that adds vertical padding above and below the section, creating whitespace and improving readability.

The id="facilities" attribute provides a unique identifier for the section. It allows navigation links such as <a href="#facilities">Facilities</a> to scroll directly to this section.


(iii) Section Header

Bootstrap classes used here are:

text-center centres all text within the container.

mb-5 adds a large bottom margin (3rem), creating space between the heading and the facility cards.


(iv) Bootstrap Grid Row

Bootstrap class used here is:

row creates a horizontal row that contains Bootstrap columns.

g-4 adds consistent spacing (gutters) between the columns, preventing the cards from appearing too close together.


(v) Grid Columns

Bootstrap class used here is:

col-md-3 divides the row into four equal columns on medium-sized screens (≥768px) and larger. On smaller screens, the columns automatically stack vertically, making the layout responsive.


(vi) Card Component

Bootstrap classes used here are:

card creates a Bootstrap card component with a white background, rounded corners, and a border for displaying related content.

shadow-sm applies a subtle box shadow, giving the card a slightly elevated appearance.

h-60 is not a standard Bootstrap class. It only has an effect if it has been defined in your own CSS. If the intention is to make all cards the same height, Bootstrap's built-in h-100 class is recommended instead.


(vii) Card Image

Bootstrap and custom classes used here are:

card-img-top places the image at the top of the card and automatically adjusts its width to match the card.

facility-img is a custom CSS class typically used to control the image's height, object-fit behaviour, or other styling to ensure all facility images have a consistent appearance.

The Flask expression {{ url_for('static', filename='assets/...') }} generates the correct URL for images stored in the application's static folder.


(viii) Card Body

Bootstrap classes used here are:

card-body adds padding around the card's content, providing consistent spacing inside the card.

text-center centres the title and description within the card.


(ix) Card Description

Custom class used here is

card-description is a custom CSS class used to style the facility description, such as adjusting the font size, text colour, line height, or spacing to improve readability.


(x) Overall Layout

This section combines Bootstrap's Grid System (row and col-md-3) with Card Components (card, card-body, and card-img-top) to create a responsive four-column facilities section. On desktop screens, four facility cards are displayed side by side, while on smaller devices the cards automatically stack vertically to provide a mobile-friendly layout.

---------------------------------------------------------------------------------------------------------------------------------------------------

🎁 Get Your FREE Flask Cheat Sheet

Get more Flask, Python automation, Docker, and HTMX tutorials delivered to your inbox.

✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates

Download my FREE Flask Cheat Sheet (PDF)

---------------------------------------------------------------------------------------------------------------------------------------------------


(2) Contact Me section

This is the final section, where the user can always connect to the club at anytime and anywhere. Since the Python code will be frozen, no more backend logic will be applicable. Therefore, I could not use the normal contact form route function; I used Formspree instead.

   
(i) What is Formspree?
Formspree is a form backend service that allows me to collect data from HTML and JavaScript forms without writing any server-side code.

(ii) How to set up Formspree?
First, I need to sign up for Formspree. Once I'm signed in, at the top left sidebar, click the '+ Add New' button. Then, pop up a create form window; I can amend the form name, project, and email address. However, I am satisfied with the default setting; therefore, I click the red 'Create Form' button located at the bottom right corner.

The endpoint field displays the URL link. I simply copy the provided link and paste it into my index.html, which I will discuss later.

The contact me is divided into 2 sections; on the left are a brief discussion, email, and phone. Meanwhile, the right section is a contact form.

This contact form displays a name, email, message, and a submit button. As I mentioned above, the website does not handle the process of data; instead, it relies on the Formspree backend. What I need is to replace the form URL with the above copy of the form URL and paste it into
action="https://formspree.io/f/your_form_id." 

(a) style.css
.hero p,
#about p,
#contact p,
footer p {
    color: white;
    text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
     font-size: 18px;
}

label { 
    color: white; 
    font-size: 16px; 
    margin-bottom: 5px;
}
(b) index.html
<!---call to action section--->
<section id="contact" class="section-padding">
    <div class="row">
        <div class="col-lg-6">
            <h2>Keep In Touch</h2>
            <p>
                Find us in Jersey City and start your
                health journey with us.
            </p>
            <p>
                Email: info@jerseycitypickleball.com
            </p>
            <p>
                Phone: (123) 456-7890
            </p>
        </div>
  

    <div class="col-lg-6">
        <form action="https://formspree.io/f/your_form_id" method="POST">

            <div class="mb-3">
                <label for="name" class="form-label">
                    Name
                </label>
                <input
                    type="text"
                    class="form-control"
                    id="name"
                    name="Name"
                    required>
            </div>

            <div class="mb-3">
                <label for="email" class="form-label">
                    Email
                </label>
                <input
                    type="email"
                    class="form-control"
                    id="email"
                    name="Email"
                    required>
            </div>

            <div class="mb-3">
                <label for="message" class="form-label">
                    Message
                </label>
                <textarea
                    class="form-control"
                    id="message"
                    name="Message"
                    rows="5"
                    required></textarea>
            </div>

            <button type="submit"
                    class="btn btn-primary">
                Send Message
            </button>

        </form>
    </div>
</section>
<hr class="section-divider">

(i) Contact Section (<section>)

The custom class used here is

"section-padding" is a custom CSS class that adds vertical spacing above and below the section, creating a clean separation from other sections of the webpage.

The id="contact" attribute provides a unique identifier for the section. It allows navigation links, such as <a href="#contact">Contact</a> to scroll directly to this section.


(ii) Bootstrap Grid Row

Bootstrap class used here is:

row creates a horizontal row that contains Bootstrap columns and organises the content into a responsive grid layout.


(iii) Grid Columns

The Bootstrap class used here is

col-lg-6 divides the row into two equal columns on large screens (≥992px). One column displays the contact information, while the other contains the contact form. On smaller screens, the columns automatically stack vertically to improve readability.


(iv) Contact Information

This column contains the club's contact details, including the heading, address information, email address, and telephone number. Standard HTML elements such as <h2> and <p> are used to structure and display the information clearly.


(v) Contact Form

The <form> element collects user information and sends it to the specified destination.

The action attribute specifies the URL where the submitted form data will be sent. In this example, the form uses Formspree, a third-party form handling service.

The method="POST" attribute sends the form data securely within the HTTP request body instead of displaying it in the URL.


(vi) Form Group

The Bootstrap class used here is

mb-3 adds a bottom margin below each form field, providing consistent spacing between the input controls.

Each form group contains a label and its corresponding input element.


(vii) Form Labels

The Bootstrap class used here is

form-label applies Bootstrap's standard styling to form labels, ensuring consistent spacing, font size, and alignment.

The for attribute links the label to its corresponding input field through the matching id value, improving accessibility and allowing users to focus on the input by clicking the label.


(viii) Input Fields

The Bootstrap class used here is

Form-control styles text boxes, email fields, and text areas with Bootstrap's default appearance, including consistent width, padding, borders, and rounded corners.

The required attribute makes each field mandatory and prevents the form from being submitted if the field is left empty.

The type attribute specifies the type of data expected, such as text for names and email for email addresses. The email type also enables built-in browser validation.


(ix) Text Area

The Bootstrap class used here is

form-control applies the same consistent Bootstrap styling used by other input fields.

The rows="5" attribute specifies the initial height of the text area by displaying approximately five lines of text.


(x) Submit Button

Bootstrap classes used here are:

btn applies Bootstrap's standard button styling.

btn-primary styles the button using Bootstrap's primary theme colour, making it stand out as the main call-to-action for submitting the form.

The type="submit" attribute submits the form data to the URL specified in the action attribute when the button is clicked.


(xi) Horizontal Divider (<hr>)

The custom class used here is

section-divider is a custom CSS class that styles the horizontal rule (<hr>), typically by adjusting its width, colour, thickness, and spacing to visually separate different sections of the webpage.


(xii) Overall Layout

This section combines Bootstrap's Grid System (row and col-lg-6) with Bootstrap Form Components (form-control, form-label, btn, and btn-primary) to create a responsive two-column contact section. On large screens, the contact information and contact form are displayed side by side, while on smaller devices they automatically stack vertically to provide a user-friendly and responsive layout.


(3) Footer 
Reinforcement of the landing page by displaying the club name, slogan, email, and copyright so the user remembers the club details.


(a) style.css
.footer {
    background: #212529;
    color: white;
}

#jc {
    font-weight: bold;
    color: #ffc107; /* Pickleball yellow */
}
(b) index.html
<!--footer section--->
<footer class="footer py-4">
    <div class="container">

        <div class="row">

            <div class="col-md-6 text-center text-md-start">
                <p class="mb-0" id="jc">Jersey City Pickleball Club</p>
                <p class="mb-0">
                    Play. Compete. Connect.
                </p>
            </div>

            <div class="col-md-6 text-center text-md-end">
                <p class="mb-0">
                    Email: info@jerseycitypickleball.com
                </p>
                <p class="mb-0">
                    © 2026 All Rights Reserved
                </p>
            </div>

        </div>

    </div>
</footer>

(i) Footer Section (<footer>)

Bootstrap and custom classes used here are

footer is a custom CSS class used to style the footer, such as setting the background colour, text colour, and overall appearance.

py-4 adds vertical padding (1.5rem) to the top and bottom of the footer, creating adequate spacing around the content.

The <footer> element is a semantic HTML5 element that identifies the footer section of the webpage, typically containing copyright information, contact details, or additional navigation.


(ii) Container

The Bootstrap class used here is

container centres the footer content horizontally and limits its maximum width, ensuring the layout remains well-aligned and responsive across different screen sizes.


(iii) Bootstrap Grid Row

The Bootstrap class used here is

row creates a horizontal row that organises the footer content into responsive columns.


(iv) Footer Columns

The Bootstrap classes used here are:

col-md-6 divides the row into two equal columns on medium-sized screens (≥768px) and larger. On smaller screens, the columns automatically stack vertically.

text-center centres the text on smaller devices, providing a clean mobile layout.

text-md-start aligns the text to the left on medium-sized screens and larger.

text-md-end aligns the text to the right on medium-sized screens and larger.

This combination creates a centred layout on mobile devices while displaying left- and right-aligned content on larger screens.


(v) Footer Text

The Bootstrap class used here is

mb-0 removes the default bottom margin from each paragraph (<p>), allowing the text elements to appear closely grouped and neatly aligned.

The id="jc" attribute uniquely identifies the "Jersey City Pickleball Club" text. It can be used for applying custom CSS styling or for JavaScript interactions if required.


(vi) Overall Layout

This footer combines Bootstrap's Grid System (container, row, and col-md-6) with Responsive Text Alignment Utilities (text-center, text-md-start, and text-md-end) to create a responsive two-column footer. On medium-sized screens and larger, the club information appears on the left while the contact details and copyright information are displayed on the right. On smaller devices, both columns automatically stack vertically with centred text, ensuring a clean and mobile-friendly layout.


Step 3: Freeze the Flask app.
An ordinary Flask app is dynamic; only if it is frozen into a static app can it be deployed to static hosting such as GitHub Pages, Netlify, or Cloudflare Pages.

To freeze it, my code is as follows:
from app import app
from flask_frozen import Freezer
# This configuration instructs Frozen-Flask
# to generate relative URLs instead of absolute URLs. app.config['FREEZER_RELATIVE_URLS'] = True # Output directory app.config['FREEZER_DESTINATION'] = 'build' # Use relative paths for links and assets app.config['FREEZER_BASE_URL'] = './' # Preserve static folder structure app.config['FREEZER_STATIC_IGNORE'] = ['*.scss', '*.less'] freezer = Freezer(app) if __name__ == '__main__': freezer.freeze() # Optional: Copy any additional static files print("Freezing complete! Check the 'build' folder")

from app import app imports the Flask application instance from the app.py file. This allows Frozen-Flask to access all the application's routes and templates.

from flask_frozen import Freezer imports the Freezer class from the Flask-Frozen library. The Freezer class converts a dynamic Flask application into a collection of static HTML files.

Using relative URLs improves portability, allowing the static website to be hosted on platforms such as GitHub Pages or copied to different directories without breaking internal links.

The Freezer object scans all registered Flask routes, renders each page, and generates the corresponding static HTML files.

I have begun the freezing process by:

  • Discovering all accessible Flask routes.
  • Rendering each template.
  • Generating static HTML files.
  • Copying the required static assets (such as CSS, JavaScript, and images) into the output directory.
Then, I need to execute the following command:
# direct to my directory
cd JCPC 

# Freeze the flask app
python freeze.py
My terminal will show "Freezing complete! Check the 'build' folder, and in my files and folders, a new build folder will appear.

                           
This build folder contains an "index.html" and a static folder, and under the static folder are an assets folder and "style.css". All images are stored inside the assets folder.
Now, I am ready to deploy to the cloud!


Step 4: Test-run the contact form submission
Let me test out the contact form submission. From the build folder, I had double-clicked the index.html, and then the landing page was successfully launched.

Next, I filled out the form details, including the name, email, and text message, then clicked the send message button, and it redirected to the thanks page. Finally, I go back to the Formspree website. 

After I sign in, I click the sidebar 'A New Form' and select the 'Submission' tab. All the submitted information was stored here.



Final Wrap-up:

In this tutorial, we completed the pickleball club landing page by adding the Facilities, Contact Me, and Footer sections. These additions make the website more informative, professional, and user-friendly. We also demonstrated how to integrate a Formspree contact form and perform a live submission test. The submitted messages can be viewed directly from the Formspree portal or processed by your web application. With these sections in place, the landing page is now ready to serve as a complete front-end foundation for future Flask-powered features.

Published: Jun 2026
Last Updated: Jun 2026

---------------------------------------------------------------------------------------------------------------------------------------------------

Thanks for reading! 

If you haven't subscribed yet, join my newsletter to receive future Python and Flask tutorials.

---------------------------------------------------------------------------------------------------------------------------------------------------

Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 

---------------------------------------------------------------------------------------------------------------------------------------------------

About the Author 

Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.

Comments

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.