Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 2026-10-16

- Preferences now links the Firefox extension to the [extension releases on GitHub](https://github.com/jonocodes/savr/releases?q=extension&expanded=true) (install the `.xpi` from the newest one) instead of the `savr.link/extension/firefox.xpi` redirect, which Firefox was blocking. The Chrome link is hidden by default (`VITE_CHROME_EXTENSION_URL` now defaults to empty) until a Chrome Web Store listing exists

## 2026-10-15

- The shared CORS proxy now only serves allowlisted calling sites — savr.link, Netlify preview/branch deploys (`*--savrlist.netlify.app`) and localhost — via a new optional `ALLOWED_ORIGINS` setting. It still fetches articles from any public site; other websites calling it get a 404, recorded in the stats. Self-hosted Savr frontends on their own domain need their own proxy. Unset (the default for self-hosted proxies) changes nothing (ADR 0006; rate limiting tracked in #117)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ A working example lives in [`browser-extension/youtube-transcript.js`](browser-e

On desktop Firefox and Chrome, the optional [browser extension](browser-extension/) adds a toolbar button with **Save current page** and **Visit Savr**. Unlike a URL save, it scrapes the page you already have open, so it works behind logins, and no CORS proxy is involved for the page itself. The article's images are downloaded by the extension too, so an extension save needs no proxy at all; any image the extension cannot fetch falls back to your configured proxy (or a placeholder). URL saves and bookmarklets still use the proxy as before.

Firefox (142+) installs a signed build from **https://savr.link/extension/firefox.xpi** and keeps it updated; on Android that link downloads the file, which you then install from a hidden settings menu (steps in the extension README). Chrome has no store listing yet, so it installs with **Load unpacked**. See [browser-extension/README.md](browser-extension/README.md) for install, settings, how releases are published, and how the delivery flow works — or use the links in **Preferences** (configurable at build time).
Firefox (142+) installs a signed build from the `.xpi` on the newest **[extension release](https://github.com/jonocodes/savr/releases?q=extension&expanded=true)** and keeps it updated; on Android tapping the `.xpi` only downloads it, which you then install from a hidden settings menu (steps in the extension README). Chrome has no store listing yet, so it installs with **Load unpacked**. See [browser-extension/README.md](browser-extension/README.md) for install, settings, how releases are published, and how the delivery flow works — or use the links in **Preferences** (configurable at build time).

## In browser

Expand Down
16 changes: 9 additions & 7 deletions browser-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ There is deliberately no page context-menu entry.

### Firefox (desktop and Android)

Open **https://savr.link/extension/firefox.xpi** in Firefox (the app's
Preferences screen links to it). Firefox asks to allow savr.link to install an
Open the [extension releases](https://github.com/jonocodes/savr/releases?q=extension&expanded=true) in
Firefox (the app's Preferences screen links there) and click the
`savr-firefox-<version>.xpi` asset of the newest one. Firefox asks to allow github.com to install an
add-on, then shows the permission prompt. It needs Firefox 142 or newer, and
updates itself (see [Updates](#updates)).

On Android, opening that link only downloads the file — Firefox for Android
On Android, tapping the `.xpi` only downloads it — Firefox for Android
does not install add-ons from a web link outside addons.mozilla.org. Install
the downloaded file instead
([Mozilla's instructions](https://extensionworkshop.com/documentation/publish/install-self-distributed/)):
Expand Down Expand Up @@ -79,8 +80,9 @@ is a documented path: ship one last self-hosted version **without**
`update_url`, and Firefox switches those installs to AMO for updates.

- **Install link.** `https://savr.link/extension/firefox.xpi` is a Netlify
redirect (`public/_redirects`) to the newest release asset, so the link the
app shows never changes. The asset is uploaded as
redirect (`public/_redirects`) to the newest release asset, so that link
never changes. Preferences links to the GitHub release list instead: clicked
from the app, Firefox blocked the install through the redirect. The asset is uploaded as
`Content-Type: application/x-xpinstall`, which is what makes Firefox start
its install flow instead of downloading the file. Any site other than AMO
gets Firefox's "Allow *site* to install an add-on?" prompt first; that is
Expand All @@ -105,8 +107,8 @@ can override it), so a hosted `.crx` would not work for most people. The plan
is an **unlisted** Chrome Web Store item (a one-time developer registration fee;
review takes days); Edge, Brave and Vivaldi can install from it too, and Edge
Add-ons is a free alternative. Until then, the release's Chromium zip is for
**Load unpacked**. The Preferences link (`VITE_CHROME_EXTENSION_URL`) points at
this README until a listing exists.
**Load unpacked**. The Preferences screen shows no Chrome link
(`VITE_CHROME_EXTENSION_URL` defaults to empty) until a listing exists.

### Versioning

Expand Down
2 changes: 1 addition & 1 deletion docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Deploy a token-locked proxy of your own from `infra/cors-worker/` (there is a De

### `VITE_FIREFOX_EXTENSION_URL` / `VITE_CHROME_EXTENSION_URL`

- **Default**: Firefox → `https://savr.link/extension/firefox.xpi` (stable link to the newest signed XPI; see [Releasing](../browser-extension/README.md#releasing)). Chrome → the [browser extension README](https://github.com/jonocodes/savr/tree/main/browser-extension) (install instructions) until a Chrome Web Store listing exists.
- **Default**: Firefox → `https://github.com/jonocodes/savr/releases?q=extension&expanded=true` (GitHub releases filtered to the extension; install the signed `.xpi` from the newest one). Chrome → empty (hidden) until a Chrome Web Store listing exists.
- **Description**: Where the Preferences screen links to install the browser extension. Set one to an empty string to hide that link.
- **Usage**: `VITE_CHROME_EXTENSION_URL=https://chromewebstore.google.com/detail/… npm run build`

Expand Down
16 changes: 8 additions & 8 deletions src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ export interface EnvironmentConfig {
};
extension: {
// Where Preferences links to install the browser extension, per browser.
// Defaults to the extension's README (install instructions); the hosted
// build can point these at the AMO listing / a hosted signed XPI and the
// Chrome Web Store once published. An empty value hides that link.
// Firefox defaults to the latest GitHub release; Chrome is hidden until a
// Chrome Web Store listing exists. An empty value hides that link.
firefoxUrl: string;
chromeUrl: string;
};
}

const EXTENSION_README_URL = "https://github.com/jonocodes/savr/tree/main/browser-extension";
// Stable install link for the signed, self-hosted Firefox build: a savr.link
// redirect (public/_redirects) to the newest XPI on GitHub Releases.
const FIREFOX_EXTENSION_INSTALL_URL = "https://savr.link/extension/firefox.xpi";
// GitHub releases filtered to the extension (newest first; the repo's "latest"
// release belongs to the app). The signed XPI is installed from the
// release's asset list. (Linking the savr.link/extension/firefox.xpi redirect
// directly got blocked by Firefox's install prompt.)
const FIREFOX_EXTENSION_INSTALL_URL = "https://github.com/jonocodes/savr/releases?q=extension&expanded=true";

// Get environment variables
const getEnvVar = (key: string, defaultValue?: string): string | undefined => {
Expand Down Expand Up @@ -84,7 +84,7 @@ export const environmentConfig: EnvironmentConfig = {
},
extension: {
firefoxUrl: getEnvVar("VITE_FIREFOX_EXTENSION_URL", FIREFOX_EXTENSION_INSTALL_URL) || "",
chromeUrl: getEnvVar("VITE_CHROME_EXTENSION_URL", EXTENSION_README_URL) || "",
chromeUrl: getEnvVar("VITE_CHROME_EXTENSION_URL", "") || "",
},
};

Expand Down
13 changes: 5 additions & 8 deletions tests/e2e/preferences-extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { test, expect } from "@playwright/test";
/**
* Preferences offers the browser extension next to the bookmarklets. The
* install targets are build-configurable (VITE_FIREFOX_EXTENSION_URL /
* VITE_CHROME_EXTENSION_URL). Firefox defaults to the stable savr.link install
* link for the signed self-hosted XPI; Chrome, until a store listing exists,
* defaults to the extension README.
* VITE_CHROME_EXTENSION_URL). Firefox defaults to the extension's GitHub releases
* (whose assets include the signed XPI); Chrome is hidden until a store
* listing exists.
*/
test.describe("Preferences: browser extension", () => {
test("links to the extension install targets", async ({ page }) => {
Expand All @@ -16,11 +16,8 @@ test.describe("Preferences: browser extension", () => {

await expect(page.getByTestId("extension-firefox-link")).toHaveAttribute(
"href",
"https://savr.link/extension/firefox.xpi"
);
await expect(page.getByTestId("extension-chrome-link")).toHaveAttribute(
"href",
/browser-extension/
"https://github.com/jonocodes/savr/releases?q=extension&expanded=true"
);
await expect(page.getByTestId("extension-chrome-link")).toHaveCount(0);
});
});
Loading