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
42 changes: 38 additions & 4 deletions src/lib/components/SEO.svelte
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
<script>
let { title = '', description = '', ogImage = '', ogType = 'website', canonical = '' } = $props()
import { page } from '$app/state'

let {
title = '',
description = '',
ogImage = '',
ogType = 'website',
canonical = '',
jsonLd = null,
} = $props()

const siteName = 'PostGuard'
const defaultDescription = 'PostGuard offers free and easy-to-use end-to-end encryption for emails and files.'
const siteUrl = 'https://postguard.eu'
const defaultDescription =
'PostGuard offers free and easy-to-use end-to-end encryption for emails and files.'
const defaultImage = '/pg_logo.png'

const canonicalUrl = $derived(
canonical || (page?.url?.pathname ? `${siteUrl}${page.url.pathname}` : '')
)
const ogImageUrl = $derived(
(ogImage || defaultImage).startsWith('http')
? ogImage || defaultImage
: `${siteUrl}${ogImage || defaultImage}`
)
const jsonLdString = $derived(jsonLd ? JSON.stringify(jsonLd) : '')
</script>

<svelte:head>
<title>{title ? `${title} | ${siteName}` : siteName}</title>
<meta name="description" content={description || defaultDescription} />
<meta property="og:title" content={title || siteName} />
<meta property="og:description" content={description || defaultDescription} />
<meta property="og:image" content={ogImage || defaultImage} />
<meta property="og:image" content={ogImageUrl} />
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content={siteName} />
{#if canonical}<link rel="canonical" href={canonical} />{/if}
{#if canonicalUrl}
<meta property="og:url" content={canonicalUrl} />
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hreflang="en" href={canonicalUrl} />
<link rel="alternate" hreflang="nl" href={canonicalUrl} />
<link rel="alternate" hreflang="x-default" href={canonicalUrl} />
{/if}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title || siteName} />
<meta name="twitter:description" content={description || defaultDescription} />
<meta name="twitter:image" content={ogImageUrl} />
{#if jsonLdString}
{@html `<script type="application/ld+json">${jsonLdString}<\/script>`}
{/if}
</svelte:head>
6 changes: 3 additions & 3 deletions src/lib/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,13 @@
"tagline": "Securely send your files",
"upperTextDropZone": "Drag & drop files <br/> or",
"lowerTextDropZone": "click to upload",
"sizeLimitText": "max. 2GB",
"sizeLimitText": "max. 5 GB per 14 days",
"dragText": "Drag files here",
"dropText": "Drop to add files",
"dropMoreText": "Drop to add more files",
"orText": "or",
"chooseFilesButton": "Choose files",
"maxSizeText": "Maximum 2 GB",
"maxSizeText": "5 GB per 14 days",
"addMoreFiles": "Add more files",
"fileSummary": "{count} {count, plural, one {file} other {files}} added, {size} GB remaining"
},
Expand Down Expand Up @@ -223,7 +223,7 @@
"title": "Fill in all required fields",
"continueButton": "Continue filling in",
"noFiles": "You haven't added any files yet",
"filesTooLarge": "The files exceed the maximum size of {max} GB",
"filesTooLarge": "The files exceed the {max} GB limit (14-day window)",
"noEmail": "You haven't filled in an email address for a recipient",
"invalidEmail": "The email address {email} is not valid",
"missingAttribute": "{attribute} of {email} is missing",
Expand Down
6 changes: 3 additions & 3 deletions src/lib/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,13 @@
"fileBox": {
"tagline": "Verstuur je bestanden veilig",
"upperTextDropZone": "Sleep bestanden hierheen <br/> of",
"lowerTextDropZone": "klik om te uploaden <br/> (max. 2GB)",
"lowerTextDropZone": "klik om te uploaden <br/> (max. 5 GB per 14 dagen)",
"dragText": "Sleep bestanden hierheen",
"dropText": "Loslaten om bestanden toe te voegen",
"dropMoreText": "Loslaten om meer bestanden toe te voegen",
"orText": "of",
"chooseFilesButton": "Kies bestanden",
"maxSizeText": "Maximaal 2 GB",
"maxSizeText": "5 GB per 14 dagen",
"addMoreFiles": "Voeg meer bestanden toe",
"fileSummary": "{count} {count, plural, one {bestand} other {bestanden}} toegevoegd, nog {size} GB over"
},
Expand Down Expand Up @@ -222,7 +222,7 @@
"title": "Vul alle verplichte velden in",
"continueButton": "Verder met invullen",
"noFiles": "Je hebt nog geen bestanden toegevoegd",
"filesTooLarge": "De bestanden overschrijden de maximale grootte van {max} GB",
"filesTooLarge": "De bestanden overschrijden de {max} GB limiet (14 dagen)",
"noEmail": "Je hebt nog geen e-mailadres voor een ontvanger ingevuld",
"invalidEmail": "Het e-mailadres {email} is niet geldig",
"missingAttribute": "{attribute} van {email} ontbreekt",
Expand Down
1 change: 1 addition & 0 deletions src/routes/(marketing)/+layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { locale, waitLocale } from 'svelte-i18n'
import { browser } from '$app/environment'

export const prerender = true
export const trailingSlash = 'always'

if (browser) {
const stored = localStorage.getItem('preferredLanguage')
Expand Down
41 changes: 41 additions & 0 deletions src/routes/(marketing)/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,52 @@
contactEl.href = `mailto:${addr}`
}
})

const homepageJsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'SoftwareApplication',
name: 'PostGuard',
description:
'Free, open-source identity-based encryption for emails and files using Yivi identity attributes.',
applicationCategory: 'SecurityApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'EUR',
},
url: 'https://postguard.eu',
license: 'https://opensource.org/licenses/MIT',
softwareRequirements: 'Yivi (IRMA) app',
sameAs: ['https://github.com/encryption4all'],
author: {
'@type': 'Organization',
name: 'Radboud University',
url: 'https://www.ru.nl',
},
},
{
'@type': 'Organization',
name: 'PostGuard',
url: 'https://postguard.eu',
logo: 'https://postguard.eu/pg_logo.png',
sameAs: ['https://github.com/encryption4all'],
parentOrganization: {
'@type': 'Organization',
name: 'Yivi',
url: 'https://yivi.app',
},
},
],
}
</script>

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

<section class="hero">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script>
import { _ } from 'svelte-i18n'
import Tabs from '$lib/components/Tabs.svelte'
import SEO from '$lib/components/SEO.svelte'
// import { fade } from 'svelte/transition'
import { Tween } from 'svelte/motion'
import { cubicOut } from 'svelte/easing'
Expand Down Expand Up @@ -31,6 +32,11 @@

</script>

<SEO
title="PostGuard Addons"
description="Install PostGuard for Thunderbird or Outlook to send and receive end-to-end encrypted emails directly from your mail client."
/>

<div class="page-wrapper">
<div class="grid-container" bind:clientWidth={containerWidth}>
<div class="grid-item header">
Expand Down
34 changes: 34 additions & 0 deletions src/routes/(marketing)/blog/[slug]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
<script lang="ts">
import SEO from '$lib/components/SEO.svelte'
import { page } from '$app/state'

let { data } = $props()

const siteUrl = 'https://postguard.eu'
const articleJsonLd = $derived({
'@context': 'https://schema.org',
'@type': 'Article',
headline: data.metadata.title,
description: data.metadata.description,
datePublished: data.metadata.date,
...(data.metadata.image
? {
image: data.metadata.image.startsWith('http')
? data.metadata.image
: `${siteUrl}${data.metadata.image}`,
}
: {}),
author: {
'@type': data.metadata.author === 'PostGuard Team' ? 'Organization' : 'Person',
name: data.metadata.author || 'PostGuard Team',
},
publisher: {
'@type': 'Organization',
name: 'PostGuard',
logo: {
'@type': 'ImageObject',
url: `${siteUrl}/pg_logo.png`,
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${siteUrl}${page.url.pathname}`,
},
})
</script>

<SEO
title={data.metadata.title}
description={data.metadata.description}
ogType="article"
ogImage={data.metadata.image || ''}
jsonLd={articleJsonLd}
/>

<article class="blog-post">
Expand Down
68 changes: 62 additions & 6 deletions src/routes/sitemap.xml/+server.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,74 @@
export const prerender = true

export function GET() {
const staticPages = ['', '/about', '/privacy', '/blog']
/**
* Static pages with their last-modified dates. Bump these when the page
* content changes meaningfully. Values are ISO-8601 date strings (YYYY-MM-DD).
* @type {{ path: string, lastmod: string, changefreq: string, priority: string }[]}
*/
const staticPages = [
{
path: '',
lastmod: '2026-04-21',
changefreq: 'monthly',
priority: '1.0',
},
{
path: '/about',
lastmod: '2026-04-21',
changefreq: 'monthly',
priority: '0.8',
},
{
path: '/addons',
lastmod: '2026-04-21',
changefreq: 'monthly',
priority: '0.8',
},
{
path: '/privacy',
lastmod: '2026-04-21',
changefreq: 'yearly',
priority: '0.5',
},
{
path: '/blog',
lastmod: '2026-04-21',
changefreq: 'weekly',
priority: '0.7',
},
]

const postFiles = import.meta.glob('/src/content/blog/*.svx', { eager: true })
const blogSlugs = Object.keys(postFiles).map(
(path) => `/blog/${/** @type {string} */ (path.split('/').pop()).replace('.svx', '')}`
/** @type {Record<string, { metadata?: { date?: string } }>} */
const postFiles = /** @type {any} */ (
import.meta.glob('/src/content/blog/*.svx', { eager: true })
)
const blogEntries = Object.entries(postFiles).map(([path, mod]) => {
const slug = /** @type {string} */ (path.split('/').pop()).replace(
'.svx',
''
)
const date = mod?.metadata?.date ?? '2026-04-21'
return {
path: `/blog/${slug}`,
lastmod: String(date).slice(0, 10),
changefreq: 'yearly',
priority: '0.6',
}
})

const entries = [...staticPages, ...blogEntries]

const pages = [...staticPages, ...blogSlugs]
const urls = entries
.map(
(e) =>
` <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>`
)
.join('\n')

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

return new Response(xml, {
Expand Down
5 changes: 5 additions & 0 deletions static/.well-known/security.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Contact: mailto:info@postguard.eu
Expires: 2027-04-21T00:00:00.000Z
Preferred-Languages: en, nl
Canonical: https://postguard.eu/.well-known/security.txt
Policy: https://github.com/encryption4all/postguard-website/security/policy
30 changes: 30 additions & 0 deletions static/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# PostGuard

> 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.

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.

## Key Pages
- [Homepage](https://postguard.eu/): Overview of PostGuard, its core features, and the product family.
- [About](https://postguard.eu/about/): How PostGuard works, the Radboud University team, and funding from NWO.
- [File Sharing](https://postguard.eu/fileshare/): Web app for sending end-to-end encrypted files to any email address.
- [Decrypt](https://postguard.eu/decrypt/): Web app for decrypting PostGuard-encrypted messages and files.
- [Addons](https://postguard.eu/addons/): Browser and mail-client addons (Thunderbird, Outlook) for encrypted email.
- [Privacy Policy](https://postguard.eu/privacy/): How PostGuard handles user data and attributes.
- [Blog](https://postguard.eu/blog/): Posts about PostGuard, identity-based encryption, and secure communication.

## Blog Posts
- [Introducing PostGuard](https://postguard.eu/blog/introducing-postguard/): Overview of PostGuard's IBE approach and why it matters.
- [Looking ahead with PostGuard](https://postguard.eu/blog/looking-ahead-with-postguard/): Roadmap and future plans.

## Developer Resources
- [Developer Documentation](https://docs.postguard.eu/): Architecture, protocols, and integration guides.
- [GitHub Organization](https://github.com/encryption4all): Source code for PostGuard and its ecosystem libraries.

## Key Facts
- Free and open source (MIT license).
- Built by the Digital Security group at Radboud University, Nijmegen, Netherlands.
- Funded by the Dutch Research Council (NWO).
- Uses Yivi (formerly IRMA) for identity verification.
- All encryption happens client-side in the browser — no plaintext is sent to any server.
- Usage limit is 5 GB per 14 days.
2 changes: 1 addition & 1 deletion svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const config = {
kit: {
adapter: adapter({ fallback: '200.html', precompress: false }),
prerender: {
entries: ['/', '/about', '/privacy', '/blog', '/sitemap.xml']
entries: ['/', '/about', '/addons', '/privacy', '/blog', '/sitemap.xml']
}
},
}
Expand Down
Loading