Skip to content

Commit 18e4ac3

Browse files
dobby-coder[bot]clauderubenhensen
authored
fix: address SEO audit findings (prerender all pages, canonical, JSON-LD, llms.txt) (#104)
* fix: address SEO audit findings from issue #88 Enable prerendering for all public pages, add canonical tags, JSON-LD structured data, hreflang hints, an llms.txt, and a security.txt. - Set `trailingSlash: 'always'` on the marketing layout so prerendered pages are emitted as `/route/index.html` instead of `/route.html`. This resolves the 403 on `/blog/` and ensures nginx's default `try_files $uri $uri/` serves the prerendered HTML for every sub-page instead of falling back to the empty SPA shell. - Move `/addons` from the `(app)` route group (CSR-only) into `(marketing)` so it inherits SSR + prerender and the site footer. - Expand the SEO component with auto-generated canonical and og:url derived from the current pathname, hreflang hints (en, nl, x-default), richer twitter-card meta, and an optional JSON-LD slot. - Add SoftwareApplication + Organization JSON-LD to the homepage and Article JSON-LD to blog posts. - Publish `/llms.txt` (site summary for AI crawlers) and `/.well-known/security.txt`. - Rewrite `sitemap.xml` to include `/addons`, use trailing-slash URLs, and add `<lastmod>`, `<changefreq>`, and `<priority>` for every URL. - Add `/addons` to the svelte.config.js prerender entries. Closes #88 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: update usage limits to 5 GB per 14 days and set Yivi as parent org Update all file size limit mentions from 2 GB to 5 GB per 14 days across EN and NL locales. Change parentOrganization in JSON-LD from Radboud University to Yivi. * fix: update usage limit in llms.txt to 5 GB per 14 days --------- Co-authored-by: dobby-yivi-agent[bot] <275734547+dobby-yivi-agent[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Ruben Hensen <ruben.hensen@protonmail.com>
1 parent d0a0288 commit 18e4ac3

11 files changed

Lines changed: 224 additions & 17 deletions

File tree

src/lib/components/SEO.svelte

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,52 @@
11
<script>
2-
let { title = '', description = '', ogImage = '', ogType = 'website', canonical = '' } = $props()
2+
import { page } from '$app/state'
3+
4+
let {
5+
title = '',
6+
description = '',
7+
ogImage = '',
8+
ogType = 'website',
9+
canonical = '',
10+
jsonLd = null,
11+
} = $props()
12+
313
const siteName = 'PostGuard'
4-
const defaultDescription = 'PostGuard offers free and easy-to-use end-to-end encryption for emails and files.'
14+
const siteUrl = 'https://postguard.eu'
15+
const defaultDescription =
16+
'PostGuard offers free and easy-to-use end-to-end encryption for emails and files.'
517
const defaultImage = '/pg_logo.png'
18+
19+
const canonicalUrl = $derived(
20+
canonical || (page?.url?.pathname ? `${siteUrl}${page.url.pathname}` : '')
21+
)
22+
const ogImageUrl = $derived(
23+
(ogImage || defaultImage).startsWith('http')
24+
? ogImage || defaultImage
25+
: `${siteUrl}${ogImage || defaultImage}`
26+
)
27+
const jsonLdString = $derived(jsonLd ? JSON.stringify(jsonLd) : '')
628
</script>
729
830
<svelte:head>
931
<title>{title ? `${title} | ${siteName}` : siteName}</title>
1032
<meta name="description" content={description || defaultDescription} />
1133
<meta property="og:title" content={title || siteName} />
1234
<meta property="og:description" content={description || defaultDescription} />
13-
<meta property="og:image" content={ogImage || defaultImage} />
35+
<meta property="og:image" content={ogImageUrl} />
1436
<meta property="og:type" content={ogType} />
1537
<meta property="og:site_name" content={siteName} />
16-
{#if canonical}<link rel="canonical" href={canonical} />{/if}
38+
{#if canonicalUrl}
39+
<meta property="og:url" content={canonicalUrl} />
40+
<link rel="canonical" href={canonicalUrl} />
41+
<link rel="alternate" hreflang="en" href={canonicalUrl} />
42+
<link rel="alternate" hreflang="nl" href={canonicalUrl} />
43+
<link rel="alternate" hreflang="x-default" href={canonicalUrl} />
44+
{/if}
1745
<meta name="twitter:card" content="summary_large_image" />
46+
<meta name="twitter:title" content={title || siteName} />
47+
<meta name="twitter:description" content={description || defaultDescription} />
48+
<meta name="twitter:image" content={ogImageUrl} />
49+
{#if jsonLdString}
50+
{@html `<script type="application/ld+json">${jsonLdString}<\/script>`}
51+
{/if}
1852
</svelte:head>

src/lib/locales/en.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,13 @@
167167
"tagline": "Securely send your files",
168168
"upperTextDropZone": "Drag & drop files <br/> or",
169169
"lowerTextDropZone": "click to upload",
170-
"sizeLimitText": "max. 2GB",
170+
"sizeLimitText": "max. 5 GB per 14 days",
171171
"dragText": "Drag files here",
172172
"dropText": "Drop to add files",
173173
"dropMoreText": "Drop to add more files",
174174
"orText": "or",
175175
"chooseFilesButton": "Choose files",
176-
"maxSizeText": "Maximum 2 GB",
176+
"maxSizeText": "5 GB per 14 days",
177177
"addMoreFiles": "Add more files",
178178
"fileSummary": "{count} {count, plural, one {file} other {files}} added, {size} GB remaining"
179179
},
@@ -223,7 +223,7 @@
223223
"title": "Fill in all required fields",
224224
"continueButton": "Continue filling in",
225225
"noFiles": "You haven't added any files yet",
226-
"filesTooLarge": "The files exceed the maximum size of {max} GB",
226+
"filesTooLarge": "The files exceed the {max} GB limit (14-day window)",
227227
"noEmail": "You haven't filled in an email address for a recipient",
228228
"invalidEmail": "The email address {email} is not valid",
229229
"missingAttribute": "{attribute} of {email} is missing",

src/lib/locales/nl.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,13 @@
166166
"fileBox": {
167167
"tagline": "Verstuur je bestanden veilig",
168168
"upperTextDropZone": "Sleep bestanden hierheen <br/> of",
169-
"lowerTextDropZone": "klik om te uploaden <br/> (max. 2GB)",
169+
"lowerTextDropZone": "klik om te uploaden <br/> (max. 5 GB per 14 dagen)",
170170
"dragText": "Sleep bestanden hierheen",
171171
"dropText": "Loslaten om bestanden toe te voegen",
172172
"dropMoreText": "Loslaten om meer bestanden toe te voegen",
173173
"orText": "of",
174174
"chooseFilesButton": "Kies bestanden",
175-
"maxSizeText": "Maximaal 2 GB",
175+
"maxSizeText": "5 GB per 14 dagen",
176176
"addMoreFiles": "Voeg meer bestanden toe",
177177
"fileSummary": "{count} {count, plural, one {bestand} other {bestanden}} toegevoegd, nog {size} GB over"
178178
},
@@ -222,7 +222,7 @@
222222
"title": "Vul alle verplichte velden in",
223223
"continueButton": "Verder met invullen",
224224
"noFiles": "Je hebt nog geen bestanden toegevoegd",
225-
"filesTooLarge": "De bestanden overschrijden de maximale grootte van {max} GB",
225+
"filesTooLarge": "De bestanden overschrijden de {max} GB limiet (14 dagen)",
226226
"noEmail": "Je hebt nog geen e-mailadres voor een ontvanger ingevuld",
227227
"invalidEmail": "Het e-mailadres {email} is niet geldig",
228228
"missingAttribute": "{attribute} van {email} ontbreekt",

src/routes/(marketing)/+layout.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { locale, waitLocale } from 'svelte-i18n'
33
import { browser } from '$app/environment'
44

55
export const prerender = true
6+
export const trailingSlash = 'always'
67

78
if (browser) {
89
const stored = localStorage.getItem('preferredLanguage')

src/routes/(marketing)/+page.svelte

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,52 @@
1313
contactEl.href = `mailto:${addr}`
1414
}
1515
})
16+
17+
const homepageJsonLd = {
18+
'@context': 'https://schema.org',
19+
'@graph': [
20+
{
21+
'@type': 'SoftwareApplication',
22+
name: 'PostGuard',
23+
description:
24+
'Free, open-source identity-based encryption for emails and files using Yivi identity attributes.',
25+
applicationCategory: 'SecurityApplication',
26+
operatingSystem: 'Web Browser',
27+
offers: {
28+
'@type': 'Offer',
29+
price: '0',
30+
priceCurrency: 'EUR',
31+
},
32+
url: 'https://postguard.eu',
33+
license: 'https://opensource.org/licenses/MIT',
34+
softwareRequirements: 'Yivi (IRMA) app',
35+
sameAs: ['https://github.com/encryption4all'],
36+
author: {
37+
'@type': 'Organization',
38+
name: 'Radboud University',
39+
url: 'https://www.ru.nl',
40+
},
41+
},
42+
{
43+
'@type': 'Organization',
44+
name: 'PostGuard',
45+
url: 'https://postguard.eu',
46+
logo: 'https://postguard.eu/pg_logo.png',
47+
sameAs: ['https://github.com/encryption4all'],
48+
parentOrganization: {
49+
'@type': 'Organization',
50+
name: 'Yivi',
51+
url: 'https://yivi.app',
52+
},
53+
},
54+
],
55+
}
1656
</script>
1757

1858
<SEO
1959
title="Secure File Sharing & Email Encryption"
2060
description="PostGuard offers free, easy-to-use end-to-end encryption for emails and files. Your data never leaves your browser unencrypted."
61+
jsonLd={homepageJsonLd}
2162
/>
2263

2364
<section class="hero">

src/routes/(app)/addons/+page.svelte renamed to src/routes/(marketing)/addons/+page.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
import { _ } from 'svelte-i18n'
33
import Tabs from '$lib/components/Tabs.svelte'
4+
import SEO from '$lib/components/SEO.svelte'
45
// import { fade } from 'svelte/transition'
56
import { Tween } from 'svelte/motion'
67
import { cubicOut } from 'svelte/easing'
@@ -31,6 +32,11 @@
3132
3233
</script>
3334

35+
<SEO
36+
title="PostGuard Addons"
37+
description="Install PostGuard for Thunderbird or Outlook to send and receive end-to-end encrypted emails directly from your mail client."
38+
/>
39+
3440
<div class="page-wrapper">
3541
<div class="grid-container" bind:clientWidth={containerWidth}>
3642
<div class="grid-item header">

src/routes/(marketing)/blog/[slug]/+page.svelte

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,48 @@
11
<script lang="ts">
22
import SEO from '$lib/components/SEO.svelte'
3+
import { page } from '$app/state'
34
45
let { data } = $props()
6+
7+
const siteUrl = 'https://postguard.eu'
8+
const articleJsonLd = $derived({
9+
'@context': 'https://schema.org',
10+
'@type': 'Article',
11+
headline: data.metadata.title,
12+
description: data.metadata.description,
13+
datePublished: data.metadata.date,
14+
...(data.metadata.image
15+
? {
16+
image: data.metadata.image.startsWith('http')
17+
? data.metadata.image
18+
: `${siteUrl}${data.metadata.image}`,
19+
}
20+
: {}),
21+
author: {
22+
'@type': data.metadata.author === 'PostGuard Team' ? 'Organization' : 'Person',
23+
name: data.metadata.author || 'PostGuard Team',
24+
},
25+
publisher: {
26+
'@type': 'Organization',
27+
name: 'PostGuard',
28+
logo: {
29+
'@type': 'ImageObject',
30+
url: `${siteUrl}/pg_logo.png`,
31+
},
32+
},
33+
mainEntityOfPage: {
34+
'@type': 'WebPage',
35+
'@id': `${siteUrl}${page.url.pathname}`,
36+
},
37+
})
538
</script>
639

740
<SEO
841
title={data.metadata.title}
942
description={data.metadata.description}
1043
ogType="article"
1144
ogImage={data.metadata.image || ''}
45+
jsonLd={articleJsonLd}
1246
/>
1347

1448
<article class="blog-post">

src/routes/sitemap.xml/+server.js

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,74 @@
11
export const prerender = true
22

33
export function GET() {
4-
const staticPages = ['', '/about', '/privacy', '/blog']
4+
/**
5+
* Static pages with their last-modified dates. Bump these when the page
6+
* content changes meaningfully. Values are ISO-8601 date strings (YYYY-MM-DD).
7+
* @type {{ path: string, lastmod: string, changefreq: string, priority: string }[]}
8+
*/
9+
const staticPages = [
10+
{
11+
path: '',
12+
lastmod: '2026-04-21',
13+
changefreq: 'monthly',
14+
priority: '1.0',
15+
},
16+
{
17+
path: '/about',
18+
lastmod: '2026-04-21',
19+
changefreq: 'monthly',
20+
priority: '0.8',
21+
},
22+
{
23+
path: '/addons',
24+
lastmod: '2026-04-21',
25+
changefreq: 'monthly',
26+
priority: '0.8',
27+
},
28+
{
29+
path: '/privacy',
30+
lastmod: '2026-04-21',
31+
changefreq: 'yearly',
32+
priority: '0.5',
33+
},
34+
{
35+
path: '/blog',
36+
lastmod: '2026-04-21',
37+
changefreq: 'weekly',
38+
priority: '0.7',
39+
},
40+
]
541

6-
const postFiles = import.meta.glob('/src/content/blog/*.svx', { eager: true })
7-
const blogSlugs = Object.keys(postFiles).map(
8-
(path) => `/blog/${/** @type {string} */ (path.split('/').pop()).replace('.svx', '')}`
42+
/** @type {Record<string, { metadata?: { date?: string } }>} */
43+
const postFiles = /** @type {any} */ (
44+
import.meta.glob('/src/content/blog/*.svx', { eager: true })
945
)
46+
const blogEntries = Object.entries(postFiles).map(([path, mod]) => {
47+
const slug = /** @type {string} */ (path.split('/').pop()).replace(
48+
'.svx',
49+
''
50+
)
51+
const date = mod?.metadata?.date ?? '2026-04-21'
52+
return {
53+
path: `/blog/${slug}`,
54+
lastmod: String(date).slice(0, 10),
55+
changefreq: 'yearly',
56+
priority: '0.6',
57+
}
58+
})
59+
60+
const entries = [...staticPages, ...blogEntries]
1061

11-
const pages = [...staticPages, ...blogSlugs]
62+
const urls = entries
63+
.map(
64+
(e) =>
65+
` <url>\n <loc>https://postguard.eu${e.path}/</loc>\n <lastmod>${e.lastmod}</lastmod>\n <changefreq>${e.changefreq}</changefreq>\n <priority>${e.priority}</priority>\n </url>`
66+
)
67+
.join('\n')
1268

1369
const xml = `<?xml version="1.0" encoding="UTF-8"?>
1470
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
15-
${pages.map((p) => ` <url><loc>https://postguard.eu${p}</loc></url>`).join('\n')}
71+
${urls}
1672
</urlset>`
1773

1874
return new Response(xml, {

static/.well-known/security.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Contact: mailto:info@postguard.eu
2+
Expires: 2027-04-21T00:00:00.000Z
3+
Preferred-Languages: en, nl
4+
Canonical: https://postguard.eu/.well-known/security.txt
5+
Policy: https://github.com/encryption4all/postguard-website/security/policy

static/llms.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# PostGuard
2+
3+
> PostGuard is an open-source, identity-based encrypted file sharing and email encryption service developed at Radboud University and funded by NWO. No key exchange is required — recipients are identified by Yivi (IRMA) attributes such as email address, making end-to-end encryption as simple as knowing someone's email address.
4+
5+
PostGuard uses Identity-Based Encryption (IBE) so senders can encrypt messages using only a recipient's attributes (e.g. email). The recipient authenticates with their Yivi (IRMA) wallet to retrieve the decryption key. PostGuard runs entirely in the browser — plaintext never leaves the user's device unencrypted.
6+
7+
## Key Pages
8+
- [Homepage](https://postguard.eu/): Overview of PostGuard, its core features, and the product family.
9+
- [About](https://postguard.eu/about/): How PostGuard works, the Radboud University team, and funding from NWO.
10+
- [File Sharing](https://postguard.eu/fileshare/): Web app for sending end-to-end encrypted files to any email address.
11+
- [Decrypt](https://postguard.eu/decrypt/): Web app for decrypting PostGuard-encrypted messages and files.
12+
- [Addons](https://postguard.eu/addons/): Browser and mail-client addons (Thunderbird, Outlook) for encrypted email.
13+
- [Privacy Policy](https://postguard.eu/privacy/): How PostGuard handles user data and attributes.
14+
- [Blog](https://postguard.eu/blog/): Posts about PostGuard, identity-based encryption, and secure communication.
15+
16+
## Blog Posts
17+
- [Introducing PostGuard](https://postguard.eu/blog/introducing-postguard/): Overview of PostGuard's IBE approach and why it matters.
18+
- [Looking ahead with PostGuard](https://postguard.eu/blog/looking-ahead-with-postguard/): Roadmap and future plans.
19+
20+
## Developer Resources
21+
- [Developer Documentation](https://docs.postguard.eu/): Architecture, protocols, and integration guides.
22+
- [GitHub Organization](https://github.com/encryption4all): Source code for PostGuard and its ecosystem libraries.
23+
24+
## Key Facts
25+
- Free and open source (MIT license).
26+
- Built by the Digital Security group at Radboud University, Nijmegen, Netherlands.
27+
- Funded by the Dutch Research Council (NWO).
28+
- Uses Yivi (formerly IRMA) for identity verification.
29+
- All encryption happens client-side in the browser — no plaintext is sent to any server.
30+
- Usage limit is 5 GB per 14 days.

0 commit comments

Comments
 (0)