SEO — Vue SSR Starter Kit
Every public page is fully server-rendered with meta, structured data, and hreflang so it ranks and earns rich results. SEO copy lives in i18n; the wiring lives in entry-server.js and server.js. There is no seo.config.js — meta is driven by route meta keys, not a central config file.
How SEO is wired
| Layer |
Where |
Responsibility |
| Route metadata |
src/router.js |
meta.title / meta.description / meta.keywords (i18n keys), meta.robots, meta.statusCode |
| SSR injection |
src/entry-server.js |
generateHead() builds the full <head> per route (meta, canonical, hreflang, OG/Twitter, JSON-LD) |
| Meta copy |
src/translate/{en,fr}.json |
the meta.* strings resolved via t() — edit copy here (via the translate agent) |
| robots / sitemap |
server.js |
dynamic robots.txt + sitemap.xml (NOT static files) |
Meta resolution: resolvePageMeta(route, t) reads route.meta.title/description/keywords as i18n
keys and resolves them with t(). The page title renders as ${pageTitle} — ${APP_NAME}.
Hard rules
robots.txt and sitemap.xml are generated by server.js — never add static copies under public/.
- Every public page gets: a unique title + meta description, a canonical, OG + Twitter tags, and hreflang alternates for
en and fr plus x-default pointing at the en URL.
- 404 pages carry no canonical and no hreflang — driven by
route.meta.statusCode === 404 (the is404 branch in generateHead).
- Non-indexable routes (signup, signin, verify-code, forgot/reset-password, dashboard, account, admin) set
meta.robots: 'noindex, ...' and are Disallowed in robots.txt. JSON-LD is suppressed when meta.robots includes noindex.
- Meta title/description/keywords text lives in i18n (
meta.* keys) — edit copy via the translate agent, never hardcode it in entry-server.js.
JSON-LD structured data (already emitted)
getSchemaMarkup() in entry-server.js emits, for indexable routes only:
- Organization — site-wide (name =
APP_NAME, url, logo, sameAs from SOCIAL_FACEBOOK / SOCIAL_INSTAGRAM / SOCIAL_TELEGRAM env vars).
- WebSite +
SearchAction — on the Index/Home route.
- BreadcrumbList — on deep pages (more than one path segment).
Optional enhancements (not currently emitted — add deliberately if a page warrants it):
SoftwareApplication (to describe the kit as a product), FAQPage, Person. Validate any
structured-data change with Google's Rich Results Test.
Open Graph, Twitter, and head extras
generateHead also emits: OG (og:title/description/url/type=website/site_name/image 1200×630
/og-image.png + og:image:alt + og:locale + og:locale:alternate), fb:app_id (when
FACEBOOK_APP_ID is set), Twitter summary_large_image, theme-color (theme-dependent), author
(= APP_NAME), apple-touch-icon, favicons, and the web manifest.
Environment inputs: NODE_HOST (canonical/site URL, defaults to http://localhost:3002), APP_NAME,
FACEBOOK_APP_ID, SOCIAL_*. Locales come from LOCALE_CODES (en, fr) in src/shared/const.js.
Sitemap coverage (server.js generateSitemap)
Public, indexable URLs only — one <url> per locale with hreflang alternates, <lastmod>,
changefreq, and priority:
publicPaths = ['', '/contact'] → home (priority 1.0) and contact (0.8), each in en and fr,
with x-default pointing at the en URL. Cached in memory (1h TTL).
- All auth/account/admin/dashboard paths and
/api/ are Disallowed in robots.txt.
- When you add a new public, indexable route, add its path to
publicPaths (delegate the
server.js edit to the server agent) and give it meta.title/meta.description i18n keys.
Per-page SEO checklist
- Route has
meta.title + meta.description keys (and meta.keywords if relevant), present in both locales.
- Canonical resolves to the absolute
NODE_HOST URL for the current locale (auto, except 404).
- hreflang
en / fr / x-default present (auto, except 404).
- OG image + type + Twitter card present (auto via
generateHead).
- Indexable? then it is in
publicPaths. Not indexable? then meta.robots: 'noindex, ...' and Disallowed.
- Appropriate JSON-LD emitted (Organization always; WebSite on home; BreadcrumbList on deep pages) and validated.
Performance is SEO
Core Web Vitals affect ranking. The kit already favors SSR with hydration, self-hosted fonts
(@fontsource/inter, @fontsource/ibm-plex-mono — no render-blocking Google Fonts), lazy-loaded
routes, and compression. Flag regressions as ➜ delegate to design (visual/CSS) or note for the
orchestrator (code). ➜ See skill: vue3-performance for client/runtime perf, vue-ssr-deployment for build.
Source: e-xode/vue-ssr — distributed by TomeVault.
1---2name: e-xode-vue-ssr-seo3description: SEO — Vue SSR Starter Kit4---56# SEO — Vue SSR Starter Kit78> Every public page is fully server-rendered with meta, structured data, and hreflang so it ranks and earns rich results. SEO copy lives in i18n; the wiring lives in `entry-server.js` and `server.js`. There is **no `seo.config.js`** — meta is driven by route `meta` keys, not a central config file.910## How SEO is wired1112| Layer | Where | Responsibility |13| ---------------- | ------------------------------ | --------------------------------------------------------------------------- |14| Route metadata | `src/router.js` | `meta.title` / `meta.description` / `meta.keywords` (i18n keys), `meta.robots`, `meta.statusCode` |15| SSR injection | `src/entry-server.js` | `generateHead()` builds the full `<head>` per route (meta, canonical, hreflang, OG/Twitter, JSON-LD) |16| Meta copy | `src/translate/{en,fr}.json` | the `meta.*` strings resolved via `t()` — edit copy here (via the translate agent) |17| robots / sitemap | `server.js` | dynamic `robots.txt` + `sitemap.xml` (NOT static files) |1819Meta resolution: `resolvePageMeta(route, t)` reads `route.meta.title/description/keywords` as i18n20keys and resolves them with `t()`. The page title renders as `${pageTitle} — ${APP_NAME}`.2122## Hard rules23241. **`robots.txt` and `sitemap.xml` are generated by `server.js`** — never add static copies under `public/`.252. **Every public page** gets: a unique title + meta description, a canonical, OG + Twitter tags, and hreflang alternates for `en` and `fr` plus `x-default` pointing at the `en` URL.263. **404 pages carry no canonical and no hreflang** — driven by `route.meta.statusCode === 404` (the `is404` branch in `generateHead`).274. **Non-indexable routes** (signup, signin, verify-code, forgot/reset-password, dashboard, account, admin) set `meta.robots: 'noindex, ...'` and are `Disallow`ed in `robots.txt`. JSON-LD is suppressed when `meta.robots` includes `noindex`.285. **Meta title/description/keywords text lives in i18n** (`meta.*` keys) — edit copy via the `translate` agent, never hardcode it in `entry-server.js`.2930## JSON-LD structured data (already emitted)3132`getSchemaMarkup()` in `entry-server.js` emits, for indexable routes only:3334- **Organization** — site-wide (name = `APP_NAME`, `url`, `logo`, `sameAs` from `SOCIAL_FACEBOOK` / `SOCIAL_INSTAGRAM` / `SOCIAL_TELEGRAM` env vars).35- **WebSite** + `SearchAction` — on the `Index`/`Home` route.36- **BreadcrumbList** — on deep pages (more than one path segment).3738Optional enhancements (not currently emitted — add deliberately if a page warrants it):39`SoftwareApplication` (to describe the kit as a product), `FAQPage`, `Person`. Validate any40structured-data change with Google's Rich Results Test.4142## Open Graph, Twitter, and head extras4344`generateHead` also emits: OG (`og:title/description/url/type=website/site_name/image` 1200×63045`/og-image.png` + `og:image:alt` + `og:locale` + `og:locale:alternate`), `fb:app_id` (when46`FACEBOOK_APP_ID` is set), Twitter `summary_large_image`, `theme-color` (theme-dependent), `author`47(= `APP_NAME`), apple-touch-icon, favicons, and the web manifest.4849Environment inputs: `NODE_HOST` (canonical/site URL, defaults to `http://localhost:3002`), `APP_NAME`,50`FACEBOOK_APP_ID`, `SOCIAL_*`. Locales come from `LOCALE_CODES` (`en`, `fr`) in `src/shared/const.js`.5152## Sitemap coverage (`server.js` `generateSitemap`)5354Public, indexable URLs only — one `<url>` per locale with hreflang alternates, `<lastmod>`,55`changefreq`, and `priority`:5657- `publicPaths = ['', '/contact']` → home (priority 1.0) and contact (0.8), each in `en` and `fr`,58 with `x-default` pointing at the `en` URL. Cached in memory (1h TTL).59- All auth/account/admin/dashboard paths and `/api/` are `Disallow`ed in `robots.txt`.60- When you add a new **public, indexable** route, add its path to `publicPaths` (delegate the61 `server.js` edit to the `server` agent) and give it `meta.title`/`meta.description` i18n keys.6263## Per-page SEO checklist64651. Route has `meta.title` + `meta.description` keys (and `meta.keywords` if relevant), present in both locales.662. Canonical resolves to the absolute `NODE_HOST` URL for the current locale (auto, except 404).673. hreflang `en` / `fr` / `x-default` present (auto, except 404).684. OG image + type + Twitter card present (auto via `generateHead`).695. Indexable? then it is in `publicPaths`. Not indexable? then `meta.robots: 'noindex, ...'` and `Disallow`ed.706. Appropriate JSON-LD emitted (Organization always; WebSite on home; BreadcrumbList on deep pages) and validated.7172## Performance is SEO7374Core Web Vitals affect ranking. The kit already favors SSR with hydration, self-hosted fonts75(`@fontsource/inter`, `@fontsource/ibm-plex-mono` — no render-blocking Google Fonts), lazy-loaded76routes, and `compression`. Flag regressions as ➜ delegate to design (visual/CSS) or note for the77orchestrator (code). ➜ See skill: vue3-performance for client/runtime perf, vue-ssr-deployment for build.7879---80> Source: [e-xode/vue-ssr](https://github.com/e-xode/vue-ssr) — distributed by [TomeVault](https://tomevault.io).81<!-- tomevault:4.0:skill_md:2026-06-16 -->