From 73f4f301a2cf058ab5879dda02790253676f234e Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Tue, 8 Sep 2026 14:56:20 +0100 Subject: [PATCH] Add a landing page for people who cannot reach their own server The two existing landing pages argue that self hosting is worth doing. That is the wrong argument for someone who already self hosts and whose server simply cannot be reached from outside. That is the audience the subscription is actually for. The relay, the domain with automatic DNS updates, the certificate and the mail relay are precisely the four things a self hoster cannot provide themselves: behind DS-Lite or CGNAT there is no port to forward, a changing IP breaks any name, TLS needs a domain you control, and mail from a residential address is rejected. The forum says the same. Ordered by views: NAT loopback 1877, certificate renewal on a custom domain 1073, a router change breaking access 999, an expired certificate after renewal 836, CGNAT access 738. A search for CGNAT also returns users asking for Tailscale as an app, which is the relay we already sell. So the page leads with the problem and answers it, rather than opening with forty apps. Variants can now override the bullet points, since a shared list written for the undecided says nothing to someone with a specific fault. German and English, both noindex like the existing landing pages, since these are ad destinations rather than pages to be found. --- web/src/landing-copy.js | 22 +++++++++++++++++++++- web/src/router/index.js | 12 ++++++++++++ web/tests/landing.test.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/web/src/landing-copy.js b/web/src/landing-copy.js index 89f1ccf..0a0e55f 100644 --- a/web/src/landing-copy.js +++ b/web/src/landing-copy.js @@ -21,6 +21,16 @@ const COPY = { pi: { title: 'Machen Sie Ihren Raspberry Pi zur privaten Cloud', subtitle: 'Image aufspielen, einstecken, fertig. Nextcloud, Fotos und Passwort-Manager installieren Sie danach per Klick.' + }, + access: { + title: 'Ihr Server läuft, ist aber von außen nicht erreichbar?', + subtitle: 'DS-Lite, CGNAT oder eine wechselnde IP-Adresse: ohne öffentliche IP hilft auch keine Portfreigabe. Syncloud stellt die Verbindung über einen Relay her.', + points: [ + 'Erreichbar ohne Portfreigabe, auch hinter DS-Lite und CGNAT', + 'Eigene Adresse wie ihrname.syncloud.it, die IP-Wechsel automatisch nachzieht', + 'HTTPS-Zertifikat wird ausgestellt und selbstständig erneuert', + 'E-Mail-Versand über einen Relay, damit Ihre Apps nicht im Spam landen' + ] } } }, @@ -43,6 +53,16 @@ const COPY = { pi: { title: 'Turn your Raspberry Pi into a private cloud', subtitle: 'Write the image, plug it in, done. Nextcloud, photos and a password manager install with one click afterwards.' + }, + access: { + title: 'Your server runs, but you cannot reach it from outside', + subtitle: 'Behind CGNAT, DS-Lite or a changing IP there is no port to forward. Syncloud reaches your server through a relay instead.', + points: [ + 'Reachable with no port forwarding, even behind CGNAT', + 'An address like yourname.syncloud.it that follows your IP when it changes', + 'An HTTPS certificate, issued and renewed for you', + 'Outgoing mail through a relay, so your apps are not treated as spam' + ] } } } @@ -59,7 +79,7 @@ export function landingCopy (variant, language = DEFAULT_LANGUAGE) { cta: copy.cta, price: copy.price, shotAlt: copy.shotAlt, - points: copy.points, + points: chosen.points || copy.points, trust: copy.trust } } diff --git a/web/src/router/index.js b/web/src/router/index.js index 43084b9..7d72e6a 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -30,6 +30,18 @@ const routes = [ component: () => import('../views/Landing.vue'), meta: { variant: 'pi', language: 'de', noindex: true, bare: true } }, + { + path: '/en/remote-access', + name: 'LandingAccessEn', + component: () => import('../views/Landing.vue'), + meta: { variant: 'access', language: 'en', noindex: true, bare: true } + }, + { + path: '/de/remote-access', + name: 'LandingAccessDe', + component: () => import('../views/Landing.vue'), + meta: { variant: 'access', language: 'de', noindex: true, bare: true } + }, { path: '/:catchAll(.*)', redirect: '/' } ] diff --git a/web/tests/landing.test.js b/web/tests/landing.test.js index e2a4650..e97d96d 100644 --- a/web/tests/landing.test.js +++ b/web/tests/landing.test.js @@ -115,3 +115,31 @@ describe('German landing pages', () => { } }) }) + +describe('the remote access variant', () => { + it('leads with not being reachable, not with what the product is', () => { + const wrapper = landing('access', 'de') + expect(wrapper.get('[data-testid="landing-title"]').text()) + .toBe(landingCopy('access', 'de').title) + expect(wrapper.get('[data-testid="landing-subtitle"]').text()).toContain('CGNAT') + }) + + it('answers the access problem rather than listing apps', () => { + for (const language of LANGUAGES) { + const copy = landingCopy('access', language) + const points = copy.points.join(' ').toLowerCase() + expect(points, language).not.toContain('40') + expect(points, language).toMatch(/relay|port|cgnat/) + expect(copy.points, language).not.toEqual(landingCopy('cloud', language).points) + } + }) + + it('keeps the shared price and call to action', () => { + for (const language of LANGUAGES) { + expect(landingCopy('access', language).price) + .toBe(landingCopy('cloud', language).price) + expect(landingCopy('access', language).cta) + .toBe(landingCopy('cloud', language).cta) + } + }) +})