Processing forms from a static site

Posted on Apr 1, 2025

Why I Love Hugo (and Static Sites in General)

I’m a huge fan of static sites because they’re like the reliable friend who never forgets your birthday: simple, secure, and always there for you. With Hugo, I can generate my entire site into pure HTML/CSS/JS, which means there’s no heavy database to slow things down or complex server code to break. There’s also a smaller attack surface—fewer moving parts generally mean fewer mistakes and vulnerabilities. But there’s one big hitch: forms. You still need some way to process user input.

One of the Main Challenges: Forms on a Static Site

While purely static hosting is awesome for speed and stability, it doesn’t magically handle contact forms. With a static site, it’s easy to display a form, but capturing and processing that form data requires a server or external service. Plenty of folks plug in something like Google Forms, but I’m a control freak who likes to keep everything in-house—plus, I’m not thrilled about letting third parties see user submissions, or dealing with the styling nightmares these providers often lock you into.

Enter form_handler.py—My Own Secure Little Backend

Instead of relying on external services, I decided to write a small Flask app called form_handler.py. Here’s a snippet showing how I generate the form’s CSRF token, which is kind of like a secret handshake at a wizard convention (if wizard conventions existed):

@app.route("/init_form")
@cross_origin(origins=ALLOWED_ORIGINS) 
def init_form():
    """Generate and return the data for a new form submission."""
    token = secrets.token_hex()
    word = secrets.choice(words)
    idx = secrets.randbelow(len(word))
    answer = word[idx]
    cur = get_db().cursor()
    cur.execute(f"""INSERT INTO validation_data (date, csrf_token, captcha_answer)
                    VALUES(datetime('now', 'localtime'), ?, ?);
                 """, (token, answer))
    get_db().commit()
    cur.close()
    return {
        "csrf_token": token,
        "word": word,
        "pos": idx+1,
        "pos_str": pos_translate[idx],
        "answer": answer
    }

CSRF Protection

When a new form loads, the server embeds a unique token. If you don’t submit that exact token—or if you try to reuse it—my code politely says, “Nope, not happening.” After all, who wants to be vulnerable to cross-site request forgery?

Custom Captcha

No one wants spam (unless you’re thinking about that canned meat). So I also add a custom captcha. We randomly pick a word from a file, ask the user for the nth letter, and confirm it with the server. While we’re at it, there’s a hidden “captcha2” field that must remain empty—if a robot tries to fill it in, that’s basically an admission of guilt.

SQLite Storage

Naturally, I need to store these submissions somewhere. For that, form_handler.py hangs on to them in an SQLite database. If a submission fails the security checks, I still log it, but mark it as invalid. If it passes, it’s saved for posterity—like a guestbook that’s only visible to me.

Summary

By writing my own backend, I keep my static site approach intact without sacrificing control over how my forms look or behave. Plus, I’m not transferring data to a third-party service (sorry, Google Forms, you’re just not my type). Combining a hidden honeypot field, a CSRF token, and a word-based captcha ensures spam is kept to a minimum—and provides me with a few laughs along the way.