diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts
index f8e329c..74b2885 100644
--- a/apps/frontend/nuxt.config.ts
+++ b/apps/frontend/nuxt.config.ts
@@ -5,6 +5,8 @@ export default defineNuxtConfig({
"@nuxtjs/color-mode",
"@vueuse/nuxt",
"@regle/nuxt",
+ "@nuxtjs/sitemap",
+ "nuxt-schema-org",
"nuxt-og-image",
"vue-sonner/nuxt",
"@nuxtjs/i18n",
@@ -60,6 +62,25 @@ export default defineNuxtConfig({
},
},
},
+ // SEO site config (consumed by nuxt-site-config — pulled in by sitemap/schema/og).
+ // Single fixed domain — absolute URL so sitemap + schema resolve correctly.
+ site: {
+ url: "https://solitar.link",
+ name: "Solitar",
+ },
+ // Global (H3). No title template is applied (seo-utils intentionally
+ // not used), so page titles render exactly as authored (H4).
+ app: {
+ head: {
+ htmlAttrs: { lang: "en" },
+ },
+ },
+ // Sitemap (C1): single flat /sitemap.xml (one locale — skip i18n index split);
+ // only public pages, exclude utility/dynamic routes.
+ sitemap: {
+ autoI18n: false,
+ exclude: ["/settings", "/unlock"],
+ },
// Build
nitro: {
prerender: {
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 63a5445..e2c8a7e 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -33,12 +33,14 @@
},
"devDependencies": {
"@iconify-json/tabler": "catalog:frontend",
+ "@nuxtjs/sitemap": "catalog:frontend",
"@takumi-rs/core": "catalog:frontend",
"@takumi-rs/wasm": "catalog:frontend",
"@unocss/nuxt": "catalog:frontend",
"@vueuse/core": "catalog:frontend",
"@vueuse/nuxt": "catalog:frontend",
"nuxt-og-image": "catalog:frontend",
+ "nuxt-schema-org": "catalog:frontend",
"oxlint": "catalog:lint",
"oxlint-tsgolint": "catalog:lint",
"unocss": "catalog:frontend",
diff --git a/apps/frontend/src/app/app.vue b/apps/frontend/src/app/app.vue
index d867a77..817c225 100644
--- a/apps/frontend/src/app/app.vue
+++ b/apps/frontend/src/app/app.vue
@@ -1,3 +1,38 @@
+
+
import { parseURL } from "ufo";
+// Redirect resolver route — never index (M1). Does not alter redirect logic.
+useSeoMeta({ robots: "noindex, follow" });
+
const route = useRoute();
const { $api } = useNuxtApp();
const urlRepository = repository($api);
diff --git a/apps/frontend/src/app/pages/index.vue b/apps/frontend/src/app/pages/index.vue
index 29edcf3..a7ffbf7 100644
--- a/apps/frontend/src/app/pages/index.vue
+++ b/apps/frontend/src/app/pages/index.vue
@@ -8,6 +8,16 @@ useSeoMeta({
ogDescription: $t("site.url_shortener.description"),
twitterDescription: $t("site.url_shortener.description"),
});
+
+// Structured data (H2): the app itself
+useSchemaOrg([
+ defineSoftwareApp({
+ name: "Solitar",
+ applicationCategory: "UtilitiesApplication",
+ operatingSystem: "Web",
+ offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
+ }),
+]);
diff --git a/apps/frontend/src/app/pages/settings.vue b/apps/frontend/src/app/pages/settings.vue
index 5414827..e315249 100644
--- a/apps/frontend/src/app/pages/settings.vue
+++ b/apps/frontend/src/app/pages/settings.vue
@@ -10,6 +10,7 @@ useSeoMeta({
description: $t("site.settings.description"),
ogDescription: $t("site.settings.description"),
twitterDescription: $t("site.settings.description"),
+ robots: "noindex, follow",
});
diff --git a/apps/frontend/src/app/pages/unlock.vue b/apps/frontend/src/app/pages/unlock.vue
index 7def25d..1e008d8 100644
--- a/apps/frontend/src/app/pages/unlock.vue
+++ b/apps/frontend/src/app/pages/unlock.vue
@@ -1,4 +1,7 @@