diff --git a/client/build.js b/client/build.js
index eaf28a54..aad00f22 100755
--- a/client/build.js
+++ b/client/build.js
@@ -84,7 +84,7 @@ function bundleHtml() {
function minifyHtml(html) {
return require('html-minifier').minify(html, {
- removeComments: true,
+ removeComments: false,
collapseWhitespace: true,
conservativeCollapse: true,
}).trim();
diff --git a/client/html/index.htm b/client/html/index.htm
index 00728903..c2bc7aef 100644
--- a/client/html/index.htm
+++ b/client/html/index.htm
@@ -1,5 +1,5 @@
-
+
@@ -10,6 +10,7 @@
Loading...
+
diff --git a/client/nginx.conf.docker b/client/nginx.conf.docker
index 98c18b35..00fc1763 100644
--- a/client/nginx.conf.docker
+++ b/client/nginx.conf.docker
@@ -23,6 +23,10 @@ http {
server __BACKEND__:6666;
}
+ upstream frontend {
+ server __FRONTEND__:6667;
+ }
+
server {
listen 80 default_server;
@@ -71,7 +75,7 @@ http {
location / {
root /var/www;
- try_files $uri /index.htm;
+ try_files $uri @frontend;
sendfile on;
tcp_nopush on;
@@ -81,6 +85,10 @@ http {
gzip_proxied expired no-cache no-store private auth;
}
+ location @frontend {
+ proxy_pass http://frontend;
+ }
+
location @unauthorized {
return 403 "Unauthorized";
default_type text/plain;
diff --git a/client/requirements.txt b/client/requirements.txt
new file mode 100644
index 00000000..f2293605
--- /dev/null
+++ b/client/requirements.txt
@@ -0,0 +1 @@
+requests
diff --git a/client/server.py b/client/server.py
new file mode 100644
index 00000000..a6a8163a
--- /dev/null
+++ b/client/server.py
@@ -0,0 +1,130 @@
+import os
+import os.path
+import html
+import urllib.parse
+import itertools
+from typing import Dict, Any, Callable, Tuple, List, Iterable
+from http import HTTPStatus
+from pathlib import Path
+
+import requests
+
+FRONTEND_WEB_ROOT = Path(os.environ["SZURUBOORU_WEB_ROOT"])
+
+with open(FRONTEND_WEB_ROOT / "index.htm") as f:
+ INDEX_HTML = f.read()
+
+BACKEND_BASE_URL = os.environ["SZURUBOORU_BASE_URL"]
+PUBLIC_BASE_URL = os.environ["SZURUBOORU_PUBLIC_BASE_URL"]
+
+Metadata = Iterable[Tuple[str, str]]
+
+def general_embed(server_info: Dict[str, Any]) -> Metadata:
+ yield "og:site_name", server_info["config"]["name"]
+
+def user_embed(username: str) -> Metadata:
+ yield "og:type", "profile"
+ yield "profile:username", username
+ yield "og:title", username
+ yield "og:image:height", "128"
+ yield "og:image:width", "128"
+ user = requests.get(BACKEND_BASE_URL + "/api/user/" + username).json()
+ yield "og:image:url", urllib.parse.join(PUBLIC_BASE_URL, user["avatarUrl"])
+
+def _image_embed(post: Dict[str, Any]) -> Metadata:
+ url = PUBLIC_BASE_URL + post["contentUrl"]
+
+ if post["type"] == "video":
+ prefix = "og:video"
+ yield "og:image", PUBLIC_BASE_URL + post["thumbnailUrl"]
+ yield "twitter:card", "player"
+ else:
+ prefix = "og:image"
+ yield "twitter:card", "summary_large_image"
+
+ yield prefix + ":width", str(post["canvasWidth"])
+ yield prefix + ":height", str(post["canvasHeight"])
+ yield prefix, url
+ if BACKEND_BASE_URL.startswith('https://'):
+ yield prefix + ":secure_url", url
+ yield prefix + ":type", post["mimeType"]
+
+def _author_embed(user: Dict[str, Any]) -> Metadata:
+ yield "article:author", PUBLIC_BASE_URL + "/user/" + user["name"]
+
+def post_embed(post_id: int) -> Metadata:
+ post = requests.get(BACKEND_BASE_URL + f"/api/post/{post_id}").json()
+ yield "og:type", "article"
+ yield from _author_embed(post["user"])
+ yield "og:title", post["user"]["name"]
+ if post["tags"]:
+ value = "Tags: " + ", ".join(tag["names"][0] for tag in post["tags"])
+ yield "og:description", value
+ yield "description", value
+ yield from _image_embed(post)
+
+def homepage_embed(server_info: Dict[str, Any]) -> Metadata:
+ yield "og:title", server_info["config"]["name"]
+ yield "og:type", "website"
+ post = server_info["featuredPost"]
+ if post is not None:
+ yield from _image_embed(post)
+
+def render_embed(metadata: Metadata) -> str:
+ out = []
+ for k, v in metadata:
+ k, v = html.escape(k), html.escape(v)
+ out.append(f'')
+ return ''.join(out)
+
+def application(env: Dict[str, Any], start_response: Callable[[str, Any], Any]) -> Tuple[bytes]:
+ def serve_file(path):
+ start_response(str(int(HTTPStatus.OK)), [("X-Accel-Redirect", path)])
+ return ()
+
+ def serve_without_embed():
+ return serve_file("/index.htm")
+
+ method = env["REQUEST_METHOD"]
+ if method != "GET":
+ start_response(str(int(HTTPStatus.BAD_REQUEST)), [("Content-Type", "text/plain")])
+ return (b"Bad request",)
+
+ path = env["PATH_INFO"].lstrip("/")
+ path = path.encode("latin-1").decode("utf-8") # PEP-3333
+
+ if path and (FRONTEND_WEB_ROOT / path).exists():
+ return serve_file("/" + path)
+
+ path = "/" + path
+ path_components = path.split("/")
+
+ if path_components[1] not in {"post", "user", ""}:
+ # serve index.htm like normal
+ return serve_without_embed()
+
+ server_info = requests.get(BACKEND_BASE_URL + "/api/info").json()
+ privileges = server_info["config"]["privileges"]
+ if path_components[1] == "user":
+ username = path_components[2]
+ if privileges["users:view"] != "anonymous" or not username:
+ return serve_without_embed()
+ metadata = user_embed(username)
+
+ elif path_components[1] == "post":
+ try:
+ post_id = int(path_components[2])
+ except ValueError:
+ return serve_without_embed()
+
+ if privileges["posts:view"] != "anonymous":
+ return serve_without_embed()
+ metadata = post_embed(post_id)
+
+ elif path_components[1] == "":
+ metadata = homepage_embed(server_info)
+
+ metadata = itertools.chain(general_embed(server_info), metadata)
+ body = INDEX_HTML.replace("", render_embed(metadata)).encode("utf-8")
+ start_response(str(int(HTTPStatus.OK)), [("Content-Type", "text/html"), ("Content-Length", str(len(body)))])
+ return (body,)
diff --git a/server/szurubooru/rest/app.py b/server/szurubooru/rest/app.py
index a6f10fbc..76b3d8d0 100644
--- a/server/szurubooru/rest/app.py
+++ b/server/szurubooru/rest/app.py
@@ -74,7 +74,8 @@ def application(
) -> Tuple[bytes]:
try:
ctx = _create_context(env)
- if "application/json" not in ctx.get_header("Accept"):
+ accept_header = ctx.get_header("Accept")
+ if accept_header != "*/*" and "application/json" not in accept_header:
raise errors.HttpNotAcceptable(
"ValidationError", "This API only supports JSON responses."
)