Launch a portfolio site with a working contact form

A fast personal site with projects, a contact form that really sends email, and your own domain with HTTPS.

24 ч beginner HTMLCSSFlaskResendRender

1 Friday evening: content before code

Write the words first. A portfolio that ships beats a beautiful empty one.

  • One sentence about what you do and for whom.
  • Three projects: problem → what you built → result (numbers if you have them).
  • One clear call to action: "Email me" or "Book a call".

Collect a screenshot per project (1600×900, compressed to under 300 KB with squoosh.app).

Done when: you have a text file with every word on the site.

2 Saturday morning: the page

One HTML file, one CSS file. Use a system font stack and a single accent colour.

<main class="wrap">
  <header>
    <h1>Hi, I'm Ada. I build web apps for small businesses.</h1>
    <a class="btn" href="#contact">Work with me</a>
  </header>
  <section id="work" class="grid">
    <article class="card">
      <img src="/static/img/shop.webp" alt="Online shop dashboard" width="800" height="450" loading="lazy">
      <h2>Bakery pre-orders</h2>
      <p>Cut phone orders by 60% with a simple pre-order form and daily email summary.</p>
    </article>
  </section>
</main>
:root { --bg: #0b0d11; --fg: #eceef3; --muted: #a7aebc; --accent: #2f6bff; }
body { margin: 0; font: 16px/1.6 system-ui, sans-serif; background: var(--bg); color: var(--fg); }
.wrap { width: min(1100px, 100% - 32px); margin-inline: auto; }
.grid { display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }

Done when: the page looks right at 390px and 1440px wide with no horizontal scroll.

3 Saturday afternoon: a contact form that sends email

Wrap the page in a tiny Flask app so the form can POST.

import os, resend
from flask import Flask, render_template, request, redirect, flash

app = Flask(__name__)
app.secret_key = os.environ["SECRET_KEY"]
resend.api_key = os.environ["RESEND_API_KEY"]

@app.post("/contact")
def contact():
    if request.form.get("website"):          # honeypot: bots fill hidden fields
        return redirect("/")
    resend.Emails.send({
        "from": "Site <hello@yourdomain.com>",
        "to": [os.environ["CONTACT_TO"]],
        "reply_to": request.form["email"],
        "subject": "New message from your site",
        "text": request.form["message"][:5000],
    })
    flash("Thanks! I will reply within a day.")
    return redirect("/#contact")

Add a rate limit (for example Flask-Limiter, 5 per hour per IP) and a CSRF token.

Done when: a message from the form lands in your inbox.

4 Sunday morning: deploy and connect the domain

  1. Push to GitHub and create a Render Web Service (gunicorn app:app).
  2. Set SECRET_KEY, RESEND_API_KEY and CONTACT_TO.
  3. In Render → Settings → Custom Domains add yourdomain.com and www.
  4. At your registrar add the records Render shows (an A/ALIAS for the apex, a CNAME for www).
  5. Verify the domain in Resend (SPF + DKIM records) so mail does not land in spam.

Done when: https://yourdomain.com loads with a padlock and the form still sends.

5 Sunday afternoon: check and share

  • Run Lighthouse in Chrome DevTools: aim for 90+ in every category.
  • Add <meta name="description"> and an Open Graph image.
  • Post the link where your clients are (LinkedIn, local groups), not only where developers are.

Done when: three people outside tech have opened the site.

Закончили проект? Отправьте ссылки на проверку. Отправить на проверку (50 баллов)

Углубиться с этими курсами

Другие гайды