fumapress 0.5.1 → 0.5.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.
- package/css/generated.css +13 -0
- package/dist/client.d.ts +2 -22
- package/dist/client.js +3 -9
- package/dist/components/blog.js +3 -3
- package/dist/components/image.d.ts +1 -2
- package/dist/components/link.d.ts +24 -0
- package/dist/components/link.js +18 -0
- package/dist/components/provider.js +2 -1
- package/dist/layouts/blog.index.js +2 -2
- package/dist/layouts/blog.js +2 -2
- package/dist/layouts/blog.tags.js +2 -2
- package/dist/lib/pathname.js +7 -4
- package/dist/lib/shared.d.ts +1 -0
- package/dist/lib/types.d.ts +10 -4
- package/dist/plugins/blog.js +2 -7
- package/dist/plugins/image/cloudflare.client.js +36 -0
- package/dist/plugins/image/cloudflare.d.ts +22 -0
- package/dist/plugins/image/cloudflare.js +21 -0
- package/dist/plugins/image/cloudflare.utils.js +32 -0
- package/dist/plugins/link-validation.d.ts +24 -0
- package/dist/plugins/link-validation.js +30 -0
- package/dist/plugins/llms.txt.js +12 -9
- package/dist/plugins/openapi.d.ts +5 -1
- package/dist/plugins/openapi.js +4 -2
- package/dist/plugins/sitemap.d.ts +196 -0
- package/dist/plugins/sitemap.js +90 -0
- package/dist/plugins/takumi.js +2 -3
- package/dist/router/index.d.ts +6 -3
- package/dist/router/index.js +17 -41
- package/dist/vite.js +2 -2
- package/package.json +16 -10
package/css/generated.css
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
@source inline("allTags");
|
|
16
16
|
@source inline("allowed");
|
|
17
17
|
@source inline("allows");
|
|
18
|
+
@source inline("always");
|
|
18
19
|
@source inline("and");
|
|
19
20
|
@source inline("and/or");
|
|
20
21
|
@source inline("any");
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
@source inline("cases");
|
|
63
64
|
@source inline("cause");
|
|
64
65
|
@source inline("causes");
|
|
66
|
+
@source inline("change");
|
|
65
67
|
@source inline("check");
|
|
66
68
|
@source inline("children");
|
|
67
69
|
@source inline("class-variance-authority");
|
|
@@ -147,6 +149,7 @@
|
|
|
147
149
|
@source inline("flex-row");
|
|
148
150
|
@source inline("flex-wrap");
|
|
149
151
|
@source inline("flexsearchStaticClient");
|
|
152
|
+
@source inline("fn");
|
|
150
153
|
@source inline("following");
|
|
151
154
|
@source inline("font-medium");
|
|
152
155
|
@source inline("font-mono");
|
|
@@ -155,6 +158,7 @@
|
|
|
155
158
|
@source inline("framework");
|
|
156
159
|
@source inline("free");
|
|
157
160
|
@source inline("from");
|
|
161
|
+
@source inline("fromPathname");
|
|
158
162
|
@source inline("full");
|
|
159
163
|
@source inline("fumadocs-core/framework");
|
|
160
164
|
@source inline("fumadocs-core/i18n");
|
|
@@ -176,6 +180,7 @@
|
|
|
176
180
|
@source inline("ghost");
|
|
177
181
|
@source inline("githubUrl");
|
|
178
182
|
@source inline("given");
|
|
183
|
+
@source inline("global");
|
|
179
184
|
@source inline("green");
|
|
180
185
|
@source inline("grid");
|
|
181
186
|
@source inline("grid-cols-1");
|
|
@@ -211,6 +216,7 @@
|
|
|
211
216
|
@source inline("including");
|
|
212
217
|
@source inline("incorrect");
|
|
213
218
|
@source inline("indexPath");
|
|
219
|
+
@source inline("indicates");
|
|
214
220
|
@source inline("inherit");
|
|
215
221
|
@source inline("inheritLayoutProps");
|
|
216
222
|
@source inline("inherited");
|
|
@@ -247,6 +253,7 @@
|
|
|
247
253
|
@source inline("level");
|
|
248
254
|
@source inline("like");
|
|
249
255
|
@source inline("limitation");
|
|
256
|
+
@source inline("link");
|
|
250
257
|
@source inline("loading");
|
|
251
258
|
@source inline("locale");
|
|
252
259
|
@source inline("lucide-react");
|
|
@@ -277,6 +284,8 @@
|
|
|
277
284
|
@source inline("multiple");
|
|
278
285
|
@source inline("mx-auto");
|
|
279
286
|
@source inline("name");
|
|
287
|
+
@source inline("navigation");
|
|
288
|
+
@source inline("never");
|
|
280
289
|
@source inline("new");
|
|
281
290
|
@source inline("normal");
|
|
282
291
|
@source inline("not");
|
|
@@ -309,6 +318,7 @@
|
|
|
309
318
|
@source inline("params");
|
|
310
319
|
@source inline("parsed");
|
|
311
320
|
@source inline("path");
|
|
321
|
+
@source inline("path/hash");
|
|
312
322
|
@source inline("pathname");
|
|
313
323
|
@source inline("payload");
|
|
314
324
|
@source inline("pb-6");
|
|
@@ -345,6 +355,7 @@
|
|
|
345
355
|
@source inline("py-6");
|
|
346
356
|
@source inline("quality");
|
|
347
357
|
@source inline("query");
|
|
358
|
+
@source inline("query-only");
|
|
348
359
|
@source inline("r");
|
|
349
360
|
@source inline("react");
|
|
350
361
|
@source inline("react-dom");
|
|
@@ -382,6 +393,7 @@
|
|
|
382
393
|
@source inline("schema");
|
|
383
394
|
@source inline("schemaId");
|
|
384
395
|
@source inline("screens");
|
|
396
|
+
@source inline("scroll");
|
|
385
397
|
@source inline("search");
|
|
386
398
|
@source inline("see");
|
|
387
399
|
@source inline("sell");
|
|
@@ -391,6 +403,7 @@
|
|
|
391
403
|
@source inline("shadow-md");
|
|
392
404
|
@source inline("shadow-sm");
|
|
393
405
|
@source inline("shall");
|
|
406
|
+
@source inline("should");
|
|
394
407
|
@source inline("shrink-0");
|
|
395
408
|
@source inline("significantly");
|
|
396
409
|
@source inline("size-3.5");
|
package/dist/client.d.ts
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
+
import { Link, LinkProps } from "./components/link.js";
|
|
1
2
|
import { Unstable_ChangeRouteCallback, Unstable_ChangeRouteEvent } from "waku/router/client";
|
|
2
|
-
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
-
import { ComponentProps, ReactNode, TransitionFunction } from "react";
|
|
4
3
|
|
|
5
4
|
//#region src/client.d.ts
|
|
6
|
-
interface LinkProps extends ComponentProps<"a"> {
|
|
7
|
-
/**
|
|
8
|
-
* indicates if the link should scroll or not on navigation
|
|
9
|
-
* - `true`: always scroll
|
|
10
|
-
* - `false`: never scroll
|
|
11
|
-
* - `undefined`: scroll on path/hash change (not on query-only change)
|
|
12
|
-
*/
|
|
13
|
-
scroll?: boolean;
|
|
14
|
-
unstable_pending?: ReactNode;
|
|
15
|
-
unstable_notPending?: ReactNode;
|
|
16
|
-
unstable_prefetchOnEnter?: boolean;
|
|
17
|
-
unstable_prefetchOnView?: boolean;
|
|
18
|
-
unstable_startTransition?: ((fn: TransitionFunction) => void) | undefined;
|
|
19
|
-
}
|
|
20
5
|
interface Router {
|
|
21
6
|
push: (to: string, options?: {
|
|
22
7
|
/**
|
|
@@ -45,11 +30,6 @@ interface Router {
|
|
|
45
30
|
query: string;
|
|
46
31
|
hash: string;
|
|
47
32
|
}
|
|
48
|
-
declare function Link({
|
|
49
|
-
href,
|
|
50
|
-
children,
|
|
51
|
-
...props
|
|
52
|
-
}: LinkProps): _$react_jsx_runtime0.JSX.Element;
|
|
53
33
|
declare function useRouter(): Router;
|
|
54
34
|
//#endregion
|
|
55
|
-
export { Link, LinkProps, Router, useRouter };
|
|
35
|
+
export { Link, type LinkProps, Router, useRouter };
|
package/dist/client.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Link
|
|
3
|
-
import {
|
|
2
|
+
import { Link } from "./components/link.js";
|
|
3
|
+
import { useRouter as useRouter$1 } from "waku/router/client";
|
|
4
4
|
//#region src/client.tsx
|
|
5
|
-
|
|
6
|
-
return /* @__PURE__ */ jsx(Link$1, {
|
|
7
|
-
to: href,
|
|
8
|
-
...props,
|
|
9
|
-
children
|
|
10
|
-
});
|
|
11
|
-
}
|
|
5
|
+
/** tiny wrapper of `waku` */
|
|
12
6
|
function useRouter() {
|
|
13
7
|
return useRouter$1();
|
|
14
8
|
}
|
package/dist/components/blog.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
+
import { Link } from "./link.js";
|
|
1
2
|
import { getCreationDate, getPressContext } from "../lib/shared.js";
|
|
2
3
|
import { cn } from "../lib/cn.js";
|
|
3
4
|
import { I18nLabel } from "./i18n.js";
|
|
4
5
|
import { joinPathname } from "../lib/pathname.js";
|
|
5
6
|
import { getBlogContext } from "../plugins/blog.js";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { Link } from "waku";
|
|
8
8
|
import { CornerLeftUpIcon } from "lucide-react";
|
|
9
9
|
import { buttonVariants } from "fumadocs-ui/components/ui/button";
|
|
10
10
|
//#region src/components/blog.tsx
|
|
11
11
|
function BlogItem({ page, date }) {
|
|
12
12
|
return /* @__PURE__ */ jsxs(Link, {
|
|
13
|
-
|
|
13
|
+
href: page.url,
|
|
14
14
|
className: "flex flex-col bg-fd-card rounded-2xl border shadow-sm p-4 transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground",
|
|
15
15
|
children: [
|
|
16
16
|
/* @__PURE__ */ jsx("p", {
|
|
@@ -52,7 +52,7 @@ function LinkToHome({ lang }) {
|
|
|
52
52
|
const { indexPath } = getBlogContext();
|
|
53
53
|
if (!indexPath) return;
|
|
54
54
|
return /* @__PURE__ */ jsxs(Link, {
|
|
55
|
-
|
|
55
|
+
href: lang ? joinPathname(lang, indexPath) : indexPath,
|
|
56
56
|
className: cn(buttonVariants({
|
|
57
57
|
variant: "ghost",
|
|
58
58
|
className: "text-fd-muted-foreground gap-2"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
1
|
import { ImgHTMLAttributes, ReactNode } from "react";
|
|
3
2
|
|
|
4
3
|
//#region src/components/image.d.ts
|
|
@@ -25,6 +24,6 @@ declare function Image({
|
|
|
25
24
|
unoptimized,
|
|
26
25
|
preload,
|
|
27
26
|
...rest
|
|
28
|
-
}: ImageProps):
|
|
27
|
+
}: ImageProps): import("react").JSX.Element;
|
|
29
28
|
//#endregion
|
|
30
29
|
export { Image, ImageProps, StaticImageData };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ComponentProps, ReactNode, TransitionFunction } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/components/link.d.ts
|
|
4
|
+
interface LinkProps extends ComponentProps<"a"> {
|
|
5
|
+
/**
|
|
6
|
+
* indicates if the link should scroll or not on navigation
|
|
7
|
+
* - `true`: always scroll
|
|
8
|
+
* - `false`: never scroll
|
|
9
|
+
* - `undefined`: scroll on path/hash change (not on query-only change)
|
|
10
|
+
*/
|
|
11
|
+
scroll?: boolean;
|
|
12
|
+
unstable_pending?: ReactNode;
|
|
13
|
+
unstable_notPending?: ReactNode;
|
|
14
|
+
unstable_prefetchOnEnter?: boolean;
|
|
15
|
+
unstable_prefetchOnView?: boolean;
|
|
16
|
+
unstable_startTransition?: ((fn: TransitionFunction) => void) | undefined;
|
|
17
|
+
}
|
|
18
|
+
declare function Link({
|
|
19
|
+
href,
|
|
20
|
+
children,
|
|
21
|
+
...props
|
|
22
|
+
}: LinkProps): import("react").JSX.Element;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { Link, LinkProps };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRouter as useRouter$1 } from "../client.js";
|
|
3
|
+
import { Link } from "waku/router/client";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/link.tsx
|
|
6
|
+
function Link$1({ href = "#", children, ...props }) {
|
|
7
|
+
if (typeof global !== "undefined" && global.LINK_SSG_CONTEXT) global.LINK_SSG_CONTEXT.links.push({
|
|
8
|
+
href,
|
|
9
|
+
fromPathname: useRouter$1().path
|
|
10
|
+
});
|
|
11
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
12
|
+
to: href,
|
|
13
|
+
...props,
|
|
14
|
+
children
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { Link$1 as Link };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Link } from "../components/link.js";
|
|
1
2
|
import { getPressContext } from "../lib/shared.js";
|
|
2
3
|
import { cn } from "../lib/cn.js";
|
|
3
4
|
import { I18nLabel } from "../components/i18n.js";
|
|
@@ -5,7 +6,6 @@ import { joinPathname } from "../lib/pathname.js";
|
|
|
5
6
|
import { getBlogContext } from "../plugins/blog.js";
|
|
6
7
|
import { OrderedBlogGrid } from "../components/blog.js";
|
|
7
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { Link } from "waku";
|
|
9
9
|
import { ListIcon } from "lucide-react";
|
|
10
10
|
import { buttonVariants } from "fumadocs-ui/components/ui/button";
|
|
11
11
|
//#region src/layouts/blog.index.tsx
|
|
@@ -26,7 +26,7 @@ function createBlogIndexPage({ heading, description } = {}) {
|
|
|
26
26
|
children: description
|
|
27
27
|
}),
|
|
28
28
|
tagsPath !== false && /* @__PURE__ */ jsxs(Link, {
|
|
29
|
-
|
|
29
|
+
href: lang ? joinPathname(lang, tagsPath) : tagsPath,
|
|
30
30
|
className: cn(buttonVariants({ variant: "primary" }), "gap-2"),
|
|
31
31
|
children: [/* @__PURE__ */ jsx(ListIcon, { className: "size-4" }), /* @__PURE__ */ jsx(I18nLabel, { label: "allTags" })]
|
|
32
32
|
})
|
package/dist/layouts/blog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Link } from "../components/link.js";
|
|
1
2
|
import { getCreationDate, getPressContext, renderBody, renderPageMeta, renderToc } from "../lib/shared.js";
|
|
2
3
|
import { BlogPanel, BlogProvider } from "../components/blog-panel.js";
|
|
3
4
|
import { getTags } from "../lib/shared/blog.js";
|
|
@@ -6,7 +7,6 @@ import { createHomeLayout } from "./home.js";
|
|
|
6
7
|
import { getBlogContext } from "../plugins/blog.js";
|
|
7
8
|
import { LinkToHome } from "../components/blog.js";
|
|
8
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import { Link } from "waku";
|
|
10
10
|
import { TagIcon } from "lucide-react";
|
|
11
11
|
//#region src/layouts/blog.tsx
|
|
12
12
|
/** You can use `createHomeLayout()` directly, this is only a wrapper */
|
|
@@ -48,7 +48,7 @@ function createBlogLayoutPage(options = {}) {
|
|
|
48
48
|
className: "flex flex-row items-center gap-2 flex-wrap w-full max-w-[900px] text-sm text-fd-primary-foreground font-mono",
|
|
49
49
|
children: [/* @__PURE__ */ jsx(TagIcon, { className: "size-4 text-fd-muted-foreground" }), tags.map((t) => {
|
|
50
50
|
if (tagsPath !== false) return /* @__PURE__ */ jsx(Link, {
|
|
51
|
-
|
|
51
|
+
href: joinPathname(lang ?? "", tagsPath, t),
|
|
52
52
|
className: "px-1.5 py-0.5 rounded-lg bg-fd-primary",
|
|
53
53
|
children: t
|
|
54
54
|
}, t);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { Link } from "../components/link.js";
|
|
1
2
|
import { getPressContext } from "../lib/shared.js";
|
|
2
3
|
import { I18nLabel } from "../components/i18n.js";
|
|
3
4
|
import { getTags, groupTags } from "../lib/shared/blog.js";
|
|
4
5
|
import { getBlogContext } from "../plugins/blog.js";
|
|
5
6
|
import { LinkToHome, OrderedBlogGrid } from "../components/blog.js";
|
|
6
7
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { Link } from "waku";
|
|
8
8
|
import { NewspaperIcon, TagIcon } from "lucide-react";
|
|
9
9
|
//#region src/layouts/blog.tags.tsx
|
|
10
10
|
function createBlogTagsPage({ heading, description } = {}) {
|
|
@@ -34,7 +34,7 @@ function createBlogTagsPage({ heading, description } = {}) {
|
|
|
34
34
|
}), /* @__PURE__ */ jsx("div", {
|
|
35
35
|
className: "grid grid-cols-2 sm:grid-cols-3 gap-2 mt-4 md:grid-cols-4 xl:grid-cols-6",
|
|
36
36
|
children: Array.from(grouped.entries()).sort((a, b) => b[1] - a[1]).map(([tag, count]) => /* @__PURE__ */ jsxs(Link, {
|
|
37
|
-
|
|
37
|
+
href: `/blog/tags/${tag}`,
|
|
38
38
|
className: "flex flex-row items-center gap-2 bg-fd-card text-fd-card-foreground border font-mono rounded-lg px-2 py-1 transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground",
|
|
39
39
|
children: [
|
|
40
40
|
/* @__PURE__ */ jsx(TagIcon, { className: "size-3.5 text-fd-muted-foreground" }),
|
package/dist/lib/pathname.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
//#region src/lib/pathname.ts
|
|
2
2
|
function joinPathname(...paths) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const segs = [];
|
|
4
|
+
for (let p of paths) {
|
|
5
|
+
if (p.startsWith("/")) p = p.slice(1);
|
|
6
|
+
if (p.endsWith("/")) p = p.slice(0, -1);
|
|
7
|
+
if (p.length > 0) segs.push(p);
|
|
8
|
+
}
|
|
9
|
+
return "/" + segs.join("/");
|
|
7
10
|
}
|
|
8
11
|
const PATHNAME_SEGMENT_REGEX = /^[A-Za-z0-9\-._~!$&'()*+,;=:@]+$/;
|
|
9
12
|
/** Check if the string is a full pathname (one that does not include `.` or `..`) */
|
package/dist/lib/shared.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Adapter, AppContextData, Awaitable, ServerPlugin } from "./types.js";
|
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
import { LoaderOutput } from "fumadocs-core/source";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
|
|
6
7
|
//#region src/lib/shared.d.ts
|
|
7
8
|
interface AppContext<C extends ConfigContext = ConfigContext> {
|
|
8
9
|
mode: BuildMode;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ import { RootLayoutContextData } from "../layouts/root.js";
|
|
|
7
7
|
import { ReactNode } from "react";
|
|
8
8
|
import { ContentStorage, LoaderOptions, LoaderPluginOption, Page } from "fumadocs-core/source";
|
|
9
9
|
import { I18nConfig } from "fumadocs-core/i18n";
|
|
10
|
-
import { CreateApi, CreateLayout, CreatePage, CreateRoot, CreateSlice } from "waku/router/server";
|
|
11
|
-
import { TOCItemType } from "fumadocs-core/toc";
|
|
10
|
+
import { CreateApi, CreateInterceptor, CreateLayout, CreatePage, CreateRoot, CreateSlice, createPages } from "waku/router/server";
|
|
12
11
|
import { StructuredData } from "fumadocs-core/mdx-plugins";
|
|
12
|
+
import { TOCItemType } from "fumadocs-core/toc";
|
|
13
|
+
import { Hono } from "hono/tiny";
|
|
13
14
|
import { MiddlewareHandler } from "hono";
|
|
14
15
|
import { unstable_createServerEntryAdapter } from "waku/adapter-builders";
|
|
15
16
|
|
|
@@ -56,8 +57,9 @@ interface ServerPlugin<C extends ConfigContext = ConfigContext> {
|
|
|
56
57
|
/** resolve content loader options */
|
|
57
58
|
configureLoader?: (this: AppContext<C>, options: PressLoaderOptions) => Awaitable<PressLoaderOptions>;
|
|
58
59
|
/** create Hono middlewares */
|
|
59
|
-
createMiddlewares?: (this: AppContext<C
|
|
60
|
-
|
|
60
|
+
createMiddlewares?: (this: AppContext<C>, env: {
|
|
61
|
+
app: Hono;
|
|
62
|
+
}) => Awaitable<MiddlewareHandler[] | undefined>;
|
|
61
63
|
unstable_onServerEntry?: (entry: ReturnType<ReturnType<typeof unstable_createServerEntryAdapter>>) => ReturnType<ReturnType<typeof unstable_createServerEntryAdapter>>;
|
|
62
64
|
}
|
|
63
65
|
interface BaseRouteFns {
|
|
@@ -66,7 +68,9 @@ interface BaseRouteFns {
|
|
|
66
68
|
createRoot: CreateRoot;
|
|
67
69
|
createApi: CreateApi;
|
|
68
70
|
createSlice: CreateSlice;
|
|
71
|
+
createInterceptor: CreateInterceptor;
|
|
69
72
|
}
|
|
73
|
+
type CreatePagesResult = ReturnType<typeof createPages>;
|
|
70
74
|
interface RouteFns extends BaseRouteFns {
|
|
71
75
|
createApiIsomorphic: (config: {
|
|
72
76
|
render: "static" | "dynamic";
|
|
@@ -76,6 +80,8 @@ interface RouteFns extends BaseRouteFns {
|
|
|
76
80
|
params: Record<string, string | string[]>;
|
|
77
81
|
}) => Promise<Response>;
|
|
78
82
|
}) => void;
|
|
83
|
+
/** access `createPages()` output */
|
|
84
|
+
unstable_getCreated: () => CreatePagesResult;
|
|
79
85
|
}
|
|
80
86
|
type ServerPluginOption<C extends ConfigContext = ConfigContext> = ServerPlugin<C> | false | undefined | null | ServerPluginOption<C>[];
|
|
81
87
|
/** can be extended from other libraries */
|
package/dist/plugins/blog.js
CHANGED
|
@@ -33,15 +33,10 @@ function blogPlugin({ paths = {}, isBlog = (page) => page.type === "blog", layou
|
|
|
33
33
|
})
|
|
34
34
|
});
|
|
35
35
|
},
|
|
36
|
-
|
|
37
|
-
return [(_c, next) => blogContext.run(blogCtx, next)];
|
|
38
|
-
},
|
|
39
|
-
unstable_onSSGRequest(_req, next) {
|
|
40
|
-
return blogContext.run(blogCtx, next);
|
|
41
|
-
},
|
|
42
|
-
async createPages({ createPage, createLayout }) {
|
|
36
|
+
async createPages({ createPage, createLayout, createInterceptor }) {
|
|
43
37
|
const renderMode = this.mode === "default" ? "static" : this.mode;
|
|
44
38
|
const blogPages = (await this.getLoader()).getPages().filter(isBlog.bind(this));
|
|
39
|
+
createInterceptor((next) => blogContext.run(blogCtx, next));
|
|
45
40
|
createLayout({
|
|
46
41
|
render: renderMode,
|
|
47
42
|
path: this.i18nConfig ? "/[lang]/(blog)" : "/(blog)",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ImageProvider } from "../../components/image.js";
|
|
3
|
+
import { joinPathname } from "../../lib/pathname.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
//#region src/plugins/image/cloudflare.client.tsx
|
|
7
|
+
function createProvider(config) {
|
|
8
|
+
return {
|
|
9
|
+
name: "cloudflare",
|
|
10
|
+
defaultQuality: config.defaultQuality,
|
|
11
|
+
deviceSizes: config.sizes,
|
|
12
|
+
sizes: config.sizes,
|
|
13
|
+
buildImageUrl({ src, width, quality }) {
|
|
14
|
+
const parts = [
|
|
15
|
+
`width=${width}`,
|
|
16
|
+
`quality=${quality}`,
|
|
17
|
+
`format=${config.format}`
|
|
18
|
+
];
|
|
19
|
+
if (config.fit) parts.push(`fit=${config.fit}`);
|
|
20
|
+
return joinPathname(config.path, parts.join(","), src);
|
|
21
|
+
},
|
|
22
|
+
canOptimize(src) {
|
|
23
|
+
if (import.meta.env.DEV) return false;
|
|
24
|
+
if (!config.dangerouslyAllowSVG && src.split("?", 1)[0].endsWith(".svg")) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function CloudflareImageProvider({ config, children }) {
|
|
30
|
+
return /* @__PURE__ */ jsx(ImageProvider, {
|
|
31
|
+
provider: useMemo(() => createProvider(config), [config]),
|
|
32
|
+
children
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { CloudflareImageProvider };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConfigContext } from "../../config.js";
|
|
2
|
+
import { ServerPlugin } from "../../lib/types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/image/cloudflare.d.ts
|
|
5
|
+
/** @see https://developers.cloudflare.com/images/transform-images/transform-via-url/ */
|
|
6
|
+
interface CloudflareImageOptions {
|
|
7
|
+
/** Image transformation path prefix. @default "/cdn-cgi/image" */
|
|
8
|
+
path?: string;
|
|
9
|
+
/** Allowed image widths for optimization and srcSet generation. (ascending order) */
|
|
10
|
+
sizes?: number[];
|
|
11
|
+
/** Allowed quality values (1–100). @default [75] */
|
|
12
|
+
qualities?: number[];
|
|
13
|
+
/** Output format. @default "auto" */
|
|
14
|
+
format?: "auto" | "avif" | "webp" | "jpeg" | "png" | "json";
|
|
15
|
+
/** How the image fits the requested dimensions. */
|
|
16
|
+
fit?: "scale-down" | "contain" | "cover" | "crop" | "pad";
|
|
17
|
+
/** Allow SVG optimization. @default false */
|
|
18
|
+
dangerouslyAllowSVG?: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function imagePlugin<C extends ConfigContext = ConfigContext>(options?: CloudflareImageOptions): ServerPlugin<C>;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { CloudflareImageOptions, imagePlugin };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CloudflareImageProvider } from "./cloudflare.client.js";
|
|
2
|
+
import { resolveCloudflareImageConfig } from "./cloudflare.utils.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/plugins/image/cloudflare.tsx
|
|
5
|
+
function imagePlugin(options = {}) {
|
|
6
|
+
const config = resolveCloudflareImageConfig(options);
|
|
7
|
+
return {
|
|
8
|
+
name: "image:cloudflare",
|
|
9
|
+
init() {
|
|
10
|
+
(this.data["core:provider"] ??= []).push((props) => {
|
|
11
|
+
props.children = /* @__PURE__ */ jsx(CloudflareImageProvider, {
|
|
12
|
+
config,
|
|
13
|
+
children: props.children
|
|
14
|
+
});
|
|
15
|
+
return props;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { imagePlugin };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/plugins/image/cloudflare.utils.ts
|
|
2
|
+
function resolveCloudflareImageConfig(options = {}) {
|
|
3
|
+
const qualities = options.qualities ?? [75];
|
|
4
|
+
return {
|
|
5
|
+
path: options.path ?? "/cdn-cgi/image",
|
|
6
|
+
dangerouslyAllowSVG: options.dangerouslyAllowSVG ?? false,
|
|
7
|
+
format: options.format ?? "auto",
|
|
8
|
+
fit: options.fit,
|
|
9
|
+
sizes: options.sizes ? options.sizes.sort((a, b) => a - b) : [
|
|
10
|
+
16,
|
|
11
|
+
32,
|
|
12
|
+
48,
|
|
13
|
+
64,
|
|
14
|
+
96,
|
|
15
|
+
128,
|
|
16
|
+
256,
|
|
17
|
+
384,
|
|
18
|
+
640,
|
|
19
|
+
750,
|
|
20
|
+
828,
|
|
21
|
+
1080,
|
|
22
|
+
1200,
|
|
23
|
+
1920,
|
|
24
|
+
2048,
|
|
25
|
+
3840
|
|
26
|
+
],
|
|
27
|
+
qualities,
|
|
28
|
+
defaultQuality: Math.max(...qualities)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { resolveCloudflareImageConfig };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ConfigContext } from "../config.js";
|
|
2
|
+
import { Awaitable, ServerPlugin } from "../lib/types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/link-validation.d.ts
|
|
5
|
+
interface LinkSSGContext {
|
|
6
|
+
links: {
|
|
7
|
+
href: string;
|
|
8
|
+
fromPathname: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
type ValidateResult = "not-found" | null;
|
|
12
|
+
declare global {
|
|
13
|
+
/** server always share the same `global` during build, we can use it to collect pre-rendered links */
|
|
14
|
+
var LINK_SSG_CONTEXT: LinkSSGContext | undefined;
|
|
15
|
+
}
|
|
16
|
+
interface LinkValidationOptions {
|
|
17
|
+
/** whether the link is skipped for validation */
|
|
18
|
+
ignored?: (href: string) => boolean;
|
|
19
|
+
/** when external link is discovered */
|
|
20
|
+
externalLink?: (href: string) => Awaitable<ValidateResult>;
|
|
21
|
+
}
|
|
22
|
+
declare function linkValidationPlugin<C extends ConfigContext>(options?: LinkValidationOptions): ServerPlugin<C>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { LinkValidationOptions, linkValidationPlugin };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/plugins/link-validation.tsx
|
|
2
|
+
function linkValidationPlugin(options = {}) {
|
|
3
|
+
const { ignored, externalLink } = options;
|
|
4
|
+
return { unstable_onServerEntry(entry) {
|
|
5
|
+
const build = entry.build;
|
|
6
|
+
entry.build = async (...args) => {
|
|
7
|
+
const context = global.LINK_SSG_CONTEXT = { links: [] };
|
|
8
|
+
const res = await build(...args);
|
|
9
|
+
const hrefMap = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const link of context.links) hrefMap.set(link.href, null);
|
|
11
|
+
await Promise.all(Array.from(hrefMap.keys()).map(async (href) => {
|
|
12
|
+
if (href.startsWith("mailto:") || href.startsWith("#")) return;
|
|
13
|
+
if (ignored && ignored(href)) return;
|
|
14
|
+
if (URL.canParse(href)) {
|
|
15
|
+
if (externalLink) hrefMap.set(href, await externalLink(href));
|
|
16
|
+
} else if ((await entry.fetch(new Request(new URL(href, "http://localhost")))).status === 404) hrefMap.set(href, "not-found");
|
|
17
|
+
}));
|
|
18
|
+
const errors = [];
|
|
19
|
+
for (const link of context.links) {
|
|
20
|
+
const info = hrefMap.get(link.href);
|
|
21
|
+
if (info) errors.push(`In "${link.fromPathname}": link "${link.href}" ${info}`);
|
|
22
|
+
}
|
|
23
|
+
if (errors.length > 0) throw new Error("\n" + errors.join("\n"));
|
|
24
|
+
return res;
|
|
25
|
+
};
|
|
26
|
+
return entry;
|
|
27
|
+
} };
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { linkValidationPlugin };
|
package/dist/plugins/llms.txt.js
CHANGED
|
@@ -38,7 +38,7 @@ function llmsPlugin(options = {}) {
|
|
|
38
38
|
return res;
|
|
39
39
|
});
|
|
40
40
|
},
|
|
41
|
-
createMiddlewares() {
|
|
41
|
+
createMiddlewares({ app }) {
|
|
42
42
|
if (this.mode === "static") return;
|
|
43
43
|
const middlewares = [];
|
|
44
44
|
const parsePathname = (pathname) => {
|
|
@@ -49,23 +49,26 @@ function llmsPlugin(options = {}) {
|
|
|
49
49
|
} : void 0;
|
|
50
50
|
return { slugs };
|
|
51
51
|
};
|
|
52
|
-
if (autoRedirect) middlewares.push(async ({ req
|
|
52
|
+
if (autoRedirect) middlewares.push(async ({ req }, next) => {
|
|
53
53
|
if (req.path.endsWith(".md") || !isMarkdownPreferred(req.raw)) return next();
|
|
54
54
|
const parsed = parsePathname(req.path);
|
|
55
55
|
if (!parsed) return next();
|
|
56
56
|
const { lang, slugs } = parsed;
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const url = new URL(slugsToMarkdownPath(slugs, lang).pathname, req.url);
|
|
58
|
+
const res = await app.fetch(new Request(url));
|
|
59
|
+
if (!res.ok) return next();
|
|
60
|
+
return res;
|
|
59
61
|
});
|
|
60
|
-
if (this.mode === "dynamic") middlewares.push(async ({ req
|
|
62
|
+
if (this.mode === "dynamic") middlewares.push(async ({ req }, next) => {
|
|
61
63
|
if (!req.path.endsWith(".md")) return next();
|
|
62
64
|
const parsed = parsePathname(req.path);
|
|
63
65
|
if (!parsed || parsed.slugs[0] === "_llms.txt") return next();
|
|
64
66
|
const { lang, slugs } = parsed;
|
|
65
|
-
const loader = await this.getLoader();
|
|
66
67
|
slugs[slugs.length - 1] = slugs[slugs.length - 1].replace(/\.md$/, "");
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const url = new URL(slugsToMarkdownPath(slugs, lang).pathname, req.url);
|
|
69
|
+
const res = await app.fetch(new Request(url));
|
|
70
|
+
if (!res.ok) return next();
|
|
71
|
+
return res;
|
|
69
72
|
});
|
|
70
73
|
return middlewares;
|
|
71
74
|
},
|
|
@@ -92,7 +95,7 @@ function llmsPlugin(options = {}) {
|
|
|
92
95
|
createApiIsomorphic({
|
|
93
96
|
render: renderMode,
|
|
94
97
|
path: joinPathname(this.i18nConfig ? "[lang]" : "", basePath, "[...slugs]"),
|
|
95
|
-
staticPaths:
|
|
98
|
+
staticPaths: (await this.getLoader()).getPages().map((page) => slugsToMarkdownPath(page.slugs, page.locale).staticPath),
|
|
96
99
|
handler: async (_req, { params }) => {
|
|
97
100
|
const page = (await this.getLoader()).getPage(markdownPathToSlugs(params.slugs), params.lang);
|
|
98
101
|
if (!page) unstable_notFound();
|
|
@@ -11,7 +11,11 @@ interface OpenAPIOptions {
|
|
|
11
11
|
disableLoaderPlugin?: boolean;
|
|
12
12
|
/** must be a client component */
|
|
13
13
|
ClientAPIPage?: FC<ClientApiPageProps>;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Create proxy server.
|
|
16
|
+
*
|
|
17
|
+
* By default, it will create one when `proxyUrl` is specified in `createOpenAPI()`.
|
|
18
|
+
*/
|
|
15
19
|
createProxy?: boolean | (() => Awaitable<ReturnType<OpenAPIServer["createProxy"]>>);
|
|
16
20
|
}
|
|
17
21
|
/**
|
package/dist/plugins/openapi.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainPathname } from "../lib/pathname.js";
|
|
1
2
|
import { PayloadProvider, WithPayload } from "../components/openapi.payload.js";
|
|
2
3
|
import { jsx } from "react/jsx-runtime";
|
|
3
4
|
//#region src/plugins/openapi.tsx
|
|
@@ -5,7 +6,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
5
6
|
* this will register the OpenAPI adapter & required layout configs.
|
|
6
7
|
*/
|
|
7
8
|
function openapiPlugin(options) {
|
|
8
|
-
const { server, disableLoaderPlugin = false
|
|
9
|
+
const { server, disableLoaderPlugin = false } = options;
|
|
9
10
|
function initRenderers(data) {
|
|
10
11
|
(data.renderers ??= []).push(function(data) {
|
|
11
12
|
if (isOpenAPI(this.page.data)) data.pageProps.full ??= true;
|
|
@@ -42,8 +43,9 @@ function openapiPlugin(options) {
|
|
|
42
43
|
return options;
|
|
43
44
|
},
|
|
44
45
|
async createPages({ createApi }) {
|
|
46
|
+
const proxyUrl = server.options.proxyUrl;
|
|
47
|
+
const { createProxy = typeof proxyUrl === "string" && isPlainPathname(proxyUrl) } = options;
|
|
45
48
|
if (createProxy) {
|
|
46
|
-
const proxyUrl = server.options.proxyUrl;
|
|
47
49
|
if (!proxyUrl) throw new Error(`[Fumapress] The "proxyUrl" option in createOpenAPI() is required to create proxy server`);
|
|
48
50
|
if (this.mode === "static") throw new Error(`[Fumapress] static mode is not compatible with proxy server`);
|
|
49
51
|
createApi({
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { AppContext } from "../lib/shared.js";
|
|
2
|
+
import { ConfigContext } from "../config.js";
|
|
3
|
+
import { Awaitable, ServerPlugin } from "../lib/types.js";
|
|
4
|
+
|
|
5
|
+
//#region src/plugins/sitemap.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* How frequently a page is likely to change.
|
|
8
|
+
*
|
|
9
|
+
* @see https://www.sitemaps.org/protocol.html#changefreqdef
|
|
10
|
+
*/
|
|
11
|
+
type SitemapChangeFrequency = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
12
|
+
/**
|
|
13
|
+
* Last modification timestamp in W3C Datetime format (ISO 8601 subset).
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.sitemaps.org/protocol.html#xmlDefinition
|
|
16
|
+
*/
|
|
17
|
+
type SitemapLastMod = Date | string;
|
|
18
|
+
/**
|
|
19
|
+
* Priority of a URL relative to other URLs on the site.
|
|
20
|
+
*
|
|
21
|
+
* Valid values are decimals between `0.0` and `1.0` inclusive.
|
|
22
|
+
*
|
|
23
|
+
* @see https://www.sitemaps.org/protocol.html#prioritydef
|
|
24
|
+
*/
|
|
25
|
+
type SitemapPriority = number;
|
|
26
|
+
/**
|
|
27
|
+
* `rel` attribute for sitemap link elements. The protocol only defines `alternate` for hreflang.
|
|
28
|
+
*
|
|
29
|
+
* @see https://www.sitemaps.org/protocol.html#xmlDefinition
|
|
30
|
+
*/
|
|
31
|
+
type SitemapLinkRel = "alternate";
|
|
32
|
+
/**
|
|
33
|
+
* An `xhtml:link` alternate language reference on a URL entry.
|
|
34
|
+
*
|
|
35
|
+
* @see https://www.sitemaps.org/protocol.html#xmlDefinition
|
|
36
|
+
*/
|
|
37
|
+
interface SitemapAlternateLink {
|
|
38
|
+
rel: SitemapLinkRel;
|
|
39
|
+
/** BCP 47 language tag (e.g. `en`, `de`, `x-default`). */
|
|
40
|
+
hreflang: string;
|
|
41
|
+
/** Fully-qualified URL of the alternate page. */
|
|
42
|
+
href: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* An `image:image` entry (Google image sitemap extension).
|
|
46
|
+
*
|
|
47
|
+
* @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/image-sitemaps
|
|
48
|
+
*/
|
|
49
|
+
interface SitemapImage {
|
|
50
|
+
/** URL of the image. */
|
|
51
|
+
loc: string;
|
|
52
|
+
caption?: string;
|
|
53
|
+
geo_location?: string;
|
|
54
|
+
title?: string;
|
|
55
|
+
license?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Publication metadata for a `news:news` entry (Google News sitemap extension).
|
|
59
|
+
*
|
|
60
|
+
* @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap
|
|
61
|
+
*/
|
|
62
|
+
interface SitemapNewsPublication {
|
|
63
|
+
/** Publication name; must match Google News exactly if submitted there. */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Primary language of the publication in ISO 639 format (two or three letter code). */
|
|
66
|
+
language: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A `news:news` entry (Google News sitemap extension).
|
|
70
|
+
*
|
|
71
|
+
* @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap
|
|
72
|
+
*/
|
|
73
|
+
interface SitemapNews {
|
|
74
|
+
publication: SitemapNewsPublication;
|
|
75
|
+
/** Publication date in W3C Datetime format. */
|
|
76
|
+
publication_date: SitemapLastMod;
|
|
77
|
+
/** Title of the news article. */
|
|
78
|
+
title: string;
|
|
79
|
+
/** Comma-separated stock tickers (optional). */
|
|
80
|
+
stock_tickers?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A `video:video` entry (Google video sitemap extension).
|
|
84
|
+
*
|
|
85
|
+
* Only common fields are listed; see Google's schema for the full set.
|
|
86
|
+
*
|
|
87
|
+
* @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps
|
|
88
|
+
*/
|
|
89
|
+
interface SitemapVideo {
|
|
90
|
+
/** URL of the player page or direct link to the video. */
|
|
91
|
+
content_loc?: string;
|
|
92
|
+
/** URL of the player page for this video. */
|
|
93
|
+
player_loc?: string;
|
|
94
|
+
/** Whether the video may be embedded. */
|
|
95
|
+
"player_loc@allow_embed"?: boolean;
|
|
96
|
+
/** Whether to show a paywall to users from the given countries. */
|
|
97
|
+
"player_loc@restrict"?: string;
|
|
98
|
+
/** A URL pointing to the video thumbnail image. */
|
|
99
|
+
thumbnail_loc: string;
|
|
100
|
+
title: string;
|
|
101
|
+
description: string;
|
|
102
|
+
/** Duration in seconds (max 28800). */
|
|
103
|
+
duration?: number;
|
|
104
|
+
/** Expiration date in W3C Datetime format; omit if the video does not expire. */
|
|
105
|
+
expiration_date?: SitemapLastMod;
|
|
106
|
+
/** Rating value. 0.0–5.0 or unrated if omitted. */
|
|
107
|
+
rating?: number;
|
|
108
|
+
/** Number of times the video has been viewed. */
|
|
109
|
+
view_count?: number;
|
|
110
|
+
/** Publication date in W3C Datetime format. */
|
|
111
|
+
publication_date?: SitemapLastMod;
|
|
112
|
+
/** Whether the video is family friendly. */
|
|
113
|
+
family_friendly?: "yes" | "no";
|
|
114
|
+
/** Comma-separated list of tags. */
|
|
115
|
+
tag?: string | string[];
|
|
116
|
+
/** Comma-separated domains the video may not be played on. */
|
|
117
|
+
restriction?: string;
|
|
118
|
+
/** Whether search engines may download the video file. */
|
|
119
|
+
"restriction@relationship"?: "allow" | "deny";
|
|
120
|
+
gallery_loc?: string;
|
|
121
|
+
/** Price currency in ISO 4217 format. */
|
|
122
|
+
price?: string;
|
|
123
|
+
"price@currency"?: string;
|
|
124
|
+
"price@type"?: "rent" | "purchase" | "own" | "subscription";
|
|
125
|
+
/** Platform restriction (e.g. `web`, `mobile`, `tv`). */
|
|
126
|
+
platform?: string;
|
|
127
|
+
"platform@relationship"?: "allow" | "deny";
|
|
128
|
+
/** Whether a subscription is required. */
|
|
129
|
+
requires_subscription?: "yes" | "no";
|
|
130
|
+
uploader?: string;
|
|
131
|
+
"uploader@info"?: string;
|
|
132
|
+
live?: "yes" | "no";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A single `<url>` entry in a sitemap.
|
|
136
|
+
*
|
|
137
|
+
* @see https://www.sitemaps.org/protocol.html#xmlDefinition
|
|
138
|
+
*/
|
|
139
|
+
interface SitemapUrl {
|
|
140
|
+
/**
|
|
141
|
+
* Fully-qualified URL of the page.
|
|
142
|
+
*
|
|
143
|
+
* Must be less than 2,048 characters and include protocol + host per the spec.
|
|
144
|
+
*/
|
|
145
|
+
loc: string;
|
|
146
|
+
/**
|
|
147
|
+
* Last modification date of the file.
|
|
148
|
+
*
|
|
149
|
+
* Search engines may use the full W3C Datetime or the date portion only.
|
|
150
|
+
*/
|
|
151
|
+
lastmod?: SitemapLastMod;
|
|
152
|
+
/**
|
|
153
|
+
* Hint for how often the page changes.
|
|
154
|
+
*
|
|
155
|
+
* Note: The value is a hint, not a command.
|
|
156
|
+
*/
|
|
157
|
+
changefreq?: SitemapChangeFrequency;
|
|
158
|
+
/**
|
|
159
|
+
* Priority relative to other URLs on your site (`0.0`–`1.0`).
|
|
160
|
+
*
|
|
161
|
+
* Default priority of pages is `0.5`; this does not affect comparison across different sites.
|
|
162
|
+
*/
|
|
163
|
+
priority?: SitemapPriority;
|
|
164
|
+
/**
|
|
165
|
+
* Alternate language or regional URLs (`xhtml:link`).
|
|
166
|
+
*
|
|
167
|
+
* Requires the `xhtml` namespace on the root `urlset` when serialized.
|
|
168
|
+
*/
|
|
169
|
+
alternates?: readonly SitemapAlternateLink[];
|
|
170
|
+
/** Image URLs associated with this page (Google image extension). */
|
|
171
|
+
images?: readonly SitemapImage[];
|
|
172
|
+
/** Video metadata associated with this page (Google video extension). */
|
|
173
|
+
videos?: readonly SitemapVideo[];
|
|
174
|
+
/** News article metadata (Google News extension). */
|
|
175
|
+
news?: readonly SitemapNews[];
|
|
176
|
+
}
|
|
177
|
+
interface SitemapOptions<C extends ConfigContext = ConfigContext> {
|
|
178
|
+
/**
|
|
179
|
+
* Path for the sitemap route.
|
|
180
|
+
*
|
|
181
|
+
* @default "/sitemap.xml"
|
|
182
|
+
*/
|
|
183
|
+
path?: string;
|
|
184
|
+
/**
|
|
185
|
+
* Customize or exclude sitemap entries.
|
|
186
|
+
* Return `undefined` to exclude a page.
|
|
187
|
+
*/
|
|
188
|
+
getEntry?: (this: AppContext<C>, page: C["page"]) => Awaitable<SitemapUrl | undefined>;
|
|
189
|
+
/**
|
|
190
|
+
* Additional entries to include in the sitemap, be careful the `loc` field must be a fully-qualified URL.
|
|
191
|
+
*/
|
|
192
|
+
additionalEntries?: SitemapUrl[] | ((this: AppContext<C>) => Awaitable<SitemapUrl[]>);
|
|
193
|
+
}
|
|
194
|
+
declare function sitemapPlugin<C extends ConfigContext = ConfigContext>(options?: SitemapOptions<NoInfer<C>>): ServerPlugin<C>;
|
|
195
|
+
//#endregion
|
|
196
|
+
export { SitemapAlternateLink, SitemapChangeFrequency, SitemapImage, SitemapLastMod, SitemapLinkRel, SitemapNews, SitemapNewsPublication, SitemapOptions, SitemapPriority, SitemapUrl, SitemapVideo, sitemapPlugin };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getLastModifiedDate } from "../lib/shared.js";
|
|
2
|
+
import { js2xml } from "xml-js";
|
|
3
|
+
//#region src/plugins/sitemap.ts
|
|
4
|
+
function formatLastmod(value) {
|
|
5
|
+
return (value instanceof Date ? value : new Date(value)).toISOString();
|
|
6
|
+
}
|
|
7
|
+
function imageToElement(image) {
|
|
8
|
+
const element = { "image:loc": { _text: image.loc } };
|
|
9
|
+
if (image.caption) element["image:caption"] = { _text: image.caption };
|
|
10
|
+
if (image.geo_location) element["image:geo_location"] = { _text: image.geo_location };
|
|
11
|
+
if (image.title) element["image:title"] = { _text: image.title };
|
|
12
|
+
if (image.license) element["image:license"] = { _text: image.license };
|
|
13
|
+
return element;
|
|
14
|
+
}
|
|
15
|
+
function entryToUrlElement(entry) {
|
|
16
|
+
const url = { loc: { _text: entry.loc } };
|
|
17
|
+
if (entry.lastmod) url.lastmod = { _text: formatLastmod(entry.lastmod) };
|
|
18
|
+
if (entry.changefreq) url.changefreq = { _text: entry.changefreq };
|
|
19
|
+
if (entry.priority !== void 0) url.priority = { _text: String(entry.priority) };
|
|
20
|
+
if (entry.alternates?.length) url["xhtml:link"] = entry.alternates.map((alternate) => ({ _attributes: {
|
|
21
|
+
rel: alternate.rel,
|
|
22
|
+
hreflang: alternate.hreflang,
|
|
23
|
+
href: alternate.href
|
|
24
|
+
} }));
|
|
25
|
+
if (entry.images?.length) url["image:image"] = entry.images.map(imageToElement);
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
function buildSitemap(entries) {
|
|
29
|
+
return js2xml({
|
|
30
|
+
_declaration: { _attributes: {
|
|
31
|
+
version: "1.0",
|
|
32
|
+
encoding: "UTF-8"
|
|
33
|
+
} },
|
|
34
|
+
urlset: {
|
|
35
|
+
_attributes: {
|
|
36
|
+
xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
|
37
|
+
"xmlns:xhtml": "http://www.w3.org/1999/xhtml",
|
|
38
|
+
"xmlns:image": "http://www.google.com/schemas/sitemap-image/1.1"
|
|
39
|
+
},
|
|
40
|
+
url: entries.map(entryToUrlElement)
|
|
41
|
+
}
|
|
42
|
+
}, {
|
|
43
|
+
compact: true,
|
|
44
|
+
spaces: 0
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function sitemapPlugin(options = {}) {
|
|
48
|
+
const { path = "/sitemap.xml", getEntry: _getEntry = async function getEntryDefault(page) {
|
|
49
|
+
return {
|
|
50
|
+
loc: new URL(page.url, this.siteConfig.baseUrl).href,
|
|
51
|
+
lastmod: await getLastModifiedDate(this, page),
|
|
52
|
+
priority: .8
|
|
53
|
+
};
|
|
54
|
+
}, additionalEntries } = options;
|
|
55
|
+
return {
|
|
56
|
+
name: "core:sitemap",
|
|
57
|
+
async createPages({ createApiIsomorphic, unstable_getCreated }) {
|
|
58
|
+
const renderMode = this.mode === "default" ? "static" : this.mode;
|
|
59
|
+
const getEntry = _getEntry.bind(this);
|
|
60
|
+
createApiIsomorphic({
|
|
61
|
+
render: renderMode,
|
|
62
|
+
path,
|
|
63
|
+
handler: async () => {
|
|
64
|
+
const source = await this.getLoader();
|
|
65
|
+
const entries = [];
|
|
66
|
+
const pageLocs = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const entry of await Promise.all(source.getPages().map(getEntry))) {
|
|
68
|
+
if (!entry) continue;
|
|
69
|
+
pageLocs.add(entry.loc);
|
|
70
|
+
entries.push(entry);
|
|
71
|
+
}
|
|
72
|
+
for (const route of await unstable_getCreated().unstable_getRouterConfigs()) if (route.isStatic && route.type === "route") {
|
|
73
|
+
const segments = route.path.map((v) => v.name);
|
|
74
|
+
if (segments.at(-1) === "404") continue;
|
|
75
|
+
const loc = new URL("/" + segments.join("/"), this.siteConfig.baseUrl).href;
|
|
76
|
+
if (pageLocs.has(loc)) continue;
|
|
77
|
+
entries.push({
|
|
78
|
+
loc,
|
|
79
|
+
priority: 1
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (additionalEntries) entries.push(...typeof additionalEntries === "function" ? await additionalEntries.call(this) : additionalEntries);
|
|
83
|
+
return new Response(buildSitemap(entries), { headers: { "Content-Type": "application/xml" } });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { sitemapPlugin };
|
package/dist/plugins/takumi.js
CHANGED
|
@@ -57,11 +57,10 @@ function takumiPlugin(options = {}) {
|
|
|
57
57
|
});
|
|
58
58
|
},
|
|
59
59
|
async createPages({ createApiIsomorphic }) {
|
|
60
|
-
const renderMode = this.mode === "default" ? "static" : this.mode;
|
|
61
60
|
createApiIsomorphic({
|
|
62
|
-
render:
|
|
61
|
+
render: this.mode === "default" ? "static" : this.mode,
|
|
63
62
|
path: joinPathname(this.i18nConfig ? "[lang]" : "", basePath, "[...slugs]"),
|
|
64
|
-
staticPaths:
|
|
63
|
+
staticPaths: (await this.getLoader()).getPages().map((page) => slugsToImagePath(page.slugs, page.locale).staticPath),
|
|
65
64
|
handler: async (_, { params }) => {
|
|
66
65
|
const page = (await this.getLoader()).getPage(imagePathToSlugs(params.slugs), params.lang);
|
|
67
66
|
if (!page) unstable_notFound();
|
package/dist/router/index.d.ts
CHANGED
|
@@ -3,15 +3,18 @@ import { ConfigBuilder, ConfigContext } from "../config.js";
|
|
|
3
3
|
import { Awaitable, RouteFns } from "../lib/types.js";
|
|
4
4
|
import { createPages } from "waku";
|
|
5
5
|
import { unstable_notFound as notFound, unstable_redirect as redirect } from "waku/router/server";
|
|
6
|
+
import { Hono } from "hono/tiny";
|
|
6
7
|
import { MiddlewareHandler } from "hono";
|
|
7
8
|
import { unstable_createServerEntryAdapter } from "waku/adapter-builders";
|
|
8
9
|
|
|
9
10
|
//#region src/router/index.d.ts
|
|
10
11
|
interface Router<C extends ConfigContext = ConfigContext> {
|
|
11
12
|
createPages: (fn?: (this: AppContext<C>, fns: RouteFns) => Awaitable<void>, options?: Options) => ReturnType<typeof createPages>;
|
|
12
|
-
createMiddlewares: () => ((
|
|
13
|
-
|
|
13
|
+
createMiddlewares: () => ((opts: {
|
|
14
|
+
app: Hono;
|
|
15
|
+
}) => MiddlewareHandler)[];
|
|
16
|
+
patchAdapter: <Options>(adapter: ReturnType<typeof unstable_createServerEntryAdapter<Options>>) => ReturnType<typeof unstable_createServerEntryAdapter<Options>>;
|
|
14
17
|
}
|
|
15
|
-
declare function createRouter<C extends ConfigContext>(userConfig: ConfigBuilder<C>): Router<C
|
|
18
|
+
declare function createRouter<C extends ConfigContext>(userConfig: ConfigBuilder<C>): Promise<Router<C>>;
|
|
16
19
|
//#endregion
|
|
17
20
|
export { Router, createRouter, notFound, redirect };
|
package/dist/router/index.js
CHANGED
|
@@ -4,17 +4,16 @@ import { Fragment as Fragment$1 } from "react";
|
|
|
4
4
|
import { createPages } from "waku";
|
|
5
5
|
import { unstable_notFound, unstable_notFound as notFound, unstable_redirect as redirect } from "waku/router/server";
|
|
6
6
|
//#region src/router/index.tsx
|
|
7
|
-
function createRouter(userConfig) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
function createPages$1(base, createPagesOptions) {
|
|
13
|
-
return createPages(async (_fns) => {
|
|
14
|
-
const context = await getAppContext();
|
|
7
|
+
async function createRouter(userConfig) {
|
|
8
|
+
const context = await initApp(userConfig);
|
|
9
|
+
function createPages$2(base, createPagesOptions) {
|
|
10
|
+
const result = createPages(async (_fns) => {
|
|
15
11
|
const layouts = context.layouts;
|
|
16
12
|
const fns = {
|
|
17
13
|
..._fns,
|
|
14
|
+
unstable_getCreated() {
|
|
15
|
+
return result;
|
|
16
|
+
},
|
|
18
17
|
createApiIsomorphic(config) {
|
|
19
18
|
if (config.render === "static") _fns.createApi({
|
|
20
19
|
render: "static",
|
|
@@ -40,11 +39,12 @@ function createRouter(userConfig) {
|
|
|
40
39
|
}
|
|
41
40
|
return page;
|
|
42
41
|
}
|
|
42
|
+
fns.createInterceptor((next) => appContext.run(context, next));
|
|
43
43
|
await base?.call(context, fns);
|
|
44
44
|
for (const plugin of context.plugins) await plugin.createPages?.call(context, fns);
|
|
45
45
|
const staticPaths = [];
|
|
46
46
|
const defaultRenderMode = context.mode === "default" ? "static" : context.mode;
|
|
47
|
-
|
|
47
|
+
outer: for (const page of (await context.getLoader()).getPages()) {
|
|
48
48
|
for (const plugin of context.plugins) if (await plugin.resolvePage?.call(context, page) === false) continue outer;
|
|
49
49
|
staticPaths.push(page.locale ? [page.locale, ...page.slugs] : page.slugs);
|
|
50
50
|
}
|
|
@@ -131,18 +131,12 @@ function createRouter(userConfig) {
|
|
|
131
131
|
}
|
|
132
132
|
return null;
|
|
133
133
|
}, createPagesOptions);
|
|
134
|
+
return result;
|
|
134
135
|
}
|
|
135
|
-
function
|
|
136
|
-
return async (_c, next) => {
|
|
137
|
-
const ctx = await getAppContext();
|
|
138
|
-
return appContext.run(ctx, next);
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
function pluginsMiddleware() {
|
|
136
|
+
function pluginsMiddleware(opts) {
|
|
142
137
|
async function init() {
|
|
143
|
-
const ctx = await getAppContext();
|
|
144
138
|
const out = [];
|
|
145
|
-
const resolved = await Promise.all(
|
|
139
|
+
const resolved = await Promise.all(context.plugins.map((plugin) => plugin.createMiddlewares?.call(context, opts)));
|
|
146
140
|
for (const v of resolved) if (v) out.push(...v);
|
|
147
141
|
return out;
|
|
148
142
|
}
|
|
@@ -162,36 +156,18 @@ function createRouter(userConfig) {
|
|
|
162
156
|
return response;
|
|
163
157
|
};
|
|
164
158
|
}
|
|
165
|
-
|
|
166
|
-
const ctx = await getAppContext();
|
|
159
|
+
function patchAdapter(adapter) {
|
|
167
160
|
return (handlers, options) => {
|
|
168
|
-
let entry = adapter(
|
|
169
|
-
|
|
170
|
-
handleBuild(utils) {
|
|
171
|
-
const hooks = [];
|
|
172
|
-
hooks.push(utils.withRequest);
|
|
173
|
-
hooks.push((_req, fn) => appContext.run(ctx, fn));
|
|
174
|
-
for (const plugin of ctx.plugins) if (plugin.unstable_onSSGRequest) hooks.push(plugin.unstable_onSSGRequest);
|
|
175
|
-
function runHook(req, fn, index = 0) {
|
|
176
|
-
const hook = hooks[index];
|
|
177
|
-
if (!hook) return fn();
|
|
178
|
-
return hook(req, () => runHook(req, fn, index + 1));
|
|
179
|
-
}
|
|
180
|
-
return handlers.handleBuild({
|
|
181
|
-
...utils,
|
|
182
|
-
withRequest: runHook
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}, options);
|
|
186
|
-
for (const plugin of ctx.plugins) if (plugin.unstable_onServerEntry) entry = plugin.unstable_onServerEntry(entry);
|
|
161
|
+
let entry = adapter(handlers, options);
|
|
162
|
+
for (const plugin of context.plugins) if (plugin.unstable_onServerEntry) entry = plugin.unstable_onServerEntry(entry);
|
|
187
163
|
return entry;
|
|
188
164
|
};
|
|
189
165
|
}
|
|
190
166
|
return {
|
|
191
|
-
createPages: createPages$
|
|
167
|
+
createPages: createPages$2,
|
|
192
168
|
patchAdapter,
|
|
193
169
|
createMiddlewares() {
|
|
194
|
-
return [
|
|
170
|
+
return [pluginsMiddleware];
|
|
195
171
|
}
|
|
196
172
|
};
|
|
197
173
|
}
|
package/dist/vite.js
CHANGED
|
@@ -60,10 +60,10 @@ const modules = import.meta.glob("./pages/**/*.{ts,tsx,js,jsx}", {
|
|
|
60
60
|
base: '/src',
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
const router = createRouter(pressConfig);
|
|
63
|
+
const router = await createRouter(pressConfig);
|
|
64
64
|
const pages = router.createPages(fsRouterFn(modules));
|
|
65
65
|
const middlewareFns = router.createMiddlewares();
|
|
66
|
-
const adapter =
|
|
66
|
+
const adapter = router.patchAdapter(_adapter);
|
|
67
67
|
|
|
68
68
|
export default adapter(pages, { middlewareFns });
|
|
69
69
|
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fumapress",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "An opinionated docs framework powered by Fumadocs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Docs",
|
|
@@ -34,12 +34,15 @@
|
|
|
34
34
|
"./layouts/switch": "./dist/layouts/switch.js",
|
|
35
35
|
"./plugins/blog": "./dist/plugins/blog.js",
|
|
36
36
|
"./plugins/flexsearch": "./dist/plugins/flexsearch.js",
|
|
37
|
+
"./plugins/image/cloudflare": "./dist/plugins/image/cloudflare.js",
|
|
37
38
|
"./plugins/image/self-hosted": "./dist/plugins/image/self-hosted.js",
|
|
38
39
|
"./plugins/image/vercel": "./dist/plugins/image/vercel.js",
|
|
39
40
|
"./plugins/image/vercel.enhancer": "./dist/plugins/image/vercel.enhancer.js",
|
|
41
|
+
"./plugins/link-validation": "./dist/plugins/link-validation.js",
|
|
40
42
|
"./plugins/llms.txt": "./dist/plugins/llms.txt.js",
|
|
41
43
|
"./plugins/openapi": "./dist/plugins/openapi.js",
|
|
42
44
|
"./plugins/orama-search": "./dist/plugins/orama-search.js",
|
|
45
|
+
"./plugins/sitemap": "./dist/plugins/sitemap.js",
|
|
43
46
|
"./plugins/takumi": "./dist/plugins/takumi.js",
|
|
44
47
|
"./router": "./dist/router/index.js",
|
|
45
48
|
"./router/fs": "./dist/router/fs.js",
|
|
@@ -56,11 +59,10 @@
|
|
|
56
59
|
"@takumi-rs/image-response": "^1.6.0",
|
|
57
60
|
"class-variance-authority": "^0.7.1",
|
|
58
61
|
"flexsearch": "^0.8.212",
|
|
59
|
-
"fumadocs-core": "^16.9.3",
|
|
60
|
-
"fumadocs-ui": "^16.9.3",
|
|
61
62
|
"lucide-react": "^1.17.0",
|
|
62
63
|
"tailwind-merge": "^3.6.0",
|
|
63
|
-
"vite": "^8.0.
|
|
64
|
+
"vite": "^8.0.16",
|
|
65
|
+
"xml-js": "^1.6.11",
|
|
64
66
|
"zod": "^4.4.3"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
|
@@ -69,24 +71,28 @@
|
|
|
69
71
|
"@types/http-cache-semantics": "^4.2.0",
|
|
70
72
|
"@types/mdx": "^2.0.13",
|
|
71
73
|
"@types/node": "^25.9.1",
|
|
72
|
-
"@types/react": "^19.2.
|
|
74
|
+
"@types/react": "^19.2.16",
|
|
73
75
|
"@types/react-dom": "^19.2.3",
|
|
76
|
+
"fumadocs-core": "^16.9.3",
|
|
74
77
|
"fumadocs-mdx": "^15.0.10",
|
|
75
|
-
"fumadocs-openapi": "^10.
|
|
78
|
+
"fumadocs-openapi": "^10.10.3",
|
|
79
|
+
"fumadocs-ui": "^16.9.3",
|
|
76
80
|
"hono": "^4.12.23",
|
|
77
81
|
"http-cache-semantics": "^4.2.0",
|
|
78
|
-
"react": "^19.2.
|
|
79
|
-
"react-dom": "^19.2.
|
|
82
|
+
"react": "^19.2.7",
|
|
83
|
+
"react-dom": "^19.2.7",
|
|
80
84
|
"sharp": "^0.34.5",
|
|
81
|
-
"tsdown": "0.22.
|
|
85
|
+
"tsdown": "0.22.1",
|
|
82
86
|
"typescript": "^6.0.3",
|
|
83
|
-
"waku": "1.0.0-beta.
|
|
87
|
+
"waku": "1.0.0-beta.2"
|
|
84
88
|
},
|
|
85
89
|
"peerDependencies": {
|
|
86
90
|
"@types/mdx": "*",
|
|
87
91
|
"@types/react": "*",
|
|
92
|
+
"fumadocs-core": "^16.9.3",
|
|
88
93
|
"fumadocs-mdx": "^15.0.0",
|
|
89
94
|
"fumadocs-openapi": "^10.8.0",
|
|
95
|
+
"fumadocs-ui": "^16.9.3",
|
|
90
96
|
"hono": "*",
|
|
91
97
|
"react": "^19.2.0",
|
|
92
98
|
"react-dom": "^19.2.0",
|