@flocon-ai/init 0.0.0-stage → 0.0.1

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/README.md CHANGED
@@ -1,3 +1,34 @@
1
- # Temporary Holding Version
1
+ # @flocon-ai/init
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One command to add Flocon AI pages to your website:
4
+
5
+ ```
6
+ npx @flocon-ai/init
7
+ ```
8
+
9
+ Supports Next.js (App Router), React Router v7 (framework mode) and Astro.
10
+
11
+ Project shapes:
12
+
13
+ - `src/app` (Next.js) and `src/pages` (Astro): detected.
14
+ - `[locale]` under the Next.js app folder: the route goes under it and the locale is passed to `<FloconPage />`.
15
+ - `src/pages/[locale]` in Astro: the route goes under it and the locale is passed to `getFloconPage`.
16
+ - Browser-only React (Vite, Lovable, Bolt, CRA): not supported (AI bots do not run JavaScript). The CLI stops and installs nothing.
17
+ - Next.js Pages Router: not supported yet. The CLI stops and installs nothing.
18
+
19
+ Options (no prompts, for AI agents):
20
+
21
+ ```
22
+ npx @flocon-ai/init --path /blog --yes
23
+ ```
24
+
25
+ - `--path <path>`: where pages live (`/` for the whole website, `/blog`…).
26
+ - `--yes`: no prompts (path defaults to `/`).
27
+ - `--skip-install`: do not install the package.
28
+ - `--no-sitemap`: do not add the sitemap and `llms.txt` files.
29
+
30
+ The sitemap and `llms.txt` files are never overwritten. If the website already has one, the CLI prints what to add and you merge it yourself.
31
+
32
+ The CLI never asks for or writes `FLOCON_TOKEN`. Copy it from https://flocon-ai.com/app/tokens, add it to your env file and to your host, then deploy.
33
+
34
+ Tests: `npm test`.
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "@flocon-ai/init",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "One command to add Flocon AI pages to your website: npx @flocon-ai/init",
5
+ "homepage": "https://flocon-ai.com/docs",
6
+ "keywords": ["flocon-ai", "seo", "geo", "mcp", "cli", "nextjs", "astro", "react-router"],
7
+ "license": "UNLICENSED",
8
+ "type": "module",
9
+ "bin": {
10
+ "init": "./src/cli.mjs"
11
+ },
12
+ "files": [
13
+ "src"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "test": "node --test test/init.test.mjs"
23
+ }
24
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process"
3
+ import fs from "node:fs"
4
+ import path from "node:path"
5
+ import readline from "node:readline/promises"
6
+ import { addSitemapAndLlms } from "./sitemap.mjs"
7
+ import { TEMPLATES } from "./templates.mjs"
8
+
9
+ const DOCS = "https://flocon-ai.com/docs"
10
+ const TOKENS_URL = "https://flocon-ai.com/app/tokens"
11
+
12
+ const FRAMEWORKS = {
13
+ nextjs: { label: "Next.js (App Router)", pkg: "@flocon-ai/next" },
14
+ react: { label: "React Router v7 (framework mode)", pkg: "@flocon-ai/react" },
15
+ astro: { label: "Astro", pkg: "@flocon-ai/astro" }
16
+ }
17
+
18
+ const log = (line = "") => console.log(line)
19
+
20
+ function fail(lines) {
21
+ for (const line of lines) console.error(line)
22
+ process.exit(1)
23
+ }
24
+
25
+ function parseArgs(argv) {
26
+ const args = { yes: false, skipInstall: false, sitemap: true, path: undefined }
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i]
29
+ if (a === "--yes") args.yes = true
30
+ else if (a === "--skip-install") args.skipInstall = true
31
+ else if (a === "--no-sitemap") args.sitemap = false
32
+ else if (a === "--path") args.path = argv[++i] ?? ""
33
+ else if (a.startsWith("--path=")) args.path = a.slice("--path=".length)
34
+ else fail([`Unknown option: ${a}`, "Options: --path <path> --yes --skip-install --no-sitemap"])
35
+ }
36
+ return args
37
+ }
38
+
39
+ function readPackageJson(cwd) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"))
42
+ } catch {
43
+ return undefined
44
+ }
45
+ }
46
+
47
+ // "app" or "src/app" (Next.js uses src/app only when app/ is absent).
48
+ function nextAppDir(cwd) {
49
+ if (fs.existsSync(path.join(cwd, "app"))) return "app"
50
+ if (fs.existsSync(path.join(cwd, "src/app"))) return "src/app"
51
+ return undefined
52
+ }
53
+
54
+ function detectFramework(cwd) {
55
+ const pkg = readPackageJson(cwd)
56
+ if (!pkg) return undefined
57
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
58
+ if (deps.next && nextAppDir(cwd)) return "nextjs"
59
+ if (deps.astro && fs.existsSync(path.join(cwd, "src/pages"))) return "astro"
60
+ if (deps["@react-router/dev"] && fs.existsSync(path.join(cwd, "app/routes.ts"))) return "react"
61
+ return undefined
62
+ }
63
+
64
+ function detectPackageManager(cwd) {
65
+ const has = (f) => fs.existsSync(path.join(cwd, f))
66
+ if (has("pnpm-lock.yaml")) return "pnpm"
67
+ if (has("yarn.lock")) return "yarn"
68
+ if (has("bun.lock") || has("bun.lockb")) return "bun"
69
+ return "npm"
70
+ }
71
+
72
+ const INSTALL_ARGS = { npm: ["install"], pnpm: ["add"], yarn: ["add"], bun: ["add"] }
73
+
74
+ // "blog" -> "/blog", "/blog/" -> "/blog". Returns { error } when the path is not valid.
75
+ function normalizePath(input) {
76
+ let p = String(input ?? "").trim()
77
+ if (/\s/.test(p)) return { error: "The path must not contain spaces." }
78
+ if (p === "") p = "/"
79
+ if (!p.startsWith("/")) p = "/" + p
80
+ if (p.length > 1) p = p.replace(/\/+$/, "") || "/"
81
+ if (!/^(\/[A-Za-z0-9_-]+)*\/?$/.test(p)) {
82
+ return { error: "Use only letters, numbers, - and _ in the path (e.g. /blog or /guides/seo)." }
83
+ }
84
+ return { path: p }
85
+ }
86
+
87
+ // Next.js (app/[locale]) or Astro (src/pages/[locale]) with a `[locale]` segment: the route goes under it.
88
+ function hasLocaleSegment(cwd, framework) {
89
+ if (framework === "nextjs") return fs.existsSync(path.join(cwd, nextAppDir(cwd), "[locale]"))
90
+ if (framework === "astro") return fs.existsSync(path.join(cwd, "src/pages/[locale]"))
91
+ return false
92
+ }
93
+
94
+ // Projects we know and do not support: stop with the reason, install nothing.
95
+ function unsupportedReason(cwd) {
96
+ const pkg = readPackageJson(cwd)
97
+ if (!pkg) return undefined
98
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
99
+ if (deps.next && !nextAppDir(cwd) && (fs.existsSync(path.join(cwd, "pages")) || fs.existsSync(path.join(cwd, "src/pages")))) {
100
+ return [
101
+ "Flocon AI: this is a Next.js Pages Router project. It is not supported yet (@flocon-ai/next needs the App Router).",
102
+ "Nothing was installed or changed."
103
+ ]
104
+ }
105
+ const ssrOnVite = deps.next || deps.astro || deps["@react-router/dev"] || deps["@tanstack/react-start"] || deps["@remix-run/dev"]
106
+ if ((deps.vite || deps["react-scripts"]) && deps.react && !ssrOnVite) {
107
+ return [
108
+ "Flocon AI: this is a browser-only React app (Vite: Lovable, Bolt, CRA…). It is not supported.",
109
+ "Why: AI bots and search crawlers do not run JavaScript. They would see an empty page.",
110
+ "Options: move to a server-rendered framework, like Next.js or React Router v7 (framework mode).",
111
+ "Nothing was installed or changed. Docs: " + DOCS
112
+ ]
113
+ }
114
+ return undefined
115
+ }
116
+
117
+ function routeFile(cwd, framework, urlPath, hasLocale) {
118
+ const prefix = urlPath === "/" ? "" : urlPath
119
+ if (framework === "nextjs") {
120
+ const locale = hasLocale ? "/[locale]" : ""
121
+ return `${nextAppDir(cwd)}${locale}${prefix}/[...slug]/page.tsx`
122
+ }
123
+ if (framework === "astro") return `src/pages${hasLocale ? "/[locale]" : ""}${prefix}/[...slug].astro`
124
+ return "app/routes/flocon.tsx"
125
+ }
126
+
127
+ function fileIsTaken(cwd, framework, file) {
128
+ if (fs.existsSync(path.join(cwd, file))) return true
129
+ if (framework === "nextjs") {
130
+ const stem = file.replace(/\.tsx$/, "")
131
+ return ["js", "jsx", "ts", "tsx"].some((ext) => fs.existsSync(path.join(cwd, `${stem}.${ext}`)))
132
+ }
133
+ return false
134
+ }
135
+
136
+ function tokenAlreadySet(cwd) {
137
+ if (process.env.FLOCON_TOKEN) return true
138
+ const re = /^\s*(export\s+)?FLOCON_TOKEN\s*=\s*\S/m
139
+ for (const f of [".env.local", ".env", ".env.development.local", ".env.development"]) {
140
+ try {
141
+ if (re.test(fs.readFileSync(path.join(cwd, f), "utf8"))) return true
142
+ } catch {}
143
+ }
144
+ return false
145
+ }
146
+
147
+ async function main() {
148
+ const args = parseArgs(process.argv.slice(2))
149
+ const cwd = process.cwd()
150
+
151
+ // 1. Framework
152
+ const framework = detectFramework(cwd)
153
+ if (!framework) {
154
+ const reason = unsupportedReason(cwd)
155
+ if (reason) fail(reason)
156
+ fail([
157
+ "Flocon AI: no supported framework found in this folder.",
158
+ "Supported: Next.js (App Router), React Router v7 (framework mode), Astro.",
159
+ "Run this command at the root of your website. Docs: " + DOCS
160
+ ])
161
+ }
162
+ const { label, pkg } = FRAMEWORKS[framework]
163
+ log(`Found: ${label}`)
164
+
165
+ // 2. Path (asked before anything is changed)
166
+ let answer = args.path
167
+ if (answer === undefined) {
168
+ if (args.yes || !process.stdin.isTTY) {
169
+ answer = "/"
170
+ } else {
171
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
172
+ answer = await rl.question("Where should Flocon pages go? (e.g. / for the whole website, /blog, /guides) ")
173
+ rl.close()
174
+ }
175
+ }
176
+ const normalized = normalizePath(answer)
177
+ if (normalized.error) fail([`Invalid path "${answer}": ${normalized.error}`])
178
+ const urlPath = normalized.path
179
+
180
+ // 3. Install
181
+ const pm = detectPackageManager(cwd)
182
+ const installCmd = [pm, ...INSTALL_ARGS[pm], pkg]
183
+ if (args.skipInstall) {
184
+ log(`Skipped install. Package: ${pkg} (package manager: ${pm}).`)
185
+ } else {
186
+ log(`Installing ${pkg} with ${pm}...`)
187
+ const res = spawnSync(installCmd[0], installCmd.slice(1), { cwd, stdio: "inherit", shell: process.platform === "win32" })
188
+ if (res.status !== 0) fail([`Install failed. Run it yourself: ${installCmd.join(" ")}`])
189
+ }
190
+
191
+ // 4. Route file (never overwrite)
192
+ const hasLocale = hasLocaleSegment(cwd, framework)
193
+ const file = routeFile(cwd, framework, urlPath, hasLocale)
194
+ const template = hasLocale ? TEMPLATES[framework + "Locale"] : TEMPLATES[framework]
195
+ let exitCode = 0
196
+ if (fileIsTaken(cwd, framework, file)) {
197
+ exitCode = 1
198
+ log(`\n${file} already exists. I did not touch it. Add this file yourself (e.g. as ${file}):\n`)
199
+ log(template)
200
+ } else {
201
+ const full = path.join(cwd, file)
202
+ fs.mkdirSync(path.dirname(full), { recursive: true })
203
+ fs.writeFileSync(full, template, { flag: "wx" })
204
+ log(`Created ${file}`)
205
+ }
206
+ if (framework === "react") {
207
+ const routePath = urlPath === "/" ? "*" : `${urlPath.slice(1)}/*`
208
+ log(`\nRegister the route last in app/routes.ts:\n route("${routePath}", "routes/flocon.tsx")`)
209
+ }
210
+
211
+ // 4b. Sitemap + llms.txt (never overwrite)
212
+ if (args.sitemap) {
213
+ const appDir = framework === "nextjs" ? nextAppDir(cwd) : undefined
214
+ if (addSitemapAndLlms({ cwd, framework, appDir, urlPath, log })) exitCode = 1
215
+ }
216
+
217
+ // 5. Token (no prompt, no file write)
218
+ log()
219
+ if (tokenAlreadySet(cwd)) {
220
+ log("FLOCON_TOKEN is already set. Good.")
221
+ } else {
222
+ log(
223
+ `Flocon needs FLOCON_TOKEN to work. Copy your website token from ${TOKENS_URL}, then add FLOCON_TOKEN=... to your env file (.env.local / .env) and to your host. Never commit it.`
224
+ )
225
+ }
226
+
227
+ // 6. Last step
228
+ log("\nNext step: add FLOCON_TOKEN to your host, then deploy.")
229
+ process.exit(exitCode)
230
+ }
231
+
232
+ await main()
@@ -0,0 +1,149 @@
1
+ // Sitemap + llms.txt files written by the CLI (`--no-sitemap` skips them).
2
+ // Copy of the `get_setup` snippets in back/functions/mcp/setup.ts (a package never imports from back/).
3
+ // test/init.test.mjs fails if they drift.
4
+ import fs from "node:fs"
5
+ import path from "node:path"
6
+
7
+ const nextSitemap = `import type { MetadataRoute } from "next"
8
+ import { floconSitemap } from "@flocon-ai/next"
9
+
10
+ // Built on getAllPages(): published pages only, never drafts.
11
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
12
+ return floconSitemap({ url: ({ locale, path }) => \`https://example.com/\${locale}\${path}\` })
13
+ }
14
+ `
15
+
16
+ const nextLlms = `import { floconLlmsTxt } from "@flocon-ai/next"
17
+
18
+ export async function GET() {
19
+ return new Response(await floconLlmsTxt({ url: ({ locale, path }) => \`https://example.com/\${locale}\${path}\` }), {
20
+ headers: { "content-type": "text/plain; charset=utf-8" }
21
+ })
22
+ }
23
+ `
24
+
25
+ const astroSitemap = `import { getAllPages } from "@flocon-ai/astro"
26
+
27
+ // Published pages only, never drafts.
28
+ export async function GET() {
29
+ const pages = await getAllPages()
30
+ const urls = pages
31
+ .map((p) => \`<url><loc>https://example.com/\${p.locale}\${p.path}</loc><lastmod>\${p.updatedAt}</lastmod></url>\`)
32
+ .join("")
33
+ return new Response(
34
+ \`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\${urls}</urlset>\`,
35
+ { headers: { "content-type": "application/xml" } }
36
+ )
37
+ }
38
+ `
39
+
40
+ const astroLlms = `import { floconLlmsTxt } from "@flocon-ai/astro"
41
+
42
+ export async function GET() {
43
+ const text = await floconLlmsTxt({ url: ({ locale, path }) => \`https://example.com/\${locale}\${path}\` })
44
+ return new Response(text, { headers: { "content-type": "text/plain; charset=utf-8" } })
45
+ }
46
+ `
47
+
48
+ const reactSitemap = `import { floconSitemap } from "@flocon-ai/react"
49
+
50
+ export async function loader() {
51
+ const xml = await floconSitemap({ url: ({ locale, path }) => \`https://example.com\${path}\` })
52
+ return new Response(xml, { headers: { "content-type": "application/xml; charset=utf-8" } })
53
+ }
54
+ `
55
+
56
+ const reactLlms = `import { floconLlmsTxt } from "@flocon-ai/react"
57
+
58
+ export async function loader() {
59
+ const text = await floconLlmsTxt({ url: ({ locale, path }) => \`https://example.com\${path}\` })
60
+ return new Response(text, { headers: { "content-type": "text/plain; charset=utf-8" } })
61
+ }
62
+ `
63
+
64
+ const exts = (base, list) => list.map((e) => base + e)
65
+
66
+ // `existing` = files that mean the website already has one.
67
+ function spec(framework, appDir) {
68
+ if (framework === "nextjs") {
69
+ return {
70
+ sitemap: {
71
+ file: `${appDir}/sitemap.ts`,
72
+ code: nextSitemap,
73
+ existing: [...exts(`${appDir}/sitemap`, [".js", ".ts", ".xml"]), ...exts(`${appDir}/sitemap.xml/route`, [".js", ".ts"]), "public/sitemap.xml"]
74
+ },
75
+ llms: {
76
+ file: `${appDir}/llms.txt/route.ts`,
77
+ code: nextLlms,
78
+ existing: [...exts(`${appDir}/llms.txt/route`, [".js", ".ts"]), `${appDir}/llms.txt`, "public/llms.txt"]
79
+ }
80
+ }
81
+ }
82
+ if (framework === "astro") {
83
+ return {
84
+ sitemap: {
85
+ file: "src/pages/sitemap.xml.ts",
86
+ code: astroSitemap,
87
+ existing: [...exts("src/pages/sitemap.xml", [".ts", ".js", ".astro"]), "src/pages/sitemap-index.xml.ts", "public/sitemap.xml", "public/sitemap-index.xml"]
88
+ },
89
+ llms: {
90
+ file: "src/pages/llms.txt.ts",
91
+ code: astroLlms,
92
+ existing: [...exts("src/pages/llms.txt", [".ts", ".js", ".astro"]), "public/llms.txt"]
93
+ }
94
+ }
95
+ }
96
+ return {
97
+ sitemap: {
98
+ file: "app/routes/sitemap[.]xml.ts",
99
+ code: reactSitemap,
100
+ existing: [...exts("app/routes/sitemap[.]xml", [".ts", ".tsx"]), ...exts("app/routes/sitemap.xml", [".ts", ".tsx"]), "public/sitemap.xml"]
101
+ },
102
+ llms: {
103
+ file: "app/routes/llms[.]txt.ts",
104
+ code: reactLlms,
105
+ existing: [...exts("app/routes/llms[.]txt", [".ts", ".tsx"]), ...exts("app/routes/llms.txt", [".ts", ".tsx"]), "public/llms.txt"]
106
+ }
107
+ }
108
+ }
109
+
110
+ // Astro: @astrojs/sitemap builds the sitemap from the config, there is no file to detect.
111
+ function astroConfigHasSitemap(cwd) {
112
+ for (const f of ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.mts"]) {
113
+ try {
114
+ if (/@astrojs\/sitemap/.test(fs.readFileSync(path.join(cwd, f), "utf8"))) return true
115
+ } catch {}
116
+ }
117
+ return false
118
+ }
119
+
120
+ // Never overwrites. Returns true when something has to be done by hand.
121
+ export function addSitemapAndLlms({ cwd, framework, appDir, urlPath, log }) {
122
+ const files = spec(framework, appDir)
123
+ const exists = (f) => fs.existsSync(path.join(cwd, f))
124
+ let manual = false
125
+ log()
126
+ for (const [kind, name] of [
127
+ ["sitemap", "sitemap"],
128
+ ["llms", "llms.txt"]
129
+ ]) {
130
+ const { file, code, existing } = files[kind]
131
+ const taken = existing.some(exists) || (kind === "sitemap" && framework === "astro" && astroConfigHasSitemap(cwd))
132
+ if (taken) {
133
+ manual = true
134
+ log(`The website already has a ${name}. I did not touch it. Add this yourself: merge the Flocon URLs (published pages, under ${urlPath}) into it.`)
135
+ log(`Example (${file}):\n`)
136
+ log(code)
137
+ } else {
138
+ const full = path.join(cwd, file)
139
+ fs.mkdirSync(path.dirname(full), { recursive: true })
140
+ fs.writeFileSync(full, code, { flag: "wx" })
141
+ log(`Created ${file}`)
142
+ }
143
+ }
144
+ log(`In the sitemap and llms.txt files, replace https://example.com with your website URL (Flocon pages live under ${urlPath}).`)
145
+ if (framework === "react") {
146
+ log('Register both routes before the catch-all in app/routes.ts:\n route("sitemap.xml", "routes/sitemap[.]xml.ts")\n route("llms.txt", "routes/llms[.]txt.ts")')
147
+ }
148
+ return manual
149
+ }
@@ -0,0 +1,113 @@
1
+ // Route files written by the CLI.
2
+ // Copy of the `get_setup` snippets in back/functions/mcp/setup.ts (a package never imports from back/).
3
+ // test/init.test.mjs fails if they drift.
4
+
5
+ const nextjs = `import { FloconPage } from "@flocon-ai/next"
6
+
7
+ // One catch-all route serves every Flocon page. Rendered on each request.
8
+ export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
9
+ const { slug } = await params
10
+ return <FloconPage path={"/" + slug.join("/")} />
11
+ }
12
+ `
13
+
14
+ // Same as `nextjs`, for a route under `[locale]`: the locale goes to the lib.
15
+ const nextjsLocale = `import { FloconPage } from "@flocon-ai/next"
16
+
17
+ // One catch-all route serves every Flocon page, under the [locale] segment. Rendered on each request.
18
+ export default async function Page({ params }: { params: Promise<{ locale: string; slug: string[] }> }) {
19
+ const { locale, slug } = await params
20
+ return <FloconPage locale={locale} path={"/" + slug.join("/")} />
21
+ }
22
+ `
23
+
24
+ const astro = `---
25
+ import FloconPage from "@flocon-ai/astro/FloconPage.astro"
26
+ import { getFloconPage, getFloconMetadata } from "@flocon-ai/astro"
27
+
28
+ // Your URL rule. Flocon AI adds no prefix of its own.
29
+ const url = ({ locale, path }: { locale: string; path: string }) => \`https://example.com/\${locale}\${path}\`
30
+
31
+ const data = await getFloconPage({ path: "/" + (Astro.params.slug ?? "") })
32
+ if (!data) return new Response(null, { status: 404 })
33
+ const meta = getFloconMetadata({ data, url })
34
+ ---
35
+
36
+ <html lang={data.page.locale}>
37
+ <head>
38
+ <meta charset="utf-8" />
39
+ <meta name="viewport" content="width=device-width" />
40
+ <title>{meta.title}</title>
41
+ {meta.meta.map((m) => <meta {...m} />)}
42
+ {meta.links.map((l) => <link {...l} />)}
43
+ </head>
44
+ <body>
45
+ <FloconPage data={data} url={url} />
46
+ </body>
47
+ </html>
48
+ `
49
+
50
+ // Same as `astro`, for a route under `src/pages/[locale]`.
51
+ const astroLocale = `---
52
+ import FloconPage from "@flocon-ai/astro/FloconPage.astro"
53
+ import { getFloconPage, getFloconMetadata } from "@flocon-ai/astro"
54
+
55
+ // Your URL rule. Flocon AI adds no prefix of its own.
56
+ const url = ({ locale, path }: { locale: string; path: string }) => \`https://example.com/\${locale}\${path}\`
57
+
58
+ const data = await getFloconPage({ path: "/" + (Astro.params.slug ?? ""), locale: Astro.params.locale })
59
+ if (!data) return new Response(null, { status: 404 })
60
+ const meta = getFloconMetadata({ data, url })
61
+ ---
62
+
63
+ <html lang={data.page.locale}>
64
+ <head>
65
+ <meta charset="utf-8" />
66
+ <meta name="viewport" content="width=device-width" />
67
+ <title>{meta.title}</title>
68
+ {meta.meta.map((m) => <meta {...m} />)}
69
+ {meta.links.map((l) => <link {...l} />)}
70
+ </head>
71
+ <body>
72
+ <FloconPage data={data} url={url} />
73
+ </body>
74
+ </html>
75
+ `
76
+
77
+ const react = `// Register it as a splat route in app/routes.ts: route("*", "routes/flocon.tsx") (or route("blog/*", ...) for a prefix)
78
+ import { FloconPage, getFloconMetadata, getPage } from "@flocon-ai/react"
79
+ import type { Route } from "./+types/flocon"
80
+
81
+ const url = ({ locale, path }: { locale: string; path: string }) => \`https://example.com\${path}\`
82
+
83
+ export async function loader({ params }: Route.LoaderArgs) {
84
+ const page = await getPage({ path: "/" + params["*"] })
85
+ if (!page) throw new Response("Not found", { status: 404 })
86
+ return page
87
+ }
88
+
89
+ export function meta({ data }: Route.MetaArgs) {
90
+ if (!data) return [{ title: "Not found" }]
91
+ const m = getFloconMetadata({ page: data, url })
92
+ return [
93
+ { title: m.title },
94
+ { name: "description", content: m.description },
95
+ { property: "og:type", content: m.openGraph.type },
96
+ { property: "og:title", content: m.openGraph.title },
97
+ { property: "og:description", content: m.openGraph.description },
98
+ { property: "og:url", content: m.openGraph.url },
99
+ { property: "og:locale", content: m.openGraph.locale },
100
+ { name: "twitter:card", content: m.twitter.card },
101
+ { name: "twitter:title", content: m.twitter.title },
102
+ { name: "twitter:description", content: m.twitter.description },
103
+ { tagName: "link", rel: "canonical", href: m.canonical },
104
+ ...Object.entries(m.languages).map(([hrefLang, href]) => ({ tagName: "link", rel: "alternate", hrefLang, href }))
105
+ ]
106
+ }
107
+
108
+ export default function Page({ loaderData }: Route.ComponentProps) {
109
+ return <FloconPage page={loaderData} url={url} />
110
+ }
111
+ `
112
+
113
+ export const TEMPLATES = { nextjs, nextjsLocale, astro, astroLocale, react }