script cache invalidation and verse menu
This commit is contained in:
@@ -394,18 +394,39 @@ for group in theological_synonym_groups:
|
||||
concept_mappings = json.load(open("data/concept_mappings.json", "r"))
|
||||
concept_mappings_dict = {mapping["query"].lower(): mapping for mapping in concept_mappings}
|
||||
|
||||
# Static assets are served with a very long cache lifetime, so their URLs carry a
|
||||
# hash of their contents ("?v=...") and change whenever the files do.
|
||||
ASSET_VERSION: str = hashlib.md5(
|
||||
b"".join(
|
||||
open(f, "rb").read()
|
||||
for f in ("styles/page.css", "js/collapsible.js", "js/shortcode.js", "js/verse_menu.js", "js/book_suggest.js")
|
||||
)
|
||||
).hexdigest()[:8]
|
||||
# Static assets: each file's URL carries a hash of its own contents ("?v=...").
|
||||
# A URL whose hash matches is served as immutable for a year; the hash changes
|
||||
# whenever the file does, so a deploy is picked up immediately and only the
|
||||
# files that actually changed get downloaded again. Any other URL for the same
|
||||
# file (no hash, or an old one) is served with "no-cache", so it is revalidated
|
||||
# every time instead of lingering in a browser for a year.
|
||||
ASSET_SOURCES = {
|
||||
"/styles/page.css": ("styles/page.css", "text/css"),
|
||||
"/js/collapsible.js": ("js/collapsible.js", "application/javascript"),
|
||||
"/js/shortcode.js": ("js/shortcode.js", "application/javascript"),
|
||||
"/js/verse_menu.js": ("js/verse_menu.js", "application/javascript"),
|
||||
"/js/book_suggest.js": ("js/book_suggest.js", "application/javascript"),
|
||||
}
|
||||
ASSET_VERSIONS: dict = {
|
||||
url: hashlib.md5(open(path, "rb").read()).hexdigest()[:8]
|
||||
for url, (path, _mime) in ASSET_SOURCES.items()
|
||||
}
|
||||
|
||||
|
||||
def asset_url(url: str) -> str:
|
||||
return f"{url}?v={ASSET_VERSIONS[url]}"
|
||||
|
||||
|
||||
app.jinja_env.globals["asset_url"] = asset_url
|
||||
|
||||
|
||||
def load_html(path: str) -> str:
|
||||
return open(path, "r").read().replace("__V__", ASSET_VERSION)
|
||||
"""Read an html file, filling each '/js/x.js?v=__V__' with that file's own version."""
|
||||
return re.sub(
|
||||
r"(/(?:js|styles)/[\w.\-]+)\?v=__V__",
|
||||
lambda m: asset_url(m.group(1)),
|
||||
open(path, "r").read(),
|
||||
)
|
||||
|
||||
|
||||
cached: dict = {
|
||||
@@ -479,41 +500,42 @@ with app.app_context():
|
||||
template_content = load_html("html/index.html")
|
||||
cached["html_index"] = render_template_string(template_content, testament_data=cached["book_info_for_index"])
|
||||
|
||||
@app.get("/js/shortcode.js")
|
||||
def serve_shortcode_js():
|
||||
response = Response(cached.get("js_shortcode"), mimetype='application/javascript')
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
return response
|
||||
def _make_asset_view(url_path: str, mimetype: str):
|
||||
def view():
|
||||
response = Response(cached["assets"][url_path], mimetype=mimetype)
|
||||
response.add_etag()
|
||||
if request.args.get("v") == ASSET_VERSIONS[url_path]:
|
||||
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
||||
else:
|
||||
response.headers["Cache-Control"] = "no-cache"
|
||||
return response.make_conditional(request)
|
||||
view.__name__ = "asset_" + url_path.strip("/").replace("/", "_").replace(".", "_")
|
||||
return view
|
||||
|
||||
@app.get("/js/book_suggest.js")
|
||||
def serve_book_suggest_js():
|
||||
response = Response(cached.get("js_book_suggest"), mimetype='application/javascript')
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
return response
|
||||
|
||||
@app.get("/js/verse_menu.js")
|
||||
def serve_verse_menu_js():
|
||||
response = Response(cached.get("js_verse_menu"), mimetype='application/javascript')
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
return response
|
||||
cached["assets"] = {
|
||||
"/styles/page.css": cached["css_page"],
|
||||
"/js/collapsible.js": cached["js_collapsible"],
|
||||
"/js/shortcode.js": cached["js_shortcode"],
|
||||
"/js/verse_menu.js": cached["js_verse_menu"],
|
||||
"/js/book_suggest.js": cached["js_book_suggest"],
|
||||
}
|
||||
for _url, (_path, _mime) in ASSET_SOURCES.items():
|
||||
app.add_url_rule(_url, view_func=_make_asset_view(_url, _mime), methods=["GET"])
|
||||
|
||||
@app.get("/js/collapsible.js")
|
||||
def serve_collapsible_js():
|
||||
response = Response(cached.get("js_collapsible"), mimetype='application/javascript')
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
return response
|
||||
|
||||
@app.get("/styles/page.css")
|
||||
def serve_page_css():
|
||||
response = Response(cached.get("css_page"), mimetype='text/css')
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
@app.after_request
|
||||
def html_pages_are_never_stale(response):
|
||||
"""Pages reference versioned assets, so they must always be revalidated to see new versions."""
|
||||
if response.mimetype == "text/html" and "Cache-Control" not in response.headers:
|
||||
response.headers["Cache-Control"] = "no-cache"
|
||||
return response
|
||||
|
||||
@app.get("/favicon.ico")
|
||||
@app.get("/favicon.svg")
|
||||
def serve_favicon():
|
||||
response = Response(cached.get("favicon_svg"), mimetype='image/svg+xml')
|
||||
response.headers['Cache-Control'] = 'public, max-age=31536000'
|
||||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||||
return response
|
||||
|
||||
def static_page(path: str):
|
||||
@@ -529,7 +551,7 @@ def static_html_doc(title: str, body: str):
|
||||
"<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
|
||||
f"<title>{title} - KJV API</title>"
|
||||
f"<link rel='stylesheet' href='/styles/page.css?v={ASSET_VERSION}'></head><body>"
|
||||
f"<link rel='stylesheet' href='{asset_url('/styles/page.css')}'></head><body>"
|
||||
"<header><a href='/'>Home</a> <a href='/find'>Quick Search</a></header>"
|
||||
f"{body}</body></html>"
|
||||
)
|
||||
@@ -928,7 +950,7 @@ def parse_query(query_override: str = None):
|
||||
result += "<style>" + cached["css_collapsible"] + "</style>"
|
||||
result += "<div class='dictionary-section'><h3>1828 Webster Definitions</h3><p>Click on the word to get the definition.</p></div>"
|
||||
result += definitions_html
|
||||
result += f"<script src='/js/collapsible.js?v={ASSET_VERSION}'></script>"
|
||||
result += f"<script src='{asset_url('/js/collapsible.js')}'></script>"
|
||||
|
||||
result += "</div>"
|
||||
return result
|
||||
@@ -1687,7 +1709,7 @@ def search():
|
||||
|
||||
@app.get("/browse") # not implemented. TO be moved to frontend.
|
||||
def bible_reader():
|
||||
return render_template("reader.html", books=cached.get("book_names"), v=ASSET_VERSION)
|
||||
return render_template("reader.html", books=cached.get("book_names"))
|
||||
# We should return an index of all books with
|
||||
# drop down menus of each chapter, and a search
|
||||
# bar at the top for quick seeking, which can use
|
||||
@@ -1718,7 +1740,7 @@ def sequential_report(uid=None):
|
||||
seq_conn = sqlite3.connect("data/seq.db")
|
||||
seq_cur = seq_conn.cursor()
|
||||
uids = seq_cur.execute("select * from reading_tracker").fetchall()
|
||||
return render_template("seq_report.html", uids=uids, v=ASSET_VERSION)
|
||||
return render_template("seq_report.html", uids=uids)
|
||||
|
||||
@app.get("/screport") # shows current remembered shortcodes
|
||||
def shortcode_report(shortcode=None):
|
||||
@@ -1730,7 +1752,7 @@ def shortcode_report(shortcode=None):
|
||||
shortcodes = short_cur.execute(
|
||||
"SELECT * FROM shortcodes"
|
||||
).fetchall()
|
||||
return render_template("shortcode_report.html", shortcodes=shortcodes, v=ASSET_VERSION)
|
||||
return render_template("shortcode_report.html", shortcodes=shortcodes)
|
||||
|
||||
|
||||
def sequential_purge():
|
||||
|
||||
Reference in New Issue
Block a user