diff --git a/docker/nginx.conf b/docker/nginx.conf
index db4c5f2..91dae72 100644
--- a/docker/nginx.conf
+++ b/docker/nginx.conf
@@ -1,7 +1,7 @@
server {
listen 80;
listen 443 ssl;
- server_name _;
+ server_name bilhej.se www.bilhej.se;
ssl_certificate /etc/nginx/certs/cert.crt;
ssl_certificate_key /etc/nginx/certs/cert.key;
@@ -11,20 +11,59 @@ server {
root /usr/share/nginx/html;
index index.html;
+ # ── Security headers ──────────────────────────────────────────
+ add_header X-Frame-Options "DENY" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
+ # Strict-Transport-Security only when HTTPS is active
+ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
+
+ # ── Gzip ──────────────────────────────────────────────────────
+ gzip on;
+ gzip_types text/plain text/css application/json application/javascript text/xml
+ application/xml text/javascript image/svg+xml text/markdown;
+ gzip_vary on;
+ gzip_min_length 256;
+ gzip_comp_level 6;
+
+ # ── API reverse proxy ─────────────────────────────────────────
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
}
+ # ── Static assets with far-future cache ───────────────────────
+ location ~* \.(?:ico|svg|css|js|woff2?|ttf|eot|png|jpg|jpeg|gif|webp|avif)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ add_header Vary "Accept-Encoding";
+ try_files $uri =404;
+ }
+
+ # ── Robots + sitemap (never cached, fresh every time) ─────────
+ location = /robots.txt {
+ add_header Cache-Control "no-cache, must-revalidate";
+ try_files $uri =404;
+ }
+ location = /sitemap.xml {
+ add_header Cache-Control "no-cache, must-revalidate";
+ try_files $uri =404;
+ }
+
+ # ── SPA fallback — serve index.html for all other routes ──────
location / {
try_files $uri $uri/ /index.html;
}
- gzip on;
- gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
- gzip_vary on;
- gzip_min_length 256;
+ # ── Deny access to dotfiles ───────────────────────────────────
+ location ~ /\. {
+ deny all;
+ return 404;
+ }
}
diff --git a/frontend/index.html b/frontend/index.html
index a4f349f..821f2c0 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,12 +4,143 @@
-
+
+
+
Bilhej — Skicka brev till fordonsägare via registreringsnummer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Bilhej — Skicka brev till fordonsägare
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt
new file mode 100644
index 0000000..3d9cbda
--- /dev/null
+++ b/frontend/public/robots.txt
@@ -0,0 +1,27 @@
+# Allow all crawlers
+User-agent: *
+Crawl-delay: 10
+Disallow: /api/
+Disallow: /admin/
+Disallow: /orders/
+Disallow: /bestallning/*/redigera
+Disallow: /betalning/*
+Disallow: /gast-bestallning*
+Disallow: /gast-betalning/*
+Disallow: /gast-order/*
+Disallow: /andra-losenord
+Disallow: /andra-epost
+Disallow: /bekrafta-epost
+Disallow: /aterstall-losenord
+Disallow: /glomt-losenord
+
+# Allow important pages
+Allow: /
+Allow: /om-oss
+Allow: /om
+Allow: /kontakt
+Allow: /integritetspolicy
+Allow: /villkor
+
+# Sitemap
+Sitemap: https://bilhej.se/sitemap.xml
diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml
new file mode 100644
index 0000000..a5541e5
--- /dev/null
+++ b/frontend/public/sitemap.xml
@@ -0,0 +1,36 @@
+
+
+
+ https://bilhej.se/
+ weekly
+ 1.0
+
+
+ https://bilhej.se/om-oss
+ monthly
+ 0.7
+
+
+ https://bilhej.se/om
+ monthly
+ 0.3
+
+
+ https://bilhej.se/kontakt
+ monthly
+ 0.6
+
+
+ https://bilhej.se/integritetspolicy
+ monthly
+ 0.5
+
+
+ https://bilhej.se/villkor
+ monthly
+ 0.5
+
+
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index ddb678e..63ae1b6 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,7 +1,29 @@
diff --git a/frontend/src/composables/useSeo.ts b/frontend/src/composables/useSeo.ts
new file mode 100644
index 0000000..7e4dd7e
--- /dev/null
+++ b/frontend/src/composables/useSeo.ts
@@ -0,0 +1,95 @@
+/**
+ * useSeo — dynamically updates document and meta tags on route change.
+ *
+ * Usage:
+ * const seo = useSeo()
+ * // On page mount / watch:
+ * watch(route, () => {
+ * seo.set({ title: 'Min sida', description: '...' })
+ * })
+ *
+ * The composable sets , meta[name=description], meta[name=keywords],
+ * meta[property=og:title], meta[property=og:description], meta[name=twitter:title],
+ * and meta[name=twitter:description].
+ */
+
+export interface SeoMeta {
+ title: string
+ description: string
+ keywords?: string
+ ogTitle?: string
+ ogDescription?: string
+ canonical?: string
+}
+
+export function useSeo() {
+ function set(meta: SeoMeta) {
+ const title = meta.title ?? 'Bilhej'
+ const description =
+ meta.description ??
+ 'Skicka brev till fordonsägare via registreringsnummer.'
+
+ // Document title
+ document.title = title
+
+ // Common meta tags
+ updateMeta('description', description)
+ if (meta.keywords) updateMeta('keywords', meta.keywords)
+
+ // Open Graph
+ updateMeta('og:title', meta.ogTitle ?? title, 'property')
+ updateMeta('og:description', meta.ogDescription ?? description, 'property')
+
+ // Twitter
+ updateMeta('twitter:title', meta.ogTitle ?? title, 'name')
+ updateMeta('twitter:description', meta.ogDescription ?? description, 'name')
+
+ // Canonical URL
+ updateCanonical(meta.canonical)
+ }
+
+ function reset() {
+ document.title = 'Bilhej — Skicka brev till fordonsägare'
+ updateMeta(
+ 'description',
+ 'Skicka ett fysiskt brev till en fordonsägare via registreringsnummer.',
+ )
+ updateMeta('og:title', 'Bilhej — Skicka brev till fordonsägare', 'property')
+ updateMeta(
+ 'og:description',
+ 'Skicka ett fysiskt brev till en fordonsägare via registreringsnummer.',
+ 'property',
+ )
+ updateCanonical('https://bilhej.se/')
+ }
+
+ return { set, reset }
+}
+
+function updateMeta(
+ nameOrProperty: string,
+ content: string,
+ attr: 'name' | 'property' = 'name',
+) {
+ let el = document.querySelector(
+ `meta[${attr}="${nameOrProperty}"]`,
+ ) as HTMLMetaElement | null
+ if (!el) {
+ el = document.createElement('meta')
+ el.setAttribute(attr, nameOrProperty)
+ document.head.appendChild(el)
+ }
+ el.setAttribute('content', content)
+}
+
+function updateCanonical(href: string | undefined) {
+ let link = document.querySelector(
+ 'link[rel="canonical"]',
+ ) as HTMLLinkElement | null
+ if (!link) {
+ link = document.createElement('link')
+ link.setAttribute('rel', 'canonical')
+ document.head.appendChild(link)
+ }
+ link.setAttribute('href', href ?? 'https://bilhej.se/')
+}
diff --git a/frontend/src/data/routeSeo.ts b/frontend/src/data/routeSeo.ts
new file mode 100644
index 0000000..6d05d12
--- /dev/null
+++ b/frontend/src/data/routeSeo.ts
@@ -0,0 +1,90 @@
+/**
+ * Route-level SEO metadata for Bilhej.
+ * Each entry maps route.name → { title, description, keywords?, canonical? }
+ * These are applied by App.vue on every route change via useSeo().
+ */
+export interface RouteSeoMeta {
+ title: string
+ description: string
+ keywords?: string
+ /** The full canonical URL for this page */
+ canonical?: string
+}
+
+export const routeSeo: Record = {
+ home: {
+ title: 'Bilhej — Skicka brev till fordonsägare via registreringsnummer',
+ description:
+ 'Skicka ett fysiskt brev till en bilägare med bara registreringsnumret. Välj mall, skriv, betala 49 kr via Swish. Vi postar med spårning.',
+ keywords:
+ 'brev till bilägare, kontakta fordonsägare, parkeringsskada, köp bil, skicka brev, registreringsnummer brev',
+ },
+ about: {
+ title: 'Om Bilhej — Skicka fysiska brev till fordonsägare online',
+ description:
+ 'Bilhej gör det enkelt att nå en bilägare med ett fysiskt brev. Skriv meddelandet, vi sköter utskick och post. Anonymt eller med dina uppgifter.',
+ keywords: 'om bilhej, hur fungerar bilhej, skicka brev till bilägare',
+ },
+ contact: {
+ title: 'Kontakt — Bilhej support, klagomål och allmänna frågor',
+ description:
+ 'Kontakta Bilhej via e-post. Support för beställningar och betalning, allmän kontakt och klagomål.',
+ keywords: 'bilhej kontakt, support bilhej, klagomål',
+ },
+ privacy: {
+ title: 'Integritetspolicy — Bilhej | Personuppgifter och GDPR',
+ description:
+ 'Läs om hur Bilhej hanterar personuppgifter: vad vi samlar in, varför, hur länge och dina rättigheter enligt GDPR.',
+ keywords: 'integritetspolicy bilhej, gdpr, personuppgifter brev',
+ },
+ terms: {
+ title: 'Användarvillkor — Bilhej | Villkor för brevtjänsten',
+ description:
+ 'Villkor för att använda Bilhej. Regler för brev till fordonsägare, betalning, ansvar och reklamation.',
+ keywords: 'användarvillkor bilhej, köpvillkor, brevtjänst villkor',
+ },
+ register: {
+ title: 'Registrera konto — Bilhej | Skapa konto för beställningar',
+ description:
+ 'Skapa ett konto på Bilhej för att följa dina beställningar, se historik och hantera dina brev.',
+ },
+ login: {
+ title: 'Logga in — Bilhej | Dina brev till fordonsägare',
+ description:
+ 'Logga in på Bilhej för att se dina beställningar, status och spårning.',
+ },
+ 'forgot-password': {
+ title: 'Glömt lösenord — Bilhej | Återställ lösenord',
+ description:
+ 'Återställ ditt lösenord på Bilhej. Få en återställningslänk via e-post.',
+ },
+ 'guest-checkout': {
+ title: 'Gästbeställning — Bilhej | Skicka brev utan konto',
+ description:
+ 'Skicka ett brev till en fordonsägare utan att skapa konto. Ange registreringsnummer, skriv meddelande, betala via Swish.',
+ keywords: 'gästbeställning bilhej, utan konto, snabb beställning',
+ },
+ compose: {
+ title: 'Skriv brev — Bilhej | Komponera meddelande till fordonsägare',
+ description:
+ 'Skriv ditt brev till bilägaren. Välj mall för köp, komplimang, parkeringsskada eller tips, eller skriv fritt.',
+ },
+ orders: {
+ title: 'Mina beställningar — Bilhej | Följ dina brev',
+ description:
+ 'Se dina tidigare och pågående beställningar. Följ status och spårning för brev du skickat.',
+ },
+ 'change-password': {
+ title: 'Byt lösenord — Bilhej | Kontoinställningar',
+ description: 'Byt lösenord på ditt Bilhej-konto.',
+ },
+ 'change-email': {
+ title: 'Byt e-postadress — Bilhej | Kontoinställningar',
+ description: 'Byt e-postadress på ditt Bilhej-konto.',
+ },
+ admin: {
+ title: 'Admin — Bilhej | Administrationspanel',
+ description:
+ 'Administration av Bilhej — hantera beställningar och användare.',
+ },
+}