@styleflow.app/flowpress-astro-core 0.2.0-beta.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0-beta.0
4
+
5
+ - Made the repository buildable outside the original monorepo.
6
+ - Added sanitized GraphQL network, timeout, non-JSON and debug diagnostics.
7
+ - Added package release checks and OIDC publishing workflow.
8
+ - Pinned the matching StyleFlow prerelease.
9
+
10
+ ## 0.1.0
11
+
12
+ - Initial extractable core package.
13
+ - WordPress-compatible template hierarchy.
14
+ - GraphQL route/capability client.
15
+ - Rank Math SEO types and base document.
16
+ - Runtime Styleflow CMS value resolver.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @styleflow.app/flowpress-astro-core
2
+
3
+ Project-agnostic runtime for WordPress route resolution, the WordPress template
4
+ hierarchy, normalized Rank Math data, feature capabilities, and safe Styleflow
5
+ CMS values.
6
+
7
+ The package must not import from an application or project plugin.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @styleflow.app/flowpress-astro-core@0.2.0-beta.0
13
+ ```
14
+
15
+ Prereleases are pinned exactly. The package expects Astro 7 or newer and the
16
+ matching StyleFlow `0.3.0-beta.x` line.
17
+
18
+ The client reports the GraphQL operation, sanitized endpoint, HTTP/content
19
+ type failures and underlying network/TLS cause. Upstream GraphQL debug details
20
+ remain hidden unless `debug: true` is explicitly set for local development.
21
+
22
+ ```ts
23
+ const client = new FlowpressClient({
24
+ endpoint: process.env.BACKEND_GRAPHQL_URL!,
25
+ debug: process.env.APP_ENV === "local"
26
+ });
27
+ ```
28
+
29
+ For local Cloudflare development, prefer an HTTP loopback endpoint dedicated
30
+ to server-to-server traffic. Keep the public HTTPS backend URL separate for
31
+ canonical URL rewriting.
32
+
33
+ ## Release
34
+
35
+ ```bash
36
+ # First bootstrap, after the StyleFlow prerelease exists on npm:
37
+ npm install
38
+ npm run release:check
39
+ npm run release:publish:next
40
+ ```
41
+
42
+ Commit the generated `package-lock.json`; use `npm ci` for every subsequent
43
+ release and in CI.
44
+
45
+ The first publish requires npm organization access and 2FA. Subsequent
46
+ publishes use `.github/workflows/publish.yml` with npm trusted publishing
47
+ (OIDC); no long-lived publish token belongs in this repository.
@@ -0,0 +1,49 @@
1
+ ---
2
+ import type { SfSeo } from "../types.js";
3
+
4
+ interface Props {
5
+ lang?: string;
6
+ seo?: SfSeo | null;
7
+ siteName?: string;
8
+ }
9
+
10
+ const {
11
+ lang = "it",
12
+ seo = null,
13
+ siteName = import.meta.env.PROJECT_NAME ?? "FlowPress"
14
+ } = Astro.props;
15
+ const title = seo?.title || siteName;
16
+ const description = seo?.description || "";
17
+ const robots = seo?.robots?.join(",") || "";
18
+ const jsonLd = seo?.jsonLd ? JSON.stringify(seo.jsonLd) : null;
19
+ ---
20
+
21
+ <!doctype html>
22
+ <html lang={lang}>
23
+ <head>
24
+ <meta charset="UTF-8" />
25
+ <meta name="viewport" content="width=device-width" />
26
+ <title>{title}</title>
27
+ {description && <meta name="description" content={description} />}
28
+ {robots && <meta name="robots" content={robots} />}
29
+ {seo?.canonicalUrl && <link rel="canonical" href={seo.canonicalUrl} />}
30
+ {seo?.hreflang?.map((alternate) => (
31
+ <link rel="alternate" hreflang={alternate.locale} href={alternate.url} />
32
+ ))}
33
+ {seo?.openGraph?.title && <meta property="og:title" content={seo.openGraph.title} />}
34
+ {seo?.openGraph?.description && <meta property="og:description" content={seo.openGraph.description} />}
35
+ {seo?.canonicalUrl && <meta property="og:url" content={seo.canonicalUrl} />}
36
+ {seo?.openGraph?.image?.url && <meta property="og:image" content={seo.openGraph.image.url} />}
37
+ {seo?.openGraph?.image?.alt && <meta property="og:image:alt" content={seo.openGraph.image.alt} />}
38
+ <meta name="twitter:card" content={seo?.twitter?.image?.url ? "summary_large_image" : "summary"} />
39
+ {seo?.twitter?.title && <meta name="twitter:title" content={seo.twitter.title} />}
40
+ {seo?.twitter?.description && <meta name="twitter:description" content={seo.twitter.description} />}
41
+ {seo?.twitter?.image?.url && <meta name="twitter:image" content={seo.twitter.image.url} />}
42
+ {seo?.twitter?.image?.alt && <meta name="twitter:image:alt" content={seo.twitter.image.alt} />}
43
+ {jsonLd && <script type="application/ld+json" set:html={jsonLd} />}
44
+ <slot name="head" />
45
+ </head>
46
+ <body>
47
+ <slot />
48
+ </body>
49
+ </html>
@@ -0,0 +1,34 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import BaseLayout from "./BaseLayout.astro";
4
+ import GutenbergRenderer from "./GutenbergRenderer.astro";
5
+ import type { FlowpressTemplateProps } from "../types.js";
6
+
7
+ const { route, content, styleflowContract } = Astro.props as FlowpressTemplateProps;
8
+ const title = content?.title || route.seo?.title || "Contenuto";
9
+ const isNotFound = route.queryType === "404" || route.status === 404;
10
+ ---
11
+
12
+ <BaseLayout lang={route.locale || "it"} seo={route.seo}>
13
+ <S.main tone="neutral" intensity={1} surface="default" layoutRole="container" density="md">
14
+ <S.section tone="neutral" intensity={1} surface="default" layoutRole="section" density="md">
15
+ <S.h1 ty="heading" v="1" w="strong">{isNotFound ? "Pagina non trovata" : title}</S.h1>
16
+ {content?.excerpt && <S.p ty="body" v="md" w="default">{content.excerpt}</S.p>}
17
+ {Array.isArray(content?.contentBlocks) && content.contentBlocks.length > 0 ? (
18
+ <GutenbergRenderer
19
+ blocks={content.contentBlocks as import("../types.js").FlowpressBlock[]}
20
+ contract={styleflowContract}
21
+ />
22
+ ) : content?.content && (
23
+ <S.div
24
+ tone="neutral"
25
+ intensity={1}
26
+ surface="default"
27
+ layoutRole="stack"
28
+ density="md"
29
+ set:html={String(content.content)}
30
+ />
31
+ )}
32
+ </S.section>
33
+ </S.main>
34
+ </BaseLayout>
@@ -0,0 +1,324 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import {
4
+ resolveDynamicStyleflowProps,
5
+ type StyleflowContract
6
+ } from "../styleflow.js";
7
+ import type { FlowpressBlock } from "../types.js";
8
+
9
+ interface Props {
10
+ blocks: FlowpressBlock[];
11
+ contract?: StyleflowContract;
12
+ }
13
+
14
+ const { blocks, contract } = Astro.props;
15
+
16
+ const defaults = {
17
+ tone: "neutral",
18
+ intensity: 1,
19
+ surface: "default",
20
+ layoutRole: "stack",
21
+ density: "md"
22
+ };
23
+
24
+ function styleflowProps(attributes: Record<string, unknown>) {
25
+ if (!contract) return defaults;
26
+ return resolveDynamicStyleflowProps(attributes, defaults, contract);
27
+ }
28
+
29
+ function dynamicProps(
30
+ attributes: Record<string, unknown>,
31
+ fallback: Record<string, unknown>
32
+ ) {
33
+ if (!contract) return fallback;
34
+ return resolveDynamicStyleflowProps(attributes, fallback, contract);
35
+ }
36
+
37
+ function text(block: FlowpressBlock, key = "text"): string {
38
+ return String(block.attributes[key] ?? "");
39
+ }
40
+
41
+ function objectValue(
42
+ block: FlowpressBlock,
43
+ key: string
44
+ ): Record<string, unknown> {
45
+ const value = block.attributes[key];
46
+ return value && typeof value === "object" && !Array.isArray(value)
47
+ ? value as Record<string, unknown>
48
+ : {};
49
+ }
50
+
51
+ function linkValue(block: FlowpressBlock, key = "link"): string {
52
+ const value = block.attributes[key];
53
+ const href = typeof value === "string"
54
+ ? value
55
+ : String(objectValue(block, key).url ?? "");
56
+ if (/^(?:\/(?!\/)|#|https?:\/\/|mailto:|tel:)/i.test(href)) {
57
+ return href;
58
+ }
59
+ return "";
60
+ }
61
+
62
+ function itemImage(item: Record<string, unknown>): Record<string, unknown> {
63
+ const value = item.image;
64
+ return value && typeof value === "object" && !Array.isArray(value)
65
+ ? value as Record<string, unknown>
66
+ : {};
67
+ }
68
+
69
+ function youtubeEmbedUrl(value: unknown): string | null {
70
+ try {
71
+ const url = new URL(String(value ?? ""));
72
+ const host = url.hostname.replace(/^www\./, "");
73
+ const id = host === "youtu.be"
74
+ ? url.pathname.split("/").filter(Boolean)[0]
75
+ : ["youtube.com", "youtube-nocookie.com"].includes(host)
76
+ ? url.searchParams.get("v") || url.pathname.match(/^\/(?:embed|shorts)\/([^/]+)/)?.[1]
77
+ : null;
78
+ return id && /^[A-Za-z0-9_-]+$/.test(id)
79
+ ? `https://www.youtube-nocookie.com/embed/${id}`
80
+ : null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+ ---
86
+
87
+ {blocks.map((block) => {
88
+ const name = block.name.replace(/^sf\//, "");
89
+ const children = block.innerBlocks ?? [];
90
+ const semantic = styleflowProps(block.attributes);
91
+
92
+ if (name === "heading") {
93
+ const level = Math.min(6, Math.max(1, Number(block.attributes.level ?? 2)));
94
+ const headingTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
95
+ const typography = dynamicProps(
96
+ {
97
+ ty: "heading",
98
+ v: String(block.attributes.variant ?? level),
99
+ w: String(block.attributes.weight ?? "strong")
100
+ },
101
+ { ty: "heading", v: String(level), w: "strong" }
102
+ );
103
+ return (
104
+ <S
105
+ as={headingTag}
106
+ {...typography}
107
+ set:html={text(block)}
108
+ />
109
+ );
110
+ }
111
+
112
+ if (name === "rich-text" || name === "core/paragraph") {
113
+ const html = String(block.attributes.content ?? block.innerHtml ?? "");
114
+ return <S as="div" {...semantic} set:html={html} />;
115
+ }
116
+
117
+ if (name === "button") {
118
+ const href = linkValue(block, "href") || "#";
119
+ const interaction = dynamicProps(
120
+ {
121
+ interactivePriority: String(
122
+ block.attributes.interactivePriority ?? "primary"
123
+ )
124
+ },
125
+ { interactivePriority: "primary" }
126
+ );
127
+ return (
128
+ <S
129
+ as="a"
130
+ href={href}
131
+ {...interaction}
132
+ >
133
+ {text(block, "label")}
134
+ </S>
135
+ );
136
+ }
137
+
138
+ if (name === "image") {
139
+ const image = block.attributes.image as {
140
+ url?: string;
141
+ sourceUrl?: string;
142
+ alt?: string;
143
+ altText?: string;
144
+ } | undefined;
145
+ const src = image?.url ?? image?.sourceUrl;
146
+ return src ? (
147
+ <S as="div" {...semantic}>
148
+ <img
149
+ src={src}
150
+ alt={String(block.attributes.alt ?? image?.alt ?? image?.altText ?? "")}
151
+ loading="lazy"
152
+ decoding="async"
153
+ />
154
+ </S>
155
+ ) : null;
156
+ }
157
+
158
+ if (name === "gallery") {
159
+ const images = Array.isArray(block.attributes.images)
160
+ ? block.attributes.images as Array<Record<string, unknown>>
161
+ : [];
162
+ return (
163
+ <S as="div" {...semantic}>
164
+ {images.map((image) => image.url ? (
165
+ <img
166
+ src={String(image.url)}
167
+ alt={String(image.alt ?? "")}
168
+ loading="lazy"
169
+ decoding="async"
170
+ />
171
+ ) : null)}
172
+ </S>
173
+ );
174
+ }
175
+
176
+ if (name === "video") {
177
+ const video = objectValue(block, "video");
178
+ const poster = objectValue(block, "poster");
179
+ return video.url ? (
180
+ <S as="div" {...semantic}>
181
+ <video
182
+ src={String(video.url)}
183
+ poster={poster.url ? String(poster.url) : undefined}
184
+ controls
185
+ autoplay={Boolean(block.attributes.autoplay)}
186
+ muted={Boolean(block.attributes.autoplay)}
187
+ playsinline
188
+ />
189
+ </S>
190
+ ) : null;
191
+ }
192
+
193
+ if (name === "video-youtube") {
194
+ const embed = youtubeEmbedUrl(block.attributes.url);
195
+ return embed ? (
196
+ <S as="div" {...semantic}>
197
+ <iframe
198
+ src={embed}
199
+ title={String(block.attributes.title ?? "YouTube video")}
200
+ loading="lazy"
201
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
202
+ allowfullscreen
203
+ />
204
+ </S>
205
+ ) : null;
206
+ }
207
+
208
+ if (name === "quote") {
209
+ return (
210
+ <blockquote>
211
+ <S as="p" set:html={text(block, "quote")} />
212
+ {block.attributes.citation && <cite>{String(block.attributes.citation)}</cite>}
213
+ </blockquote>
214
+ );
215
+ }
216
+
217
+ if (name === "chip") {
218
+ return <S as="span" {...semantic}>{text(block, "label")}</S>;
219
+ }
220
+
221
+ if (name === "card") {
222
+ const media = objectValue(block, "image");
223
+ const href = linkValue(block);
224
+ return (
225
+ <S as="article" {...semantic}>
226
+ {media.url && (
227
+ <img src={String(media.url)} alt={String(media.alt ?? "")} loading="lazy" />
228
+ )}
229
+ {block.attributes.title && <S as="h3">{String(block.attributes.title)}</S>}
230
+ {block.attributes.text && <S as="p">{String(block.attributes.text)}</S>}
231
+ {href && <S as="a" href={href}>Scopri</S>}
232
+ </S>
233
+ );
234
+ }
235
+
236
+ if (name === "content-list" || name === "term-list") {
237
+ const items = Array.isArray(block.attributes.resolvedItems)
238
+ ? block.attributes.resolvedItems as Array<Record<string, unknown>>
239
+ : [];
240
+ return (
241
+ <S as="ul" {...semantic}>
242
+ {items.map((item) => (
243
+ <S as="li">
244
+ {itemImage(item).url && (
245
+ <img
246
+ src={String(itemImage(item).url)}
247
+ alt={String(itemImage(item).alt ?? "")}
248
+ loading="lazy"
249
+ decoding="async"
250
+ />
251
+ )}
252
+ {item.uri ? (
253
+ <S as="a" href={String(item.uri)}>{String(item.title ?? "")}</S>
254
+ ) : (
255
+ <S as="span">{String(item.title ?? "")}</S>
256
+ )}
257
+ {item.excerpt && <S as="p">{String(item.excerpt)}</S>}
258
+ {item.priceHtml && <S as="div" set:html={String(item.priceHtml)} />}
259
+ </S>
260
+ ))}
261
+ </S>
262
+ );
263
+ }
264
+
265
+ if (name === "accordion-item") {
266
+ return (
267
+ <S as="div" {...semantic}>
268
+ <details>
269
+ <summary>{String(block.attributes.title ?? "")}</summary>
270
+ <Astro.self blocks={children} contract={contract} />
271
+ </details>
272
+ </S>
273
+ );
274
+ }
275
+
276
+ if (name === "map") {
277
+ const address = String(block.attributes.address ?? "");
278
+ const latitude = String(block.attributes.latitude ?? "");
279
+ const longitude = String(block.attributes.longitude ?? "");
280
+ const query = latitude && longitude ? `${latitude},${longitude}` : address;
281
+ const provider = String(block.attributes.provider ?? "openstreetmap");
282
+ const googleMapsApiKey = import.meta.env.GOOGLE_MAPS_API_KEY;
283
+ if (provider === "google" && query && !googleMapsApiKey) {
284
+ throw new Error(
285
+ "GOOGLE_MAPS_API_KEY is required by the selected CMS map provider."
286
+ );
287
+ }
288
+ if (provider === "google" && query) {
289
+ const source = "https://www.google.com/maps/embed/v1/place"
290
+ + `?key=${encodeURIComponent(googleMapsApiKey)}`
291
+ + `&q=${encodeURIComponent(query)}`;
292
+ return (
293
+ <S as="div" {...semantic}>
294
+ <iframe
295
+ src={source}
296
+ title={address || "Map"}
297
+ loading="lazy"
298
+ referrerpolicy="no-referrer-when-downgrade"
299
+ />
300
+ </S>
301
+ );
302
+ }
303
+ return query ? (
304
+ <S as="div" {...semantic}>
305
+ <S
306
+ as="a"
307
+ href={`https://www.openstreetmap.org/search?query=${encodeURIComponent(query)}`}
308
+ rel="noreferrer"
309
+ >
310
+ {address || query}
311
+ </S>
312
+ </S>
313
+ ) : null;
314
+ }
315
+
316
+ const layoutTag = ["section", "hero"].includes(name) ? "section" : "div";
317
+ return (
318
+ <S as={layoutTag} {...semantic}>
319
+ {block.innerHtml && children.length === 0
320
+ ? <S as="div" set:html={block.innerHtml} />
321
+ : <Astro.self blocks={children} contract={contract} />}
322
+ </S>
323
+ );
324
+ })}
@@ -0,0 +1,35 @@
1
+ import type { FlowpressCapabilities, FlowpressContent, FlowpressRoute } from "./types.js";
2
+ export interface FlowpressGraphqlIssue {
3
+ message?: string;
4
+ extensions?: {
5
+ debugMessage?: string;
6
+ category?: string;
7
+ [key: string]: unknown;
8
+ };
9
+ }
10
+ export interface FlowpressClientOptions {
11
+ endpoint: string;
12
+ timeoutMs?: number;
13
+ headers?: Record<string, string>;
14
+ debug?: boolean;
15
+ }
16
+ export declare class FlowpressGraphqlError extends Error {
17
+ readonly status?: number | undefined;
18
+ readonly errors?: FlowpressGraphqlIssue[] | undefined;
19
+ readonly operationName?: string | undefined;
20
+ readonly endpoint?: string | undefined;
21
+ constructor(message: string, status?: number | undefined, errors?: FlowpressGraphqlIssue[] | undefined, operationName?: string | undefined, endpoint?: string | undefined, cause?: unknown);
22
+ }
23
+ export declare class FlowpressClient {
24
+ readonly endpoint: string;
25
+ readonly timeoutMs: number;
26
+ readonly headers: Record<string, string>;
27
+ readonly debug: boolean;
28
+ constructor(options: FlowpressClientOptions);
29
+ request<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
30
+ getCapabilities(): Promise<FlowpressCapabilities>;
31
+ getRoute(uri: string, locale?: string): Promise<FlowpressRoute | null>;
32
+ getRoutes(locale?: string): Promise<FlowpressRoute[]>;
33
+ getContent(loader: string, variables: Record<string, unknown>): Promise<FlowpressContent | null>;
34
+ }
35
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,gBAAgB,EAChB,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE;QACX,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAOD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAG5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,qBAAqB,EAAE;IACzC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM;gBAJ1B,OAAO,EAAE,MAAM,EACN,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,MAAM,CAAC,EAAE,qBAAqB,EAAE,YAAA,EAChC,aAAa,CAAC,EAAE,MAAM,YAAA,EACtB,QAAQ,CAAC,EAAE,MAAM,YAAA,EAC1B,KAAK,CAAC,EAAE,OAAO;CAKlB;AAED,qBAAa,eAAe;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;gBAEZ,OAAO,EAAE,sBAAsB;IAarC,OAAO,CAAC,CAAC,EACb,KAAK,EAAE,MAAM,EACb,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACtC,OAAO,CAAC,CAAC,CAAC;IAmFP,eAAe,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAoBjD,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAgCtE,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA6BrD,UAAU,CACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;CA2CpC"}