@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/README.md +37 -6
- package/package.json +16 -3
- package/src/cli.mjs +794 -72
- package/src/sitemap.mjs +50 -19
- package/src/templates.mjs +38 -11
- package/src/url.mjs +17 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
106
|
+
file: `${routes}/sitemap[.]xml.ts`,
|
|
99
107
|
code: reactSitemap,
|
|
100
|
-
existing: [...exts(
|
|
108
|
+
existing: [...exts(`${routes}/sitemap[.]xml`, [".ts", ".tsx"]), ...exts(`${routes}/sitemap.xml`, [".ts", ".tsx"]), "public/sitemap.xml"]
|
|
101
109
|
},
|
|
102
110
|
llms: {
|
|
103
|
-
file:
|
|
111
|
+
file: `${routes}/llms[.]txt.ts`,
|
|
104
112
|
code: reactLlms,
|
|
105
|
-
existing: [...exts(
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
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,
|
|
131
|
-
const
|
|
132
|
-
|
|
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
|
-
|
|
145
|
-
if (framework === "react") {
|
|
146
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
90
|
-
if (!
|
|
91
|
-
const m = getFloconMetadata({ page:
|
|
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
|
+
}
|