Put static assets in a separate directory so they can be delivered directly by nginx

This commit is contained in:
Ian Renton
2026-07-25 16:49:49 +01:00
parent 555a8418a9
commit 3c4c595288
20 changed files with 157 additions and 130 deletions
+33 -48
View File
@@ -318,93 +318,78 @@ To set up nginx as a reverse proxy that sits in front of Spothole, first ensure
Create a file at `/etc/nginx/sites-available/` called `spothole`. Give it the following contents, replacing
`spothole.app` with the domain name on which you want to run Spothole. If you changed the port on which Spothole runs,
update that on the "proxy_pass" line too.
update that on the "proxy_pass" line, and if you installed Spothole somewhere other than `/home/spothole/spothole`,
adjust the alias location for serving static files.
(The latter section, configuring the nginx server to serve static files directly, improves efficiency because it saves
Spothole itself from serving JS, CSS etc. files. If you can't do this for some reason, e.g. your nginx and spothole are
on different computers, you can omit the `location /static/ {}` block.)
```nginx
server {
server_name spothole.app;
# Global proxy settings
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 10s;
proxy_buffering on;
# Pass on IP address and host information to Spothole, in case logging this information is required
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
# Wellknown area for Lets Encrypt
location /.well-known/ {
alias /var/www/html/.well-known/;
}
# Load static assets directly from the Spothole webassets directory
location /static/ {
alias /home/spothole/spothole/webassets/;
expires 1h;
add_header Cache-Control "public, max-age=3600, must-revalidate";
}
# SSE endpoints
location ~ ^/api/v1/(spots|alerts)/stream {
location ~ ^/api/v1/(spots|alerts)/stream/? {
proxy_pass http://127.0.0.1:8080;
# Allow keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Set correct content type for SSE API calls
add_header Content-Type text/event-stream always;
# Set remove buffering, remove caching, add suitable timeouts for SSE API calls
# Remove buffering, remove caching, add suitable timeouts for SSE API calls
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;
proxy_connect_timeout 10s;
proxy_send_timeout 24h;
proxy_set_header X-Accel-Buffering no;
add_header Cache-Control no-store always;
# Allow cross-origin requests to API
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin * always;
# Pass on IP address and host information to Spothole, in case logging this information is required
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Access-Control-Allow-Origin * always;
}
# Other API endpoints
location /api/ {
proxy_pass http://127.0.0.1:8080;
# Allow keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Set up buffering, remove caching, add suitable timeouts for API calls
proxy_buffering on;
# Remove buffering, remove caching, add suitable timeouts for API calls
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 30s;
proxy_connect_timeout 10s;
add_header Cache-Control no-store always;
# Allow cross-origin requests to API
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin * always;
# Pass on IP address and host information to Spothole, in case logging this information is required
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Static assets
# Templated pages
location / {
proxy_pass http://127.0.0.1:8080;
# Allow keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Set up buffering and caching, add suitable timeouts for static asset requests
proxy_buffering on;
proxy_read_timeout 30s;
proxy_connect_timeout 10s;
add_header Cache-Control "public, max-age=3600, must-revalidate" always;
# Pass on IP address and host information to Spothole, in case logging this information is required
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Cache-Control "no-cache, must-revalidate" always;
}
}
```
@@ -467,7 +452,7 @@ To navigate your way around the source code, this list may help.
*HTML/JS/CSS front-end code*
* `/webassets` - Root for static files served by the web server
* `/webassets` - Root for static files served by the web server. These are all served from a path starting `/static/`.
* `/webassets/apidocs` - Contains the OpenAPI spec (`openapi.yml`)
* `/webassets/css` - CSS files used by the web front-end
* `/webassets/img` - image files used by the web front-end
+11
View File
@@ -0,0 +1,11 @@
import tornado.web
from core.config import BASE_URL
class ManifestHandler(tornado.web.RequestHandler):
"""Handler for manifest.webmanifest, which needs BASE_URL inserted and a custom content-type"""
def get(self):
self.set_header("Content-Type", "application/manifest+json; charset=UTF-8")
self.render("manifest.webmanifest", baseurl=BASE_URL)
+4 -2
View File
@@ -15,6 +15,7 @@ from server.handlers.api.options import APIOptionsHandler
from server.handlers.api.solar_conditions import APISolarConditionsHandler
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
from server.handlers.api.status import APIStatusHandler
from server.handlers.manifesthandler import ManifestHandler
from server.handlers.metrics import PrometheusMetricsHandler
from server.handlers.pagetemplate import PageTemplateHandler
from server.handlers.quietstaticfilehandler import QuietStaticFileHandler
@@ -100,11 +101,12 @@ class WebServer:
if ALLOW_SPOTTING:
ui_routes += [(r"/add-spot", PageTemplateHandler, {"template_name": "add_spot", **handler_opts})]
# API docs, Prometheus metrics, and finally static assets are always available regardless of API-only mode.
# API docs, Prometheus metrics, webapp manifest and static assets are always available regardless of API-only mode.
misc_routes = [
(r"/apidocs", PageTemplateHandler, {"template_name": "apidocs", **handler_opts}),
(r"/metrics", PrometheusMetricsHandler),
(r"/(.*)", QuietStaticFileHandler, {"path": os.path.join(_HERE, "../webassets")})
(r"/manifest.webmanifest", ManifestHandler),
(r"/static/(.*)", QuietStaticFileHandler, {"path": os.path.join(_HERE, "../webassets")})
]
app = tornado.web.Application(api_routes + ui_routes + misc_routes,
+1 -1
View File
@@ -76,7 +76,7 @@
</div>
<script src="/js/add-spot.js?v=1784989634"></script>
<script src="/static/js/add-spot.js?v=1784994589"></script>
<script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -75,7 +75,7 @@
</div>
<script src="/js/alerts.js?v=1784989634"></script>
<script src="/static/js/alerts.js?v=1784994589"></script>
<script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="row justify-content-center">
<div class="col-md-8">
<div class="text-center mb-4">
<img src="/img/logo.png" width="192" height="60" alt="Spothole">
<img src="/static/img/logo.png" width="192" height="60" alt="Spothole">
</div>
<div class="card">
<div class="card-body">
+1 -1
View File
@@ -1,5 +1,5 @@
{% extends "skeleton.html" %}
{% block body %}
<redoc spec-url="/apidocs/openapi.yml"></redoc>
<redoc spec-url="/static/apidocs/openapi.yml"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
{% end %}
+2 -2
View File
@@ -75,8 +75,8 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/spotsbandsandmap.js?v=1784989634"></script>
<script src="/js/bands.js?v=1784989634"></script>
<script src="/static/js/spotsbandsandmap.js?v=1784994589"></script>
<script src="/static/js/bands.js?v=1784994589"></script>
<script>$(document).ready(function () {
$("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script>
+13 -13
View File
@@ -1,26 +1,26 @@
{% extends "skeleton.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1784989634" type="text/css">
<link href="/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css?v=1784994589" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
<script src="/vendor/js/jquery-3.7.1.min.js"></script>
<script src="/vendor/js/moment-2.29.4.min.js"></script>
<script src="/vendor/js/bootstrap-5.3.8.bundle.min.js"></script>
<script src="/vendor/js/tinycolor2-1.6.0.min.js"></script>
<script src="/static/vendor/js/jquery-3.7.1.min.js"></script>
<script src="/static/vendor/js/moment-2.29.4.min.js"></script>
<script src="/static/vendor/js/bootstrap-5.3.8.bundle.min.js"></script>
<script src="/static/vendor/js/tinycolor2-1.6.0.min.js"></script>
<script src="/js/utils.js?v=1784989634"></script>
<script src="/js/ui-ham.js?v=1784989634"></script>
<script src="/js/geo.js?v=1784989634"></script>
<script src="/js/common.js?v=1784989634"></script>
<script src="/static/js/utils.js?v=1784994589"></script>
<script src="/static/js/ui-ham.js?v=1784994589"></script>
<script src="/static/js/geo.js?v=1784994589"></script>
<script src="/static/js/common.js?v=1784994589"></script>
{% end %}
{% block body %}
<div class="container">
<nav id="header" class="navbar navbar-expand-lg bg-body p-0 border-bottom">
<div class="container-fluid p-0">
<a class="navbar-brand" href="/">
<img src="/img/logo.png" class="logo" width="192" height="60" alt="Spothole">
<img src="/static/img/logo.png" class="logo" width="192" height="60" alt="Spothole">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbar-toggler-content" aria-controls="navbar-toggler-content"
+2 -2
View File
@@ -283,8 +283,8 @@
</div>
</div>
<script src="/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/js/conditions.js?v=1784989634"></script>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1784994589"></script>
<script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script>
@@ -5,7 +5,7 @@
"short_name": "Spothole",
"scope": "/",
"display": "standalone",
"start_url": "https://spothole.app/",
"start_url": "{{ baseurl }}",
"background_color": "white",
"theme_color": "white",
"description": "An Amateur Radio spotting tool bringing together DX clusters and outdoor programmes, providing a universal JSON API and web interface.",
@@ -13,17 +13,17 @@
"prefer_related_applications": false,
"icons": [
{
"src": "/img/icon-192-pwa.png",
"src": "/static/img/icon-192-pwa.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "maskable"
},
{
"src": "/img/icon-512-pwa.png",
"src": "/static/img/icon-512-pwa.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"url": "https://spothole.app"
"url": "{{ baseurl }}"
}
+14 -14
View File
@@ -92,24 +92,24 @@
<link rel="stylesheet" href="/vendor/css/leaflet-1.9.4.min.css">
<link rel="stylesheet" href="/vendor/css/leaflet-extra-markers-1.2.2.min.css">
<script src="/vendor/js/leaflet-1.9.4.min.js"></script>
<script src="/vendor/js/oms-leaflet-0.2.7.min.js"></script>
<script src="/vendor/js/leaflet-providers-2.0.0.js"></script>
<script src="/vendor/js/leaflet-extra-markers-1.2.2.min.js"></script>
<script src="/vendor/js/leaflet-geodesic-2.7.2.umd.min.js"></script>
<script src="/vendor/js/leaflet-vectorgrid-1.3.0.js"></script>
<script src="/vendor/js/text-image-0.7.0.js"></script>
<script src="/vendor/js/leaflet-terminator-1.1.0.min.js"></script>
<script src="/vendor/js/leaflet-maidenhead.js"></script>
<script src="/vendor/js/leaflet-ituzones.js"></script>
<script src="/vendor/js/leaflet-cqzones.js"></script>
<script src="/vendor/js/leaflet-workedallbritainireland.js"></script>
<script src="/static/vendor/js/leaflet-1.9.4.min.js"></script>
<script src="/static/vendor/js/oms-leaflet-0.2.7.min.js"></script>
<script src="/static/vendor/js/leaflet-providers-2.0.0.js"></script>
<script src="/static/vendor/js/leaflet-extra-markers-1.2.2.min.js"></script>
<script src="/static/vendor/js/leaflet-geodesic-2.7.2.umd.min.js"></script>
<script src="/static/vendor/js/leaflet-vectorgrid-1.3.0.js"></script>
<script src="/static/vendor/js/text-image-0.7.0.js"></script>
<script src="/static/vendor/js/leaflet-terminator-1.1.0.min.js"></script>
<script src="/static/vendor/js/leaflet-maidenhead.js"></script>
<script src="/static/vendor/js/leaflet-ituzones.js"></script>
<script src="/static/vendor/js/leaflet-cqzones.js"></script>
<script src="/static/vendor/js/leaflet-workedallbritainireland.js"></script>
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/spotsbandsandmap.js?v=1784989633"></script>
<script src="/js/map.js?v=1784989633"></script>
<script src="/static/js/spotsbandsandmap.js?v=1784994589"></script>
<script src="/static/js/map.js?v=1784994589"></script>
<script>$(document).ready(function () {
$("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script>
+9 -9
View File
@@ -15,10 +15,10 @@
content="An Amateur Radio spotting tool bringing together DX clusters and outdoor programmes, providing a universal JSON API and web interface."/>
<meta property="og:description"
content="An Amateur Radio spotting tool bringing together DX clusters and outdoor programmes, providing a universal JSON API and web interface."/>
<link rel="canonical" href="https://spothole.app/"/>
<meta property="og:url" content="https://spothole.app/"/>
<meta property="og:image" content="https://spothole.app/img/banner.png"/>
<meta property="twitter:image" content="https://spothole.app/img/banner.png"/>
<link rel="canonical" href="{{ baseurl }}"/>
<meta property="og:url" content="{{ baseurl }}"/>
<meta property="og:image" content="{{ baseurl }}/static/img/banner.png"/>
<meta property="twitter:image" content="{{ baseurl }}/static/img/banner.png"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="author" content="Ian Renton"/>
<meta property="og:locale" content="en_GB"/>
@@ -26,14 +26,14 @@
<title>Spothole</title>
<link rel="icon" type="image/png" href="/img/icon-512.png">
<link rel="icon" type="image/png" href="/static/img/icon-512.png">
<link rel="apple-touch-icon" href="img/icon-512-pwa.png">
<link rel="alternate icon" type="image/png" href="/img/icon-192.png">
<link rel="alternate icon" type="image/png" href="/img/icon-32.png">
<link rel="alternate icon" type="image/png" href="/img/icon-16.png">
<link rel="alternate icon" type="image/png" href="/static/img/icon-192.png">
<link rel="alternate icon" type="image/png" href="/static/img/icon-32.png">
<link rel="alternate icon" type="image/png" href="/static/img/icon-16.png">
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico">
<link rel="manifest" href="manifest.webmanifest">
<link rel="manifest" href="/manifest.webmanifest">
{% block head_extra %}{% end %}
</head>
+2 -2
View File
@@ -116,8 +116,8 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/spotsbandsandmap.js?v=1784989633"></script>
<script src="/js/spots.js?v=1784989633"></script>
<script src="/static/js/spotsbandsandmap.js?v=1784994589"></script>
<script src="/static/js/spots.js?v=1784994589"></script>
<script>$(document).ready(function () {
$("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -59,7 +59,7 @@
</div>
</div>
<script src="/js/status.js?v=1784989634"></script>
<script src="/static/js/status.js?v=1784994589"></script>
<script>
$(document).ready(function () {
$("#nav-link-status").addClass("active");
+2 -2
View File
@@ -4,10 +4,10 @@ from datetime import timedelta
from PIL import Image, ImageDraw, ImageFont
from requests_cache import CachedSession
test = os.listdir("./")
test = os.listdir("/")
for item in test:
if item.endswith(".png"):
os.remove(os.path.join("./", item))
os.remove(os.path.join("/", item))
cache = CachedSession("/tmp/cache", expire_after=timedelta(days=30))
data = cache.get("https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json").json()
+1 -1
View File
@@ -184,7 +184,7 @@ function addAlertRowsToTable(tbody, alerts) {
// Format DX flag
let dx_flag = "<i class='fa-solid fa-globe-africa'></i>";
if (a["dx_dxcc_id"] && a["dx_dxcc_id"] != null && a["dx_dxcc_id"] !== 0) {
dx_flag = `<img src="img/flags/${a['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
dx_flag = `<img src="static/img/flags/${a['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
}
// Format dx calls
+2 -2
View File
@@ -249,7 +249,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
dx_flag = "";
}
if (s["dx_dxcc_id"] && s["dx_dxcc_id"] != null && s["dx_dxcc_id"] !== 0) {
dx_flag = `<img src="img/flags/${s['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
dx_flag = `<img src="static/img/flags/${s['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
}
// Format the frequency
@@ -319,7 +319,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
// Format DE flag
let de_flag = "<i class='fa-solid fa-circle-question'></i>";
if (s["de_dxcc_id"] && s["de_dxcc_id"] != null && s["de_dxcc_id"] !== 0) {
de_flag = `<img src="img/flags/${s['de_dxcc_id']}.png" class="flag" width="24" alt="${de_country}" title="${de_country}"/>`;
de_flag = `<img src="static/img/flags/${s['de_dxcc_id']}.png" class="flag" width="24" alt="${de_country}" title="${de_country}"/>`;
}
// Format de call
+52 -23
View File
@@ -1,28 +1,57 @@
const CACHE_NAME = 'Spothole';
const CACHE_URLS = [
'index.html',
'./',
'apidocs',
'apidocs/openapi.yml',
'about',
'css/style.css',
'js/add-spot.js',
'js/alerts.js',
'js/bands.js',
'js/common.js',
'js/map.js',
'js/spots.js',
'js/spotsbandsandmap.js',
'js/status.js',
'img/logo.png',
'img/favicon.ico',
'img/icon-32.png',
'img/icon-192.png',
'img/icon-512.png',
'fa/css/fontawesome.min.css',
'fa/css/solid.min.css',
'fa/webfonts/fa-solid-900.ttf',
'fa/webfonts/fa-solid-900.woff2'
'static/apidocs/openapi.yml',
'static/audio/ping.mp3',
'static/css/style.css',
'static/js/add-spot.js',
'static/js/alerts.js',
'static/js/bands.js',
'static/js/common.js',
'static/js/geo.js',
'static/js/map.js',
'static/js/spots.js',
'static/js/spotsbandsandmap.js',
'static/js/status.js',
'static/js/ui-ham.js',
'static/js/utils.js',
'static/img/logo.png',
'static/img/favicon.ico',
'static/img/icon-32.png',
'static/img/icon-192.png',
'static/img/icon-512.png',
'static/vendor/img/markers_default@2x.png',
'static/vendor/img/markers_shadow.png',
'static/vendor/img/markers_default.png',
'static/vendor/img/markers_shadow@2x.png',
'static/vendor/css/bootstrap-5.3.8.min.css',
'static/vendor/css/images/marker-icon.png',
'static/vendor/css/images/layers-2x.png',
'static/vendor/css/images/marker-shadow.png',
'static/vendor/css/images/layers.png',
'static/vendor/css/images/marker-icon-2x.png',
'static/vendor/css/leaflet-extra-markers-1.2.2.min.css',
'static/vendor/css/leaflet-1.9.4.min.css',
'static/vendor/css/fontawesome-6.7.2.min.css',
'static/vendor/css/solid-6.7.2.min.css',
'static/vendor/js/jquery-3.7.1.min.js',
'static/vendor/js/chart-4.4.9.umd.min.js',
'static/vendor/js/oms-leaflet-0.2.7.min.js',
'static/vendor/js/leaflet-workedallbritainireland.js',
'static/vendor/js/leaflet-providers-2.0.0.js',
'static/vendor/js/text-image-0.7.0.js',
'static/vendor/js/leaflet-vectorgrid-1.3.0.js',
'static/vendor/js/leaflet-ituzones.js',
'static/vendor/js/tinycolor2-1.6.0.min.js',
'static/vendor/js/bootstrap-5.3.8.bundle.min.js',
'static/vendor/js/moment-2.29.4.min.js',
'static/vendor/js/leaflet-cqzones.js',
'static/vendor/js/leaflet-extra-markers-1.2.2.min.js',
'static/vendor/js/leaflet-terminator-1.1.0.min.js',
'static/vendor/js/leaflet-1.9.4.min.js',
'static/vendor/js/leaflet-geodesic-2.7.2.umd.min.js',
'static/vendor/js/leaflet-maidenhead.js',
'static/vendor/webfonts/fa-solid-900.ttf',
'static/vendor/webfonts/fa-solid-900.woff2'
];
self.addEventListener('fetch', (event) => {
File diff suppressed because one or more lines are too long