From 6150c764873b22eee02a19e19da85a73d463401e Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 16 Oct 2025 18:34:45 +0300 Subject: [PATCH 01/12] fix: default to auto on empty PUB_VERTD_URL --- src/lib/sections/settings/vertdSettings.svelte.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/sections/settings/vertdSettings.svelte.ts b/src/lib/sections/settings/vertdSettings.svelte.ts index ccdbf4dc..d9cb5e49 100644 --- a/src/lib/sections/settings/vertdSettings.svelte.ts +++ b/src/lib/sections/settings/vertdSettings.svelte.ts @@ -55,7 +55,8 @@ export class VertdInstance { // if custom vertd url and no saved setting, default to the custom url if (!ls) { - const isCustomUrl = PUB_VERTD_URL !== "https://vertd.vert.sh"; + const isCustomUrl = + PUB_VERTD_URL && PUB_VERTD_URL !== "https://vertd.vert.sh"; if (isCustomUrl) { this.inner = { type: "custom" }; return; From 1032fc91fd13396457843149bbd175edcd8295f7 Mon Sep 17 00:00:00 2001 From: Maya Date: Thu, 16 Oct 2025 22:13:32 +0300 Subject: [PATCH 02/12] feat: privacy policy --- messages/en.json | 6 +- src/lib/components/layout/Footer.svelte | 1 + src/lib/components/layout/Gradients.svelte | 5 + src/routes/privacy/+page.svelte | 112 +++++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/routes/privacy/+page.svelte diff --git a/messages/en.json b/messages/en.json index 59f725d6..08263c27 100644 --- a/messages/en.json +++ b/messages/en.json @@ -10,7 +10,8 @@ "footer": { "copyright": "© {year} VERT.", "source_code": "Source code", - "discord_server": "Discord server" + "discord_server": "Discord server", + "privacy_policy": "Privacy policy" }, "upload": { "title": "The file converter you'll love.", @@ -248,5 +249,8 @@ "button": "JPEGIFY {compression}%!!!", "download": "Download", "delete": "Delete" + }, + "privacy": { + "title": "Privacy Policy" } } diff --git a/src/lib/components/layout/Footer.svelte b/src/lib/components/layout/Footer.svelte index cb0226a0..0e4867fb 100644 --- a/src/lib/components/layout/Footer.svelte +++ b/src/lib/components/layout/Footer.svelte @@ -5,6 +5,7 @@ const items = $derived([ [m["footer.source_code"](), GITHUB_URL_VERT], [m["footer.discord_server"](), DISCORD_URL], + [m["footer.privacy_policy"](), "/privacy"], ]); const year = new Date().getFullYear(); diff --git a/src/lib/components/layout/Gradients.svelte b/src/lib/components/layout/Gradients.svelte index 7a7d85b5..27df8e79 100644 --- a/src/lib/components/layout/Gradients.svelte +++ b/src/lib/components/layout/Gradients.svelte @@ -41,6 +41,11 @@ color: "var(--bg-gradient-red-from)", at: 100, }, + { + matcher: (path) => path === "/privacy/", + color: "var(--bg-gradient-red-from)", + at: 100, + }, ]); const color = $derived( diff --git a/src/routes/privacy/+page.svelte b/src/routes/privacy/+page.svelte new file mode 100644 index 00000000..8350d1e1 --- /dev/null +++ b/src/routes/privacy/+page.svelte @@ -0,0 +1,112 @@ + + +
+

+ + {m["privacy.title"]()} +

+ +
+
+

Summary

+

+ VERT's privacy policy is very simple: we do not collect or store + any data on you at all. We don't use cookies or trackers, + analytics are completely private, and all conversions (except + videos) happen locally on your browser. Videos are deleted after + being downloaded, or an hour, unless explicitly given permission + by you to be stored; it will only be used for the purpose of + troubleshooting. +
+
+ Note this may only apply to the official VERT instance at + vert.sh; + third-party instances may handle your data differently. +

+ +

Conversions

+

+ Most conversions (images, documents, audio) happen entirely + locally on your device using WebAssembly versions of the + relevant tools (e.g. ImageMagick, Pandoc, FFmpeg). This means + your files never leave your device and we will never have access + to them. +
+
+ Video conversions are performed on our servers because they require + more processing power and cannot be done very quickly on the browser + yet. Videos you convert with VERT are deleted after being downloaded, + or after one hour, unless you explicitly give permission for us to + store them longer purely for troubleshooting purposes. +

+ +

Conversion Errors

+
+ When a video conversion fails, we may collect some anonymous + data to help us diagnose the issue. This data may include: +
    +
  • The job ID, which is the anonymized file name
  • +
  • The format you converted from
  • +
  • The format you converted to
  • +
  • + The FFmpeg stderr output of your job (error message) +
  • +
  • + The actual video file (if given explicit permission) +
  • +
+ This information is used solely for the purpose of diagnosing conversion + issues. The actual video file will only ever be collected if you + give us permission to do so, where it will only be used for troubleshooting. +
+ +

Analytics

+

+ We self-host a Plausible instance for completely anonymous and + aggregated analytics. Plausible does not use cookies and + complies with all major privacy regulations (GDPR/CCPA/PECR). + You can opt out of analytics in the "Privacy & data" section in settings + and read more about Plausible's privacy practices + here. +

+ +

Local Storage

+

+ We use your browser's local storage to save your settings, and + your browser's session storage to temporarily store the GitHub + contributors list for the "About" section to reduce repeated + GitHub API requests. No personal data is stored or transmitted. +
+
+ The WebAssembly versions of the conversion tools we use (FFmpeg, + ImageMagick, Pandoc) are also stored locally on your browser when + you first visit the website, so you don’t need to redownload them + each visit. No personal data is stored or transmitted. You may view + or delete this data at any time in the "Privacy & data" section in + settings. +

+ +

Contact

+

+ For questions, email us at: hello@vert.sh. If you are using a third-party instance of VERT, please + contact the hoster of that instance instead. +

+ +

Last updated: 2025-10-16

+
+
+
From 91a061a2ef5a34cfaa78054b8a93cda3b3a90337 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 17 Oct 2025 23:01:12 +0300 Subject: [PATCH 03/12] chore: vert self-hosting --- src/routes/privacy/+page.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/privacy/+page.svelte b/src/routes/privacy/+page.svelte index 8350d1e1..0a601590 100644 --- a/src/routes/privacy/+page.svelte +++ b/src/routes/privacy/+page.svelte @@ -21,7 +21,9 @@ videos) happen locally on your browser. Videos are deleted after being downloaded, or an hour, unless explicitly given permission by you to be stored; it will only be used for the purpose of - troubleshooting. + troubleshooting. VERT self-hosts a Coolify instance for hosting + the website and vertd (for video conversion), and a Plausible + instance for completely anonymous and aggregated analytics.

Note this may only apply to the official VERT instance at @@ -103,7 +105,7 @@ href="mailto:hello@vert.sh" class="underline">hello@vert.sh. If you are using a third-party instance of VERT, please - contact the hoster of that instance instead. + contact the hoster of that instance instead.

Last updated: 2025-10-16

From e5ff309d83268a499356b4b51929b2c9b250c2d7 Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 18 Oct 2025 20:57:52 +0300 Subject: [PATCH 04/12] feat: view info submitted for transparency, see the exact details that is sent automatically to the owner of the instance: - job id - convert from - convert to - ffmpeg stderr - actual video file (if submitted) --- messages/en.json | 7 +++ src/lib/components/functional/Dialog.svelte | 25 ++++---- .../components/functional/VertdError.svelte | 54 +++++++++++++--- .../functional/VertdErrorDetails.svelte | 52 +++++++++++++++ src/lib/components/layout/Dialogs.svelte | 4 +- src/lib/converters/vertd.svelte.ts | 3 + src/lib/store/DialogProvider.ts | 63 ++++++++++++++----- 7 files changed, 170 insertions(+), 38 deletions(-) create mode 100644 src/lib/components/functional/VertdErrorDetails.svelte diff --git a/messages/en.json b/messages/en.json index 08263c27..d873daa3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -85,6 +85,13 @@ "vertd_generic_yes": "Submit video", "vertd_generic_no": "Don't submit", "vertd_failed_to_keep": "Failed to keep the video on the server: {error}", + "vertd_details": "View error details", + "vertd_details_body": "If you submit your file, your video will also be attached alongside the error log being sent to us for review. The following information is the log that we automatically receive:", + "vertd_details_job_id": "Job ID: {jobId}", + "vertd_details_from": "From format: {from}", + "vertd_details_to": "To format: {to}", + "vertd_details_error_message": "Error message: [view_link]View error logs[/view_link]", + "vertd_details_close": "Close", "unsupported_format": "Only image, video, audio, and document files are supported", "vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?", "worker_downloading": "The {type} converter is currently being initialized, please wait a few moments.", diff --git a/src/lib/components/functional/Dialog.svelte b/src/lib/components/functional/Dialog.svelte index a0931088..1bd5f970 100644 --- a/src/lib/components/functional/Dialog.svelte +++ b/src/lib/components/functional/Dialog.svelte @@ -3,19 +3,13 @@ import { removeDialog } from "$lib/store/DialogProvider"; import { BanIcon, CheckIcon, InfoIcon, TriangleAlert } from "lucide-svelte"; import { quintOut } from "svelte/easing"; + import type { Dialog as DialogType } from "$lib/store/DialogProvider"; - type Props = { - id: number; - title: string; - message: string; - buttons: { - text: string; - action: () => void; - }[]; - type: "success" | "error" | "info" | "warning"; - }; + type Props = DialogType; - let { id, title, message, buttons, type }: Props = $props(); + let props: Props = $props(); + const { id, title, message, buttons, type } = props; + const additional = "additional" in props ? props.additional : undefined; const colors = { success: "purple", @@ -59,7 +53,14 @@
-

{message}

+ {#if typeof message === "string"} +

{message}

+ {:else} + {@const MessageComponent = message} +
+ +
+ {/if}
{#each buttons as { text, action }, i} diff --git a/src/lib/components/functional/VertdError.svelte b/src/lib/components/functional/VertdError.svelte index 827e09d9..091bcaae 100644 --- a/src/lib/components/functional/VertdError.svelte +++ b/src/lib/components/functional/VertdError.svelte @@ -2,6 +2,10 @@ export interface VertdErrorProps { jobId: string; auth: string; + from?: string; + to?: string; + errorMessage?: string; + fileName?: string; } @@ -10,6 +14,8 @@ import { m } from "$lib/paraglide/messages"; import { ToastManager, type ToastProps } from "$lib/toast/index.svelte"; + import { addDialog } from "$lib/store/DialogProvider"; + import VertdErrorDetails from "./VertdErrorDetails.svelte"; const toast: ToastProps = $props(); @@ -52,22 +58,50 @@ ToastManager.remove(toast.id); }; + + const showDetails = () => { + addDialog( + m["convert.errors.vertd_details"](), + VertdErrorDetails as any, + [ + { + text: "Close", + action: () => {}, + }, + ], + "info", + { + jobId: toast.additional.jobId || "Unknown", + from: toast.additional.from || "Unknown", + to: toast.additional.to || "Unknown", + errorMessage: toast.additional.errorMessage || "Unknown error", + }, + ); + };

{m["convert.errors.vertd_generic_body"]()}

-
- +
View Details Submitted +
+ + +
diff --git a/src/lib/components/functional/VertdErrorDetails.svelte b/src/lib/components/functional/VertdErrorDetails.svelte new file mode 100644 index 00000000..140b09d6 --- /dev/null +++ b/src/lib/components/functional/VertdErrorDetails.svelte @@ -0,0 +1,52 @@ + + +
+

{@html m["convert.errors.vertd_details_body"]()}

+

+ + {@html m["convert.errors.vertd_details_job_id"]({ + jobId: additional.jobId, + })} + +

+

+ + {@html m["convert.errors.vertd_details_from"]({ from: additional.from })} + +

+

+ + {@html m["convert.errors.vertd_details_to"]({ to: additional.to })} + +

+

+ + {@html link( + ["view_link"], + m["convert.errors.vertd_details_error_message"](), + [ + URL.createObjectURL( + new Blob([additional.errorMessage], { type: "text/plain" }) + ) + ], + [true], + ["text-blue-500 font-normal hover:underline"] + )} + +

+
diff --git a/src/lib/components/layout/Dialogs.svelte b/src/lib/components/layout/Dialogs.svelte index d8c4fa20..7914117e 100644 --- a/src/lib/components/layout/Dialogs.svelte +++ b/src/lib/components/layout/Dialogs.svelte @@ -26,9 +26,9 @@ easing: quintOut, }} > - {#each dialogList as { id, title, message, buttons, type }, i} + {#each dialogList as dialog, i} {#if i === 0} - + {/if} {/each}
diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 52dd596b..2db5c724 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -377,6 +377,9 @@ export class VertdConverter extends Converter { additional: { jobId: uploadRes.id, auth: uploadRes.auth, + from: input.from, + to: to, + errorMessage: msg.data.message, }, }); } diff --git a/src/lib/store/DialogProvider.ts b/src/lib/store/DialogProvider.ts index fa12840a..c76ccf86 100644 --- a/src/lib/store/DialogProvider.ts +++ b/src/lib/store/DialogProvider.ts @@ -1,17 +1,39 @@ +import type { Component } from "svelte"; import { writable } from "svelte/store"; type DialogType = "success" | "error" | "info" | "warning"; -export interface Dialog { +type BaseDialog = { id: number; title: string; - message: string; buttons: { text: string; action: () => void; }[]; type: DialogType; -} +}; + +export type StringDialog = BaseDialog & { + message: string; +}; + +export type ComponentDialog = BaseDialog & { + message: Component>; + additional: T; +}; + +export type Dialog = StringDialog | ComponentDialog; + +export type DialogProps = { + id: number; + title: string; + type: DialogType; + buttons: { + text: string; + action: () => void; + }[]; + additional: T; +}; const dialogs = writable([]); @@ -19,20 +41,33 @@ let dialogId = 0; function addDialog( title: string, - message: string, - buttons: Dialog["buttons"], + message: string | Component, + buttons: BaseDialog["buttons"], type: DialogType, -) { + additional?: unknown, +): number { const id = dialogId++; - const newDialog: Dialog = { - id, - title, - message, - buttons, - type, - }; - dialogs.update((currentDialogs) => [...currentDialogs, newDialog]); + if (typeof message === "string") { + const newDialog: StringDialog = { + id, + title, + message, + buttons, + type, + }; + dialogs.update((currentDialogs) => [...currentDialogs, newDialog]); + } else { + const newDialog: ComponentDialog = { + id, + title, + message, + buttons, + type, + additional, + }; + dialogs.update((currentDialogs) => [...currentDialogs, newDialog]); + } return id; } From f97a7e909b7c7411ee6b1ca12357ab6503e7de9c Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 19 Oct 2025 15:06:44 +0300 Subject: [PATCH 05/12] feat: vertd error details footer --- messages/en.json | 3 +- .../functional/VertdErrorDetails.svelte | 45 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/messages/en.json b/messages/en.json index d873daa3..84fe77c4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -86,7 +86,8 @@ "vertd_generic_no": "Don't submit", "vertd_failed_to_keep": "Failed to keep the video on the server: {error}", "vertd_details": "View error details", - "vertd_details_body": "If you submit your file, your video will also be attached alongside the error log being sent to us for review. The following information is the log that we automatically receive:", + "vertd_details_body": "If you press submit, your video will also be attached alongside the error log being sent to us for review. The following information is the log that we automatically receive:", + "vertd_details_footer": "This information will only be used for troubleshooting purposes and will never be shared. View our [privacy_link]privacy policy[/privacy_link] for more details.", "vertd_details_job_id": "Job ID: {jobId}", "vertd_details_from": "From format: {from}", "vertd_details_to": "To format: {to}", diff --git a/src/lib/components/functional/VertdErrorDetails.svelte b/src/lib/components/functional/VertdErrorDetails.svelte index 140b09d6..44b0cee8 100644 --- a/src/lib/components/functional/VertdErrorDetails.svelte +++ b/src/lib/components/functional/VertdErrorDetails.svelte @@ -26,7 +26,9 @@

- {@html m["convert.errors.vertd_details_from"]({ from: additional.from })} + {@html m["convert.errors.vertd_details_from"]({ + from: additional.from, + })}

@@ -34,19 +36,30 @@ {@html m["convert.errors.vertd_details_to"]({ to: additional.to })}

-

- - {@html link( - ["view_link"], - m["convert.errors.vertd_details_error_message"](), - [ - URL.createObjectURL( - new Blob([additional.errorMessage], { type: "text/plain" }) - ) - ], - [true], - ["text-blue-500 font-normal hover:underline"] - )} - -

+

+ + {@html link( + ["view_link"], + m["convert.errors.vertd_details_error_message"](), + [ + URL.createObjectURL( + new Blob([additional.errorMessage], { + type: "text/plain", + }), + ), + ], + [true], + ["text-blue-500 font-normal hover:underline"], + )} + +

+

+ {@html link( + ["privacy_link"], + m["convert.errors.vertd_details_footer"](), + "/privacy", + [true], + ["text-blue-500 font-normal hover:underline"], + )} +

From 677ec9250ea10e3fc16b9f908a176549b2e1177b Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 19 Oct 2025 16:25:23 +0300 Subject: [PATCH 06/12] fix: sanitize translations ...my bad i realize this earlier --- bun.lock | 16 ++++++ package.json | 2 + .../functional/VertdErrorDetails.svelte | 22 ++++---- src/lib/sections/about/Credits.svelte | 50 +++++++++---------- src/lib/sections/about/Sponsors.svelte | 6 +-- src/lib/sections/about/Why.svelte | 3 +- src/lib/sections/settings/Conversion.svelte | 4 +- src/lib/sections/settings/Privacy.svelte | 6 +-- src/lib/sections/settings/Vertd.svelte | 8 +-- src/lib/store/index.svelte.ts | 15 ++++++ src/routes/+page.svelte | 5 +- 11 files changed, 85 insertions(+), 52 deletions(-) diff --git a/bun.lock b/bun.lock index 5de4f057..00773a28 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "overlayscrollbars-svelte": "^0.5.5", "p-queue": "^8.1.1", "riff-file": "^1.0.3", + "sanitize-html": "^2.17.0", "svelte-stripe": "^1.4.0", "vert-wasm": "^0.0.2", "vite-plugin-wasm": "^3.5.0", @@ -32,6 +33,7 @@ "@sveltejs/kit": "^2.42.2", "@sveltejs/vite-plugin-svelte": "^4.0.4", "@types/eslint": "^9.6.1", + "@types/sanitize-html": "^2.16.0", "autoprefixer": "^10.4.21", "css-select": "5.1.0", "eslint": "^9.36.0", @@ -317,6 +319,10 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="], + + "@types/sanitize-html": ["@types/sanitize-html@2.16.0", "", { "dependencies": { "htmlparser2": "^8.0.0" } }, "sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.44.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/type-utils": "8.44.0", "@typescript-eslint/utils": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.44.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", "@typescript-eslint/typescript-estree": "8.44.0", "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw=="], @@ -541,6 +547,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], + "human-id": ["human-id@4.1.1", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -567,6 +575,8 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -671,6 +681,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-srcset": ["parse-srcset@1.0.2", "", {}, "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -737,6 +749,8 @@ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + "sanitize-html": ["sanitize-html@2.17.0", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA=="], + "sass": ["sass@1.93.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-CQi5/AzCwiubU3dSqRDJ93RfOfg/hhpW1l6wCIvolmehfwgCI35R/0QDs1+R+Ygrl8jFawwwIojE2w47/mf94A=="], "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -807,6 +821,8 @@ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + "unplugin": ["unplugin@2.3.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw=="], "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], diff --git a/package.json b/package.json index fe5e7d49..fc11b0fa 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@sveltejs/kit": "^2.42.2", "@sveltejs/vite-plugin-svelte": "^4.0.4", "@types/eslint": "^9.6.1", + "@types/sanitize-html": "^2.16.0", "autoprefixer": "^10.4.21", "css-select": "5.1.0", "eslint": "^9.36.0", @@ -54,6 +55,7 @@ "overlayscrollbars-svelte": "^0.5.5", "p-queue": "^8.1.1", "riff-file": "^1.0.3", + "sanitize-html": "^2.17.0", "svelte-stripe": "^1.4.0", "vert-wasm": "^0.0.2", "vite-plugin-wasm": "^3.5.0" diff --git a/src/lib/components/functional/VertdErrorDetails.svelte b/src/lib/components/functional/VertdErrorDetails.svelte index 44b0cee8..119bb279 100644 --- a/src/lib/components/functional/VertdErrorDetails.svelte +++ b/src/lib/components/functional/VertdErrorDetails.svelte @@ -1,7 +1,7 @@
-

{@html m["convert.errors.vertd_details_body"]()}

+

{@html sanitize(m["convert.errors.vertd_details_body"]())}

- {@html m["convert.errors.vertd_details_job_id"]({ + {@html sanitize(m["convert.errors.vertd_details_job_id"]({ jobId: additional.jobId, - })} + }))}

- {@html m["convert.errors.vertd_details_from"]({ + {@html sanitize(m["convert.errors.vertd_details_from"]({ from: additional.from, - })} + }))}

- {@html m["convert.errors.vertd_details_to"]({ to: additional.to })} + {@html sanitize(m["convert.errors.vertd_details_to"]({ to: additional.to }))}

- {@html link( + {@html sanitize(link( ["view_link"], m["convert.errors.vertd_details_error_message"](), [ @@ -50,16 +50,16 @@ ], [true], ["text-blue-500 font-normal hover:underline"], - )} + ))}

- {@html link( + {@html sanitize(link( ["privacy_link"], m["convert.errors.vertd_details_footer"](), "/privacy", [true], ["text-blue-500 font-normal hover:underline"], - )} + ))}

diff --git a/src/lib/sections/about/Credits.svelte b/src/lib/sections/about/Credits.svelte index c8800d8b..4ad16f09 100644 --- a/src/lib/sections/about/Credits.svelte +++ b/src/lib/sections/about/Credits.svelte @@ -3,7 +3,7 @@ import { HeartHandshakeIcon } from "lucide-svelte"; import { GITHUB_URL_VERT } from "$lib/consts"; import { m } from "$lib/paraglide/messages"; - import { link } from "$lib/store/index.svelte"; + import { link, sanitize } from "$lib/store/index.svelte"; let { mainContribs, notableContribs, ghContribs } = $props(); @@ -94,32 +94,30 @@

{m["about.credits.github_contributors"]()} -

- {#if ghContribs && ghContribs.length > 0} -

- {@html link( - ["jpegify_link", "github_link"], - m["about.credits.github_description"](), - ["/jpegify", GITHUB_URL_VERT], - [false, true], - [ - "text-black dynadark:text-white", - "text-blue-500 font-normal hover:underline", - ], - )} -

- {:else} -

- {@html link( - "contribute_link", - m["about.credits.no_contributors"](), - GITHUB_URL_VERT, - )} -

- {/if} -
- + {#if ghContribs && ghContribs.length > 0} +

+ {@html sanitize(link( + ["jpegify_link", "github_link"], + m["about.credits.github_description"](), + ["/jpegify", GITHUB_URL_VERT], + [false, true], + [ + "text-black dynadark:text-white", + "text-blue-500 font-normal hover:underline", + ], + ))} +

+ {:else} +

+ {@html sanitize(link( + "contribute_link", + m["about.credits.no_contributors"](), + GITHUB_URL_VERT, + ))} +

+ {/if} + {#if ghContribs && ghContribs.length > 0}
{#each ghContribs as contrib} {@const { name, github, avatar } = contrib} diff --git a/src/lib/sections/about/Sponsors.svelte b/src/lib/sections/about/Sponsors.svelte index 71dce299..e004c28f 100644 --- a/src/lib/sections/about/Sponsors.svelte +++ b/src/lib/sections/about/Sponsors.svelte @@ -5,7 +5,7 @@ import { DISCORD_URL } from "$lib/consts"; import { error } from "$lib/logger"; import { m } from "$lib/paraglide/messages"; - import { link } from "$lib/store/index.svelte"; + import { link, sanitize } from "$lib/store/index.svelte"; import { ToastManager } from "$lib/toast/index.svelte"; let copied = false; @@ -48,11 +48,11 @@

- {@html link( + {@html sanitize(link( "discord_link", m["about.sponsors.description"](), DISCORD_URL, - )} + ))} -

- {#each images as file, i (file.id)} -
- -
- {file.name} - {file.name} -
-
- - -
-
-
- {/each} -
- From 10b8811fc44ec852132d5d9477d04b9859199ff4 Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 19 Oct 2025 17:47:21 +0300 Subject: [PATCH 08/12] fix: blob scheme --- src/lib/store/index.svelte.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 3eab49c6..80a1fb97 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -384,6 +384,6 @@ export function sanitize( a: ["href", "target", "rel", "class"], "*": ["class"], }, - allowedSchemes: ["http", "https", "mailto"], + allowedSchemes: ["http", "https", "mailto", "blob"], }); } From cf2e351424647b102dd05e33fa58c56ea4792834 Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 19 Oct 2025 18:36:45 +0300 Subject: [PATCH 09/12] fix: seo fixes don't index static language urls (doesn't do anything) - robots.txt & sitemap.xml --- src/routes/+layout.svelte | 1 + static/robots.txt | 15 +++++++++++++++ static/sitemap.xml | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 static/robots.txt create mode 100644 static/sitemap.xml diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2f784178..07a26460 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -143,6 +143,7 @@ /> + {#if enablePlausible} @@ -13,102 +14,80 @@ class="w-full max-w-[1280px] flex flex-col md:flex-row gap-4 p-4 md:px-4 md:py-0" >
-

Summary

+

{m["privacy.summary.title"]()}

- VERT's privacy policy is very simple: we do not collect or store - any data on you at all. We don't use cookies or trackers, - analytics are completely private, and all conversions (except - videos) happen locally on your browser. Videos are deleted after - being downloaded, or an hour, unless explicitly given permission - by you to be stored; it will only be used for the purpose of - troubleshooting. VERT self-hosts a Coolify instance for hosting - the website and vertd (for video conversion), and a Plausible - instance for completely anonymous and aggregated analytics. -
-
- Note this may only apply to the official VERT instance at - vert.sh; - third-party instances may handle your data differently. + {@html sanitize( + link( + ["vert_link"], + m["privacy.summary.description"](), + ["https://vert.sh"], + [true], + ), + )}

-

Conversions

+

{m["privacy.conversions.title"]()}

- Most conversions (images, documents, audio) happen entirely - locally on your device using WebAssembly versions of the - relevant tools (e.g. ImageMagick, Pandoc, FFmpeg). This means - your files never leave your device and we will never have access - to them. -
-
- Video conversions are performed on our servers because they require - more processing power and cannot be done very quickly on the browser - yet. Videos you convert with VERT are deleted after being downloaded, - or after one hour, unless you explicitly give permission for us to - store them longer purely for troubleshooting purposes. + {@html sanitize(m["privacy.conversions.description"]())}

-

Conversion Errors

+

+ {m["privacy.conversion_errors.title"]()} +

- When a video conversion fails, we may collect some anonymous - data to help us diagnose the issue. This data may include: + {m["privacy.conversion_errors.description"]()}
    -
  • The job ID, which is the anonymized file name
  • -
  • The format you converted from
  • -
  • The format you converted to
  • -
  • - The FFmpeg stderr output of your job (error message) -
  • -
  • - The actual video file (if given explicit permission) -
  • +
  • {m["privacy.conversion_errors.list_job_id"]()}
  • +
  • {m["privacy.conversion_errors.list_format_from"]()}
  • +
  • {m["privacy.conversion_errors.list_format_to"]()}
  • +
  • {m["privacy.conversion_errors.list_stderr"]()}
  • +
  • {m["privacy.conversion_errors.list_video"]()}
- This information is used solely for the purpose of diagnosing conversion - issues. The actual video file will only ever be collected if you - give us permission to do so, where it will only be used for troubleshooting. + {m["privacy.conversion_errors.footer"]()}
-

Analytics

+

{m["privacy.analytics.title"]()}

- We self-host a Plausible instance for completely anonymous and - aggregated analytics. Plausible does not use cookies and - complies with all major privacy regulations (GDPR/CCPA/PECR). - You can opt out of analytics in the "Privacy & data" section in settings - and read more about Plausible's privacy practices - here. + {@html sanitize( + link( + ["settings_link", "plausible_link"], + m["privacy.analytics.description"](), + [ + "/settings", + "https://plausible.io/privacy-focused-web-analytics", + ], + [false, true], + ), + )}

-

Local Storage

+

+ {m["privacy.local_storage.title"]()} +

- We use your browser's local storage to save your settings, and - your browser's session storage to temporarily store the GitHub - contributors list for the "About" section to reduce repeated - GitHub API requests. No personal data is stored or transmitted. -
-
- The WebAssembly versions of the conversion tools we use (FFmpeg, - ImageMagick, Pandoc) are also stored locally on your browser when - you first visit the website, so you don’t need to redownload them - each visit. No personal data is stored or transmitted. You may view - or delete this data at any time in the "Privacy & data" section in - settings. + {@html sanitize( + link( + ["settings_link"], + m["privacy.local_storage.description"](), + ["/settings"], + [false], + ), + )}

-

Contact

+

{m["privacy.contact.title"]()}

- For questions, email us at: hello@vert.sh. If you are using a third-party instance of VERT, please - contact the hoster of that instance instead. + {@html sanitize( + link( + ["email_link"], + m["privacy.contact.description"](), + ["mailto:hello@vert.sh"], + [false], + ), + )}

-

Last updated: 2025-10-16

+

{m["privacy.last_updated"]()}

From bd8f782d85dbfc34aca43ff68b9597723e14d902 Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 19 Oct 2025 20:10:29 +0300 Subject: [PATCH 11/12] fix: privacy translation fixes --- messages/en.json | 3 ++- src/lib/components/functional/VertdError.svelte | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/messages/en.json b/messages/en.json index 8b990ba2..620a1e46 100644 --- a/messages/en.json +++ b/messages/en.json @@ -79,13 +79,14 @@ "errors": { "cant_convert": "We can't convert this file.", "vertd_server": "what are you doing..? you're supposed to run the vertd server!", + "vertd_generic_view": "View error details", "vertd_generic_body": "An error occurred whilst whilst trying convert your video. Would you like to submit this video to the developers to help fix this bug? Only your video file will be sent. No identifiers will be uploaded.", "vertd_generic_title": "Video conversion error", "vertd_generic_yes": "Submit video", "vertd_generic_no": "Don't submit", "vertd_failed_to_keep": "Failed to keep the video on the server: {error}", "vertd_details": "View error details", - "vertd_details_body": "If you press submit, your video will also be attached alongside the error log being sent to us for review. The following information is the log that we automatically receive:", + "vertd_details_body": "If you press submit, your video will also be attached alongside the error log which is always reported to us for review. The following information is the log that we automatically receive:", "vertd_details_footer": "This information will only be used for troubleshooting purposes and will never be shared. View our [privacy_link]privacy policy[/privacy_link] for more details.", "vertd_details_job_id": "Job ID: {jobId}", "vertd_details_from": "From format: {from}", diff --git a/src/lib/components/functional/VertdError.svelte b/src/lib/components/functional/VertdError.svelte index 091bcaae..a603902a 100644 --- a/src/lib/components/functional/VertdError.svelte +++ b/src/lib/components/functional/VertdError.svelte @@ -87,7 +87,7 @@ onclick={showDetails} class="btn rounded-lg h-fit py-2 w-full bg-accent-blue text-black" disabled={submitting} - >View Details Submitted{m["convert.errors.vertd_generic_view"]()}
{#if ghContribs && ghContribs.length > 0} -

- {@html sanitize( - link( - "github_link", - m["about.credits.github_description"](), - GITHUB_URL_VERT, - true, - ), - )} -

- {:else} -

- {@html sanitize( - link( - "contribute_link", - m["about.credits.no_contributors"](), - GITHUB_URL_VERT, - true, - ), - )} -

+
+ {#each ghContribs as contrib} + {@const { name, github, avatar } = contrib} + {@render contributor(name, github, avatar)} + {/each} +
{/if} - {#if ghContribs && ghContribs.length > 0} -
- {#each ghContribs as contrib} - {@const { name, github, avatar } = contrib} - {@render contributor(name, github, avatar)} - {/each} -
- {/if}

{m["about.credits.libraries"]()}

{m["about.credits.libraries_description"]()}

- - + {/if} + +