diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt
new file mode 100644
index 0000000..6bd7b9c
--- /dev/null
+++ b/frontend/public/robots.txt
@@ -0,0 +1,21 @@
+User-agent: *
+Allow: /
+
+Disallow: /admin
+Disallow: /admin/
+Disallow: /*/admin
+Disallow: /*/admin/
+Disallow: /order/
+Disallow: /*/order/
+Disallow: /co/
+Disallow: /*/co/
+Disallow: /checkout
+Disallow: /checkout/
+Disallow: /*/checkout
+Disallow: /*/checkout/
+Disallow: /shop
+Disallow: /shop/
+Disallow: /*/shop
+Disallow: /*/shop/
+
+Sitemap: https://3d-fab.ch/sitemap.xml
diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml
new file mode 100644
index 0000000..a1195d2
--- /dev/null
+++ b/frontend/public/sitemap.xml
@@ -0,0 +1,144 @@
+
+
+
+ https://3d-fab.ch/it
+
+
+
+
+
+ weekly
+ 1.0
+
+
+ https://3d-fab.ch/it/calculator/basic
+
+
+
+
+
+ weekly
+ 0.9
+
+
+ https://3d-fab.ch/it/calculator/advanced
+
+
+
+
+
+ weekly
+ 0.8
+
+
+ https://3d-fab.ch/it/about
+
+
+
+
+
+ monthly
+ 0.7
+
+
+ https://3d-fab.ch/it/contact
+
+
+
+
+
+ monthly
+ 0.7
+
+
+ https://3d-fab.ch/it/privacy
+
+
+
+
+
+ yearly
+ 0.4
+
+
+ https://3d-fab.ch/it/terms
+
+
+
+
+
+ yearly
+ 0.4
+
+
diff --git a/frontend/src/app/core/services/seo.service.ts b/frontend/src/app/core/services/seo.service.ts
new file mode 100644
index 0000000..70505ad
--- /dev/null
+++ b/frontend/src/app/core/services/seo.service.ts
@@ -0,0 +1,121 @@
+import { DOCUMENT } from '@angular/common';
+import { Inject, Injectable } from '@angular/core';
+import { Title, Meta } from '@angular/platform-browser';
+import {
+ ActivatedRouteSnapshot,
+ NavigationEnd,
+ Router,
+} from '@angular/router';
+import { filter } from 'rxjs/operators';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class SeoService {
+ private readonly defaultTitle = '3D fab | Stampa 3D su misura';
+ private readonly defaultDescription =
+ 'Stampa 3D su misura con preventivo online immediato. Carica il file, scegli materiale e qualità , ricevi prezzo e tempi in pochi secondi.';
+ private readonly supportedLangs = new Set(['it', 'en', 'de', 'fr']);
+
+ constructor(
+ private router: Router,
+ private titleService: Title,
+ private metaService: Meta,
+ @Inject(DOCUMENT) private document: Document,
+ ) {
+ this.applyRouteSeo(this.router.routerState.snapshot.root);
+ this.router.events
+ .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
+ .subscribe(() => {
+ this.applyRouteSeo(this.router.routerState.snapshot.root);
+ });
+ }
+
+ private applyRouteSeo(rootSnapshot: ActivatedRouteSnapshot): void {
+ const mergedData = this.getMergedRouteData(rootSnapshot);
+ const title = this.asString(mergedData['seoTitle']) ?? this.defaultTitle;
+ const description =
+ this.asString(mergedData['seoDescription']) ?? this.defaultDescription;
+ const robots = this.asString(mergedData['seoRobots']) ?? 'index, follow';
+
+ this.titleService.setTitle(title);
+ this.metaService.updateTag({ name: 'description', content: description });
+ this.metaService.updateTag({ name: 'robots', content: robots });
+ this.metaService.updateTag({ property: 'og:title', content: title });
+ this.metaService.updateTag({
+ property: 'og:description',
+ content: description,
+ });
+ this.metaService.updateTag({ property: 'og:type', content: 'website' });
+ this.metaService.updateTag({ name: 'twitter:card', content: 'summary' });
+
+ const cleanPath = this.getCleanPath(this.router.url);
+ const canonical = `${this.document.location.origin}${cleanPath}`;
+ this.metaService.updateTag({ property: 'og:url', content: canonical });
+ this.updateCanonicalTag(canonical);
+ this.updateLangAndAlternates(cleanPath);
+ }
+
+ private getMergedRouteData(snapshot: ActivatedRouteSnapshot): Record {
+ const merged: Record = {};
+ let cursor: ActivatedRouteSnapshot | null = snapshot;
+ while (cursor) {
+ Object.assign(merged, cursor.data ?? {});
+ cursor = cursor.firstChild;
+ }
+ return merged;
+ }
+
+ private asString(value: unknown): string | undefined {
+ return typeof value === 'string' ? value : undefined;
+ }
+
+ private getCleanPath(url: string): string {
+ const path = (url || '/').split('?')[0].split('#')[0];
+ return path || '/';
+ }
+
+ private updateCanonicalTag(url: string): void {
+ let link = this.document.head.querySelector(
+ 'link[rel="canonical"]',
+ ) as HTMLLinkElement | null;
+ if (!link) {
+ link = this.document.createElement('link');
+ link.setAttribute('rel', 'canonical');
+ this.document.head.appendChild(link);
+ }
+ link.setAttribute('href', url);
+ }
+
+ private updateLangAndAlternates(path: string): void {
+ const segments = path.split('/').filter(Boolean);
+ const firstSegment = segments[0]?.toLowerCase();
+ const hasLang = Boolean(firstSegment && this.supportedLangs.has(firstSegment));
+ const lang = hasLang ? firstSegment : 'it';
+ const suffixSegments = hasLang ? segments.slice(1) : segments;
+ const suffix = suffixSegments.length > 0 ? `/${suffixSegments.join('/')}` : '';
+
+ this.document.documentElement.lang = lang;
+
+ this.document.head
+ .querySelectorAll('link[rel="alternate"][data-seo-managed="true"]')
+ .forEach((node) => node.remove());
+
+ for (const alt of ['it', 'en', 'de', 'fr']) {
+ this.appendAlternateLink(alt, `${this.document.location.origin}/${alt}${suffix}`);
+ }
+ this.appendAlternateLink(
+ 'x-default',
+ `${this.document.location.origin}/it${suffix}`,
+ );
+ }
+
+ private appendAlternateLink(hreflang: string, href: string): void {
+ const link = this.document.createElement('link');
+ link.setAttribute('rel', 'alternate');
+ link.setAttribute('hreflang', hreflang);
+ link.setAttribute('href', href);
+ link.setAttribute('data-seo-managed', 'true');
+ this.document.head.appendChild(link);
+ }
+}