penpot/backend/src/app/http/link_preview.clj
Andrey Antukh 2d5cb9e315 🚧 Port subpath-safe public URI helpers to link-preview
Port join-uri and get-public-uri from the unified jobs substrate WIP and use them for the link-preview image URLs, replacing the naive string concatenation that dropped configured subpaths. Includes the ported config tests.

AI-assisted-by: muse-spark-1.3-contributor
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-09-14 09:53:56 +02:00

83 lines
3.2 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.http.link-preview
"Link preview (Open Graph metadata) related handlers.
Serves a minimal HTML page with Open Graph metadata used by link
preview crawlers (Slack, Discord, Twitter, ...). The reverse proxy
routes crawler requests for the application root to this endpoint,
preserving the query string params that the frontend mirrors on
navigation (`file-id`, `project-id` and `team-id`)."
(:require
[app.common.data :as d]
[app.config :as cf]
[app.db :as db]
[app.util.template :as tmpl]
[clojure.java.io :as io]
[integrant.core :as ig]
[yetti.response :as-alias yres]))
(def ^:private default-context
{:title "Penpot | Full-stack design"
:description "Penpot is the open-source design platform for teams that build digital products at scale."})
(def ^:private sql:get-file
"SELECT f.name, ft.media_id
FROM file AS f
LEFT JOIN file_thumbnail AS ft
ON (ft.file_id = f.id AND ft.deleted_at IS NULL)
WHERE f.id = ?
AND f.deleted_at IS NULL
ORDER BY ft.revn DESC NULLS LAST
LIMIT 1")
(defn- get-file-context
"Return the link preview context for a file link: the file name as title
and, when available, the last dashboard thumbnail as image."
[pool file-id]
(when-let [{:keys [name media-id]} (db/exec-one! pool [sql:get-file file-id])]
(cond-> (assoc default-context :title (str name " | Penpot"))
(some? media-id)
(assoc :image (cf/get-public-uri (str "assets/by-id/" media-id))))))
(defn- get-context
[pool params]
(let [project-id (some-> (:project-id params) d/parse-uuid)
team-id (some-> (:team-id params) d/parse-uuid)]
;; A present file-id is decisive: file links never fall through to the
;; project/team card, even when the value is malformed or unknown (both
;; yield nil and the handler falls back to the default context).
(if (contains? params :file-id)
(when-some [file-id (d/parse-uuid (:file-id params))]
(get-file-context pool file-id))
(cond
(some? project-id) (assoc default-context :title "Project | Penpot")
(some? team-id) (assoc default-context :title "Team dashboard | Penpot")))))
(defn- handler
[{:keys [::db/pool]} request]
(let [context (when (contains? cf/flags :link-preview)
(get-context pool (:query-params request)))
context (-> (or context default-context)
(update :image #(or % (cf/get-public-uri "images/penpot-link-preview.png"))))]
{::yres/status 200
::yres/headers {"content-type" "text/html; charset=utf-8"
"cache-control" "no-store, no-cache, max-age=0"}
::yres/body (-> (io/resource "app/templates/link-preview.tmpl")
(tmpl/render context))}))
;; --- Initialization
(defmethod ig/assert-key ::routes
[_ params]
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
(defmethod ig/init-key ::routes
[_ cfg]
["/link-preview" {:handler (partial handler cfg)
:allowed-methods #{:get :head}}])