fimo 0.11.0-staging.5 → 0.11.0-staging.7
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.
- package/assets/agent-templates/content-translator/GOAL.md +2 -2
- package/assets/agent-templates/seo-audit/config.yaml +1 -1
- package/assets/skills/fimo/SKILL.md +1 -0
- package/assets/skills/fimo-cli/SKILL.md +10 -5
- package/assets/skills/fimo-cli/references/agents.md +63 -18
- package/assets/skills/fimo-cli/references/credits.md +1 -1
- package/assets/skills/fimo-cli/references/deploy.md +1 -1
- package/assets/skills/fimo-cli/references/describe.md +1 -1
- package/assets/skills/fimo-cli/references/history.md +1 -1
- package/assets/skills/fimo-cli/references/init.md +51 -16
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +12 -3
- package/assets/skills/fimo-cli/references/integrations.md +15 -3
- package/assets/skills/fimo-cli/references/invite.md +1 -1
- package/assets/skills/fimo-cli/references/labels.md +1 -1
- package/assets/skills/fimo-cli/references/locales.md +4 -4
- package/assets/skills/fimo-cli/references/mcp.md +85 -0
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +1 -1
- package/assets/skills/fimo-cli/references/projects.md +7 -7
- package/assets/skills/fimo-code/references/labels.md +2 -2
- package/assets/skills/fimo-code/references/locales.md +5 -5
- package/assets/skills/fimo-code/references/setup-plain-vite.md +4 -4
- package/assets/skills/fimo-code/references/setup-react-router.md +5 -5
- package/assets/skills/fimo-code/references/ui.md +1 -1
- package/assets/skills/fimo-migration/references/workflow.md +1 -1
- package/assets/skills/fimo-migration/scripts/build-canonical-manifest.mjs +32 -3
- package/assets/starters/fimo-site/manifest.json +64 -0
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/GOAL.md +8 -0
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/capabilities.yaml +2 -0
- package/assets/starters/fimo-site/template/.fimo/agents/content-guide/config.yaml +9 -0
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/GOAL.md +14 -0
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/capabilities.yaml +4 -0
- package/assets/starters/fimo-site/template/.fimo/agents/site-review/config.yaml +9 -0
- package/assets/starters/fimo-site/template/src/forms/contact.json +14 -0
- package/assets/starters/fimo-site/template/src/forms/contact.ts +46 -0
- package/assets/starters/fimo-site/template/src/pages/Contact.tsx +193 -0
- package/assets/starters/fimo-site/template/src/pages/Index.tsx +27 -0
- package/assets/starters/fimo-site/template/src/pages/SiteContent.tsx +191 -0
- package/assets/starters/fimo-site/template/src/schemas/HomePage.json +28 -0
- package/assets/starters/fimo-site/template/src/schemas/HomePage.ts +29 -0
- package/assets/starters/fimo-site/template/src/schemas/HomePage.types.ts +64 -0
- package/dist/build/vite/index.d.ts +3 -3
- package/dist/build/vite/plugins/fimo-config.d.ts +2 -2
- package/dist/build/vite/plugins/fimo-config.js +5 -5
- package/dist/build/vite/plugins/robots.d.ts +1 -1
- package/dist/build/vite/plugins/robots.js +3 -3
- package/dist/build/vite/plugins/server-config.js +1 -1
- package/dist/build/vite/plugins/sitemap.js +2 -2
- package/dist/build/vite/plugins/translations.js +14 -10
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +130 -115
- package/dist/runtime/app/prefetch.d.ts +1 -1
- package/dist/runtime/app/prefetch.js +2 -2
- package/dist/runtime/paths/get-fimo-paths.d.ts +1 -1
- package/dist/runtime/paths/get-fimo-paths.js +2 -2
- package/dist/runtime/paths/types.d.ts +1 -1
- package/dist/runtime/project-layout.d.ts +14 -0
- package/dist/runtime/project-layout.js +57 -0
- package/dist/runtime/seo/resolver.d.ts +1 -1
- package/dist/runtime/seo/resolver.js +1 -1
- package/dist/runtime/shared/fimo-config.server.d.ts +2 -0
- package/dist/runtime/shared/fimo-config.server.js +17 -4
- package/dist/runtime/templates.d.ts +4 -4
- package/dist/runtime/templates.js +11 -6
- package/dist/scripts/validate-route-metadata.js +8 -6
- package/package.json +7 -1
- package/templates/react-router/AGENTS.md +1 -1
- package/templates/react-router/package.json +1 -1
- /package/templates/react-router/{fimo-config.json → .fimo/config.json} +0 -0
|
@@ -62,7 +62,7 @@ fimo agents secrets remove seo-publisher FIRECRAWL_API_KEY
|
|
|
62
62
|
fimo agents validate
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
`fimo agents secrets add` edits
|
|
65
|
+
`fimo agents secrets add` edits `.fimo/agents/<name>/config.yaml`. It does not create or rotate the secret value. Use `fimo secrets add` to create the value or `fimo secrets update` to rotate it.
|
|
66
66
|
|
|
67
67
|
`fimo agents validate` checks that required agent secrets exist in the project. A running agent keeps the environment it started with; the next run reads the deployed agent config again.
|
|
68
68
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Run via `npx fimo@latest <command>`** — always the latest CLI, no install needed.
|
|
4
4
|
|
|
5
|
-
Commands for listing projects across an org, cloning a remote project locally, and managing the lifecycle of a single project (delete, publish, unpublish, rename). Most lifecycle commands operate on the **current project** (read from `.fimo.settings.json
|
|
5
|
+
Commands for listing projects across an org, cloning a remote project locally, and managing the lifecycle of a single project (delete, publish, unpublish, rename). Most lifecycle commands operate on the **current project** (read from `.fimo/project.json`, or the legacy `.fimo.settings.json`, in the cwd) by default; for `delete`, `publish`, and `unpublish` you can pass an explicit `[projectId]` to target another. (`rename` only edits the linked local project; see below.)
|
|
6
6
|
|
|
7
7
|
## The `fimo projects` namespace
|
|
8
8
|
|
|
@@ -13,15 +13,15 @@ fimo projects clone [projectId] [dir] [--env <name>] # clone an existing project
|
|
|
13
13
|
fimo projects delete [projectId] [-C <dir>] [-y] # delete (defaults to current project)
|
|
14
14
|
fimo projects publish [projectId] [-C <dir>] # make the project live (publish from main)
|
|
15
15
|
fimo projects unpublish [projectId] [-C <dir>] # take the project offline
|
|
16
|
-
fimo projects rename <name> [projectId] [-C <dir>] #
|
|
16
|
+
fimo projects rename <name> [projectId] [-C <dir>] # set seo.siteName in the linked project config
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
- **`list`** — projects you can see in the org. Live projects are marked.
|
|
20
20
|
- **`create`** — identical to `fimo init` (create-new path); see `references/init.md`.
|
|
21
|
-
- **`clone`** — clone a remote Fimo project's git repo to your machine and write the local `.fimo.
|
|
21
|
+
- **`clone`** — clone a remote Fimo project's git repo to your machine and write the local `.fimo/project.json` and agent files. Use `fimo clone <projectId>` when scripting or when the ID is known. In an interactive terminal, bare `fimo clone` opens a project picker for the active org; JSON mode and non-TTY shells require an explicit project ID and fail fast instead of prompting. By default it clones `main`; pass `--env <name>` to clone a specific env. The target directory defaults to the project display name/name, or pass `[dir]` explicitly.
|
|
22
22
|
- **`delete`** — destructive; prompts unless `-y`. Removes the remote project.
|
|
23
23
|
- **`publish` / `unpublish`** — toggle the project's public visibility. `publish` flips the current `main` state live (the same "go live" as `fimo deploy --publish`, minus the git push). This is **not** the versioned release path — for that, see `fimo publish` in `references/branches.md`.
|
|
24
|
-
- **`rename`** —
|
|
24
|
+
- **`rename`** — edits only the linked local project immediately. It requires a project marker in the cwd, and if you pass a `[projectId]` that doesn't match the linked project it errors and exits 1 (so you can't rename a different project). It writes `seo.siteName` into the current or legacy project config; commit and run `fimo deploy` to reconcile the remote `displayName` and deploy the new site name.
|
|
25
25
|
|
|
26
26
|
## Top-level clone shortcut
|
|
27
27
|
|
|
@@ -33,15 +33,15 @@ Use the top-level shortcut for the common "get this project onto my machine" wor
|
|
|
33
33
|
|
|
34
34
|
## Top-level shortcuts (current project)
|
|
35
35
|
|
|
36
|
-
These target the project linked in the cwd's `.fimo.settings.json` — handy from inside a project directory:
|
|
36
|
+
These target the project linked in the cwd's `.fimo/project.json` (or legacy `.fimo.settings.json`) — handy from inside a project directory:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
fimo delete [-C <dir>] [-y] # delete the current project
|
|
40
40
|
fimo unpublish [-C <dir>] # unpublish the current project
|
|
41
|
-
fimo rename "<Site Name>" # local edit: set seo.siteName in
|
|
41
|
+
fimo rename "<Site Name>" # local edit: set seo.siteName in the project config
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
`fimo rename` sets the **site name** (`seo.siteName`) in the local
|
|
44
|
+
`fimo rename` sets the **site name** (`seo.siteName`) in the local project config — it is a local edit, not an immediate remote update, so it requires a follow-up commit + `fimo deploy` to take effect. Deploy reconciles the project's remote `displayName` with the committed site name while pushing the config. See `references/init.md` and `references/deploy.md`.
|
|
45
45
|
|
|
46
46
|
## Three "publish" verbs — don't confuse them
|
|
47
47
|
|
|
@@ -21,11 +21,11 @@ In development, the generated app reloads when DB label values change. In produc
|
|
|
21
21
|
|
|
22
22
|
## Locale behavior
|
|
23
23
|
|
|
24
|
-
The default label locale is `i18n.defaultLocale` in
|
|
24
|
+
The default label locale is `i18n.defaultLocale` in `.fimo/config.json`, falling back to `en` when it is not set. `fimo validate`, the generated app runtime, and `fimo labels set` without `--locale` all use that default locale.
|
|
25
25
|
|
|
26
26
|
For non-default locales, create explicit DB values with `fimo labels set --locale <locale>` or `fimo labels set-many --locale <locale>`. Keep the code shape the same for every locale: `t('hero.title')` declares the key, and the DB decides which locale value is shown.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
`.fimo/config.json` owns the locale list:
|
|
29
29
|
|
|
30
30
|
```json
|
|
31
31
|
{
|
|
@@ -4,7 +4,7 @@ Fimo provides locale utilities that follow the Fimo standard, but they are helpe
|
|
|
4
4
|
|
|
5
5
|
## Configuration
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`.fimo/config.json` owns the locale list and routing strategy. Keep one locale unless the user asks for multilingual or localized output.
|
|
8
8
|
|
|
9
9
|
```jsonc
|
|
10
10
|
{
|
|
@@ -28,7 +28,7 @@ Locale tags should be structurally valid, for example `en`, `es`, `pt-BR`, or `z
|
|
|
28
28
|
|
|
29
29
|
## Canonical Routes
|
|
30
30
|
|
|
31
|
-
Keep
|
|
31
|
+
Keep `.fimo/config.json#routes` canonical and unlocalized:
|
|
32
32
|
|
|
33
33
|
```json
|
|
34
34
|
{
|
|
@@ -71,7 +71,7 @@ Copyable examples live in `assets/react-router-locales/routes.ts` and `assets/re
|
|
|
71
71
|
|
|
72
72
|
Before changing a multilingual app, inspect its locale provider, route table, language switcher, redirects, and current public URLs. Preserve that behavior while migrating.
|
|
73
73
|
|
|
74
|
-
For React Router projects, prefer converging on one locale contract owned by
|
|
74
|
+
For React Router projects, prefer converging on one locale contract owned by `.fimo/config.json`, `fimoRoutes()`, and `FimoLink`. Move existing providers and language-switcher UI into the framework root instead of replacing them or adding a second locale context. Keep route metadata canonical and reuse page modules across locales.
|
|
75
75
|
|
|
76
76
|
If adopting the standard helper would change established public URLs, do not silently rewrite them. Keep the custom router temporarily, pass its resolved locale to `<FimoProviders locale={locale}>`, and ask the user to choose the canonical URLs and redirects before normalizing the route table.
|
|
77
77
|
|
|
@@ -86,7 +86,7 @@ import { FimoLink } from 'fimo/react-router';
|
|
|
86
86
|
<FimoLink to="/pricing" locale="es">Spanish pricing</FimoLink>
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
`FimoLink` localizes absolute internal paths according to
|
|
89
|
+
`FimoLink` localizes absolute internal paths according to `.fimo/config.json#i18n.routing`. It leaves relative, hash, query-only, and external URLs alone. Use React Router's raw `Link` only when you intentionally want to bypass Fimo's locale behavior.
|
|
90
90
|
|
|
91
91
|
Read the active locale with `useLocale()` when component logic needs it:
|
|
92
92
|
|
|
@@ -125,5 +125,5 @@ When using custom routing, also provide localized internal-link helpers in that
|
|
|
125
125
|
## What Not To Do
|
|
126
126
|
|
|
127
127
|
- Do not create `src/pages/__localized/*` copies just for locales.
|
|
128
|
-
- Do not duplicate every route in
|
|
128
|
+
- Do not duplicate every route in `.fimo/config.json` per locale.
|
|
129
129
|
- Do not use `/es` as a normal canonical route when `es` is an enabled locale.
|
|
@@ -85,7 +85,7 @@ Notes:
|
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
The `seo` plugin rewrites `<title>`, og, twitter, canonical tags from
|
|
88
|
-
|
|
88
|
+
`.fimo/config.json#seo` at build time. You can leave a placeholder title in
|
|
89
89
|
`index.html`; it'll be replaced.
|
|
90
90
|
|
|
91
91
|
## File: `src/main.tsx`
|
|
@@ -140,7 +140,7 @@ export function App() {
|
|
|
140
140
|
}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
## File:
|
|
143
|
+
## File: `.fimo/config.json`
|
|
144
144
|
|
|
145
145
|
```json
|
|
146
146
|
{
|
|
@@ -165,7 +165,7 @@ this version field. It also never injects the React Router plugin: in a
|
|
|
165
165
|
framework-mode project that `reactRouter()` plugin is added by your own
|
|
166
166
|
`vite.config.ts`, not by `fimo/vite`.
|
|
167
167
|
|
|
168
|
-
## File: `.fimo.
|
|
168
|
+
## File: `.fimo/project.json`
|
|
169
169
|
|
|
170
170
|
```json
|
|
171
171
|
{
|
|
@@ -222,6 +222,6 @@ shows the **deployed** app, not the localhost dev server.
|
|
|
222
222
|
## What to do next
|
|
223
223
|
|
|
224
224
|
- Add a sitemap: pass `fimo({ seo: true, sitemap: true })` and set
|
|
225
|
-
|
|
225
|
+
`.fimo/config.json#seo.url` to your production base URL.
|
|
226
226
|
- Look up exports: see `references/ui.md`.
|
|
227
227
|
- Use schema content: see `references/content.md`.
|
|
@@ -166,7 +166,7 @@ export default fimoRoutes(routes) satisfies RouteConfig;
|
|
|
166
166
|
Page filenames are your choice; the scaffold uses PascalCase (e.g. `Index.tsx`,
|
|
167
167
|
`Privacy.tsx`).
|
|
168
168
|
|
|
169
|
-
`fimoRoutes()` is Fimo's React Router helper for path-prefix locales. It is a no-op for single-locale projects and expands concrete locale aliases when
|
|
169
|
+
`fimoRoutes()` is Fimo's React Router helper for path-prefix locales. It is a no-op for single-locale projects and expands concrete locale aliases when `.fimo/config.json#i18n.locales` contains more locales. Custom routers can ignore this helper and pass their own locale to `<FimoProviders locale={locale}>`; see `references/locales.md`.
|
|
170
170
|
|
|
171
171
|
## File: `src/pages/Index.tsx`
|
|
172
172
|
|
|
@@ -182,7 +182,7 @@ export default function HomePage() {
|
|
|
182
182
|
}
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
## File:
|
|
185
|
+
## File: `.fimo/config.json`
|
|
186
186
|
|
|
187
187
|
```json
|
|
188
188
|
{
|
|
@@ -200,7 +200,7 @@ export default function HomePage() {
|
|
|
200
200
|
}
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
## File: `.fimo.
|
|
203
|
+
## File: `.fimo/project.json`
|
|
204
204
|
|
|
205
205
|
```json
|
|
206
206
|
{
|
|
@@ -268,9 +268,9 @@ changes after a `fimo deploy` (the local dev server is localhost-only).
|
|
|
268
268
|
## What to do next
|
|
269
269
|
|
|
270
270
|
- Add more routes: extend `src/routes.ts`, create matching `src/pages/*.tsx`,
|
|
271
|
-
add route entries in
|
|
271
|
+
add route entries in `.fimo/config.json#routes`.
|
|
272
272
|
- Add a sitemap: pass `fimo({ sitemap: true })` (opt-in, off by default; the
|
|
273
273
|
canonical template enables it). To customize the base URL, set
|
|
274
|
-
|
|
274
|
+
`.fimo/config.json#seo.url`.
|
|
275
275
|
- Override SEO per route: see `references/seo-overrides.md`.
|
|
276
276
|
- Look up an export: see `references/api.md`.
|
|
@@ -27,7 +27,7 @@ Use these to render content-type field values consistently. They carry source me
|
|
|
27
27
|
1. **Schema text → `<Text>`**, never `<h1>{post.title}</h1>`.
|
|
28
28
|
2. **Schema media → `<Image>` / `<Video>`**, never `<img src={post.coverImage.url} />`. Provide `width` / `height` when known so Fimo can optimize and reserve layout space.
|
|
29
29
|
3. **Non-schema images** (decorative, hard-coded URLs, generated assets referenced directly) → `<StaticImage>`. Never hand-construct a `FimoMedia` to feed `<Image>`.
|
|
30
|
-
4. **Label text → `<Text value={t('key')} />`**, not `<h1>{t(...)}</h1>`. Label values live in the DB for `i18n.defaultLocale` from
|
|
30
|
+
4. **Label text → `<Text value={t('key')} />`**, not `<h1>{t(...)}</h1>`. Label values live in the DB for `i18n.defaultLocale` from `.fimo/config.json` unless a non-default locale is explicitly targeted. See `references/labels.md`.
|
|
31
31
|
5. **Mixed schema + translated text** → the `` fimo`...` `` template literal inside `<Text>`.
|
|
32
32
|
6. **Never hand-roll richtext rendering** — always `<RichText>`. To restyle a node, pass `components={{ heading: ..., link: ..., ... }}`.
|
|
33
33
|
7. **Schema `json` → `<Json>`**, not a hand-rolled `<pre data-fimo-source={getFimoSource(post.metadata)}>{JSON.stringify(post.metadata)}</pre>`. Default renders as a `<span>`; pass `as="pre"` (with a `whitespace-pre-wrap` class) for a formatted block, or a children render function for custom display of specific keys.
|
|
@@ -6,7 +6,7 @@ Use this as the phase contract for complete migrations.
|
|
|
6
6
|
|
|
7
7
|
1. Confirm the target path.
|
|
8
8
|
2. If the target path is missing, create exactly one Fimo project there.
|
|
9
|
-
3. If it has `.fimo.settings.json`, reuse it.
|
|
9
|
+
3. If it has `.fimo/project.json` or the legacy `.fimo.settings.json`, reuse it.
|
|
10
10
|
4. If it is non-empty and not a Fimo project, stop and ask for a target path.
|
|
11
11
|
5. Never overwrite user work outside generated migration folders without reading
|
|
12
12
|
the existing files first.
|
|
@@ -110,7 +110,7 @@ function buildCanonicalManifest({ projectRoot, urlLedger, urlLedgerPath, sourceL
|
|
|
110
110
|
forms,
|
|
111
111
|
redirects: normalizeItems(sourceLedger?.redirects, 'redirect'),
|
|
112
112
|
locales: normalizeItems(sourceLedger?.locales, 'locale'),
|
|
113
|
-
seo: normalizeSeo(urlLedger),
|
|
113
|
+
seo: normalizeSeo(urlLedger, projectRoot),
|
|
114
114
|
unsupported: normalizeItems(sourceLedger?.unsupported, 'unsupported'),
|
|
115
115
|
unresolved: mergeItems([
|
|
116
116
|
...normalizeUnresolved(sourceLedger?.unresolved),
|
|
@@ -326,14 +326,15 @@ function normalizeUrlForms(forms) {
|
|
|
326
326
|
}));
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
function normalizeSeo(urlLedger) {
|
|
329
|
+
function normalizeSeo(urlLedger, projectRoot) {
|
|
330
|
+
const configPath = resolveProjectConfigPath(projectRoot);
|
|
330
331
|
return asArray(urlLedger?.pages).map((page) => ({
|
|
331
332
|
id: `seo:${page.route}`,
|
|
332
333
|
status: 'discovered',
|
|
333
334
|
route: page.route,
|
|
334
335
|
sourceUrls: compact([page.url]),
|
|
335
336
|
sourceFiles: compact([page.artifacts?.html]),
|
|
336
|
-
targetFiles: [
|
|
337
|
+
targetFiles: [configPath],
|
|
337
338
|
confidence: numberOr(page.confidence, 0.7),
|
|
338
339
|
title: page.seo?.title ?? page.title ?? null,
|
|
339
340
|
description: page.seo?.description ?? null,
|
|
@@ -341,6 +342,34 @@ function normalizeSeo(urlLedger) {
|
|
|
341
342
|
}));
|
|
342
343
|
}
|
|
343
344
|
|
|
345
|
+
function resolveProjectConfigPath(projectRoot) {
|
|
346
|
+
if (fs.existsSync(path.join(projectRoot, '.fimo/project.json'))) {
|
|
347
|
+
return '.fimo/config.json';
|
|
348
|
+
}
|
|
349
|
+
if (fs.existsSync(path.join(projectRoot, '.fimo.settings.json'))) {
|
|
350
|
+
if (fs.existsSync(path.join(projectRoot, 'fimo-config.json'))) {
|
|
351
|
+
return 'fimo-config.json';
|
|
352
|
+
}
|
|
353
|
+
if (fs.existsSync(path.join(projectRoot, 'fimo.config.json'))) {
|
|
354
|
+
return 'fimo.config.json';
|
|
355
|
+
}
|
|
356
|
+
return 'fimo-config.json';
|
|
357
|
+
}
|
|
358
|
+
if (
|
|
359
|
+
fs.existsSync(path.join(projectRoot, '.fimo/config.json')) ||
|
|
360
|
+
fs.existsSync(path.join(projectRoot, '.fimo/agents'))
|
|
361
|
+
) {
|
|
362
|
+
return '.fimo/config.json';
|
|
363
|
+
}
|
|
364
|
+
if (fs.existsSync(path.join(projectRoot, 'fimo-config.json'))) {
|
|
365
|
+
return 'fimo-config.json';
|
|
366
|
+
}
|
|
367
|
+
if (fs.existsSync(path.join(projectRoot, 'fimo.config.json'))) {
|
|
368
|
+
return 'fimo.config.json';
|
|
369
|
+
}
|
|
370
|
+
return '.fimo/config.json';
|
|
371
|
+
}
|
|
372
|
+
|
|
344
373
|
function normalizeUnresolved(items) {
|
|
345
374
|
return asArray(items).map((item, index) => {
|
|
346
375
|
if (typeof item === 'string') {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fimo-site",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "A content-led site with a homepage, contact form, labels, and manual review agents.",
|
|
5
|
+
"singletons": [
|
|
6
|
+
{
|
|
7
|
+
"type": "HomePage",
|
|
8
|
+
"data": {
|
|
9
|
+
"eyebrow": "Field notes",
|
|
10
|
+
"title": "Your website, ready to edit.",
|
|
11
|
+
"intro": {
|
|
12
|
+
"type": "doc",
|
|
13
|
+
"content": [
|
|
14
|
+
{
|
|
15
|
+
"type": "paragraph",
|
|
16
|
+
"content": [
|
|
17
|
+
{
|
|
18
|
+
"type": "text",
|
|
19
|
+
"text": "This starter is real website content. Edit the copy in Studio, change the code with your favorite agent, and keep both surfaces in sync."
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"primaryCtaLabel": "Read the field notes",
|
|
26
|
+
"primaryCtaHref": "#stories",
|
|
27
|
+
"featured": [
|
|
28
|
+
{
|
|
29
|
+
"label": "Start here",
|
|
30
|
+
"title": "A calmer content workflow",
|
|
31
|
+
"summary": "Keep content, code, previews, and the team in one editable project.",
|
|
32
|
+
"href": "#stories"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"label": "For agents",
|
|
36
|
+
"title": "Build once, keep iterating",
|
|
37
|
+
"summary": "Give your coding agent a real CMS surface to work with after the first deploy.",
|
|
38
|
+
"href": "#stories"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"label": "For teams",
|
|
42
|
+
"title": "Make every edit reviewable",
|
|
43
|
+
"summary": "Let teammates shape the site without asking them to open a code editor.",
|
|
44
|
+
"href": "#stories"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"contactHeading": "Make something useful",
|
|
48
|
+
"contactIntro": "Tell us what you are building and we will help you find the shortest path from first draft to a site your team can own."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"labels": [
|
|
53
|
+
{ "key": "nav.stories", "value": "Field notes", "locale": "en" },
|
|
54
|
+
{ "key": "nav.contact", "value": "Start a conversation", "locale": "en" },
|
|
55
|
+
{ "key": "hero.scroll", "value": "Explore the starter", "locale": "en" },
|
|
56
|
+
{ "key": "contact.name", "value": "Your name", "locale": "en" },
|
|
57
|
+
{ "key": "contact.email", "value": "Email address", "locale": "en" },
|
|
58
|
+
{ "key": "contact.message", "value": "What are you building?", "locale": "en" },
|
|
59
|
+
{ "key": "contact.submit", "value": "Send a note", "locale": "en" },
|
|
60
|
+
{ "key": "contact.sent", "value": "Thanks. Your note is on its way.", "locale": "en" },
|
|
61
|
+
{ "key": "contact.failed", "value": "That did not send. Please try again.", "locale": "en" }
|
|
62
|
+
],
|
|
63
|
+
"forms": ["contact"]
|
|
64
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Content guide
|
|
2
|
+
|
|
3
|
+
Inspect the site's content model and homepage copy. Suggest a small editorial
|
|
4
|
+
backlog that makes the site more useful to its intended audience.
|
|
5
|
+
|
|
6
|
+
Return three suggestions, each with the content type or field it touches and
|
|
7
|
+
why it matters. Use the existing content as evidence. Do not write files,
|
|
8
|
+
change entries, or create labels.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Keep starter agents manual-only. Adding a schedule or webhook and pushing to
|
|
2
|
+
# main registers an automation for the project.
|
|
3
|
+
version: 1
|
|
4
|
+
description: 'Suggest useful editorial improvements from the existing CMS content.'
|
|
5
|
+
data_sources:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
triggers:
|
|
9
|
+
- type: manual
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Site review
|
|
2
|
+
|
|
3
|
+
Review the starter project as a teammate would before the first publish.
|
|
4
|
+
|
|
5
|
+
Read the declared routes, the CMS content, the labels, and the public site.
|
|
6
|
+
Return a short report with:
|
|
7
|
+
|
|
8
|
+
- one thing that already feels clear;
|
|
9
|
+
- the three highest-value improvements;
|
|
10
|
+
- any missing content or broken route you can prove;
|
|
11
|
+
- one next action that a human or coding agent can review.
|
|
12
|
+
|
|
13
|
+
This is a read-only review. Do not edit files, change CMS content, create
|
|
14
|
+
branches, or publish anything.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Keep starter agents manual-only. Adding a schedule or webhook and pushing to
|
|
2
|
+
# main registers an automation for the project.
|
|
3
|
+
version: 1
|
|
4
|
+
description: 'Review the starter site and return a concise, read-only launch report.'
|
|
5
|
+
data_sources:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
triggers:
|
|
9
|
+
- type: manual
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contact",
|
|
3
|
+
"label": "Start a conversation",
|
|
4
|
+
"fields": {
|
|
5
|
+
"name": { "type": "text", "label": "Your name", "required": true },
|
|
6
|
+
"email": { "type": "email", "label": "Email address", "required": true },
|
|
7
|
+
"message": {
|
|
8
|
+
"type": "textarea",
|
|
9
|
+
"label": "What are you building?",
|
|
10
|
+
"required": true,
|
|
11
|
+
"validation": { "minLength": 10, "maxLength": 2000 }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// AUTO-GENERATED ⚠️ — do not edit by hand
|
|
2
|
+
// Source: src/forms/contact.json
|
|
3
|
+
// The CLI replaces the placeholder form id while seeding the starter. `fimo
|
|
4
|
+
// validate` regenerates this file from the JSON spec on later edits.
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
export const FORM_ID = '__FIMO_FORM_ID__';
|
|
9
|
+
|
|
10
|
+
export const ContactSchema = z.object({
|
|
11
|
+
name: z.string().min(1, 'Your name is required'),
|
|
12
|
+
email: z.string().min(1, 'Email address is required').email('Invalid email address'),
|
|
13
|
+
message: z.string().min(1, 'What are you building? is required').min(10).max(2000),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type ContactData = z.infer<typeof ContactSchema>;
|
|
17
|
+
|
|
18
|
+
// @ts-ignore - meta.env is a vite feature
|
|
19
|
+
const base = import.meta.env.VITE_API_URL;
|
|
20
|
+
|
|
21
|
+
export async function submitContact(data: ContactData): Promise<{
|
|
22
|
+
success: boolean;
|
|
23
|
+
errors?: Record<string, string>;
|
|
24
|
+
}> {
|
|
25
|
+
let response: Response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetch(`${base}/forms/${FORM_ID}/submit`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(data),
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return { success: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (response.ok) {
|
|
37
|
+
return { success: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (response.status === 422) {
|
|
41
|
+
const json = (await response.json()) as { errors?: Record<string, string> };
|
|
42
|
+
return { success: false, errors: json.errors };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { success: false };
|
|
46
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { type FimoString, Text, useLabels } from 'fimo/ui';
|
|
2
|
+
import { ArrowUpRight, Check } from 'lucide-react';
|
|
3
|
+
import { type FormEvent, type ReactNode, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Input } from '@/components/ui/input';
|
|
7
|
+
import { Label } from '@/components/ui/label';
|
|
8
|
+
import { Spinner } from '@/components/ui/spinner';
|
|
9
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
10
|
+
import { ContactSchema, submitContact } from '@/forms/contact';
|
|
11
|
+
import { cn } from '@/lib/utils';
|
|
12
|
+
import type { HomePage } from '@/schemas/HomePage';
|
|
13
|
+
|
|
14
|
+
import { Bar } from './SiteContent';
|
|
15
|
+
|
|
16
|
+
export function Contact({ page, loading }: { page?: HomePage; loading: boolean }) {
|
|
17
|
+
return (
|
|
18
|
+
<section id="contact" className="dark bg-background text-foreground scroll-mt-16">
|
|
19
|
+
<div className="mx-auto grid w-full max-w-6xl gap-12 px-6 py-20 lg:grid-cols-12 lg:gap-16 lg:px-10 lg:py-28">
|
|
20
|
+
<div className="lg:col-span-5">
|
|
21
|
+
{page ? (
|
|
22
|
+
<>
|
|
23
|
+
<Text
|
|
24
|
+
value={page.contactHeading}
|
|
25
|
+
as="h2"
|
|
26
|
+
className="text-4xl font-semibold tracking-[-0.03em] text-balance lg:text-5xl"
|
|
27
|
+
/>
|
|
28
|
+
<Text value={page.contactIntro} as="p" className="text-muted-foreground mt-6 max-w-md leading-relaxed" />
|
|
29
|
+
</>
|
|
30
|
+
) : (
|
|
31
|
+
<div aria-hidden>
|
|
32
|
+
<div className="grid gap-3">
|
|
33
|
+
<Bar loading={loading} className="h-10 w-4/5" />
|
|
34
|
+
<Bar loading={loading} className="h-10 w-2/5" />
|
|
35
|
+
</div>
|
|
36
|
+
<div className="mt-8 grid gap-2">
|
|
37
|
+
<Bar loading={loading} className="h-4 w-full max-w-md" />
|
|
38
|
+
<Bar loading={loading} className="h-4 w-11/12 max-w-md" />
|
|
39
|
+
<Bar loading={loading} className="h-4 w-2/3 max-w-md" />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="lg:col-span-7">
|
|
46
|
+
<ContactForm />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type SubmitState = 'idle' | 'submitting' | 'sent' | 'invalid' | 'failed';
|
|
54
|
+
|
|
55
|
+
function ContactForm() {
|
|
56
|
+
const { t } = useLabels();
|
|
57
|
+
const [state, setState] = useState<SubmitState>('idle');
|
|
58
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
59
|
+
|
|
60
|
+
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
// `currentTarget` is cleared once the handler yields, so keep the node.
|
|
63
|
+
const form = event.currentTarget;
|
|
64
|
+
setErrors({});
|
|
65
|
+
|
|
66
|
+
const parsed = ContactSchema.safeParse(Object.fromEntries(new FormData(form)));
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
const fieldErrors: Record<string, string> = {};
|
|
69
|
+
for (const issue of parsed.error.issues) {
|
|
70
|
+
fieldErrors[issue.path[0] as string] = issue.message;
|
|
71
|
+
}
|
|
72
|
+
setErrors(fieldErrors);
|
|
73
|
+
setState('invalid');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setState('submitting');
|
|
78
|
+
const result = await submitContact(parsed.data);
|
|
79
|
+
if (result.success) {
|
|
80
|
+
form.reset();
|
|
81
|
+
setState('sent');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A rejected submission comes back with per-field messages; a network or
|
|
86
|
+
// server failure has nothing to attach to a field, so it needs its own.
|
|
87
|
+
const fieldErrors = result.errors ?? {};
|
|
88
|
+
setErrors(fieldErrors);
|
|
89
|
+
setState(Object.keys(fieldErrors).length > 0 ? 'invalid' : 'failed');
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const busy = state === 'submitting';
|
|
93
|
+
const sent = state === 'sent';
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<form
|
|
97
|
+
onSubmit={handleSubmit}
|
|
98
|
+
noValidate
|
|
99
|
+
aria-busy={busy}
|
|
100
|
+
className="border-border bg-card rounded-xl border p-6 sm:p-8"
|
|
101
|
+
>
|
|
102
|
+
<div className="grid gap-6 sm:grid-cols-2">
|
|
103
|
+
<FormField id="contact-name" label={t('contact.name')} error={errors.name}>
|
|
104
|
+
<Input
|
|
105
|
+
id="contact-name"
|
|
106
|
+
name="name"
|
|
107
|
+
type="text"
|
|
108
|
+
autoComplete="name"
|
|
109
|
+
disabled={busy || sent}
|
|
110
|
+
aria-invalid={Boolean(errors.name)}
|
|
111
|
+
aria-describedby={errors.name ? 'contact-name-error' : undefined}
|
|
112
|
+
/>
|
|
113
|
+
</FormField>
|
|
114
|
+
|
|
115
|
+
<FormField id="contact-email" label={t('contact.email')} error={errors.email}>
|
|
116
|
+
<Input
|
|
117
|
+
id="contact-email"
|
|
118
|
+
name="email"
|
|
119
|
+
type="email"
|
|
120
|
+
autoComplete="email"
|
|
121
|
+
disabled={busy || sent}
|
|
122
|
+
aria-invalid={Boolean(errors.email)}
|
|
123
|
+
aria-describedby={errors.email ? 'contact-email-error' : undefined}
|
|
124
|
+
/>
|
|
125
|
+
</FormField>
|
|
126
|
+
|
|
127
|
+
<div className="sm:col-span-2">
|
|
128
|
+
<FormField id="contact-message" label={t('contact.message')} error={errors.message}>
|
|
129
|
+
<Textarea
|
|
130
|
+
id="contact-message"
|
|
131
|
+
name="message"
|
|
132
|
+
rows={5}
|
|
133
|
+
disabled={busy || sent}
|
|
134
|
+
aria-invalid={Boolean(errors.message)}
|
|
135
|
+
aria-describedby={errors.message ? 'contact-message-error' : undefined}
|
|
136
|
+
/>
|
|
137
|
+
</FormField>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="mt-8 flex flex-wrap items-center gap-x-4 gap-y-3">
|
|
142
|
+
<Button
|
|
143
|
+
type="submit"
|
|
144
|
+
size="lg"
|
|
145
|
+
disabled={busy || sent}
|
|
146
|
+
className={cn('group', state === 'invalid' && 'ring-destructive/40 ring-[3px]')}
|
|
147
|
+
>
|
|
148
|
+
{busy ? <Spinner /> : null}
|
|
149
|
+
{sent ? <Check /> : null}
|
|
150
|
+
<Text value={t('contact.submit')} />
|
|
151
|
+
{!busy && !sent ? (
|
|
152
|
+
<ArrowUpRight className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
|
153
|
+
) : null}
|
|
154
|
+
</Button>
|
|
155
|
+
|
|
156
|
+
{/* Both regions stay mounted and empty: a live region inserted at the same
|
|
157
|
+
moment as its text is announced unreliably. */}
|
|
158
|
+
<p role="status" className="text-muted-foreground text-sm">
|
|
159
|
+
{sent ? <Text value={t('contact.sent')} /> : null}
|
|
160
|
+
</p>
|
|
161
|
+
<p role="alert" className="text-destructive text-sm">
|
|
162
|
+
{state === 'failed' ? <Text value={t('contact.failed')} /> : null}
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
</form>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function FormField({
|
|
170
|
+
id,
|
|
171
|
+
label,
|
|
172
|
+
error,
|
|
173
|
+
children,
|
|
174
|
+
}: {
|
|
175
|
+
id: string;
|
|
176
|
+
label: FimoString;
|
|
177
|
+
error?: string;
|
|
178
|
+
children: ReactNode;
|
|
179
|
+
}) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="grid gap-2">
|
|
182
|
+
<Label htmlFor={id} className="text-muted-foreground text-[11px] font-medium tracking-[0.16em] uppercase">
|
|
183
|
+
<Text value={label} />
|
|
184
|
+
</Label>
|
|
185
|
+
{children}
|
|
186
|
+
{error ? (
|
|
187
|
+
<p id={`${id}-error`} role="alert" className="text-destructive text-xs">
|
|
188
|
+
{error}
|
|
189
|
+
</p>
|
|
190
|
+
) : null}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|