diff --git a/.gitignore b/.gitignore
index 7487dec..63eb1b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ wheels/
media/
deployment/secret.yaml
*.json
+static/
diff --git a/Dockerfile b/Dockerfile
index 89fa732..21e6ece 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -35,6 +35,6 @@ RUN uv sync --frozen \
&& chgrp -R 0 /app \
&& chmod -R g=u /app \
&& chmod g+w /app/config/caddy/Caddyfile \
- && SECRET_KEY= python -m hub collectstatic --noinput
+ && SECRET_KEY=dummy python -m hub build_assets --force
CMD ["/usr/local/bin/runhub.sh"]
\ No newline at end of file
diff --git a/hub/services/management/commands/build_assets.py b/hub/services/management/commands/build_assets.py
new file mode 100644
index 0000000..e46813a
--- /dev/null
+++ b/hub/services/management/commands/build_assets.py
@@ -0,0 +1,32 @@
+from django.core.management.base import BaseCommand
+from django.core.management import call_command
+
+
+class Command(BaseCommand):
+ help = "Build and compress static assets for production"
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--force",
+ action="store_true",
+ help="Force compression even if files exist",
+ )
+
+ def handle(self, *args, **options):
+ self.stdout.write("Building static assets...")
+
+ # Compress CSS and JS files
+ self.stdout.write("Compressing CSS and JavaScript...")
+ call_command(
+ "compress",
+ force=options.get("force", False),
+ verbosity=options.get("verbosity", 1),
+ )
+
+ # Collect all static files
+ self.stdout.write("Collecting static files...")
+ call_command(
+ "collectstatic", interactive=False, verbosity=options.get("verbosity", 1)
+ )
+
+ self.stdout.write(self.style.SUCCESS("Successfully built static assets"))
diff --git a/hub/services/templates/services/offering_detail.html b/hub/services/templates/services/offering_detail.html
index 8ede07d..9db60d7 100644
--- a/hub/services/templates/services/offering_detail.html
+++ b/hub/services/templates/services/offering_detail.html
@@ -1,12 +1,36 @@
{% extends 'base.html' %}
{% load static %}
+{% load compress %}
{% load contact_tags %}
{% load json_ld_tags %}
{% block title %}Managed {{ offering.service.name }} on {{ offering.cloud_provider.name }}{% endblock %}
{% block extra_js %}
-
+{% if debug %}
+
+
+{% else %}
+
+{% compress js %}
+
+
+
+
+
+
+
+
+{% endcompress %}
+{% endif %}
{% json_ld_structured_data %}
diff --git a/hub/settings.py b/hub/settings.py
index 850e03f..ab542cb 100644
--- a/hub/settings.py
+++ b/hub/settings.py
@@ -76,6 +76,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"django.contrib.sitemaps",
# 3rd party
+ "compressor",
"django_prose_editor",
"rest_framework",
"schema_viewer",
@@ -186,6 +187,25 @@ USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = env.path("STATIC_ROOT", default=BASE_DIR / "static")
+# Static files configuration
+STATICFILES_FINDERS = [
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+ "compressor.finders.CompressorFinder",
+]
+
+# Django Compressor settings
+COMPRESS_ENABLED = True
+COMPRESS_OFFLINE = True # Compress during build, not runtime
+COMPRESS_CSS_FILTERS = [
+ "compressor.filters.css_default.CssAbsoluteFilter",
+ "compressor.filters.cssmin.rCSSMinFilter",
+]
+COMPRESS_JS_FILTERS = [
+ "compressor.filters.jsmin.rJSMinFilter",
+]
+COMPRESS_OUTPUT_DIR = "CACHE"
+
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
diff --git a/pyproject.toml b/pyproject.toml
index 3a6f525..9e0c702 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,6 +7,7 @@ requires-python = ">=3.13"
dependencies = [
"django>=5.2",
"django-admin-sortable2>=2.2.4",
+ "django-compressor>=4.5.1",
"django-import-export>=4.3.7",
"django-jazzmin>=3.0.1",
"django-nested-admin>=4.1.1",
diff --git a/uv.lock b/uv.lock
index 5c335f2..c4e3800 100644
--- a/uv.lock
+++ b/uv.lock
@@ -81,6 +81,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/c3/e804b1f04546c1060e566f35177c346590820a95bfb981d1f6360b419437/django_admin_sortable2-2.2.4-py3-none-any.whl", hash = "sha256:406c5b6d6e84ad982cc6e53c3f34b5db5f0a3f34891126af90c9fb2c372f53d5", size = 90816, upload-time = "2024-11-15T09:43:13.665Z" },
]
+[[package]]
+name = "django-appconf"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "django" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/a9/dcf95ff3fa0620b6818fc02276fbbb8926e7f286039b6d015e56e8b7af39/django-appconf-1.1.0.tar.gz", hash = "sha256:9fcead372f82a0f21ee189434e7ae9c007cbb29af1118c18251720f3d06243e4", size = 15986, upload-time = "2025-02-13T16:09:40.258Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/9e/f3a899991e4aaae4b69c1aa187ba4a32e34742475c91eb13010ee7fbe9db/django_appconf-1.1.0-py3-none-any.whl", hash = "sha256:7abd5a163ff57557f216e84d3ce9dac36c37ffce1ab9a044d3d53b7c943dd10f", size = 6389, upload-time = "2025-02-13T16:09:39.133Z" },
+]
+
[[package]]
name = "django-browser-reload"
version = "1.17.0"
@@ -103,6 +115,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/90/01755e4a42558b763f7021e9369aa6aa94c2ede7313deed56cb7483834ab/django_cache_url-3.4.5-py2.py3-none-any.whl", hash = "sha256:5f350759978483ab85dc0e3e17b3d53eed3394a28148f6bf0f53d11d0feb5b3c", size = 4760, upload-time = "2023-12-04T17:19:44.355Z" },
]
+[[package]]
+name = "django-compressor"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "django" },
+ { name = "django-appconf" },
+ { name = "rcssmin" },
+ { name = "rjsmin" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/15/30/a9994277ae05082ba5df22c5678a87082253a034927c8d9915c3bf3b8c36/django_compressor-4.5.1.tar.gz", hash = "sha256:c1d8a48a2ee4d8b7f23c411eb9c97e2d88db18a18ba1c9e8178d5f5b8366a822", size = 124734, upload-time = "2024-07-22T09:56:47.554Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/d9/ac374a1f7a432230cdf4d2ffbe957fd0d4d5d6426bf4d5c17f382b0801c4/django_compressor-4.5.1-py2.py3-none-any.whl", hash = "sha256:87741edee4e7f24f3e0b8072d94a990cfb010cb2ca7cc443944da8e193cdea65", size = 145465, upload-time = "2024-07-22T09:56:45.822Z" },
+]
+
[[package]]
name = "django-import-export"
version = "4.3.7"
@@ -351,6 +378,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
+[[package]]
+name = "rcssmin"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ef/26/f38d49c21d933e3e4320ed31c6025c381dbd973e9936edd0af52ce521534/rcssmin-1.1.2.tar.gz", hash = "sha256:bc75eb75bd6d345c0c51fd80fc487ddd6f9fd409dd7861b3fe98dee85018e1e9", size = 582213, upload-time = "2023-10-03T19:57:48.536Z" }
+
+[[package]]
+name = "rjsmin"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/1c/c0355e8b8b8aca9c0d43519d2a7c473940deae0297ff8544eff359d7f715/rjsmin-1.2.2.tar.gz", hash = "sha256:8c1bcd821143fecf23242012b55e13610840a839cd467b358f16359010d62dae", size = 420634, upload-time = "2023-10-05T07:19:30.857Z" }
+
[[package]]
name = "servala-fe"
version = "0.1.0"
@@ -358,6 +397,7 @@ source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-admin-sortable2" },
+ { name = "django-compressor" },
{ name = "django-import-export" },
{ name = "django-jazzmin" },
{ name = "django-nested-admin" },
@@ -381,6 +421,7 @@ requires-dist = [
{ name = "django", specifier = ">=5.2" },
{ name = "django-admin-sortable2", specifier = ">=2.2.4" },
{ name = "django-browser-reload", marker = "extra == 'dev'", specifier = "~=1.13" },
+ { name = "django-compressor", specifier = ">=4.5.1" },
{ name = "django-import-export", specifier = ">=4.3.7" },
{ name = "django-jazzmin", specifier = ">=3.0.1" },
{ name = "django-nested-admin", specifier = ">=4.1.1" },