@pyreon/zero 0.11.8 → 0.11.10
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/lib/font.js +20 -7
- package/lib/font.js.map +1 -1
- package/lib/fs-router-BkbIWqek.js.map +1 -1
- package/lib/{fs-router-n4VA4lxu.js → fs-router-Dil4IKZR.js} +23 -19
- package/lib/fs-router-Dil4IKZR.js.map +1 -0
- package/lib/image-plugin.js.map +1 -1
- package/lib/index.js +893 -24
- package/lib/index.js.map +1 -1
- package/lib/link.js +13 -1
- package/lib/link.js.map +1 -1
- package/lib/types/actions.d.ts +57 -0
- package/lib/types/actions.d.ts.map +1 -0
- package/lib/types/adapters/bun.d.ts +6 -0
- package/lib/types/adapters/bun.d.ts.map +1 -0
- package/lib/types/adapters/index.d.ts +10 -0
- package/lib/types/adapters/index.d.ts.map +1 -0
- package/lib/types/adapters/node.d.ts +6 -0
- package/lib/types/adapters/node.d.ts.map +1 -0
- package/lib/types/adapters/static.d.ts +7 -0
- package/lib/types/adapters/static.d.ts.map +1 -0
- package/lib/types/api-routes.d.ts +66 -0
- package/lib/types/api-routes.d.ts.map +1 -0
- package/lib/types/app.d.ts +24 -0
- package/lib/types/app.d.ts.map +1 -0
- package/lib/types/cache.d.ts +54 -0
- package/lib/types/cache.d.ts.map +1 -0
- package/lib/types/client.d.ts +19 -0
- package/lib/types/client.d.ts.map +1 -0
- package/lib/types/compression.d.ts +33 -0
- package/lib/types/compression.d.ts.map +1 -0
- package/lib/types/config.d.ts +18 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/cors.d.ts +32 -0
- package/lib/types/cors.d.ts.map +1 -0
- package/lib/types/entry-server.d.ts +37 -0
- package/lib/types/entry-server.d.ts.map +1 -0
- package/lib/types/error-overlay.d.ts +6 -0
- package/lib/types/error-overlay.d.ts.map +1 -0
- package/lib/types/favicon.d.ts +43 -0
- package/lib/types/favicon.d.ts.map +1 -0
- package/lib/types/font.d.ts +119 -0
- package/lib/types/font.d.ts.map +1 -0
- package/lib/types/fs-router.d.ts +47 -0
- package/lib/types/fs-router.d.ts.map +1 -0
- package/lib/types/i18n-routing.d.ts +98 -0
- package/lib/types/i18n-routing.d.ts.map +1 -0
- package/lib/types/image-plugin.d.ts +79 -0
- package/lib/types/image-plugin.d.ts.map +1 -0
- package/lib/types/image.d.ts +51 -0
- package/lib/types/image.d.ts.map +1 -0
- package/lib/types/index.d.ts +46 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/isr.d.ts +9 -0
- package/lib/types/isr.d.ts.map +1 -0
- package/lib/types/link.d.ts +127 -0
- package/lib/types/link.d.ts.map +1 -0
- package/lib/types/meta.d.ts +91 -0
- package/lib/types/meta.d.ts.map +1 -0
- package/lib/types/middleware.d.ts +35 -0
- package/lib/types/middleware.d.ts.map +1 -0
- package/lib/types/not-found.d.ts +7 -0
- package/lib/types/not-found.d.ts.map +1 -0
- package/lib/types/rate-limit.d.ts +34 -0
- package/lib/types/rate-limit.d.ts.map +1 -0
- package/lib/types/script.d.ts +35 -0
- package/lib/types/script.d.ts.map +1 -0
- package/lib/types/seo.d.ts +88 -0
- package/lib/types/seo.d.ts.map +1 -0
- package/lib/types/testing.d.ts +85 -0
- package/lib/types/testing.d.ts.map +1 -0
- package/lib/types/theme.d.ts +39 -0
- package/lib/types/theme.d.ts.map +1 -0
- package/lib/types/types.d.ts +111 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/utils/use-intersection-observer.d.ts +10 -0
- package/lib/types/utils/use-intersection-observer.d.ts.map +1 -0
- package/lib/types/utils/with-headers.d.ts +6 -0
- package/lib/types/utils/with-headers.d.ts.map +1 -0
- package/lib/types/vite-plugin.d.ts +17 -0
- package/lib/types/vite-plugin.d.ts.map +1 -0
- package/package.json +10 -10
- package/src/entry-server.ts +124 -76
- package/src/favicon.ts +380 -0
- package/src/font.ts +32 -8
- package/src/fs-router.ts +54 -13
- package/src/i18n-routing.ts +299 -0
- package/src/image-plugin.ts +1 -1
- package/src/index.ts +125 -76
- package/src/link.tsx +19 -0
- package/src/meta.tsx +210 -0
- package/src/middleware.ts +65 -0
- package/src/not-found.ts +44 -0
- package/src/types.ts +2 -0
- package/src/vite-plugin.ts +258 -127
- package/lib/fs-router-n4VA4lxu.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -1,136 +1,185 @@
|
|
|
1
1
|
// ─── Core ─────────────────────────────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
export type { CreateAppOptions } from
|
|
4
|
-
export { createApp } from
|
|
5
|
-
export type { CreateServerOptions } from
|
|
6
|
-
export { createServer } from
|
|
3
|
+
export type { CreateAppOptions } from "./app";
|
|
4
|
+
export { createApp } from "./app";
|
|
5
|
+
export type { CreateServerOptions } from "./entry-server";
|
|
6
|
+
export { createServer } from "./entry-server";
|
|
7
7
|
|
|
8
8
|
// ─── Vite plugin ─────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
export { zeroPlugin as default } from
|
|
10
|
+
export { zeroPlugin as default } from "./vite-plugin";
|
|
11
11
|
|
|
12
12
|
// ─── File-system routing ─────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
+
export type { GenerateRouteModuleOptions } from './fs-router'
|
|
14
15
|
export {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
filePathToUrlPath,
|
|
17
|
+
generateMiddlewareModule,
|
|
18
|
+
generateRouteModule,
|
|
19
|
+
parseFileRoutes,
|
|
20
|
+
scanRouteFiles,
|
|
20
21
|
} from './fs-router'
|
|
21
22
|
|
|
22
23
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
23
24
|
|
|
24
|
-
export { defineConfig, resolveConfig } from
|
|
25
|
+
export { defineConfig, resolveConfig } from "./config";
|
|
25
26
|
|
|
26
27
|
// ─── ISR ─────────────────────────────────────────────────────────────────────
|
|
27
28
|
|
|
28
|
-
export { createISRHandler } from
|
|
29
|
+
export { createISRHandler } from "./isr";
|
|
29
30
|
|
|
30
31
|
// ─── Adapters ────────────────────────────────────────────────────────────────
|
|
31
32
|
|
|
32
|
-
export {
|
|
33
|
+
export {
|
|
34
|
+
bunAdapter,
|
|
35
|
+
nodeAdapter,
|
|
36
|
+
resolveAdapter,
|
|
37
|
+
staticAdapter,
|
|
38
|
+
} from "./adapters";
|
|
33
39
|
|
|
34
40
|
// ─── Components ─────────────────────────────────────────────────────────────
|
|
35
41
|
|
|
36
|
-
export type { ImageProps, ImageSource } from
|
|
37
|
-
export { Image } from
|
|
38
|
-
export type { LinkProps, LinkRenderProps, UseLinkReturn } from
|
|
39
|
-
export { createLink, Link, useLink } from
|
|
40
|
-
export type { ScriptProps, ScriptStrategy } from
|
|
41
|
-
export { Script } from
|
|
42
|
+
export type { ImageProps, ImageSource } from "./image";
|
|
43
|
+
export { Image } from "./image";
|
|
44
|
+
export type { LinkProps, LinkRenderProps, UseLinkReturn } from "./link";
|
|
45
|
+
export { createLink, Link, prefetchRoute, useLink } from "./link";
|
|
46
|
+
export type { ScriptProps, ScriptStrategy } from "./script";
|
|
47
|
+
export { Script } from "./script";
|
|
48
|
+
|
|
49
|
+
// ─── 404 Not Found ──────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export { render404Page } from "./not-found";
|
|
42
52
|
|
|
43
53
|
// ─── Middleware ──────────────────────────────────────────────────────────────
|
|
44
54
|
|
|
45
|
-
export type { CacheConfig, CacheRule } from
|
|
46
|
-
export { cacheMiddleware, securityHeaders, varyEncoding } from
|
|
55
|
+
export type { CacheConfig, CacheRule } from "./cache";
|
|
56
|
+
export { cacheMiddleware, securityHeaders, varyEncoding } from "./cache";
|
|
57
|
+
export { compose, getContext } from "./middleware";
|
|
47
58
|
|
|
48
59
|
// ─── Font optimization ─────────────────────────────────────────────────────
|
|
49
60
|
|
|
50
61
|
export type {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} from
|
|
59
|
-
export { fontPlugin, fontVariables } from
|
|
62
|
+
FallbackMetrics,
|
|
63
|
+
FontConfig,
|
|
64
|
+
FontDisplay,
|
|
65
|
+
GoogleFontInput,
|
|
66
|
+
GoogleFontStatic,
|
|
67
|
+
GoogleFontVariable,
|
|
68
|
+
LocalFont,
|
|
69
|
+
} from "./font";
|
|
70
|
+
export { fontPlugin, fontVariables } from "./font";
|
|
60
71
|
|
|
61
72
|
// ─── Image processing ──────────────────────────────────────────────────────
|
|
62
73
|
|
|
63
|
-
export type {
|
|
64
|
-
|
|
74
|
+
export type {
|
|
75
|
+
FormatSource,
|
|
76
|
+
ImageFormat,
|
|
77
|
+
ImagePluginConfig,
|
|
78
|
+
ProcessedImage,
|
|
79
|
+
} from "./image-plugin";
|
|
80
|
+
export { imagePlugin } from "./image-plugin";
|
|
65
81
|
|
|
66
82
|
// ─── Theme ──────────────────────────────────────────────────────────────────
|
|
67
83
|
|
|
68
|
-
export type { Theme } from
|
|
84
|
+
export type { Theme } from "./theme";
|
|
69
85
|
export {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} from
|
|
86
|
+
initTheme,
|
|
87
|
+
resolvedTheme,
|
|
88
|
+
setTheme,
|
|
89
|
+
ThemeToggle,
|
|
90
|
+
theme,
|
|
91
|
+
themeScript,
|
|
92
|
+
toggleTheme,
|
|
93
|
+
} from "./theme";
|
|
78
94
|
|
|
79
95
|
// ─── SEO ────────────────────────────────────────────────────────────────────
|
|
80
96
|
|
|
81
97
|
export type {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} from
|
|
90
|
-
export {
|
|
98
|
+
ChangeFreq,
|
|
99
|
+
JsonLdType,
|
|
100
|
+
RobotsConfig,
|
|
101
|
+
RobotsRule,
|
|
102
|
+
SeoPluginConfig,
|
|
103
|
+
SitemapConfig,
|
|
104
|
+
SitemapEntry,
|
|
105
|
+
} from "./seo";
|
|
106
|
+
export {
|
|
107
|
+
generateRobots,
|
|
108
|
+
generateSitemap,
|
|
109
|
+
jsonLd,
|
|
110
|
+
seoMiddleware,
|
|
111
|
+
seoPlugin,
|
|
112
|
+
} from "./seo";
|
|
91
113
|
|
|
92
114
|
// ─── API routes ──────────────────────────────────────────────────────────────
|
|
93
115
|
|
|
94
116
|
export type {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
} from
|
|
101
|
-
export { createApiMiddleware, generateApiRouteModule } from
|
|
117
|
+
ApiContext,
|
|
118
|
+
ApiHandler,
|
|
119
|
+
ApiRouteEntry,
|
|
120
|
+
ApiRouteModule,
|
|
121
|
+
HttpMethod,
|
|
122
|
+
} from "./api-routes";
|
|
123
|
+
export { createApiMiddleware, generateApiRouteModule } from "./api-routes";
|
|
102
124
|
|
|
103
125
|
// ─── CORS ────────────────────────────────────────────────────────────────────
|
|
104
126
|
|
|
105
|
-
export type { CorsConfig } from
|
|
106
|
-
export { corsMiddleware } from
|
|
127
|
+
export type { CorsConfig } from "./cors";
|
|
128
|
+
export { corsMiddleware } from "./cors";
|
|
107
129
|
|
|
108
130
|
// ─── Rate limiting ──────────────────────────────────────────────────────────
|
|
109
131
|
|
|
110
|
-
export type { RateLimitConfig } from
|
|
111
|
-
export { rateLimitMiddleware } from
|
|
132
|
+
export type { RateLimitConfig } from "./rate-limit";
|
|
133
|
+
export { rateLimitMiddleware } from "./rate-limit";
|
|
112
134
|
|
|
113
135
|
// ─── Compression ────────────────────────────────────────────────────────────
|
|
114
136
|
|
|
115
|
-
export type { CompressionConfig } from
|
|
116
|
-
export {
|
|
137
|
+
export type { CompressionConfig } from "./compression";
|
|
138
|
+
export {
|
|
139
|
+
compressionMiddleware,
|
|
140
|
+
compressResponse,
|
|
141
|
+
isCompressible,
|
|
142
|
+
} from "./compression";
|
|
117
143
|
|
|
118
144
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
|
119
145
|
|
|
120
|
-
export type { Action, ActionContext, ActionHandler } from
|
|
121
|
-
export { createActionMiddleware, defineAction } from
|
|
146
|
+
export type { Action, ActionContext, ActionHandler } from "./actions";
|
|
147
|
+
export { createActionMiddleware, defineAction } from "./actions";
|
|
148
|
+
|
|
149
|
+
// ─── Favicon ────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export type { FaviconPluginConfig } from "./favicon";
|
|
152
|
+
export { faviconPlugin } from "./favicon";
|
|
153
|
+
|
|
154
|
+
// ─── Meta ───────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export type { MetaProps } from "./meta";
|
|
157
|
+
export { buildMetaTags, Meta } from "./meta";
|
|
158
|
+
|
|
159
|
+
// ─── I18n routing ───────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export type { I18nRoutingConfig, LocaleContext } from "./i18n-routing";
|
|
162
|
+
export {
|
|
163
|
+
buildLocalePath,
|
|
164
|
+
createLocaleContext,
|
|
165
|
+
detectLocaleFromHeader,
|
|
166
|
+
extractLocaleFromPath,
|
|
167
|
+
i18nRouting,
|
|
168
|
+
setLocale,
|
|
169
|
+
useLocale,
|
|
170
|
+
} from "./i18n-routing";
|
|
122
171
|
|
|
123
172
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
124
173
|
|
|
125
174
|
export type {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} from
|
|
175
|
+
Adapter,
|
|
176
|
+
AdapterBuildOptions,
|
|
177
|
+
FileRoute,
|
|
178
|
+
ISRConfig,
|
|
179
|
+
LoaderContext,
|
|
180
|
+
RenderMode,
|
|
181
|
+
RouteMeta,
|
|
182
|
+
RouteMiddlewareEntry,
|
|
183
|
+
RouteModule,
|
|
184
|
+
ZeroConfig,
|
|
185
|
+
} from "./types";
|
package/src/link.tsx
CHANGED
|
@@ -30,6 +30,8 @@ export interface LinkProps {
|
|
|
30
30
|
style?: string
|
|
31
31
|
/** ARIA label. */
|
|
32
32
|
'aria-label'?: string
|
|
33
|
+
/** Additional click handler — called before navigation. Call e.preventDefault() to cancel. */
|
|
34
|
+
onClick?: ((e: MouseEvent) => void) | undefined
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** Props passed to a custom component via createLink. */
|
|
@@ -90,6 +92,18 @@ function doPrefetch(href: string) {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Prefetch a route's JS chunk by injecting `<link rel="prefetch">` into the
|
|
97
|
+
* document head. Deduplicates — calling with the same href twice is a no-op.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* prefetchRoute('/about')
|
|
101
|
+
* prefetchRoute('/dashboard')
|
|
102
|
+
*/
|
|
103
|
+
export function prefetchRoute(href: string): void {
|
|
104
|
+
doPrefetch(href)
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
/**
|
|
94
108
|
* Composable that provides all link behavior — navigation, prefetching,
|
|
95
109
|
* active state, and viewport observation.
|
|
@@ -112,6 +126,11 @@ export function useLink(props: LinkProps): UseLinkReturn {
|
|
|
112
126
|
const strategy = props.prefetch ?? 'hover'
|
|
113
127
|
|
|
114
128
|
function handleClick(e: MouseEvent) {
|
|
129
|
+
// Call user's onClick first — they may call e.preventDefault()
|
|
130
|
+
if (props.onClick) {
|
|
131
|
+
;(props.onClick as (e: MouseEvent) => void)(e)
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
if (
|
|
116
135
|
e.defaultPrevented ||
|
|
117
136
|
e.button !== 0 ||
|
package/src/meta.tsx
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
+
import { useHead } from '@pyreon/head'
|
|
3
|
+
import type { I18nRoutingConfig } from './i18n-routing'
|
|
4
|
+
import { extractLocaleFromPath } from './i18n-routing'
|
|
5
|
+
|
|
6
|
+
// ─── Meta component ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface MetaProps {
|
|
9
|
+
/** Page title. Accepts reactive accessor `() => string`. */
|
|
10
|
+
title?: string | (() => string)
|
|
11
|
+
/** Page description. Accepts reactive accessor. */
|
|
12
|
+
description?: string | (() => string)
|
|
13
|
+
/** Canonical URL. Also sets og:url. */
|
|
14
|
+
canonical?: string
|
|
15
|
+
/** Open Graph image URL. Also sets twitter:image. */
|
|
16
|
+
image?: string
|
|
17
|
+
/** Image alt text for accessibility. */
|
|
18
|
+
imageAlt?: string
|
|
19
|
+
/** Open Graph type. Default: "website" */
|
|
20
|
+
type?: 'website' | 'article' | 'product' | 'profile'
|
|
21
|
+
/** Site name for og:site_name. */
|
|
22
|
+
siteName?: string
|
|
23
|
+
/** Twitter card type. Default: "summary_large_image" */
|
|
24
|
+
twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'
|
|
25
|
+
/** Twitter @handle. */
|
|
26
|
+
twitterSite?: string
|
|
27
|
+
/** Twitter creator @handle. */
|
|
28
|
+
twitterCreator?: string
|
|
29
|
+
/** Locale. Default: "en_US" */
|
|
30
|
+
locale?: string
|
|
31
|
+
/** Alternate locales for hreflang. */
|
|
32
|
+
alternateLocales?: Array<{ locale: string; url: string }>
|
|
33
|
+
/** Robots directives. Default: "index, follow" */
|
|
34
|
+
robots?: string
|
|
35
|
+
/** Published time (ISO 8601) for article type. */
|
|
36
|
+
publishedTime?: string
|
|
37
|
+
/** Modified time (ISO 8601) for article type. */
|
|
38
|
+
modifiedTime?: string
|
|
39
|
+
/** Article author. */
|
|
40
|
+
author?: string
|
|
41
|
+
/** Article tags. */
|
|
42
|
+
tags?: string[]
|
|
43
|
+
/** JSON-LD structured data object. */
|
|
44
|
+
jsonLd?: Record<string, unknown>
|
|
45
|
+
/** Additional custom meta tags. */
|
|
46
|
+
extra?: Array<{ name?: string; property?: string; content: string }>
|
|
47
|
+
/**
|
|
48
|
+
* I18n routing config — when provided, auto-generates hreflang alternate
|
|
49
|
+
* links for all locales based on the current path.
|
|
50
|
+
* Also sets og:locale and og:locale:alternate.
|
|
51
|
+
*/
|
|
52
|
+
i18n?: I18nRoutingConfig
|
|
53
|
+
/** Base URL for building absolute hreflang URLs. e.g. "https://example.com" */
|
|
54
|
+
origin?: string
|
|
55
|
+
children?: VNodeChild
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolveStr = (v: string | (() => string) | undefined): string | undefined =>
|
|
59
|
+
typeof v === 'function' ? v() : v
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Declarative meta component for SSR-compatible page metadata.
|
|
63
|
+
*
|
|
64
|
+
* Supports reactive title/description — when passed as `() => string` accessors,
|
|
65
|
+
* they are forwarded to `useHead()` as a reactive getter so updates propagate
|
|
66
|
+
* automatically via signal tracking.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* <Meta title="My Page" description="..." image="/og.jpg" canonical="https://..." />
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @example Reactive title
|
|
74
|
+
* ```tsx
|
|
75
|
+
* const count = signal(0)
|
|
76
|
+
* <Meta title={() => `${count()} items`} />
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function Meta(props: MetaProps): VNodeChild {
|
|
80
|
+
const hasReactiveTitle = typeof props.title === 'function'
|
|
81
|
+
const hasReactiveDescription = typeof props.description === 'function'
|
|
82
|
+
|
|
83
|
+
// If title or description are reactive accessors, pass a getter to useHead
|
|
84
|
+
// so it re-evaluates when the signals change.
|
|
85
|
+
if (hasReactiveTitle || hasReactiveDescription) {
|
|
86
|
+
useHead((() => {
|
|
87
|
+
const title = resolveStr(props.title)
|
|
88
|
+
const description = resolveStr(props.description)
|
|
89
|
+
const tags = buildMetaTags({ ...props, title, description } as any)
|
|
90
|
+
return { title, meta: tags.meta, link: tags.link, script: tags.script }
|
|
91
|
+
}) as any)
|
|
92
|
+
} else {
|
|
93
|
+
const title = resolveStr(props.title)
|
|
94
|
+
const description = resolveStr(props.description)
|
|
95
|
+
const tags = buildMetaTags({ ...props, title, description } as any)
|
|
96
|
+
useHead({ title, meta: tags.meta, link: tags.link, script: tags.script } as any)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return props.children ?? null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface MetaTags {
|
|
103
|
+
meta: Array<Record<string, string>>
|
|
104
|
+
link: Array<Record<string, string>>
|
|
105
|
+
script: Array<{ type: string; children: string }>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildMetaTags(
|
|
109
|
+
props: Omit<MetaProps, 'title' | 'description' | 'children'> & {
|
|
110
|
+
title?: string
|
|
111
|
+
description?: string
|
|
112
|
+
},
|
|
113
|
+
): MetaTags {
|
|
114
|
+
const meta: Array<Record<string, string>> = []
|
|
115
|
+
const link: Array<Record<string, string>> = []
|
|
116
|
+
const script: Array<{ type: string; children: string }> = []
|
|
117
|
+
|
|
118
|
+
const {
|
|
119
|
+
title, description, canonical, image, imageAlt,
|
|
120
|
+
type = 'website', siteName,
|
|
121
|
+
twitterCard = 'summary_large_image', twitterSite, twitterCreator,
|
|
122
|
+
locale = 'en_US', alternateLocales,
|
|
123
|
+
robots = 'index, follow',
|
|
124
|
+
publishedTime, modifiedTime, author, tags, jsonLd, extra,
|
|
125
|
+
} = props
|
|
126
|
+
|
|
127
|
+
if (description) meta.push({ name: 'description', content: description })
|
|
128
|
+
if (robots) meta.push({ name: 'robots', content: robots })
|
|
129
|
+
if (author) meta.push({ name: 'author', content: author })
|
|
130
|
+
|
|
131
|
+
if (title) meta.push({ property: 'og:title', content: title })
|
|
132
|
+
if (description) meta.push({ property: 'og:description', content: description })
|
|
133
|
+
if (canonical) meta.push({ property: 'og:url', content: canonical })
|
|
134
|
+
if (image) meta.push({ property: 'og:image', content: image })
|
|
135
|
+
if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })
|
|
136
|
+
meta.push({ property: 'og:type', content: type })
|
|
137
|
+
if (siteName) meta.push({ property: 'og:site_name', content: siteName })
|
|
138
|
+
meta.push({ property: 'og:locale', content: locale })
|
|
139
|
+
|
|
140
|
+
if (type === 'article') {
|
|
141
|
+
if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })
|
|
142
|
+
if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })
|
|
143
|
+
if (author) meta.push({ property: 'article:author', content: author })
|
|
144
|
+
if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
meta.push({ name: 'twitter:card', content: twitterCard })
|
|
148
|
+
if (title) meta.push({ name: 'twitter:title', content: title })
|
|
149
|
+
if (description) meta.push({ name: 'twitter:description', content: description })
|
|
150
|
+
if (image) meta.push({ name: 'twitter:image', content: image })
|
|
151
|
+
if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })
|
|
152
|
+
if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })
|
|
153
|
+
if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })
|
|
154
|
+
|
|
155
|
+
if (canonical) link.push({ rel: 'canonical', href: canonical })
|
|
156
|
+
if (alternateLocales) {
|
|
157
|
+
for (const alt of alternateLocales) {
|
|
158
|
+
link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (jsonLd) {
|
|
163
|
+
script.push({
|
|
164
|
+
type: 'application/ld+json',
|
|
165
|
+
children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (extra) for (const tag of extra) meta.push(tag)
|
|
170
|
+
|
|
171
|
+
// I18n: auto-generate hreflang alternates from i18nRouting config
|
|
172
|
+
if (props.i18n) {
|
|
173
|
+
const i18nConfig = props.i18n
|
|
174
|
+
const origin = props.origin ?? ''
|
|
175
|
+
const currentPath = canonical?.replace(origin, '') ?? '/'
|
|
176
|
+
const { pathWithoutLocale } = extractLocaleFromPath(
|
|
177
|
+
currentPath,
|
|
178
|
+
i18nConfig.locales,
|
|
179
|
+
i18nConfig.defaultLocale,
|
|
180
|
+
)
|
|
181
|
+
const strategy = i18nConfig.strategy ?? 'prefix-except-default'
|
|
182
|
+
|
|
183
|
+
for (const loc of i18nConfig.locales) {
|
|
184
|
+
const localizedPath =
|
|
185
|
+
strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale
|
|
186
|
+
? pathWithoutLocale
|
|
187
|
+
: `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`
|
|
188
|
+
|
|
189
|
+
link.push({
|
|
190
|
+
rel: 'alternate',
|
|
191
|
+
hreflang: loc,
|
|
192
|
+
href: `${origin}${localizedPath}`,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// og:locale:alternate for non-current locales
|
|
196
|
+
if (loc !== locale) {
|
|
197
|
+
meta.push({ property: 'og:locale:alternate', content: loc })
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// x-default hreflang pointing to default locale
|
|
202
|
+
link.push({
|
|
203
|
+
rel: 'alternate',
|
|
204
|
+
hreflang: 'x-default',
|
|
205
|
+
href: `${origin}${pathWithoutLocale}`,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { meta, link, script }
|
|
210
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Middleware, MiddlewareContext } from '@pyreon/server'
|
|
2
|
+
|
|
3
|
+
// ─── Middleware composition ─────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Chains multiple middleware functions into a single middleware.
|
|
6
|
+
// Each middleware runs in order. If any returns a Response, the chain
|
|
7
|
+
// short-circuits and that Response is returned. If all return void,
|
|
8
|
+
// the composed middleware returns void (continues to rendering).
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compose multiple middleware into a single middleware function.
|
|
12
|
+
* Middleware runs sequentially — if any returns a Response, the chain stops.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { compose } from "@pyreon/zero/middleware"
|
|
16
|
+
* import { corsMiddleware } from "@pyreon/zero/cors"
|
|
17
|
+
* import { rateLimitMiddleware } from "@pyreon/zero/rate-limit"
|
|
18
|
+
*
|
|
19
|
+
* const combined = compose(
|
|
20
|
+
* corsMiddleware({ origin: "*" }),
|
|
21
|
+
* rateLimitMiddleware({ max: 100 }),
|
|
22
|
+
* cacheMiddleware(),
|
|
23
|
+
* )
|
|
24
|
+
*/
|
|
25
|
+
export function compose(...middlewares: Middleware[]): Middleware {
|
|
26
|
+
return async (ctx: MiddlewareContext) => {
|
|
27
|
+
for (const mw of middlewares) {
|
|
28
|
+
const result = await mw(ctx)
|
|
29
|
+
if (result instanceof Response) return result
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Shared request context ─────────────────────────────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// Lightweight context bag attached to MiddlewareContext.locals so middleware
|
|
37
|
+
// can communicate without coupling. Uses a namespaced key to avoid collisions
|
|
38
|
+
// with user-defined locals.
|
|
39
|
+
|
|
40
|
+
const ZERO_CTX_KEY = '__zeroCtx'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the shared Zero context from a middleware context.
|
|
44
|
+
* Creates one if it doesn't exist. Middleware can use this to
|
|
45
|
+
* pass data to downstream middleware without polluting `ctx.locals`.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const authMiddleware: Middleware = (ctx) => {
|
|
49
|
+
* const zctx = getContext(ctx)
|
|
50
|
+
* zctx.userId = "user_123"
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* const loggingMiddleware: Middleware = (ctx) => {
|
|
54
|
+
* const zctx = getContext(ctx)
|
|
55
|
+
* console.log("User:", zctx.userId)
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
export function getContext(ctx: MiddlewareContext): Record<string, unknown> {
|
|
59
|
+
let zctx = ctx.locals[ZERO_CTX_KEY] as Record<string, unknown> | undefined
|
|
60
|
+
if (!zctx) {
|
|
61
|
+
zctx = {}
|
|
62
|
+
ctx.locals[ZERO_CTX_KEY] = zctx
|
|
63
|
+
}
|
|
64
|
+
return zctx
|
|
65
|
+
}
|
package/src/not-found.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ComponentFn } from "@pyreon/core";
|
|
2
|
+
import { h } from "@pyreon/core";
|
|
3
|
+
import { renderToString } from "@pyreon/runtime-server";
|
|
4
|
+
|
|
5
|
+
// ─── 404 Not Found rendering ────────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Shared utility for rendering 404 pages in both dev (vite-plugin) and
|
|
8
|
+
// production (entry-server). Renders the notFoundComponent into HTML
|
|
9
|
+
// and wraps it in a minimal document if no template is provided.
|
|
10
|
+
|
|
11
|
+
const DEFAULT_404_BODY =
|
|
12
|
+
"<h1>404 — Not Found</h1><p>The page you requested does not exist.</p>";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render a 404 component to a full HTML string.
|
|
16
|
+
* If no component is provided, returns a default 404 page.
|
|
17
|
+
*/
|
|
18
|
+
export async function render404Page(
|
|
19
|
+
component: ComponentFn | undefined,
|
|
20
|
+
template?: string,
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
let body: string;
|
|
23
|
+
if (component) {
|
|
24
|
+
body = await renderToString(h(component, null));
|
|
25
|
+
} else {
|
|
26
|
+
body = DEFAULT_404_BODY;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (template?.includes("<!--pyreon-app-->")) {
|
|
30
|
+
return template.replace("<!--pyreon-app-->", body);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `<!DOCTYPE html>
|
|
34
|
+
<html lang="en">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="UTF-8">
|
|
37
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
38
|
+
<title>404 — Not Found</title>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
${body}
|
|
42
|
+
</body>
|
|
43
|
+
</html>`;
|
|
44
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -105,6 +105,8 @@ export interface FileRoute {
|
|
|
105
105
|
isError: boolean
|
|
106
106
|
/** Whether this is a loading fallback file. */
|
|
107
107
|
isLoading: boolean
|
|
108
|
+
/** Whether this is a not-found (404) file. */
|
|
109
|
+
isNotFound: boolean
|
|
108
110
|
/** Whether this is a catch-all route. */
|
|
109
111
|
isCatchAll: boolean
|
|
110
112
|
/** Resolved rendering mode. */
|