@flocon-ai/init 0.0.1 → 0.0.2

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/src/sitemap.mjs CHANGED
@@ -3,20 +3,27 @@
3
3
  // test/init.test.mjs fails if they drift.
4
4
  import fs from "node:fs"
5
5
  import path from "node:path"
6
+ import { applyUrlRule } from "./url.mjs"
6
7
 
7
8
  const nextSitemap = `import type { MetadataRoute } from "next"
8
9
  import { floconSitemap } from "@flocon-ai/next"
10
+ import { connection } from "next/server"
9
11
 
10
12
  // Built on getAllPages(): published pages only, never drafts.
13
+ // connection(): rendered on each request, so new pages show up without a deploy and the build never needs FLOCON_TOKEN.
11
14
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
12
- return floconSitemap({ url: ({ locale, path }) => \`https://example.com/\${locale}\${path}\` })
15
+ await connection()
16
+ return floconSitemap({ url: ({ locale, path }) => \`https://example.com\${path}\` })
13
17
  }
14
18
  `
15
19
 
16
20
  const nextLlms = `import { floconLlmsTxt } from "@flocon-ai/next"
21
+ import { connection } from "next/server"
17
22
 
23
+ // connection(): rendered on each request (also with cacheComponents).
18
24
  export async function GET() {
19
- return new Response(await floconLlmsTxt({ url: ({ locale, path }) => \`https://example.com/\${locale}\${path}\` }), {
25
+ await connection()
26
+ return new Response(await floconLlmsTxt({ url: ({ locale, path }) => \`https://example.com\${path}\` }), {
20
27
  headers: { "content-type": "text/plain; charset=utf-8" }
21
28
  })
22
29
  }
@@ -28,7 +35,7 @@ const astroSitemap = `import { getAllPages } from "@flocon-ai/astro"
28
35
  export async function GET() {
29
36
  const pages = await getAllPages()
30
37
  const urls = pages
31
- .map((p) => \`<url><loc>https://example.com/\${p.locale}\${p.path}</loc><lastmod>\${p.updatedAt}</lastmod></url>\`)
38
+ .map((p) => \`<url><loc>https://example.com\${p.path}</loc><lastmod>\${p.updatedAt}</lastmod></url>\`)
32
39
  .join("")
33
40
  return new Response(
34
41
  \`<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\${urls}</urlset>\`,
@@ -40,7 +47,7 @@ export async function GET() {
40
47
  const astroLlms = `import { floconLlmsTxt } from "@flocon-ai/astro"
41
48
 
42
49
  export async function GET() {
43
- const text = await floconLlmsTxt({ url: ({ locale, path }) => \`https://example.com/\${locale}\${path}\` })
50
+ const text = await floconLlmsTxt({ url: ({ locale, path }) => \`https://example.com\${path}\` })
44
51
  return new Response(text, { headers: { "content-type": "text/plain; charset=utf-8" } })
45
52
  }
46
53
  `
@@ -64,7 +71,7 @@ export async function loader() {
64
71
  const exts = (base, list) => list.map((e) => base + e)
65
72
 
66
73
  // `existing` = files that mean the website already has one.
67
- function spec(framework, appDir) {
74
+ function spec(framework, appDir, rrApp = "app", rrDir = "routes") {
68
75
  if (framework === "nextjs") {
69
76
  return {
70
77
  sitemap: {
@@ -93,16 +100,17 @@ function spec(framework, appDir) {
93
100
  }
94
101
  }
95
102
  }
103
+ const routes = `${rrApp}/${rrDir}`
96
104
  return {
97
105
  sitemap: {
98
- file: "app/routes/sitemap[.]xml.ts",
106
+ file: `${routes}/sitemap[.]xml.ts`,
99
107
  code: reactSitemap,
100
- existing: [...exts("app/routes/sitemap[.]xml", [".ts", ".tsx"]), ...exts("app/routes/sitemap.xml", [".ts", ".tsx"]), "public/sitemap.xml"]
108
+ existing: [...exts(`${routes}/sitemap[.]xml`, [".ts", ".tsx"]), ...exts(`${routes}/sitemap.xml`, [".ts", ".tsx"]), "public/sitemap.xml"]
101
109
  },
102
110
  llms: {
103
- file: "app/routes/llms[.]txt.ts",
111
+ file: `${routes}/llms[.]txt.ts`,
104
112
  code: reactLlms,
105
- existing: [...exts("app/routes/llms[.]txt", [".ts", ".tsx"]), ...exts("app/routes/llms.txt", [".ts", ".tsx"]), "public/llms.txt"]
113
+ existing: [...exts(`${routes}/llms[.]txt`, [".ts", ".tsx"]), ...exts(`${routes}/llms.txt`, [".ts", ".tsx"]), "public/llms.txt"]
106
114
  }
107
115
  }
108
116
  }
@@ -117,19 +125,27 @@ function astroConfigHasSitemap(cwd) {
117
125
  return false
118
126
  }
119
127
 
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)
128
+ // Never overwrites. Returns { manual, files }: manual = something has to be done by hand,
129
+ // files = the files (written or printed) that still have https://example.com (or the domain the CLI found).
130
+ // A file that is already ours (imports @flocon-ai/) is skipped: the command ran here before.
131
+ export function addSitemapAndLlms({ cwd, framework, appDir, rrApp, rrDir, routesFile, urlPath, hasLocale, flat, domain, log }) {
132
+ const files = spec(framework, appDir, rrApp, rrDir)
123
133
  const exists = (f) => fs.existsSync(path.join(cwd, f))
134
+ const ours = (f) => fs.statSync(path.join(cwd, f)).isFile() && fs.readFileSync(path.join(cwd, f), "utf8").includes("@flocon-ai/")
124
135
  let manual = false
136
+ const created = []
137
+ const urlFiles = []
125
138
  log()
126
139
  for (const [kind, name] of [
127
140
  ["sitemap", "sitemap"],
128
141
  ["llms", "llms.txt"]
129
142
  ]) {
130
- const { file, code, existing } = files[kind]
131
- const taken = existing.some(exists) || (kind === "sitemap" && framework === "astro" && astroConfigHasSitemap(cwd))
132
- if (taken) {
143
+ const { file, existing } = files[kind]
144
+ const code = applyUrlRule(files[kind].code, urlPath, hasLocale, domain)
145
+ const found = existing.filter(exists)
146
+ if (found.some(ours)) continue
147
+ urlFiles.push(file)
148
+ if (found.length || (kind === "sitemap" && framework === "astro" && astroConfigHasSitemap(cwd))) {
133
149
  manual = true
134
150
  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
151
  log(`Example (${file}):\n`)
@@ -139,11 +155,26 @@ export function addSitemapAndLlms({ cwd, framework, appDir, urlPath, log }) {
139
155
  fs.mkdirSync(path.dirname(full), { recursive: true })
140
156
  fs.writeFileSync(full, code, { flag: "wx" })
141
157
  log(`Created ${file}`)
158
+ created.push(file)
142
159
  }
143
160
  }
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")')
161
+ // flatRoutes(): the file names are the URLs, nothing to register.
162
+ if (framework === "react" && !flat && created.length) {
163
+ const r = `${files.sitemap.file.split("/").slice(-2).join("/")}`
164
+ const l = `${files.llms.file.split("/").slice(-2).join("/")}`
165
+ log(`Register both routes before the catch-all in ${routesFile}:\n route("sitemap.xml", "${r}")\n route("llms.txt", "${l}")`)
147
166
  }
148
- return manual
167
+ return { manual, files: urlFiles, created }
168
+ }
169
+
170
+ // The sitemap and llms.txt files that are Flocon's own (they import @flocon-ai/): a second run may fix their domain.
171
+ export function ownSitemapFiles({ cwd, framework, appDir, rrApp, rrDir }) {
172
+ const files = spec(framework, appDir, rrApp, rrDir)
173
+ return [files.sitemap.file, files.llms.file].filter((f) => {
174
+ try {
175
+ return fs.readFileSync(path.join(cwd, f), "utf8").includes("@flocon-ai/")
176
+ } catch {
177
+ return false
178
+ }
179
+ })
149
180
  }
package/src/templates.mjs CHANGED
@@ -2,22 +2,48 @@
2
2
  // Copy of the `get_setup` snippets in back/functions/mcp/setup.ts (a package never imports from back/).
3
3
  // test/init.test.mjs fails if they drift.
4
4
 
5
- const nextjs = `import { FloconPage } from "@flocon-ai/next"
5
+ const nextjs = `import { FloconPage, getFloconMetadata } from "@flocon-ai/next"
6
6
 
7
7
  // One catch-all route serves every Flocon page. Rendered on each request.
8
- export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
8
+ export const dynamic = "force-dynamic"
9
+
10
+ // Your URL rule. Flocon AI adds no prefix of its own.
11
+ const url = ({ locale, path }: { locale: string; path: string }) => \`https://example.com\${path}\`
12
+
13
+ type Props = { params: Promise<{ slug: string[] }> }
14
+
15
+ // Title, description, canonical URL and hreflang.
16
+ export async function generateMetadata({ params }: Props) {
9
17
  const { slug } = await params
10
- return <FloconPage path={"/" + slug.join("/")} />
18
+ return getFloconMetadata({ path: "/" + slug.join("/"), url })
19
+ }
20
+
21
+ export default async function Page({ params }: Props) {
22
+ const { slug } = await params
23
+ return <FloconPage path={"/" + slug.join("/")} url={url} />
11
24
  }
12
25
  `
13
26
 
14
27
  // Same as `nextjs`, for a route under `[locale]`: the locale goes to the lib.
15
- const nextjsLocale = `import { FloconPage } from "@flocon-ai/next"
28
+ const nextjsLocale = `import { FloconPage, getFloconMetadata } from "@flocon-ai/next"
16
29
 
17
30
  // 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[] }> }) {
31
+ export const dynamic = "force-dynamic"
32
+
33
+ // Your URL rule. Flocon AI adds no prefix of its own.
34
+ const url = ({ locale, path }: { locale: string; path: string }) => \`https://example.com/\${locale}\${path}\`
35
+
36
+ type Props = { params: Promise<{ locale: string; slug: string[] }> }
37
+
38
+ // Title, description, canonical URL and hreflang.
39
+ export async function generateMetadata({ params }: Props) {
19
40
  const { locale, slug } = await params
20
- return <FloconPage locale={locale} path={"/" + slug.join("/")} />
41
+ return getFloconMetadata({ locale, path: "/" + slug.join("/"), url })
42
+ }
43
+
44
+ export default async function Page({ params }: Props) {
45
+ const { locale, slug } = await params
46
+ return <FloconPage locale={locale} path={"/" + slug.join("/")} url={url} />
21
47
  }
22
48
  `
23
49
 
@@ -26,7 +52,7 @@ import FloconPage from "@flocon-ai/astro/FloconPage.astro"
26
52
  import { getFloconPage, getFloconMetadata } from "@flocon-ai/astro"
27
53
 
28
54
  // 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}\`
55
+ const url = ({ locale, path }: { locale: string; path: string }) => \`https://example.com\${path}\`
30
56
 
31
57
  const data = await getFloconPage({ path: "/" + (Astro.params.slug ?? "") })
32
58
  if (!data) return new Response(null, { status: 404 })
@@ -55,7 +81,8 @@ import { getFloconPage, getFloconMetadata } from "@flocon-ai/astro"
55
81
  // Your URL rule. Flocon AI adds no prefix of its own.
56
82
  const url = ({ locale, path }: { locale: string; path: string }) => \`https://example.com/\${locale}\${path}\`
57
83
 
58
- const data = await getFloconPage({ path: "/" + (Astro.params.slug ?? ""), locale: Astro.params.locale })
84
+ // Always set: this file is under [locale].
85
+ const data = await getFloconPage({ path: "/" + (Astro.params.slug ?? ""), locale: Astro.params.locale as string })
59
86
  if (!data) return new Response(null, { status: 404 })
60
87
  const meta = getFloconMetadata({ data, url })
61
88
  ---
@@ -86,9 +113,9 @@ export async function loader({ params }: Route.LoaderArgs) {
86
113
  return page
87
114
  }
88
115
 
89
- export function meta({ data }: Route.MetaArgs) {
90
- if (!data) return [{ title: "Not found" }]
91
- const m = getFloconMetadata({ page: data, url })
116
+ export function meta({ loaderData }: Route.MetaArgs) {
117
+ if (!loaderData) return [{ title: "Not found" }]
118
+ const m = getFloconMetadata({ page: loaderData, url })
92
119
  return [
93
120
  { title: m.title },
94
121
  { name: "description", content: m.description },
package/src/url.mjs ADDED
@@ -0,0 +1,17 @@
1
+ // Fills the URL rule of a get_setup snippet for this website.
2
+ // get_setup snippets use https://example.com${path} (no locale, no folder).
3
+ // The CLI knows the folder (e.g. /blog) and if the website has a [locale] segment,
4
+ // so it writes the real rule. A website without a locale in its URLs never gets one.
5
+ // `domain`: the website's domain when the CLI found it (e.g. https://acme.com), else example.com stays.
6
+ export function applyUrlRule(code, urlPath, hasLocale, domain) {
7
+ const folder = urlPath === "/" ? "" : urlPath
8
+ const rule = (locale, p) => `https://example.com${hasLocale ? "/${" + locale + "}" : ""}${folder}\${${p}}`
9
+ let out = code
10
+ .replaceAll("https://example.com/${locale}${path}", rule("locale", "path"))
11
+ .replaceAll("https://example.com${path}", rule("locale", "path"))
12
+ .replaceAll("https://example.com${p.path}", rule("p.locale", "p.path"))
13
+ // No locale in the URLs: drop the unused `locale` param (strict TypeScript, noUnusedParameters, fails the build on it).
14
+ if (!hasLocale) out = out.replaceAll("({ locale, path })", "({ path })").replaceAll("({ locale, path }:", "({ path }:")
15
+ if (domain) out = out.replaceAll("https://example.com", domain)
16
+ return out
17
+ }