From e73aa9e981dd2857459ea0b2495237cf36e064e1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 10 Jul 2026 10:41:49 +0200 Subject: [PATCH 1/3] :sparkles: Add PENPOT_INTERNAL_URI to exporter for separate internal and public URI handling (#10630) Add PENPOT_INTERNAL_URI environment variable to the exporter. This allows separating the URI used for internal communication (headless browser to frontend) from the public URI used for resource references in exported SVGs. Previously, PENPOT_PUBLIC_URI served both purposes, which caused exported SVGs to contain broken font URLs when the internal Docker address was used. Changes: - Add :internal-uri to exporter config schema with fallback to :public-uri - Add get-internal-uri helper function - Use internal-uri for browser navigation in SVG/PDF/bitmap renderers - Post-process SVG output to replace internal URI with public URI - Use internal-uri for backend API calls in resource handler - Log both URIs on startup - Update docker-compose.yaml to use both variables - Document the new variable in configuration.md Closes #10627 AI-assisted-by: mimo-v2.5-pro --- docker/images/docker-compose.yaml | 4 ++-- docs/technical-guide/configuration.md | 15 +++++++++++++++ exporter/src/app/config.cljs | 8 ++++++++ exporter/src/app/core.cljs | 1 + exporter/src/app/handlers/resources.cljs | 2 +- exporter/src/app/renderer/bitmap.cljs | 2 +- exporter/src/app/renderer/pdf.cljs | 2 +- exporter/src/app/renderer/svg.cljs | 18 ++++++++++++++++-- 8 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 94193a977c..59b326c76d 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -197,10 +197,10 @@ services: - penpot environment: - << : [*penpot-secret-key] + << : [*penpot-secret-key, *penpot-public-uri] # Don't touch it; this uses an internal docker network to # communicate with the frontend. - PENPOT_PUBLIC_URI: http://penpot-frontend:8080 + PENPOT_INTERNAL_URI: http://penpot-frontend:8080 ## Valkey (or previously Redis) is used for the websockets notifications. PENPOT_REDIS_URI: redis://penpot-valkey/0 diff --git a/docs/technical-guide/configuration.md b/docs/technical-guide/configuration.md index eec62f63e4..49d5a23e8d 100644 --- a/docs/technical-guide/configuration.md +++ b/docs/technical-guide/configuration.md @@ -651,6 +651,21 @@ PENPOT_EXPORTER_URI: http://your-penpot-exporter:6061 These variables are used for generate correct nginx.conf file on container startup. +### Exporter + +The exporter uses this variable: + +```bash +# Exporter +PENPOT_INTERNAL_URI: http://penpot-frontend:8080 +``` + +- `PENPOT_INTERNAL_URI`: The URI used by the exporter's headless browser to + communicate with the frontend (internal Docker network). Defaults to + `PENPOT_PUBLIC_URI` if not set. The default value + `http://penpot-frontend:8080` used in the docker-compose is a good default and + it is recommended to keep it unchanged. + ## Other flags There are other flags that are useful for a more customized Penpot experience. This section has the list of the flags meant diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index ff32391f82..8010032680 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -22,6 +22,7 @@ (def ^:private defaults {:public-uri "http://localhost:3449" + :internal-uri nil :tenant "default" :host "localhost" :http-server-port 6061 @@ -33,6 +34,7 @@ [:map {:title "config"} [:secret-key :string] [:public-uri {:optional true} ::sm/uri] + [:internal-uri {:optional true} ::sm/uri] [:exporter-shared-key {:optional true} :string] [:host {:optional true} :string] [:tenant {:optional true} :string] @@ -100,6 +102,12 @@ ([key default] (c/get config key default))) +(defn get-internal-uri + "Returns internal-uri if set, otherwise falls back to public-uri." + [] + (or (c/get config :internal-uri) + (c/get config :public-uri))) + (def management-key (let [key (or (c/get config :exporter-shared-key) (let [secret-key (c/get config :secret-key) diff --git a/exporter/src/app/core.cljs b/exporter/src/app/core.cljs index 91d4ee702e..5ce68ebf98 100644 --- a/exporter/src/app/core.cljs +++ b/exporter/src/app/core.cljs @@ -21,6 +21,7 @@ [& _] (l/info :msg "initializing" :public-uri (str (cf/get :public-uri)) + :internal-uri (str (cf/get-internal-uri)) :version (:full cf/version)) (p/do! (bwr/init) diff --git a/exporter/src/app/handlers/resources.cljs b/exporter/src/app/handlers/resources.cljs index 9b2f37d6cc..4c4bb7225c 100644 --- a/exporter/src/app/handlers/resources.cljs +++ b/exporter/src/app/handlers/resources.cljs @@ -79,7 +79,7 @@ :method "POST" :body fdata :dispatcher agent} - uri (-> (cf/get :public-uri) + uri (-> (cf/get-internal-uri) (u/ensure-path-slash) (u/join "api/management/methods/upload-tempfile") (str))] diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index 3579a4839e..c2720eb025 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -62,7 +62,7 @@ :skip-children skip-children :wasm (when is-wasm "true") :scale scale} - uri (-> (cf/get :public-uri) + uri (-> (cf/get-internal-uri) (u/ensure-path-slash) (u/join "render.html") (assoc :query (u/map->query-string params)))] diff --git a/exporter/src/app/renderer/pdf.cljs b/exporter/src/app/renderer/pdf.cljs index b953730be4..ba4118c1e8 100644 --- a/exporter/src/app/renderer/pdf.cljs +++ b/exporter/src/app/renderer/pdf.cljs @@ -77,7 +77,7 @@ (on-object (assoc object :path path)) (p/recur (rest objects))))))] - (let [base-uri (-> (cf/get :public-uri) + (let [base-uri (-> (cf/get-internal-uri) (u/ensure-path-slash))] (bw/exec! (prepare-options base-uri) (partial render base-uri))))) diff --git a/exporter/src/app/renderer/svg.cljs b/exporter/src/app/renderer/svg.cljs index 7a47e46fe0..c9fee2f764 100644 --- a/exporter/src/app/renderer/svg.cljs +++ b/exporter/src/app/renderer/svg.cljs @@ -108,6 +108,18 @@ {:width width :height height})) +(defn- replace-internal-uris + "Replaces internal-uri references with public-uri in SVG output. + This ensures that font URLs and other resource references in the + exported SVG use the public-facing URI accessible to end users." + [svg-content] + (let [internal-uri (str (cf/get-internal-uri)) + public-uri (str (cf/get :public-uri))] + (if (and (not= internal-uri public-uri) + (str/includes? svg-content internal-uri)) + (str/replace svg-content internal-uri public-uri) + svg-content))) + (defn render [{:keys [page-id file-id share-id objects token scale type]} on-object] (letfn [(convert-to-ppm [pngpath] @@ -320,7 +332,9 @@ result (if (contains? cf/flags :exporter-svgo) (svgo/optimize result svgo/defaultOptions) - result)] + result) + + result (replace-internal-uris result)] ;; (println "------- ORIGIN:") ;; (cljs.pprint/pprint (xml->clj xmldata)) @@ -349,7 +363,7 @@ :render-embed true :object-id (mapv :id objects) :route "objects"} - uri (-> (cf/get :public-uri) + uri (-> (cf/get-internal-uri) (u/ensure-path-slash) (u/join "render.html") (assoc :query (u/map->query-string params)))] From 3708cf31d40581fb14c9b928a21b8b8f8c7be0ae Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 10 Jul 2026 11:05:09 +0200 Subject: [PATCH 2/3] :bug: Fix stroke cap selects (#10631) --- .../src/app/main/ui/components/select.scss | 2 +- .../sidebar/options/rows/stroke_row.cljs | 50 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/main/ui/components/select.scss b/frontend/src/app/main/ui/components/select.scss index 6baab75ac1..438e0873fa 100644 --- a/frontend/src/app/main/ui/components/select.scss +++ b/frontend/src/app/main/ui/components/select.scss @@ -150,7 +150,7 @@ .separator { margin: 0; block-size: $sz-12; - border-block-start: $b-1 solid var(--color-background-primary); + border-block-start: $b-1 solid var(--color-background-quaternary); } &[data-direction="up"] { diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index b39d62c1cb..8e21f18232 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -171,12 +171,20 @@ on-caps-start-change (mf/use-fn (mf/deps index on-stroke-cap-start-change) - #(on-stroke-cap-start-change index (keyword %))) + (fn [cap] + (let [cap (if (= cap "none") + nil + (keyword cap))] + (on-stroke-cap-start-change index cap)))) on-caps-end-change (mf/use-fn (mf/deps index on-stroke-cap-end-change) - #(on-stroke-cap-end-change index (keyword %))) + (fn [cap] + (let [cap (if (= cap "none") + nil + (keyword cap))] + (on-stroke-cap-end-change index cap)))) on-detach-token-color (mf/use-fn @@ -191,16 +199,16 @@ (on-detach-token token #{:stroke-width}))) stroke-caps-options - [{:value nil :label (tr "workspace.options.stroke-cap.none")} - :separator - {:value :line-arrow :label (tr "workspace.options.stroke-cap.line-arrow-short") :icon :stroke-arrow} - {:value :triangle-arrow :label (tr "workspace.options.stroke-cap.triangle-arrow-short") :icon :stroke-triangle} - {:value :square-marker :label (tr "workspace.options.stroke-cap.square-marker-short") :icon :stroke-rectangle} - {:value :circle-marker :label (tr "workspace.options.stroke-cap.circle-marker-short") :icon :stroke-circle} - {:value :diamond-marker :label (tr "workspace.options.stroke-cap.diamond-marker-short") :icon :stroke-diamond} - :separator - {:value :round :label (tr "workspace.options.stroke-cap.round") :icon :stroke-rounded} - {:value :square :label (tr "workspace.options.stroke-cap.square") :icon :stroke-squared}] + [{:id "none" :value "none" :label (tr "workspace.options.stroke-cap.none")} + {:label "" :type :separator :id "separator"} + {:id "line-arrow" :value :line-arrow :label (tr "workspace.options.stroke-cap.line-arrow-short") :icon i/stroke-arrow} + {:id "triangle-arrow" :value :triangle-arrow :label (tr "workspace.options.stroke-cap.triangle-arrow-short") :icon i/stroke-triangle} + {:id "square-marker" :value :square-marker :label (tr "workspace.options.stroke-cap.square-marker-short") :icon i/stroke-rectangle} + {:id "circle-marker" :value :circle-marker :label (tr "workspace.options.stroke-cap.circle-marker-short") :icon i/stroke-circle} + {:id "diamond-marker" :value :diamond-marker :label (tr "workspace.options.stroke-cap.diamond-marker-short") :icon i/stroke-diamond} + {:label "" :type :separator :id "separator"} + {:id "round" :value :round :label (tr "workspace.options.stroke-cap.round") :icon i/stroke-rounded} + {:id "square" :value :square :label (tr "workspace.options.stroke-cap.square") :icon i/stroke-squared}] on-cap-switch (mf/use-fn @@ -340,16 +348,18 @@ ;; Stroke Caps (when show-caps [:div {:class (stl/css :stroke-caps-options)} - [:& select {:default-value (:stroke-cap-start stroke) - :options stroke-caps-options - :disabled hidden? - :on-change on-caps-start-change}] + [:> select* {:default-selected (or (d/name (:stroke-cap-start stroke)) "none") + :options stroke-caps-options + :data-testid "stroke.cap-start" + :disabled hidden? + :on-change on-caps-start-change}] [:> icon-button* {:variant "secondary" :aria-label (tr "labels.switch") :disabled hidden? :on-click on-cap-switch :icon i/switch}] - [:& select {:default-value (:stroke-cap-end stroke) - :options stroke-caps-options - :disabled hidden? - :on-change on-caps-end-change}]])])) + [:> select* {:default-selected (or (d/name (:stroke-cap-end stroke)) "none") + :options stroke-caps-options + :data-testid "stroke.cap-end" + :disabled hidden? + :on-change on-caps-end-change}]])])) From 1f4b85209ebe6e1831fa08f2df581e9049689e61 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 10 Jul 2026 11:22:44 +0200 Subject: [PATCH 3/3] :books: Simplify the ia asistance note on creating-prs serena workflow --- .serena/memories/workflow/creating-prs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.serena/memories/workflow/creating-prs.md b/.serena/memories/workflow/creating-prs.md index 204fd49c76..c0f763f9e3 100644 --- a/.serena/memories/workflow/creating-prs.md +++ b/.serena/memories/workflow/creating-prs.md @@ -38,7 +38,7 @@ Include concise sections covering: PR descriptions follow this structure: ```markdown -**Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative. +**Note:** This PR was created with AI assistance. ## What