fimo 2.4.0-staging.1 → 2.4.0-staging.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/assets/skills/fimo-cli/SKILL.md +2 -2
  2. package/assets/skills/fimo-cli/references/branches.md +4 -4
  3. package/assets/skills/fimo-cli/references/deploy.md +2 -5
  4. package/assets/skills/fimo-cli/references/domains.md +5 -5
  5. package/assets/skills/fimo-cli/references/hosting.md +5 -4
  6. package/assets/skills/fimo-cli/references/preview.md +1 -1
  7. package/assets/skills/fimo-cli/references/projects.md +5 -5
  8. package/assets/skills/fimo-code/SKILL.md +1 -0
  9. package/assets/skills/fimo-code/references/assets.md +4 -2
  10. package/assets/skills/fimo-code/references/best-practices/next.md +0 -2
  11. package/assets/skills/fimo-code/references/content/schema.md +4 -1
  12. package/assets/skills/fimo-code/references/design.md +2 -2
  13. package/assets/skills/fimo-code/references/frameworks/astro.md +37 -1
  14. package/assets/skills/fimo-code/references/frameworks/hosting.md +23 -2
  15. package/assets/skills/fimo-code/references/frameworks/next/app-router.md +7 -21
  16. package/assets/skills/fimo-code/references/frameworks/next/seo.md +168 -0
  17. package/assets/skills/fimo-code/references/frameworks/next.md +1 -1
  18. package/assets/skills/fimo-code/references/frameworks/seo-head.md +78 -0
  19. package/assets/skills/fimo-code/references/frameworks/sveltekit.md +57 -4
  20. package/assets/skills/fimo-migration/SKILL.md +33 -33
  21. package/assets/skills/fimo-migration/references/content.md +1 -1
  22. package/assets/skills/fimo-migration/references/frameworks/nextjs.md +7 -0
  23. package/assets/skills/fimo-migration/references/import-contract.md +4 -6
  24. package/assets/skills/fimo-migration/references/manifest.md +4 -3
  25. package/assets/skills/fimo-migration/references/qa.md +6 -0
  26. package/assets/skills/fimo-migration/references/questions-and-improvements.md +1 -1
  27. package/assets/skills/fimo-migration/references/workflow.md +7 -5
  28. package/assets/skills/fimo-studio/references/billing-and-credits.md +7 -4
  29. package/assets/skills/fimo-studio/references/seo-settings.md +2 -0
  30. package/dist/cli/bundle.json +2 -2
  31. package/dist/cli/index.js +494 -978
  32. package/dist/runtime/seo/head.d.ts +31 -0
  33. package/dist/runtime/seo/head.js +47 -0
  34. package/dist/runtime/seo/index.d.ts +2 -0
  35. package/dist/runtime/seo/index.js +1 -0
  36. package/dist/runtime/seo/resolver.d.ts +16 -2
  37. package/dist/runtime/seo/resolver.js +10 -0
  38. package/dist/runtime/seo/server.d.ts +13 -0
  39. package/dist/runtime/seo/server.js +23 -0
  40. package/dist/runtime/shared/fimo-config.server.d.ts +2 -0
  41. package/dist/runtime/shared/fimo-config.server.js +18 -14
  42. package/package.json +12 -8
  43. package/templates/astro/.fimo/config.json +1 -0
  44. package/templates/astro/AGENTS.md +1 -1
  45. package/templates/astro/astro.config.mjs +1 -0
  46. package/templates/astro/package.json +2 -2
  47. package/templates/astro/src/layouts/Base.astro +20 -4
  48. package/templates/astro/src/pages/[locale]/index.astro +1 -1
  49. package/templates/astro/src/pages/[locale]/projects/[slug].astro +4 -1
  50. package/templates/astro/src/pages/index.astro +1 -1
  51. package/templates/astro/src/pages/projects/[slug].astro +4 -1
  52. package/templates/nextjs/AGENTS.md +1 -1
  53. package/templates/nextjs/package.json +3 -3
  54. package/templates/nextjs/src/app/layout.tsx +5 -3
  55. package/templates/nextjs/src/app/page.tsx +6 -0
  56. package/templates/react-router/package.json +4 -4
  57. package/templates/sveltekit/AGENTS.md +1 -1
  58. package/templates/sveltekit/package.json +2 -2
  59. package/templates/sveltekit/src/routes/+layout.server.ts +4 -0
  60. package/templates/sveltekit/src/routes/+layout.svelte +12 -0
  61. package/templates/sveltekit/src/routes/[[locale=locale]]/+page.svelte +0 -5
  62. package/templates/vite/package.json +4 -4
  63. package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +0 -595
@@ -0,0 +1,31 @@
1
+ import type { FimoSEO } from '../shared/fimo-config.types.js';
2
+ export type FimoHeadMeta = {
3
+ name: string;
4
+ content: string;
5
+ } | {
6
+ property: string;
7
+ content: string;
8
+ };
9
+ export interface FimoHeadLink {
10
+ rel: string;
11
+ href: string;
12
+ }
13
+ /** Document head for one page: spread each entry onto a `<meta>` or `<link>`. */
14
+ export interface FimoHead {
15
+ title?: string;
16
+ meta: FimoHeadMeta[];
17
+ links: FimoHeadLink[];
18
+ }
19
+ export interface SeoHeadOptions {
20
+ /** Final canonical URL chosen by the app, also used for og:url. Omit to emit neither tag. */
21
+ canonicalUrl?: string;
22
+ }
23
+ /**
24
+ * Convert final SEO values to native tag attributes without I/O, route resolution
25
+ * or merging. The app resolves content and policy first. Absent/empty fields emit
26
+ * no tags; omitted SEO emits an empty head unless a canonical URL is supplied.
27
+ * `seo.url` is a site base, not a page canonical: only options.canonicalUrl emits
28
+ * canonical/og:url. Render attributes through the framework, never as raw HTML.
29
+ */
30
+ export declare function toSeoHead(seo?: FimoSEO, options?: SeoHeadOptions): FimoHead;
31
+ //# sourceMappingURL=head.d.ts.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Convert final SEO values to native tag attributes without I/O, route resolution
3
+ * or merging. The app resolves content and policy first. Absent/empty fields emit
4
+ * no tags; omitted SEO emits an empty head unless a canonical URL is supplied.
5
+ * `seo.url` is a site base, not a page canonical: only options.canonicalUrl emits
6
+ * canonical/og:url. Render attributes through the framework, never as raw HTML.
7
+ */
8
+ export function toSeoHead(seo = {}, options = {}) {
9
+ const canonical = options.canonicalUrl;
10
+ if (!Object.values(seo).some(Boolean) && !canonical) {
11
+ return { meta: [], links: [] };
12
+ }
13
+ const meta = [];
14
+ const named = (name, content) => {
15
+ if (content) {
16
+ meta.push({ name, content });
17
+ }
18
+ };
19
+ const property = (name, content) => {
20
+ if (content) {
21
+ meta.push({ property: name, content });
22
+ }
23
+ };
24
+ named('description', seo.description);
25
+ named('robots', seo.robots);
26
+ named('theme-color', seo.themeColor);
27
+ property('og:type', 'website');
28
+ property('og:site_name', seo.siteName);
29
+ property('og:title', seo.title);
30
+ property('og:description', seo.description);
31
+ property('og:url', canonical);
32
+ property('og:image', seo.image);
33
+ property('og:locale', seo.locale);
34
+ named('twitter:card', 'summary_large_image');
35
+ named('twitter:site', seo.twitterSite);
36
+ named('twitter:title', seo.title);
37
+ named('twitter:description', seo.description);
38
+ named('twitter:image', seo.image);
39
+ const links = [];
40
+ if (canonical) {
41
+ links.push({ rel: 'canonical', href: canonical });
42
+ }
43
+ if (seo.favicon) {
44
+ links.push({ rel: 'icon', href: seo.favicon }, { rel: 'apple-touch-icon', href: seo.favicon });
45
+ }
46
+ return { ...(seo.title ? { title: seo.title } : {}), meta, links };
47
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './resolver.js';
2
+ export * from './head.js';
3
+ export type { FimoSEO } from '../shared/fimo-config.types.js';
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1,2 @@
1
1
  export * from './resolver.js';
2
+ export * from './head.js';
@@ -1,4 +1,4 @@
1
- import type { FimoConfig, FimoRouteConfig, FimoSEO } from '../shared/fimo-config.types.js';
1
+ import type { FimoRouteConfig, FimoSEO, RouteSEO } from '../shared/fimo-config.types.js';
2
2
  /**
3
3
  * Shared SEO resolver for the generated app.
4
4
  *
@@ -40,5 +40,19 @@ export declare function matchRoutePath(pathname: string, pathPattern: string): {
40
40
  export declare function resolveRouteConfig(pathname: string, routes: Record<string, FimoRouteConfig> | undefined): FimoRouteConfig | undefined;
41
41
  export declare function getNestedValue(input: unknown, path: string): unknown;
42
42
  export declare function resolveTemplate(template: string, context: Record<string, unknown>): string;
43
- export declare function resolveSeoForPath(pathname: string, config: FimoConfig, routeData?: unknown): FimoSEO | undefined;
43
+ /** SEO slices of an imported JSON config; unrelated framework literal unions need not be preserved. */
44
+ export interface FimoSeoConfig {
45
+ seo?: FimoSEO;
46
+ i18n?: {
47
+ defaultLocale?: string;
48
+ locales?: string[];
49
+ };
50
+ routes?: Record<string, {
51
+ path?: string;
52
+ label?: string;
53
+ seo?: RouteSEO;
54
+ params?: object;
55
+ }>;
56
+ }
57
+ export declare function resolveSeoForPath(pathname: string, config: FimoSeoConfig, routeData?: unknown): FimoSEO | undefined;
44
58
  //# sourceMappingURL=resolver.d.ts.map
@@ -86,6 +86,16 @@ export function resolveTemplate(template, context) {
86
86
  }
87
87
  function resolveSeoField(routeValue, globalValue, context) {
88
88
  if (routeValue) {
89
+ for (const token of routeValue.matchAll(/\{\{([^}]+)\}\}/g)) {
90
+ const raw = getNestedValue(context, token[1].trim());
91
+ const value = raw instanceof String || raw instanceof Number || raw instanceof Boolean ? raw.valueOf() : raw;
92
+ if (value == null ||
93
+ (typeof value === 'string' && value.trim() === '') ||
94
+ !['string', 'number', 'boolean'].includes(typeof value) ||
95
+ (typeof value === 'number' && !Number.isFinite(value))) {
96
+ return globalValue;
97
+ }
98
+ }
89
99
  return resolveTemplate(routeValue, context);
90
100
  }
91
101
  return globalValue;
@@ -0,0 +1,13 @@
1
+ import type { FimoSEO } from '../shared/fimo-config.types.js';
2
+ export interface GetFimoSeoOptions {
3
+ pathname?: string;
4
+ routeData?: Record<string, unknown>;
5
+ rootDir?: string;
6
+ }
7
+ /**
8
+ * Read global SEO, or resolve an explicit pathname against the project's route
9
+ * catalog. Node only. Missing config/SEO returns undefined; other read and JSON
10
+ * errors propagate. No cache: rendering/revalidation remains application-owned.
11
+ */
12
+ export declare function getFimoSeo({ pathname, routeData, rootDir }?: GetFimoSeoOptions): FimoSEO | undefined;
13
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { readConfigServer } from '../shared/fimo-config.server.js';
2
+ import { resolveSeoForPath } from './resolver.js';
3
+ /**
4
+ * Read global SEO, or resolve an explicit pathname against the project's route
5
+ * catalog. Node only. Missing config/SEO returns undefined; other read and JSON
6
+ * errors propagate. No cache: rendering/revalidation remains application-owned.
7
+ */
8
+ export function getFimoSeo({ pathname, routeData, rootDir = process.cwd() } = {}) {
9
+ let config;
10
+ try {
11
+ config = readConfigServer(rootDir);
12
+ }
13
+ catch (error) {
14
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
15
+ return undefined;
16
+ }
17
+ throw error;
18
+ }
19
+ if (pathname !== undefined) {
20
+ return resolveSeoForPath(pathname, config, routeData);
21
+ }
22
+ return config.seo && Object.keys(config.seo).length > 0 ? config.seo : undefined;
23
+ }
@@ -4,6 +4,8 @@ import type { FimoConfig } from './fimo-config.types.js';
4
4
  * Reads JSON directly to avoid virtual module issues during config loading.
5
5
  */
6
6
  export declare function getConfigServer(rootDir: string): FimoConfig;
7
+ /** Read the selected config without swallowing I/O or JSON errors. */
8
+ export declare function readConfigServer(rootDir: string): FimoConfig;
7
9
  export declare function getConfigVersion(rootDir: string): number;
8
10
  /** Resolve the current config path while keeping legacy projects readable. */
9
11
  export declare function resolveFimoConfigPath(rootDir: string): string;
@@ -13,25 +13,29 @@ const defaultConfig = {
13
13
  */
14
14
  export function getConfigServer(rootDir) {
15
15
  try {
16
- const configPath = resolveFimoConfigPath(rootDir);
17
- const content = readFileSync(configPath, 'utf-8');
18
- // `fimo validate` owns strict config validation. The public runtime only
19
- // reads the already-validated values and must stay self-contained: importing
20
- // the repository-private API contracts package would break installed apps.
21
- const raw = JSON.parse(content);
22
- return {
23
- runtime: raw.runtime,
24
- hosting: raw.hosting,
25
- seo: raw.seo,
26
- i18n: raw.i18n,
27
- routes: raw.routes,
28
- version: raw.version,
29
- };
16
+ return readConfigServer(rootDir);
30
17
  }
31
18
  catch {
32
19
  return defaultConfig;
33
20
  }
34
21
  }
22
+ /** Read the selected config without swallowing I/O or JSON errors. */
23
+ export function readConfigServer(rootDir) {
24
+ const configPath = resolveFimoConfigPath(rootDir);
25
+ const content = readFileSync(configPath, 'utf-8');
26
+ // `fimo validate` owns strict config validation. The public runtime only
27
+ // reads the already-validated values and must stay self-contained: importing
28
+ // the repository-private API contracts package would break installed apps.
29
+ const raw = JSON.parse(content);
30
+ return {
31
+ runtime: raw.runtime,
32
+ hosting: raw.hosting,
33
+ seo: raw.seo,
34
+ i18n: raw.i18n,
35
+ routes: raw.routes,
36
+ version: raw.version,
37
+ };
38
+ }
35
39
  export function getConfigVersion(rootDir) {
36
40
  try {
37
41
  const configPath = resolveFimoConfigPath(rootDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fimo",
3
- "version": "2.4.0-staging.1",
3
+ "version": "2.4.0-staging.3",
4
4
  "description": "Fimo CLI - create, deploy, and manage Fimo projects",
5
5
  "keywords": [
6
6
  "astro-component"
@@ -57,6 +57,10 @@
57
57
  "types": "./dist/runtime/seo/index.d.ts",
58
58
  "import": "./dist/runtime/seo/index.js"
59
59
  },
60
+ "./seo/server": {
61
+ "types": "./dist/runtime/seo/server.d.ts",
62
+ "import": "./dist/runtime/seo/server.js"
63
+ },
60
64
  "./sync": {
61
65
  "types": "./dist/scripts/index.d.ts",
62
66
  "import": "./dist/scripts/index.js"
@@ -189,13 +193,13 @@
189
193
  "devDependencies": {
190
194
  "@clack/core": "^1.2.0",
191
195
  "@clack/prompts": "^1.2.0",
192
- "@fimo/agents-bundle": "2.4.0-staging.1",
193
- "@fimo/api-contracts": "2.4.0-staging.1",
194
- "@fimo/cli-kit": "2.4.0-staging.1",
195
- "@fimo/framework-detect": "2.4.0-staging.1",
196
- "@fimo/routes": "2.4.0-staging.1",
197
- "@fimo/sdk": "2.4.0-staging.1",
198
- "@fimo/tsconfig": "2.4.0-staging.1",
196
+ "@fimo/agents-bundle": "2.4.0-staging.3",
197
+ "@fimo/api-contracts": "2.4.0-staging.3",
198
+ "@fimo/cli-kit": "2.4.0-staging.3",
199
+ "@fimo/framework-detect": "2.4.0-staging.3",
200
+ "@fimo/routes": "2.4.0-staging.3",
201
+ "@fimo/sdk": "2.4.0-staging.3",
202
+ "@fimo/tsconfig": "2.4.0-staging.3",
199
203
  "@types/node": "22.18.6",
200
204
  "agent-install": "^0.0.5",
201
205
  "astro": "^7.1.6",
@@ -18,6 +18,7 @@
18
18
  "project": {
19
19
  "label": "Project",
20
20
  "path": "/:locale/projects/:slug",
21
+ "seo": { "title": "{{project.title}}", "description": "{{project.summary}}" },
21
22
  "params": {
22
23
  "locale": { "type": "locale", "optional": true },
23
24
  "slug": { "schema": "Project", "queryField": "slug", "displayField": "title" }
@@ -31,7 +31,7 @@ a code-only task, or use `npx fimo@latest` for project-local code generation and
31
31
  - Treat the user's language request as part of setup. The first explicitly requested locale is the primary locale. Load `fimo skills get fimo-cli --ref locales` and use `fimo locales` to configure the exact locale set and default. Never edit `i18n.defaultLocale` or `i18n.locales` directly. If no language is requested, keep `en` and state that choice. Astro reads the synchronized config, keeps the default locale unprefixed, and prefixes the others.
32
32
  - Preserve both the unprefixed default pages and the `[locale]` page tree. Astro has no optional single-segment file route, so the paired native pages keep one-locale URLs clean and allow another locale without changing the route shape. Use `Astro.currentLocale` for Fimo content reads, `getLabels`, and document language.
33
33
  - Keep `.fimo/config.json#routes` as Studio metadata aligned with Astro's page files. It does not configure Astro routing.
34
- - Astro owns routes, locale resolution, links, HTML language/direction, and SEO. Fimo connects to those decisions.
34
+ - Astro owns routes, locale resolution, links and SEO. Base.astro resolves the actual path with `resolveSeoForPath` and renders `toSeoHead` from `fimo/seo`. Content pages pass already-resolved `seo` with their route data. Keep title/social tags in agreement and preserve the app's canonical policy.
35
35
  - Use generated schema clients, `getLabels({ locale })`, and native components from `fimo/astro/components` for editable values.
36
36
  - Do not add React or a second deployment platform.
37
37
  - Never edit `.fimo/project.json`; it links this directory to the remote project.
@@ -4,6 +4,7 @@ import { fimo } from 'fimo-astro/config';
4
4
  import fimoConfig from './.fimo/config.json' with { type: 'json' };
5
5
 
6
6
  export default defineConfig({
7
+ devToolbar: { enabled: false },
7
8
  integrations: [fimo()],
8
9
  i18n: {
9
10
  defaultLocale: fimoConfig.i18n.defaultLocale,
@@ -9,8 +9,8 @@
9
9
  "check": "astro sync && astro check"
10
10
  },
11
11
  "dependencies": {
12
- "fimo": "2.4.0-staging.1",
13
- "fimo-astro": "2.4.0-staging.1"
12
+ "fimo": "2.4.0-staging.3",
13
+ "fimo-astro": "2.4.0-staging.3"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@astrojs/check": "^0.9.10",
@@ -1,10 +1,25 @@
1
1
  ---
2
+ import { resolveSeoForPath, toSeoHead, type FimoSEO } from 'fimo/seo';
3
+
4
+ import fimoConfig from '../../.fimo/config.json';
5
+
2
6
  interface Props {
3
7
  lang: string;
4
- title: string;
8
+ title?: string;
9
+ description?: string;
10
+ seo?: FimoSEO;
11
+ canonicalUrl?: string | null;
5
12
  }
6
13
 
7
- const { lang, title } = Astro.props;
14
+ const { lang, title, description, seo = resolveSeoForPath(Astro.url.pathname, fimoConfig) } = Astro.props;
15
+ const canonicalUrl = Astro.props.canonicalUrl === undefined
16
+ ? (seo?.url ? new URL(Astro.url.pathname, seo.url).href : undefined)
17
+ : Astro.props.canonicalUrl ?? undefined;
18
+ const head = toSeoHead({
19
+ ...seo,
20
+ ...(title !== undefined ? { title } : {}),
21
+ ...(description !== undefined ? { description } : {}),
22
+ }, { canonicalUrl });
8
23
  ---
9
24
 
10
25
  <!doctype html>
@@ -12,10 +27,11 @@ const { lang, title } = Astro.props;
12
27
  <head>
13
28
  <meta charset="UTF-8" />
14
29
  <meta name="viewport" content="width=device-width" />
15
- <meta name="description" content="A new Fimo project built with Astro." />
30
+ <title>{head.title ?? 'Fimo + Astro'}</title>
31
+ {head.meta.map((attributes) => <meta {...attributes} />)}
32
+ {head.links.map((attributes) => <link {...attributes} />)}
16
33
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
17
34
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
18
- <title>{title}</title>
19
35
  </head>
20
36
  <body>
21
37
  <slot />
@@ -14,6 +14,6 @@ if (!routeLocale || !prefixedLocales.has(routeLocale)) {
14
14
  const locale = Astro.currentLocale ?? routeLocale;
15
15
  ---
16
16
 
17
- <Base lang={locale} title="Fimo + Astro">
17
+ <Base lang={locale}>
18
18
  <StarterHome />
19
19
  </Base>
@@ -1,7 +1,9 @@
1
1
  ---
2
2
  import { getLabels } from 'fimo/astro';
3
3
  import { Text } from 'fimo/astro/components';
4
+ import { resolveSeoForPath } from 'fimo/seo';
4
5
 
6
+ import config from '../../../../.fimo/config.json';
5
7
  import Base from '../../../layouts/Base.astro';
6
8
  import { prefixedLocales } from '../../../locales';
7
9
  import * as Project from '../../../schemas/Project';
@@ -22,9 +24,10 @@ const [project, labels] = await Promise.all([Project.getBySlug(slug, { locale })
22
24
  if (!project) {
23
25
  return Astro.redirect('/404');
24
26
  }
27
+ const seo = resolveSeoForPath(Astro.url.pathname, config, { project });
25
28
  ---
26
29
 
27
- <Base lang={locale} title={String(project.title)}>
30
+ <Base lang={locale} seo={seo} title={seo?.title ?? String(project.title)}>
28
31
  <main data-framework="astro" data-rendering="server" data-project-slug={slug}>
29
32
  <a href={`/${locale}/projects`}><Text as="span" value={labels.t('navigation.projects')} /></a>
30
33
  <article>
@@ -8,6 +8,6 @@ export const prerender = true;
8
8
  const locale = Astro.currentLocale ?? defaultLocale;
9
9
  ---
10
10
 
11
- <Base lang={locale} title="Fimo + Astro">
11
+ <Base lang={locale}>
12
12
  <StarterHome />
13
13
  </Base>
@@ -1,7 +1,9 @@
1
1
  ---
2
2
  import { getLabels } from 'fimo/astro';
3
3
  import { Text } from 'fimo/astro/components';
4
+ import { resolveSeoForPath } from 'fimo/seo';
4
5
 
6
+ import config from '../../../.fimo/config.json';
5
7
  import Base from '../../layouts/Base.astro';
6
8
  import { defaultLocale } from '../../locales';
7
9
  import * as Project from '../../schemas/Project';
@@ -15,9 +17,10 @@ const [project, labels] = await Promise.all([Project.getBySlug(slug, { locale })
15
17
  if (!project) {
16
18
  return Astro.redirect('/404');
17
19
  }
20
+ const seo = resolveSeoForPath(Astro.url.pathname, config, { project });
18
21
  ---
19
22
 
20
- <Base lang={locale} title={String(project.title)}>
23
+ <Base lang={locale} seo={seo} title={seo?.title ?? String(project.title)}>
21
24
  <main data-framework="astro" data-rendering="server" data-project-slug={slug}>
22
25
  <a href="/projects"><Text as="span" value={labels.t('navigation.projects')} /></a>
23
26
  <article>
@@ -40,7 +40,7 @@ The scaffolded homepage is disposable. Replace it on the first real generation.
40
40
  - Next's `app` tree is the router and stays authoritative: the default shell is one ordinary root layout plus `/` as the homepage, and unknown paths get Next's native 404. When a product requires locale URLs, implement the framework-native strategy (`[locale]` segments, middleware, cookies, domains, or an i18n library), pass the resolved locale into Fimo server reads and `FimoProvider`, and load `fimo skills get fimo-code --ref frameworks/next/locales` first. Do not impose locale-prefixed URLs on a default-only project.
41
41
  - `FimoProvider` owns the optional Studio preview runtime. Do not add a preview bridge component, read Fimo preview environment variables, or inject Fimo scripts in application code.
42
42
  - Keep `.fimo/config.json#routes` as Studio metadata, not as the Next.js router. Describe only URLs the application actually serves; add a `:locale` segment to a route's metadata only after the application implements that locale routing. There, `params.locale.optional: true` means omit the configured default locale from the URL; use `optional: false` only when every locale, including the default, must be prefixed.
43
- - The application owns routes, locale resolution, links, HTML language/direction, and per-page SEO overrides. Fimo supplies the site-wide SEO defaults (title, description, favicon, share image) from `.fimo/config.json#seo` through the root layout's `generateMetadata`/`generateViewport` (`buildFimoMetadata`/`buildFimoViewport` from `fimo-next`); keep that wiring, and add page-level `metadata`/`generateMetadata` exports where a page needs its own values — they merge over the defaults.
43
+ - The application owns SEO policy. Keep the root's global SEO wiring and the homepage's explicit route wiring. When creating or adding a page, load `fimo skills get fimo-code --ref frameworks/next/seo` and reuse the established route pattern. Resolve the real pathname and content in that route module; add route viewport wiring only for route-specific theme color. Preserve app-owned metadata and file-based images; Next replaces nested metadata objects rather than merging their fields.
44
44
  - Do not add a second deployment platform.
45
45
  - Never edit `.fimo/project.json`; it links this directory to the remote project.
46
46
  - Keep `.fimo/config.json#runtime` true to the code: framework, `dev`/`build`/`install` commands, output directory. Update it when you rename a package script, move the build output, or change framework; the reference is `fimo skills get fimo-code --ref frameworks/hosting`.
@@ -12,9 +12,9 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@tanstack/react-query": "^5.80.6",
15
- "fimo": "2.4.0-staging.1",
16
- "fimo-next": "2.4.0-staging.1",
17
- "fimo-react": "2.4.0-staging.1",
15
+ "fimo": "2.4.0-staging.3",
16
+ "fimo-next": "2.4.0-staging.3",
17
+ "fimo-react": "2.4.0-staging.3",
18
18
  "next": "^16.3.3",
19
19
  "react": "^19.2.7",
20
20
  "react-dom": "^19.2.7"
@@ -1,12 +1,14 @@
1
- import { FimoProvider, buildFimoMetadata, buildFimoViewport } from 'fimo-next';
1
+ import { FimoProvider } from 'fimo-next';
2
+ import { toNextMetadata, toNextViewport } from 'fimo-next/seo';
3
+ import { getFimoSeo } from 'fimo/seo/server';
2
4
  import type { ReactNode } from 'react';
3
5
 
4
6
  import { defaultLocale } from '../locales';
5
7
 
6
8
  import './globals.css';
7
9
 
8
- export const generateMetadata = () => buildFimoMetadata();
9
- export const generateViewport = () => buildFimoViewport();
10
+ export const generateMetadata = () => toNextMetadata(getFimoSeo());
11
+ export const generateViewport = () => toNextViewport(getFimoSeo());
10
12
 
11
13
  export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
12
14
  return (
@@ -1,3 +1,9 @@
1
+ import { toNextMetadata, toNextViewport } from 'fimo-next/seo';
2
+ import { getFimoSeo } from 'fimo/seo/server';
3
+
4
+ export const generateMetadata = () => toNextMetadata(getFimoSeo({ pathname: '/' }));
5
+ export const generateViewport = () => toNextViewport(getFimoSeo({ pathname: '/' }));
6
+
1
7
  export default function HomePage() {
2
8
  return (
3
9
  <div className="starter">
@@ -21,10 +21,10 @@
21
21
  "cn": "0.2.4",
22
22
  "date-fns": "^4.1.0",
23
23
  "embla-carousel-react": "^8.6.0",
24
- "fimo": "2.4.0-staging.1",
25
- "fimo-react": "2.4.0-staging.1",
26
- "fimo-react-router": "2.4.0-staging.1",
27
- "fimo-vite": "2.4.0-staging.1",
24
+ "fimo": "2.4.0-staging.3",
25
+ "fimo-react": "2.4.0-staging.3",
26
+ "fimo-react-router": "2.4.0-staging.3",
27
+ "fimo-vite": "2.4.0-staging.3",
28
28
  "input-otp": "^1.4.2",
29
29
  "isbot": "^5",
30
30
  "lucide-react": "^0.577.0",
@@ -32,7 +32,7 @@ a code-only task, or use `npx fimo@latest` for project-local code generation and
32
32
  - Preserve the native `[[locale=locale]]` route boundary. The optional parameter keeps the default locale at `/`; the matcher prevents ordinary top-level routes from being mistaken for locales. The root server layout passes the tracked `event.params` through `resolveLocale`, then sends that locale to `getLabels` and `FimoProvider`. Remove the extra brackets only when every locale must be prefixed.
33
33
  - Keep the document language request-aware: `src/hooks.server.ts` renders the initial `<html lang>`, and the root layout updates it after client navigation.
34
34
  - Keep `.fimo/config.json#routes` as Studio metadata aligned with SvelteKit's route files. It does not configure SvelteKit routing.
35
- - The application owns routes, locale resolution, links, HTML language/direction, and SEO. Fimo connects to those decisions.
35
+ - The application owns routes, locale resolution, links and SEO. Return final `seoHead` from route loads using `resolveSeoForPath` and `toSeoHead` from `fimo/seo`. The root renders `page.data.seoHead` once. Content pages supply route data; do not add competing page head tags. Check client navigation for stale tags.
36
36
  - Do not add a second deployment platform.
37
37
  - Never edit `.fimo/project.json`; it links this directory to the remote project.
38
38
  - Keep `.fimo/config.json#runtime` true to the code: framework, `dev`/`build`/`install` commands, output directory. Update it when you rename a package script, move the build output, or change framework; the reference is `fimo skills get fimo-code --ref frameworks/hosting`.
@@ -11,8 +11,8 @@
11
11
  "prepare": "svelte-kit sync"
12
12
  },
13
13
  "dependencies": {
14
- "fimo": "2.4.0-staging.1",
15
- "fimo-sveltekit": "2.4.0-staging.1"
14
+ "fimo": "2.4.0-staging.3",
15
+ "fimo-sveltekit": "2.4.0-staging.3"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@sveltejs/kit": "^2.70.2",
@@ -1,12 +1,16 @@
1
1
  import { getLabels } from 'fimo-sveltekit';
2
+ import { resolveSeoForPath, toSeoHead } from 'fimo/seo';
2
3
 
4
+ import fimoConfig from '../../.fimo/config.json';
3
5
  import { resolveLocale } from '../locales';
4
6
 
5
7
  export async function load(event) {
6
8
  const locale = resolveLocale(event.params);
9
+ const seo = resolveSeoForPath(event.url.pathname, fimoConfig);
7
10
 
8
11
  return {
9
12
  locale,
13
+ seoHead: toSeoHead(seo, { canonicalUrl: seo?.url ? new URL(event.url.pathname, seo.url).href : undefined }),
10
14
  labels: await getLabels({ event, locale }),
11
15
  };
12
16
  }
@@ -1,17 +1,29 @@
1
1
  <script lang="ts">
2
2
  import { afterNavigate } from '$app/navigation';
3
+ import { page } from '$app/state';
3
4
  import { FimoProvider } from 'fimo-sveltekit/components';
4
5
  import type { Snippet } from 'svelte';
5
6
 
6
7
  import type { LayoutData } from './$types';
7
8
 
8
9
  let { data, children }: { data: LayoutData; children: Snippet } = $props();
10
+ const head = $derived(page.data.seoHead ?? { meta: [], links: [] });
9
11
 
10
12
  afterNavigate(() => {
11
13
  document.documentElement.lang = data.locale;
12
14
  });
13
15
  </script>
14
16
 
17
+ <svelte:head>
18
+ <title>{head.title ?? 'Fimo + SvelteKit'}</title>
19
+ {#each head.meta as attributes}
20
+ <meta {...attributes} />
21
+ {/each}
22
+ {#each head.links as attributes}
23
+ <link {...attributes} />
24
+ {/each}
25
+ </svelte:head>
26
+
15
27
  <FimoProvider labels={data.labels}>
16
28
  {@render children()}
17
29
  </FimoProvider>
@@ -1,8 +1,3 @@
1
- <svelte:head>
2
- <title>Fimo + SvelteKit</title>
3
- <meta name="description" content="A new Fimo project." />
4
- </svelte:head>
5
-
6
1
  <div class="starter">
7
2
  <main class="starter-main">
8
3
  <div class="starter-panel">
@@ -11,10 +11,10 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@tanstack/react-query": "^5.80.6",
14
- "fimo": "2.4.0-staging.1",
15
- "fimo-react": "2.4.0-staging.1",
16
- "fimo-react-router": "2.4.0-staging.1",
17
- "fimo-vite": "2.4.0-staging.1",
14
+ "fimo": "2.4.0-staging.3",
15
+ "fimo-react": "2.4.0-staging.3",
16
+ "fimo-react-router": "2.4.0-staging.3",
17
+ "fimo-vite": "2.4.0-staging.3",
18
18
  "react": "^19.2.7",
19
19
  "react-dom": "^19.2.7",
20
20
  "react-router": "^8.3.0"