A keyword search bar that filters prompts across platform, shot, and text — GET route, list comprehension, bookmarkable URLs
Day 33 of 80
A search bar at the top of the Prompt Vault. Type any keyword — "aerial", "golf", "kling", "sunset" — and the page updates to show only prompts where that word appears anywhere: in the platform name, the shot description, or the prompt text. The search is case-insensitive. The URL updates to /search?q=aerial so you can bookmark or share your search.
Fewer than 25 new lines in Python. Another 10 in HTML. This is a fast build day.
There's a consistent rule in web development: actions that read data use GET, actions that write data use POST. Search reads — it doesn't change anything on the server. Using GET for search means the query ends up in the URL: /search?q=aerial. That's a feature, not a side effect. You can bookmark it, share it, hit the back button and return to exactly the same results. POST searches lose the query on refresh.
| Action | HTTP Method | Why |
|---|---|---|
| View prompts | GET | Reading only — no data changes |
| Search prompts | GET | Reading only — query belongs in URL |
| Filter by platform | GET | Reading only — filter in URL is bookmarkable |
| Add a prompt | POST | Writing data — form body, not URL |
| Delete a prompt | GET* | *Technically should be DELETE, but links can only GET — acceptable for a personal tool |
| Generate with Claude | POST | Sending data (the shot description) to be processed |
@app.route("/search")
def search():
query = request.args.get("q", "").lower()
all_prompts = load_prompts()
if query:
results = [
p for p in all_prompts
if query in p["platform"].lower()
or query in p["shot"].lower()
or query in p["prompt"].lower()
]
else:
results = all_prompts
# Build counts from ALL prompts (not just search results)
counts = {}
for p in all_prompts:
plat = p["platform"]
counts[plat] = counts.get(plat, 0) + 1
return render_template(
"index.html",
prompts=results,
query=query,
all_count=len(all_prompts),
counts=counts,
active_filter="all"
)
request.args.get("q", "") — request.args is Flask's dictionary of GET parameters from the URL query string. If someone visits /search?q=aerial, then request.args.get("q") returns "aerial". The default "" handles a visit to /search with no query.
if ... or ... or ... inside the comprehension checks all three fields. If the query matches any of them, the prompt is included. Both the query and the field values are lowercased so the match is case-insensitive.
query=query — Pass the query string back to the template so you can show "Showing results for: aerial" and pre-fill the search input with the current query.
Add this search bar to templates/index.html — put it just below the page title/subtitle and above the filter bar:
{# ── Search Bar ── #}
<form action="/search" method="GET" class="search-form">
<input
type="search"
name="q"
placeholder="Search prompts..."
value="{{ query if query else '' }}"
autocomplete="off"
>
<button type="submit">Search</button>
{% if query %}
<a href="/" class="clear-search">× Clear</a>
{% endif %}
</form>
{# Show active search indicator #}
{% if query %}
<p class="search-status">
Showing {{ prompts | length }} result{% if prompts | length != 1 %}s{% endif %}
for <strong>"{{ query }}"</strong>
</p>
{% endif %}
value="{{ query if query else '' }}" — Pre-fills the search input with the current query. After searching for "golf", when the page reloads the search box still shows "golf". Without this, the input clears on every submit — frustrating UX.
/ (the home route), which shows all prompts with no filter. Simple and intuitive.
Add these CSS rules to your <style> block:
/* Search */
.search-form { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
.search-form input[type="search"] {
flex: 1; max-width: 400px;
background: #1a1a1a; border: 1px solid #333; color: #e5e5e5;
border-radius: 6px; padding: 0.6rem 0.9rem; font-size: 0.9rem;
}
.search-form input[type="search"]:focus { outline: none; border-color: #2563eb; }
.search-form button { background: #222; border: 1px solid #333; color: #aaa; padding: 0.6rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; }
.search-form button:hover { color: #e5e5e5; border-color: #555; }
.clear-search { color: #888; font-size: 0.85rem; text-decoration: none; }
.clear-search:hover { color: #e5e5e5; }
.search-status { color: #888; font-size: 0.85rem; margin-bottom: 1rem; }
$ python app.py
* Running on http://127.0.0.1:5000
# Search for "golf" → only golf prompts appear, URL = /search?q=golf
# Search for "kling" → only Kling platform prompts, URL = /search?q=kling
# Search for "aerial" → any prompt mentioning aerial
# Search for "" → all prompts (same as home page)
# Click Clear → back to /, all prompts, search input empty
Search for a term you use often. Copy the URL from the browser address bar — something like http://localhost:5000/search?q=aerial. Paste it into a new tab. Your search results load immediately. This is the benefit of GET for search: the query is part of the URL, making it a first-class shareable resource.
Notice that home(), filter_by_platform(), and search() all render the same index.html. They pass different data — all prompts vs filtered prompts vs search results — but the template handles all cases. This is the power of keeping display logic in Jinja2 and business logic in Python. One template, multiple views.
| Route | prompts contains |
query |
active_filter |
|---|---|---|---|
/ |
All prompts | not passed (falsy) | "all" |
/filter/Kling |
Kling prompts only | not passed | "Kling" |
/search?q=aerial |
Matching prompts | "aerial" |
"all" |
/search route to app.py using request.argsindex.html uses method="GET" and action="/search"/ and resets the searchTomorrow you apply your DVP design skills to the frontend. Color system, typography, card styling, platform tag colors, hover effects. HTML and CSS are your territory — Day 34 is the day you make this thing look like a real product.