@risali/react 0.2.0 → 0.6.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/README.md +22 -0
- package/dist/blog-jsonld.d.ts +74 -0
- package/dist/blog-jsonld.d.ts.map +1 -0
- package/dist/blog-jsonld.js +111 -0
- package/dist/blog-jsonld.js.map +1 -0
- package/dist/blog-list.d.ts +29 -0
- package/dist/blog-list.d.ts.map +1 -0
- package/dist/blog-list.js +113 -0
- package/dist/blog-list.js.map +1 -0
- package/dist/blog-post.d.ts +40 -0
- package/dist/blog-post.d.ts.map +1 -0
- package/dist/blog-post.js +109 -0
- package/dist/blog-post.js.map +1 -0
- package/dist/blog.d.ts +69 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +79 -0
- package/dist/blog.js.map +1 -0
- package/dist/brand.d.ts +9 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +68 -0
- package/dist/brand.js.map +1 -0
- package/dist/event-marker.d.ts +35 -0
- package/dist/event-marker.d.ts.map +1 -0
- package/dist/event-marker.js +17 -0
- package/dist/event-marker.js.map +1 -0
- package/dist/image.d.ts +34 -34
- package/dist/image.d.ts.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/page.d.ts +18 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +41 -0
- package/dist/page.js.map +1 -0
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +3 -0
- package/dist/rich-text.js.map +1 -1
- package/dist/sanitize.d.ts.map +1 -1
- package/dist/sanitize.js +154 -10
- package/dist/sanitize.js.map +1 -1
- package/dist/sections/blocks.d.ts +10 -0
- package/dist/sections/blocks.d.ts.map +1 -0
- package/dist/sections/blocks.js +164 -0
- package/dist/sections/blocks.js.map +1 -0
- package/dist/sections/index.d.ts +12 -0
- package/dist/sections/index.d.ts.map +1 -0
- package/dist/sections/index.js +104 -0
- package/dist/sections/index.js.map +1 -0
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +2 -1
- package/dist/style.js.map +1 -1
- package/dist/tracking-component.d.ts +29 -0
- package/dist/tracking-component.d.ts.map +1 -0
- package/dist/tracking-component.js +100 -0
- package/dist/tracking-component.js.map +1 -0
- package/dist/tracking.d.ts +27 -0
- package/dist/tracking.d.ts.map +1 -0
- package/dist/tracking.js +49 -0
- package/dist/tracking.js.map +1 -0
- package/dist/types.d.ts +37 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,6 +141,28 @@ export function HomePage({ content }: { content: RisaliContent | null }) {
|
|
|
141
141
|
|
|
142
142
|
Helpers always validate the block type (a block typed `image` will return `defaultValue` from `getBlockValue`) and `getBlockRichText` always sanitises — including the fallback HTML — so a stored XSS payload can't reach the browser even if the block is missing.
|
|
143
143
|
|
|
144
|
+
### Builder pages (v0.6.0)
|
|
145
|
+
|
|
146
|
+
A page with `render_mode='builder'` in the Risali dashboard is a DB-driven list of sections (hero, text + image, features, gallery, CTA, contact, plain). `<RisaliPage>` renders the whole page server-side; pages still in `code` mode return `notFound`, so a catch-all route coexists with your static routes:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// app/[[...rest]]/page.tsx
|
|
150
|
+
import { notFound } from "next/navigation";
|
|
151
|
+
import { RisaliPage } from "@risali/react";
|
|
152
|
+
|
|
153
|
+
export default async function Page({ params }: { params: Promise<{ rest?: string[] }> }) {
|
|
154
|
+
const { rest } = await params;
|
|
155
|
+
const path = "/" + (rest ?? []).join("/");
|
|
156
|
+
const page = await RisaliPage({ path });
|
|
157
|
+
if (!page) notFound();
|
|
158
|
+
return page;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Signature sections you build in code register via `customSections={{ moja_sekcia: MyComponent }}` and win over the built-in catalog on a type collision.
|
|
163
|
+
|
|
164
|
+
`<RisaliBrand />` in the root layout emits the brand palette + fonts as CSS variables (`--risali-color-<key>`, `--risali-font-heading`, `--risali-font-body`) — builder sections consume them automatically, and your own components/Tailwind theme can too. A brand change in the dashboard revalidates the site instantly, no deploy.
|
|
165
|
+
|
|
144
166
|
## Editor mode
|
|
145
167
|
|
|
146
168
|
Every rendered block carries a `data-risali-key="<pageKey>"` attribute. The Risali editor iframe (`/risali.js?site=<slug>&risali_edit=1`) uses that marker to wire up click-to-edit — no CSS selector generation, no content-hash drift, no fragile DOM scanning.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { RisaliBlogPost } from "./blog.js";
|
|
2
|
+
export type BuildBlogPostJsonLdOptions = {
|
|
3
|
+
/** Absolute origin of the client web (https://klient.sk). Required for
|
|
4
|
+
* `mainEntityOfPage` and absolute image URLs. */
|
|
5
|
+
origin: string;
|
|
6
|
+
/** Site name — shown in the `publisher.name` slot. */
|
|
7
|
+
siteName: string;
|
|
8
|
+
/** Optional publisher logo URL (used in `publisher.logo.url`). */
|
|
9
|
+
publisherLogoUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Produce a `BlogPosting` JSON-LD object compliant with schema.org.
|
|
13
|
+
* Drop fields when source data is missing — Google's structured data
|
|
14
|
+
* validator silently ignores empty strings but warns on null/undefined.
|
|
15
|
+
*
|
|
16
|
+
* The HTML embed pattern:
|
|
17
|
+
*
|
|
18
|
+
* const ld = buildBlogPostJsonLd(post, { origin, siteName });
|
|
19
|
+
* <script
|
|
20
|
+
* type="application/ld+json"
|
|
21
|
+
* dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
|
|
22
|
+
* />
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildBlogPostJsonLd(post: RisaliBlogPost, opts: BuildBlogPostJsonLdOptions): Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Build a Next.js `Metadata` partial for a blog post (PR 36d helper).
|
|
27
|
+
* The consumer uses it in `generateMetadata`:
|
|
28
|
+
*
|
|
29
|
+
* export async function generateMetadata({ params }) {
|
|
30
|
+
* const { slug } = await params;
|
|
31
|
+
* const post = await getRisaliBlogPost(slug);
|
|
32
|
+
* return buildBlogPostMetadata(post, { siteName, origin });
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* Returns the empty title fallback so an unknown post renders a
|
|
36
|
+
* generic "Article" tab title rather than `undefined - …`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildBlogPostMetadata(post: RisaliBlogPost | null, opts: {
|
|
39
|
+
siteName: string;
|
|
40
|
+
origin: string;
|
|
41
|
+
defaultTitle?: string;
|
|
42
|
+
}): {
|
|
43
|
+
title: string;
|
|
44
|
+
description?: undefined;
|
|
45
|
+
alternates?: undefined;
|
|
46
|
+
openGraph?: undefined;
|
|
47
|
+
twitter?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
title: string;
|
|
50
|
+
description: string | undefined;
|
|
51
|
+
alternates: {
|
|
52
|
+
canonical: string;
|
|
53
|
+
};
|
|
54
|
+
openGraph: {
|
|
55
|
+
title: string;
|
|
56
|
+
description: string | undefined;
|
|
57
|
+
url: string;
|
|
58
|
+
type: string;
|
|
59
|
+
siteName: string;
|
|
60
|
+
publishedTime: string;
|
|
61
|
+
modifiedTime: string;
|
|
62
|
+
authors: string[] | undefined;
|
|
63
|
+
images: {
|
|
64
|
+
url: string;
|
|
65
|
+
}[] | undefined;
|
|
66
|
+
};
|
|
67
|
+
twitter: {
|
|
68
|
+
card: string;
|
|
69
|
+
title: string;
|
|
70
|
+
description: string | undefined;
|
|
71
|
+
images: string[] | undefined;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=blog-jsonld.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-jsonld.d.ts","sourceRoot":"","sources":["../src/blog-jsonld.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,MAAM,0BAA0B,GAAG;IACvC;qDACiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,0BAA0B,GAC/B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA8CzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,cAAc,GAAG,IAAI,EAC3B,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmClE"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// JSON-LD `BlogPosting` schema builder for the blog post page (PR 36d).
|
|
2
|
+
// Pure: takes a post payload (typed) + site context, returns a JS
|
|
3
|
+
// object the consumer can JSON.stringify into a `<script
|
|
4
|
+
// type="application/ld+json">` tag.
|
|
5
|
+
//
|
|
6
|
+
// Standalone helper so the consumer can wire it into their
|
|
7
|
+
// `generateMetadata` flow or inline-emit it next to <RisaliBlogPost />.
|
|
8
|
+
/**
|
|
9
|
+
* Produce a `BlogPosting` JSON-LD object compliant with schema.org.
|
|
10
|
+
* Drop fields when source data is missing — Google's structured data
|
|
11
|
+
* validator silently ignores empty strings but warns on null/undefined.
|
|
12
|
+
*
|
|
13
|
+
* The HTML embed pattern:
|
|
14
|
+
*
|
|
15
|
+
* const ld = buildBlogPostJsonLd(post, { origin, siteName });
|
|
16
|
+
* <script
|
|
17
|
+
* type="application/ld+json"
|
|
18
|
+
* dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}
|
|
19
|
+
* />
|
|
20
|
+
*/
|
|
21
|
+
export function buildBlogPostJsonLd(post, opts) {
|
|
22
|
+
const baseOrigin = opts.origin.replace(/\/+$/, "");
|
|
23
|
+
const url = `${baseOrigin}/blog/${post.slug}`;
|
|
24
|
+
const headline = (post.seo_meta.title || post.title || "").slice(0, 110);
|
|
25
|
+
const description = post.seo_meta.description || post.excerpt || undefined;
|
|
26
|
+
const imageUrl = post.seo_meta.og_image_url || post.hero_image_url || undefined;
|
|
27
|
+
const author = post.author_display
|
|
28
|
+
? { "@type": "Person", name: post.author_display }
|
|
29
|
+
: { "@type": "Organization", name: opts.siteName };
|
|
30
|
+
const publisher = {
|
|
31
|
+
"@type": "Organization",
|
|
32
|
+
name: opts.siteName,
|
|
33
|
+
};
|
|
34
|
+
if (opts.publisherLogoUrl) {
|
|
35
|
+
publisher.logo = {
|
|
36
|
+
"@type": "ImageObject",
|
|
37
|
+
url: opts.publisherLogoUrl,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const ld = {
|
|
41
|
+
"@context": "https://schema.org",
|
|
42
|
+
"@type": "BlogPosting",
|
|
43
|
+
headline,
|
|
44
|
+
mainEntityOfPage: {
|
|
45
|
+
"@type": "WebPage",
|
|
46
|
+
"@id": url,
|
|
47
|
+
},
|
|
48
|
+
url,
|
|
49
|
+
author,
|
|
50
|
+
publisher,
|
|
51
|
+
datePublished: post.published_at,
|
|
52
|
+
dateModified: post.updated_at || post.published_at,
|
|
53
|
+
};
|
|
54
|
+
if (description)
|
|
55
|
+
ld.description = description;
|
|
56
|
+
if (imageUrl)
|
|
57
|
+
ld.image = imageUrl;
|
|
58
|
+
if (post.category)
|
|
59
|
+
ld.articleSection = post.category;
|
|
60
|
+
if (post.reading_minutes) {
|
|
61
|
+
ld.timeRequired = `PT${post.reading_minutes}M`;
|
|
62
|
+
}
|
|
63
|
+
return ld;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a Next.js `Metadata` partial for a blog post (PR 36d helper).
|
|
67
|
+
* The consumer uses it in `generateMetadata`:
|
|
68
|
+
*
|
|
69
|
+
* export async function generateMetadata({ params }) {
|
|
70
|
+
* const { slug } = await params;
|
|
71
|
+
* const post = await getRisaliBlogPost(slug);
|
|
72
|
+
* return buildBlogPostMetadata(post, { siteName, origin });
|
|
73
|
+
* }
|
|
74
|
+
*
|
|
75
|
+
* Returns the empty title fallback so an unknown post renders a
|
|
76
|
+
* generic "Article" tab title rather than `undefined - …`.
|
|
77
|
+
*/
|
|
78
|
+
export function buildBlogPostMetadata(post, opts) {
|
|
79
|
+
if (!post) {
|
|
80
|
+
return {
|
|
81
|
+
title: opts.defaultTitle ?? "Článok",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const title = post.seo_meta.title || post.title;
|
|
85
|
+
const description = post.seo_meta.description || post.excerpt || undefined;
|
|
86
|
+
const ogImage = post.seo_meta.og_image_url || post.hero_image_url || undefined;
|
|
87
|
+
const url = `${opts.origin.replace(/\/+$/, "")}/blog/${post.slug}`;
|
|
88
|
+
return {
|
|
89
|
+
title: `${title} — ${opts.siteName}`,
|
|
90
|
+
description,
|
|
91
|
+
alternates: { canonical: url },
|
|
92
|
+
openGraph: {
|
|
93
|
+
title,
|
|
94
|
+
description,
|
|
95
|
+
url,
|
|
96
|
+
type: "article",
|
|
97
|
+
siteName: opts.siteName,
|
|
98
|
+
publishedTime: post.published_at,
|
|
99
|
+
modifiedTime: post.updated_at,
|
|
100
|
+
authors: post.author_display ? [post.author_display] : undefined,
|
|
101
|
+
images: ogImage ? [{ url: ogImage }] : undefined,
|
|
102
|
+
},
|
|
103
|
+
twitter: {
|
|
104
|
+
card: ogImage ? "summary_large_image" : "summary",
|
|
105
|
+
title,
|
|
106
|
+
description,
|
|
107
|
+
images: ogImage ? [ogImage] : undefined,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=blog-jsonld.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-jsonld.js","sourceRoot":"","sources":["../src/blog-jsonld.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { type GetBlogPostsOptions } from "./blog.js";
|
|
3
|
+
export type RisaliBlogListProps = GetBlogPostsOptions & {
|
|
4
|
+
/** Hide the empty-state placeholder when the list is empty. */
|
|
5
|
+
hideEmptyState?: boolean;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Server component that renders a responsive grid of blog cards
|
|
11
|
+
* (hero image + title + excerpt + date + reading minutes + category
|
|
12
|
+
* chip). Use it on the client web's `/blog` route:
|
|
13
|
+
*
|
|
14
|
+
* // app/blog/page.tsx
|
|
15
|
+
* import { RisaliBlogList } from "@risali/react";
|
|
16
|
+
* export default function BlogIndex() {
|
|
17
|
+
* return <RisaliBlogList limit={12} />;
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Renders an empty-state placeholder when the site has no published
|
|
21
|
+
* posts yet (suppress with `hideEmptyState`).
|
|
22
|
+
*
|
|
23
|
+
* Styling: unopinionated. Tailwind-friendly class names on the wrapper;
|
|
24
|
+
* pass a `className` to override. The card layout uses inline CSS so it
|
|
25
|
+
* works on hosts without a CSS framework, but tenants are free to
|
|
26
|
+
* style via `.risali-blog-*` class names.
|
|
27
|
+
*/
|
|
28
|
+
export declare function RisaliBlogList(props: RisaliBlogListProps): Promise<import("react/jsx-runtime").JSX.Element | null>;
|
|
29
|
+
//# sourceMappingURL=blog-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-list.d.ts","sourceRoot":"","sources":["../src/blog-list.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,WAAW,CAAC;AAEnB,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GAAG;IACtD,+DAA+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,CAAC;AAgBF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,mBAAmB,2DAoC9D"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getRisaliBlogPosts, } from "./blog.js";
|
|
3
|
+
// Format ISO date for the SK locale, falling back to the raw value on
|
|
4
|
+
// any parse failure (never throws — runtime errors in a server
|
|
5
|
+
// component crash the whole route).
|
|
6
|
+
function fmtDate(iso) {
|
|
7
|
+
if (!iso)
|
|
8
|
+
return "";
|
|
9
|
+
const d = new Date(iso);
|
|
10
|
+
if (!Number.isFinite(d.getTime()))
|
|
11
|
+
return "";
|
|
12
|
+
return d.toLocaleDateString("sk-SK", {
|
|
13
|
+
day: "numeric",
|
|
14
|
+
month: "long",
|
|
15
|
+
year: "numeric",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Server component that renders a responsive grid of blog cards
|
|
20
|
+
* (hero image + title + excerpt + date + reading minutes + category
|
|
21
|
+
* chip). Use it on the client web's `/blog` route:
|
|
22
|
+
*
|
|
23
|
+
* // app/blog/page.tsx
|
|
24
|
+
* import { RisaliBlogList } from "@risali/react";
|
|
25
|
+
* export default function BlogIndex() {
|
|
26
|
+
* return <RisaliBlogList limit={12} />;
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* Renders an empty-state placeholder when the site has no published
|
|
30
|
+
* posts yet (suppress with `hideEmptyState`).
|
|
31
|
+
*
|
|
32
|
+
* Styling: unopinionated. Tailwind-friendly class names on the wrapper;
|
|
33
|
+
* pass a `className` to override. The card layout uses inline CSS so it
|
|
34
|
+
* works on hosts without a CSS framework, but tenants are free to
|
|
35
|
+
* style via `.risali-blog-*` class names.
|
|
36
|
+
*/
|
|
37
|
+
export async function RisaliBlogList(props) {
|
|
38
|
+
const posts = await getRisaliBlogPosts({
|
|
39
|
+
siteSlug: props.siteSlug,
|
|
40
|
+
category: props.category,
|
|
41
|
+
limit: props.limit,
|
|
42
|
+
offset: props.offset,
|
|
43
|
+
});
|
|
44
|
+
if (posts.length === 0) {
|
|
45
|
+
if (props.hideEmptyState)
|
|
46
|
+
return null;
|
|
47
|
+
return (_jsx("div", { className: props.className ?? "risali-blog-empty", style: props.style, children: _jsx("p", { style: { color: "#6b7280" }, children: "Zatia\u013E tu nie s\u00FA \u017Eiadne \u010Dl\u00E1nky." }) }));
|
|
48
|
+
}
|
|
49
|
+
return (_jsx("div", { className: props.className ?? "risali-blog-list", style: {
|
|
50
|
+
display: "grid",
|
|
51
|
+
gap: "1.5rem",
|
|
52
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))",
|
|
53
|
+
...props.style,
|
|
54
|
+
}, children: posts.map((post) => (_jsx(BlogCard, { post: post }, post.slug))) }));
|
|
55
|
+
}
|
|
56
|
+
function BlogCard({ post }) {
|
|
57
|
+
return (_jsx("article", { className: "risali-blog-card", style: {
|
|
58
|
+
display: "flex",
|
|
59
|
+
flexDirection: "column",
|
|
60
|
+
overflow: "hidden",
|
|
61
|
+
borderRadius: "0.75rem",
|
|
62
|
+
background: "#fff",
|
|
63
|
+
boxShadow: "0 1px 2px rgba(0,0,0,0.05)",
|
|
64
|
+
border: "1px solid #e5e7eb",
|
|
65
|
+
}, children: _jsxs("a", { href: `/blog/${encodeURIComponent(post.slug)}`, className: "risali-blog-card-link", style: {
|
|
66
|
+
color: "inherit",
|
|
67
|
+
textDecoration: "none",
|
|
68
|
+
display: "flex",
|
|
69
|
+
flexDirection: "column",
|
|
70
|
+
height: "100%",
|
|
71
|
+
}, children: [post.hero_image_url ? (
|
|
72
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
73
|
+
_jsx("img", { src: post.hero_image_url, alt: "", loading: "lazy", style: {
|
|
74
|
+
width: "100%",
|
|
75
|
+
aspectRatio: "16 / 9",
|
|
76
|
+
objectFit: "cover",
|
|
77
|
+
display: "block",
|
|
78
|
+
} })) : (_jsx("div", { style: {
|
|
79
|
+
width: "100%",
|
|
80
|
+
aspectRatio: "16 / 9",
|
|
81
|
+
background: "#f3f4f6",
|
|
82
|
+
} })), _jsxs("div", { style: {
|
|
83
|
+
padding: "1.25rem",
|
|
84
|
+
display: "flex",
|
|
85
|
+
flexDirection: "column",
|
|
86
|
+
gap: "0.5rem",
|
|
87
|
+
flex: 1,
|
|
88
|
+
}, children: [post.category ? (_jsx("span", { className: "risali-blog-card-category", style: {
|
|
89
|
+
alignSelf: "flex-start",
|
|
90
|
+
fontSize: "0.75rem",
|
|
91
|
+
textTransform: "uppercase",
|
|
92
|
+
letterSpacing: "0.05em",
|
|
93
|
+
color: "#6b7280",
|
|
94
|
+
}, children: post.category })) : null, _jsx("h2", { className: "risali-blog-card-title", style: {
|
|
95
|
+
fontSize: "1.25rem",
|
|
96
|
+
fontWeight: 600,
|
|
97
|
+
lineHeight: 1.3,
|
|
98
|
+
margin: 0,
|
|
99
|
+
}, children: post.title }), post.excerpt ? (_jsx("p", { className: "risali-blog-card-excerpt", style: {
|
|
100
|
+
color: "#4b5563",
|
|
101
|
+
fontSize: "0.95rem",
|
|
102
|
+
lineHeight: 1.5,
|
|
103
|
+
margin: 0,
|
|
104
|
+
}, children: post.excerpt })) : null, _jsxs("div", { style: {
|
|
105
|
+
marginTop: "auto",
|
|
106
|
+
paddingTop: "0.75rem",
|
|
107
|
+
display: "flex",
|
|
108
|
+
gap: "0.75rem",
|
|
109
|
+
fontSize: "0.85rem",
|
|
110
|
+
color: "#6b7280",
|
|
111
|
+
}, children: [_jsx("span", { children: fmtDate(post.published_at) }), post.reading_minutes ? (_jsxs("span", { children: ["\u00B7 ", post.reading_minutes, " min \u010D\u00EDtania"] })) : null, post.author_display ? (_jsxs("span", { children: ["\u00B7 ", post.author_display] })) : null] })] })] }) }));
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=blog-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-list.js","sourceRoot":"","sources":["../src/blog-list.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { type GetBlogPostOptions } from "./blog.js";
|
|
3
|
+
export type RisaliBlogPostProps = GetBlogPostOptions & {
|
|
4
|
+
/** Required: the post slug from the route segment. */
|
|
5
|
+
slug: string;
|
|
6
|
+
/** Fallback to render when the post isn't available. */
|
|
7
|
+
fallback?: React.ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Origin of the client web (https://klient.sk). When supplied
|
|
10
|
+
* together with `siteName`, the component emits a `BlogPosting`
|
|
11
|
+
* JSON-LD `<script>` tag for SEO rich results.
|
|
12
|
+
*/
|
|
13
|
+
origin?: string;
|
|
14
|
+
/** Site name — required for JSON-LD `publisher.name`. */
|
|
15
|
+
siteName?: string;
|
|
16
|
+
/** Optional publisher logo URL for JSON-LD. */
|
|
17
|
+
publisherLogoUrl?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Server component for a single blog post. Use it on the client web's
|
|
23
|
+
* `/blog/[slug]` route:
|
|
24
|
+
*
|
|
25
|
+
* // app/blog/[slug]/page.tsx
|
|
26
|
+
* import { RisaliBlogPost } from "@risali/react";
|
|
27
|
+
* export default async function PostPage(
|
|
28
|
+
* { params }: { params: Promise<{ slug: string }> },
|
|
29
|
+
* ) {
|
|
30
|
+
* const { slug } = await params;
|
|
31
|
+
* return <RisaliBlogPost slug={slug} />;
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* Returns the `fallback` prop (or a generic "not found" message) when
|
|
35
|
+
* the post isn't available. Renders the sanitised HTML via
|
|
36
|
+
* dangerouslySetInnerHTML — the API has already sanitised the payload
|
|
37
|
+
* server-side; we sanitise again here as defense-in-depth.
|
|
38
|
+
*/
|
|
39
|
+
export declare function RisaliBlogPost(props: RisaliBlogPostProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
40
|
+
//# sourceMappingURL=blog-post.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post.d.ts","sourceRoot":"","sources":["../src/blog-post.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AAInB,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,GAAG;IACrD,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,CAAC;AAaF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,mBAAmB,oDAyI9D"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getRisaliBlogPost, } from "./blog.js";
|
|
3
|
+
import { sanitiseRichText } from "./sanitize.js";
|
|
4
|
+
import { buildBlogPostJsonLd } from "./blog-jsonld.js";
|
|
5
|
+
function fmtDate(iso) {
|
|
6
|
+
if (!iso)
|
|
7
|
+
return "";
|
|
8
|
+
const d = new Date(iso);
|
|
9
|
+
if (!Number.isFinite(d.getTime()))
|
|
10
|
+
return "";
|
|
11
|
+
return d.toLocaleDateString("sk-SK", {
|
|
12
|
+
day: "numeric",
|
|
13
|
+
month: "long",
|
|
14
|
+
year: "numeric",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Server component for a single blog post. Use it on the client web's
|
|
19
|
+
* `/blog/[slug]` route:
|
|
20
|
+
*
|
|
21
|
+
* // app/blog/[slug]/page.tsx
|
|
22
|
+
* import { RisaliBlogPost } from "@risali/react";
|
|
23
|
+
* export default async function PostPage(
|
|
24
|
+
* { params }: { params: Promise<{ slug: string }> },
|
|
25
|
+
* ) {
|
|
26
|
+
* const { slug } = await params;
|
|
27
|
+
* return <RisaliBlogPost slug={slug} />;
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* Returns the `fallback` prop (or a generic "not found" message) when
|
|
31
|
+
* the post isn't available. Renders the sanitised HTML via
|
|
32
|
+
* dangerouslySetInnerHTML — the API has already sanitised the payload
|
|
33
|
+
* server-side; we sanitise again here as defense-in-depth.
|
|
34
|
+
*/
|
|
35
|
+
export async function RisaliBlogPost(props) {
|
|
36
|
+
const post = await getRisaliBlogPost(props.slug, {
|
|
37
|
+
siteSlug: props.siteSlug,
|
|
38
|
+
previewToken: props.previewToken,
|
|
39
|
+
});
|
|
40
|
+
if (!post) {
|
|
41
|
+
if (props.fallback !== undefined)
|
|
42
|
+
return _jsx(_Fragment, { children: props.fallback });
|
|
43
|
+
return (_jsx("div", { className: props.className ?? "risali-blog-not-found", style: props.style, children: _jsx("p", { style: { color: "#6b7280" }, children: "Tento \u010Dl\u00E1nok sa nena\u0161iel." }) }));
|
|
44
|
+
}
|
|
45
|
+
const safeHtml = sanitiseRichText(post.content_html);
|
|
46
|
+
const jsonLd = props.origin && props.siteName
|
|
47
|
+
? buildBlogPostJsonLd(post, {
|
|
48
|
+
origin: props.origin,
|
|
49
|
+
siteName: props.siteName,
|
|
50
|
+
publisherLogoUrl: props.publisherLogoUrl,
|
|
51
|
+
})
|
|
52
|
+
: null;
|
|
53
|
+
return (_jsxs("article", { className: props.className ?? "risali-blog-post", style: {
|
|
54
|
+
maxWidth: "780px",
|
|
55
|
+
margin: "0 auto",
|
|
56
|
+
...props.style,
|
|
57
|
+
}, children: [jsonLd ? (_jsx("script", { type: "application/ld+json",
|
|
58
|
+
// JSON.stringify is XSS-safe enough for JSON-LD; the
|
|
59
|
+
// payload contains only typed primitives from the
|
|
60
|
+
// sanitised post + caller-supplied origin/name. The HTML
|
|
61
|
+
// injection vector for JSON-LD is `</script>` inside a
|
|
62
|
+
// string — replace `<` so the parser can't close the tag
|
|
63
|
+
// early.
|
|
64
|
+
dangerouslySetInnerHTML: {
|
|
65
|
+
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
|
|
66
|
+
} })) : null, props.previewToken ? (_jsx("div", { className: "risali-blog-preview-banner", style: {
|
|
67
|
+
background: "#fef3c7",
|
|
68
|
+
color: "#854d0e",
|
|
69
|
+
padding: "0.5rem 1rem",
|
|
70
|
+
borderRadius: "0.5rem",
|
|
71
|
+
marginBottom: "1rem",
|
|
72
|
+
fontSize: "0.9rem",
|
|
73
|
+
textAlign: "center",
|
|
74
|
+
}, children: "N\u00E1h\u013Ead \u2014 tento odkaz je platn\u00FD 15 min\u00FAt a uvid\u00ED\u0161 ho len ty." })) : null, post.hero_image_url ? (
|
|
75
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
76
|
+
_jsx("img", { src: post.hero_image_url, alt: "", loading: "eager", fetchPriority: "high", style: {
|
|
77
|
+
width: "100%",
|
|
78
|
+
aspectRatio: "16 / 9",
|
|
79
|
+
objectFit: "cover",
|
|
80
|
+
borderRadius: "0.75rem",
|
|
81
|
+
marginBottom: "1.5rem",
|
|
82
|
+
} })) : null, post.category ? (_jsx("div", { className: "risali-blog-post-category", style: {
|
|
83
|
+
fontSize: "0.85rem",
|
|
84
|
+
textTransform: "uppercase",
|
|
85
|
+
letterSpacing: "0.05em",
|
|
86
|
+
color: "#6b7280",
|
|
87
|
+
marginBottom: "0.5rem",
|
|
88
|
+
}, children: post.category })) : null, _jsx("h1", { className: "risali-blog-post-title", style: {
|
|
89
|
+
fontSize: "2.25rem",
|
|
90
|
+
fontWeight: 700,
|
|
91
|
+
lineHeight: 1.2,
|
|
92
|
+
margin: "0 0 1rem 0",
|
|
93
|
+
}, children: post.title }), _jsxs("div", { className: "risali-blog-post-meta", style: {
|
|
94
|
+
display: "flex",
|
|
95
|
+
gap: "0.75rem",
|
|
96
|
+
fontSize: "0.9rem",
|
|
97
|
+
color: "#6b7280",
|
|
98
|
+
marginBottom: "2rem",
|
|
99
|
+
}, children: [post.author_display ? _jsx("span", { children: post.author_display }) : null, _jsx("span", { children: fmtDate(post.published_at) }), post.reading_minutes ? (_jsxs("span", { children: ["\u00B7 ", post.reading_minutes, " min \u010D\u00EDtania"] })) : null] }), _jsx("div", { className: "risali-blog-post-body", style: {
|
|
100
|
+
fontSize: "1.05rem",
|
|
101
|
+
lineHeight: 1.7,
|
|
102
|
+
color: "#111827",
|
|
103
|
+
},
|
|
104
|
+
// Server-side sanitised HTML. The render-time sanitiseRichText
|
|
105
|
+
// run is a defense-in-depth second pass over what the API
|
|
106
|
+
// already cleaned at save time (PR 36a normalizer).
|
|
107
|
+
dangerouslySetInnerHTML: { __html: safeHtml } })] }));
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=blog-post.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post.js","sourceRoot":"","sources":["../src/blog-post.tsx"],"names":[],"mappings":"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"}
|
package/dist/blog.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type RisaliBlogPostCard = {
|
|
2
|
+
slug: string;
|
|
3
|
+
title: string;
|
|
4
|
+
excerpt: string | null;
|
|
5
|
+
hero_image_url: string | null;
|
|
6
|
+
category: string | null;
|
|
7
|
+
published_at: string;
|
|
8
|
+
author_display: string | null;
|
|
9
|
+
reading_minutes: number | null;
|
|
10
|
+
};
|
|
11
|
+
export type RisaliBlogPostSeo = {
|
|
12
|
+
title?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
og_image_url?: string;
|
|
15
|
+
};
|
|
16
|
+
export type RisaliBlogPost = {
|
|
17
|
+
slug: string;
|
|
18
|
+
title: string;
|
|
19
|
+
content_html: string;
|
|
20
|
+
excerpt: string | null;
|
|
21
|
+
hero_image_url: string | null;
|
|
22
|
+
category: string | null;
|
|
23
|
+
published_at: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
author_display: string | null;
|
|
26
|
+
reading_minutes: number | null;
|
|
27
|
+
seo_meta: RisaliBlogPostSeo;
|
|
28
|
+
};
|
|
29
|
+
export type GetBlogPostsOptions = {
|
|
30
|
+
/** Override the resolved site slug (default: RISALI_SITE_SLUG env). */
|
|
31
|
+
siteSlug?: string;
|
|
32
|
+
/** Filter by single free-text category. */
|
|
33
|
+
category?: string;
|
|
34
|
+
/** Page size (1..50, default 12). */
|
|
35
|
+
limit?: number;
|
|
36
|
+
/** Pagination offset (default 0). */
|
|
37
|
+
offset?: number;
|
|
38
|
+
};
|
|
39
|
+
export type GetBlogPostOptions = {
|
|
40
|
+
/** Override the resolved site slug. */
|
|
41
|
+
siteSlug?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Optional preview token from the dashboard "Preview" button. When
|
|
44
|
+
* supplied, the fetch hits the preview path which bypasses
|
|
45
|
+
* `status='published'` and returns drafts + scheduled posts.
|
|
46
|
+
* Tokens expire 15 minutes after issuance.
|
|
47
|
+
*/
|
|
48
|
+
previewToken?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Fetch the published blog post list for the resolved site. Returns
|
|
52
|
+
* an empty array on any failure (unknown site, plan downgrade,
|
|
53
|
+
* network) so the consumer can fall back to an empty grid without
|
|
54
|
+
* dropping the whole page.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getRisaliBlogPosts(opts?: GetBlogPostsOptions): Promise<RisaliBlogPostCard[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch a single blog post by slug. Returns null on any failure so
|
|
59
|
+
* the consumer can render a notFound() page instead of throwing.
|
|
60
|
+
* When `previewToken` is supplied, the call goes through the
|
|
61
|
+
* preview path (no cache, drafts allowed).
|
|
62
|
+
*/
|
|
63
|
+
export declare function getRisaliBlogPost(postSlug: string, opts?: GetBlogPostOptions): Promise<RisaliBlogPost | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Build a public `/blog/<slug>` URL for the client web. Helper for
|
|
66
|
+
* hand-rolled card grids that don't use `<RisaliBlogList />`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildBlogPostHref(postSlug: string): string;
|
|
69
|
+
//# sourceMappingURL=blog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog.d.ts","sourceRoot":"","sources":["../src/blog.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,EAAE,iBAAiB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAqB/B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA2BhC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D"}
|