diff --git a/client/Dockerfile b/client/Dockerfile
index ea5151fa..a5a37c59 100644
--- a/client/Dockerfile
+++ b/client/Dockerfile
@@ -17,12 +17,13 @@ COPY docker-start.sh /
WORKDIR /etc/nginx
COPY nginx.conf.docker ./nginx.conf
+COPY metatags.lua .
WORKDIR /var/www
COPY --from=builder /opt/app/public/ .
-FROM nginx:alpine as release
+FROM openresty/openresty:alpine as release
RUN apk --no-cache add dumb-init
COPY --from=approot / /
diff --git a/client/docker-start.sh b/client/docker-start.sh
index 0b2bec8a..bca05fe2 100755
--- a/client/docker-start.sh
+++ b/client/docker-start.sh
@@ -8,4 +8,5 @@ sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
/var/www/manifest.json
# Start server
-exec nginx
+echo "$0 starting server..."
+exec openresty -c /etc/nginx/nginx.conf
diff --git a/client/html/index.htm b/client/html/index.htm
index 2f0f4e40..98bf63d2 100644
--- a/client/html/index.htm
+++ b/client/html/index.htm
@@ -22,6 +22,7 @@
+ {{ generated_head_tags }}
diff --git a/client/metatags.lua b/client/metatags.lua
new file mode 100644
index 00000000..bebd51e1
--- /dev/null
+++ b/client/metatags.lua
@@ -0,0 +1,135 @@
+-- Set the response content type back to HTML
+ngx.header.content_type = 'text/html';
+
+ngx.req.read_body()
+
+local page_html = ngx.location.capture("/index.htm").body
+
+local before_content, placeholder, after_content
+before_content, placeholder, after_content = page_html:match(
+ string.format("^(.*)(%s)(.*)$", html_head_tag_replacement_str)
+)
+
+-- check for placeholder to replace
+if not placeholder then
+ ngx.log(ngx.STDERR, "WARNING: Meta tag placeholder was not found in page html.")
+ ngx.say(page_html)
+ return
+else
+ -- start the response
+ ngx.print(before_content)
+ -- send partial content to the client to allow preloading the app
+ ngx.flush()
+end
+
+ngx.req.set_header("Accept", "application/json")
+local server_info_resp = ngx.location.capture("/_internal_api/info")
+if server_info_resp.status ~= 200 then
+ ngx.log(ngx.STDERR, "Failed to acquire server info from szurubooru API, unable to generate meta tags: HTTP status "..server_info_resp.status)
+ ngx.print(after_content)
+ return
+end
+local server_info = cjson.decode(server_info_resp.body)
+
+local additional_tags = ""
+local function add_meta_tag (property, content)
+ -- NOTE do not allow user-provided data in the property name, only the content has quotes escaped
+ additional_tags = additional_tags .. ""
+end
+
+-- Add the site name tag
+add_meta_tag("og:site_name", server_info.config.name)
+add_meta_tag("og:url", ngx.var.external_host_url .. ngx.var.request_uri_path)
+
+local function generate_meta_tags ()
+ if ngx.var.request_uri_path:match('^/$') then -- Site root
+ add_meta_tag("og:type", "website")
+ add_meta_tag("og:title", server_info.config.name)
+ add_meta_tag("twitter:title", server_info.config.name)
+ -- if there's a featured post, let's use that as the image
+ if server_info.featuredPost then
+ local post_info = server_info.featuredPost
+ -- NOTE this is different from the normal Post tags,
+ -- notably we avoid setting the article type, author, time, etc
+ local og_media_prefix
+ if post_info.type == "image" then
+ og_media_prefix = "og:image"
+ add_meta_tag("twitter:card", "summary_large_image")
+ add_meta_tag("twitter:image", ngx.var.external_host_url .. '/' .. post_info.contentUrl)
+ elseif post_info.type == "video" then
+ og_media_prefix = "og:video"
+ -- some sites don't preview video, so at least provide a thumbnail
+ add_meta_tag("og:image", ngx.var.external_host_url .. '/' .. post_info.thumbnailUrl)
+ end
+ add_meta_tag(og_media_prefix..":url", ngx.var.external_host_url .. '/' .. post_info.contentUrl)
+ add_meta_tag(og_media_prefix..":width", post_info.canvasWidth)
+ add_meta_tag(og_media_prefix..":height", post_info.canvasHeight)
+ end
+ elseif ngx.var.request_uri_path:match('^/post/([0-9]+)') then -- Post metadata
+ -- check if posts are accessible to anonymous users:
+ if server_info.config.privileges["posts:view"] == "anonymous" then
+ local match, err = ngx.re.match(ngx.var.request_uri_path, "^/post/(?[0-9]+)")
+ local post_id = match["post_id"]
+ add_meta_tag("og:type", "article")
+ local post_info = cjson.decode((ngx.location.capture("/_internal_api/post/"..post_id)).body)
+ add_meta_tag("og:title", server_info.config.name .. " - Post " .. post_info.id)
+ add_meta_tag("twitter:title", server_info.config.name .. " - Post " .. post_info.id)
+ add_meta_tag("article:published_time", post_info.creationTime)
+ local og_media_prefix
+ if post_info.type == "video" then
+ og_media_prefix = "og:video"
+ add_meta_tag("twitter:card", "player")
+ add_meta_tag("twitter:player:width", post_info.canvasWidth)
+ add_meta_tag("twitter:player:height", post_info.canvasHeight)
+ -- some sites don't preview video, so at least provide a thumbnail
+ add_meta_tag("og:image", ngx.var.external_host_url .. '/' .. post_info.thumbnailUrl)
+ else
+ og_media_prefix = "og:image"
+ add_meta_tag("twitter:card", "summary_large_image")
+ add_meta_tag("twitter:image", ngx.var.external_host_url .. '/' .. post_info.contentUrl)
+ end
+ add_meta_tag(og_media_prefix..":url", ngx.var.external_host_url .. '/' .. post_info.contentUrl)
+ add_meta_tag(og_media_prefix..":width", post_info.canvasWidth)
+ add_meta_tag(og_media_prefix..":height", post_info.canvasHeight)
+ -- user is not present for anonymous uploads:
+ if post_info.user then
+ add_meta_tag("article:author", post_info.user.name)
+ end
+ else
+ -- no permission to retrieve post data
+ add_meta_tag("og:title", server_info.config.name .. " - Login required")
+ end
+ elseif ngx.var.request_uri_path:match('^/user/([^/]+)/?$') then -- User metadata
+ local username = ngx.var.request_uri_path:match('^/user/([^/]+)/?$')
+ -- check for permission to access user profiles
+ if server_info.config.privileges["users:view"] == "anonymous" then
+ local user_info_request = ngx.location.capture("/_internal_api/user/"..username)
+ add_meta_tag("og:type", "profile")
+ if user_info_request.status == 200 then
+ add_meta_tag("og:title", server_info.config.name .. " - " .. username)
+ local user_info = cjson.decode(user_info_request.body)
+ add_meta_tag("profile:username", user_info.name)
+ local avatar_url = user_info.avatarUrl
+ if avatar_url:match("^https?://") then
+ add_meta_tag("og:image", avatar_url)
+ else
+ add_meta_tag("og:image", ngx.var.external_host_url .. '/' .. avatar_url)
+ end
+ else
+ -- could not retrieve the user profile, show some defaults
+ add_meta_tag("og:title", server_info.config.name .. " - User not found")
+ end
+ else
+ -- no permission to view user data
+ end
+ end
+end
+
+local status, err = pcall(generate_meta_tags)
+if not status then
+ ngx.log(ngx.STDERR, "Failed to generate meta tags: "..tostring(err))
+end
+
+-- Once tags have been generated, write them and then finish the response
+ngx.print(additional_tags)
+ngx.print(after_content)
diff --git a/client/nginx.conf.docker b/client/nginx.conf.docker
index 98c18b35..b4c63e7e 100644
--- a/client/nginx.conf.docker
+++ b/client/nginx.conf.docker
@@ -1,5 +1,7 @@
worker_processes 1;
-user nginx;
+user nobody;
+
+pcre_jit on;
error_log /dev/stderr warn;
pid /var/run/nginx.pid;
@@ -9,7 +11,7 @@ events {
}
http {
- include /etc/nginx/mime.types;
+ include /usr/local/openresty/nginx/conf/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr -> $request [$status] - '
@@ -23,6 +25,15 @@ http {
server __BACKEND__:6666;
}
+ map $request_uri $request_uri_path {
+ "~^(?P[^?]*)(\?.*)?$" $path;
+ }
+
+ init_by_lua_block {
+ cjson = require("cjson")
+ html_head_tag_replacement_str = "{{ generated_head_tags }}"
+ }
+
server {
listen 80 default_server;
@@ -50,6 +61,9 @@ http {
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/json;
+ proxy_connect_timeout 180s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 600s;
if ($request_uri ~* "/api/(.*)") {
proxy_pass http://backend/$1;
}
@@ -71,7 +85,7 @@ http {
location / {
root /var/www;
- try_files $uri /index.htm;
+ try_files $uri /_meta_tags_html;
sendfile on;
tcp_nopush on;
@@ -81,6 +95,30 @@ http {
gzip_proxied expired no-cache no-store private auth;
}
+ location ~ ^/_internal_api/(.*)$ {
+ internal;
+ tcp_nodelay on;
+
+ add_header 'Access-Control-Allow-Origin' '*';
+
+ gzip off;
+ proxy_connect_timeout 10s;
+ proxy_send_timeout 10s;
+ proxy_read_timeout 10s;
+ proxy_pass http://backend/$1;
+ }
+
+ location /_meta_tags_html {
+ internal;
+ set $original_scheme $scheme;
+ if ( $http_x_forwarded_proto = 'https' ) {
+ set $original_scheme "https";
+ }
+ root /var/www;
+ set $external_host_url "${original_scheme}://${http_host}";
+ content_by_lua_file /etc/nginx/metatags.lua;
+ }
+
location @unauthorized {
return 403 "Unauthorized";
default_type text/plain;