@solutiondesk/blog 1.0.0-35d9e26 → 1.0.0-5e7e6d3
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 +46 -1
- package/dist/index.cjs +29 -0
- package/dist/index.d.cts +62 -1
- package/dist/index.d.ts +62 -1
- package/dist/index.js +28 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ const { post } = await blog.getPost("metadata-that-pays-off");
|
|
|
31
31
|
| --- | --- | --- |
|
|
32
32
|
| `listPosts({ lang, tag, author, limit, cursor })` | `GET /v1/blog/{workspace}/posts` | One page, newest first, without the text; `nextCursor` for the next page (`limit` 1–50, default 20). |
|
|
33
33
|
| `allPosts({ lang, tag, author })` | the same, page by page | An async iterator over every published post, e.g. for a sitemap. |
|
|
34
|
-
| `getPost(slug, { lang, redirect })` | `GET /v1/blog/{workspace}/posts/{slug}` | One post with `markdown`, `html`, `headings`, `readingMinutes` and the blog's own building blocks it uses (`customBlocks`, `blocks`). |
|
|
34
|
+
| `getPost(slug, { lang, redirect })` | `GET /v1/blog/{workspace}/posts/{slug}` | One post with `markdown`, `html`, `headings`, `readingMinutes`, `summary`, `faq`, `seo` and the blog's own building blocks it uses (`customBlocks`, `blocks`). |
|
|
35
35
|
| `listTags({ lang })` | `GET /v1/blog/{workspace}/tags` | Every tag with its label and how many published posts carry it. |
|
|
36
36
|
| `listAuthors({ lang })` | `GET /v1/blog/{workspace}/authors` | Every author with their role. |
|
|
37
37
|
| `getPreview(token, { lang })` | `GET /v1/blog/{workspace}/preview?token=` | The current state of a post, drafts too, for your preview route. |
|
|
@@ -83,6 +83,51 @@ export default async function PostPage({ params }: { params: Promise<{ slug: str
|
|
|
83
83
|
|
|
84
84
|
Anything that runs on a server works the same way: Astro, Remix, SvelteKit, Nuxt or a plain Node script that renders your site at build time.
|
|
85
85
|
|
|
86
|
+
## Search engines, link previews and answer engines
|
|
87
|
+
|
|
88
|
+
Every post carries `seo`: what belongs in the page's `<head>`, with the fallbacks already applied — the search title or else the title, the search description or else the description, the link preview image or else the cover.
|
|
89
|
+
|
|
90
|
+
| Field | Use it for |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `seo.title`, `seo.description` | `<title>` and `<meta name="description">` |
|
|
93
|
+
| `seo.canonicalUrl` | `<link rel="canonical">` — the original address the team set, else the post's address on your site |
|
|
94
|
+
| `seo.alternates` | `<link rel="alternate" hreflang>` for every language (when the post address has `{lang}`) |
|
|
95
|
+
| `seo.noindex` | the team asked search engines not to list the post (`robots` is in `meta` too) |
|
|
96
|
+
| `seo.meta` | every meta tag: description, robots, Open Graph (`og:*`, `article:*`) and Twitter cards |
|
|
97
|
+
| `seo.jsonLd` | schema.org structured data: `BlogPosting`, and `FAQPage` when the post has a FAQ |
|
|
98
|
+
|
|
99
|
+
Canonical links, alternates, `og:url` and the address in the structured data need the post address from **Blog → Website** (e.g. `https://www.example.com/{lang}/blog/{slug}`); every post then also has its `url`. The website's name there becomes `og:site_name` and the publisher.
|
|
100
|
+
|
|
101
|
+
`renderBlogHead(post)` renders all of it as escaped HTML. With a framework's own head API, map the fields instead:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
// app/blog/[slug]/page.tsx (Next.js)
|
|
105
|
+
import { renderBlogJsonLd } from "@solutiondesk/blog";
|
|
106
|
+
|
|
107
|
+
export async function generateMetadata({ params }) {
|
|
108
|
+
const { post } = await blog.getPost((await params).slug);
|
|
109
|
+
return {
|
|
110
|
+
title: post.seo.title,
|
|
111
|
+
description: post.seo.description,
|
|
112
|
+
alternates: {
|
|
113
|
+
canonical: post.seo.canonicalUrl,
|
|
114
|
+
languages: Object.fromEntries(post.seo.alternates.map((a) => [a.locale, a.url])),
|
|
115
|
+
},
|
|
116
|
+
robots: post.seo.noindex ? { index: false, follow: true } : undefined,
|
|
117
|
+
openGraph: { images: post.seo.image ? [post.seo.image.url] : [] },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// in the page component:
|
|
122
|
+
{post.seo.jsonLd.map((data, i) => (
|
|
123
|
+
<script key={i} type="application/ld+json" dangerouslySetInnerHTML={{ __html: renderBlogJsonLd(data) }} />
|
|
124
|
+
))}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For answer engines (ChatGPT, Perplexity, Google's AI answers) a post can have a `summary` (the key points in a few sentences) and a `faq` (questions and answers). Show them on the page — answer engines quote what readers can see — the FAQ is in `seo.jsonLd` already.
|
|
128
|
+
|
|
129
|
+
`GET /v1/blog/{workspace}/llms.txt?lang=` lists every published post that search engines may list, as an [llms.txt](https://llmstxt.org) file. Serve it at `/llms.txt` on your site, e.g. with a rewrite to that address.
|
|
130
|
+
|
|
86
131
|
## Your own building blocks
|
|
87
132
|
|
|
88
133
|
In SolutionDesk, the blog can define its own building blocks under **Blog → Website**: what a block is called, whether it sits in a sentence, on its own line or around content, and which fields it has — a newsletter box, a product card, an app button. Writers add them to posts from **Block**. How a block looks is up to your website.
|
package/dist/index.cjs
CHANGED
|
@@ -406,6 +406,33 @@ function parseBlogWebhook(secret, rawBody, signatureHeader, options) {
|
|
|
406
406
|
return parseWebhook(secret, rawBody, signatureHeader, options);
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
// src/head.ts
|
|
410
|
+
function renderBlogJsonLd(data) {
|
|
411
|
+
return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
412
|
+
}
|
|
413
|
+
function renderBlogHead(post) {
|
|
414
|
+
const { seo } = post;
|
|
415
|
+
const lines = [`<title>${escapeBlogHtml(seo.title)}</title>`];
|
|
416
|
+
if (seo.canonicalUrl) {
|
|
417
|
+
lines.push(`<link rel="canonical" href="${escapeBlogHtml(seo.canonicalUrl)}">`);
|
|
418
|
+
}
|
|
419
|
+
for (const alternate of seo.alternates) {
|
|
420
|
+
lines.push(
|
|
421
|
+
`<link rel="alternate" hreflang="${escapeBlogHtml(alternate.locale)}" href="${escapeBlogHtml(alternate.url)}">`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
for (const tag of seo.meta) {
|
|
425
|
+
const [attribute, key] = "property" in tag ? ["property", tag.property] : ["name", tag.name];
|
|
426
|
+
lines.push(
|
|
427
|
+
`<meta ${attribute}="${escapeBlogHtml(key)}" content="${escapeBlogHtml(tag.content)}">`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
for (const data of seo.jsonLd) {
|
|
431
|
+
lines.push(`<script type="application/ld+json">${renderBlogJsonLd(data)}</script>`);
|
|
432
|
+
}
|
|
433
|
+
return lines.join("\n");
|
|
434
|
+
}
|
|
435
|
+
|
|
409
436
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
410
437
|
exports.EVENT_HEADER = EVENT_HEADER;
|
|
411
438
|
exports.IMAGE_WIDTHS = IMAGE_WIDTHS;
|
|
@@ -418,4 +445,6 @@ exports.imageUrl = imageUrl;
|
|
|
418
445
|
exports.imageWidthFor = imageWidthFor;
|
|
419
446
|
exports.parseBlogWebhook = parseBlogWebhook;
|
|
420
447
|
exports.renderBlogBlocks = renderBlogBlocks;
|
|
448
|
+
exports.renderBlogHead = renderBlogHead;
|
|
449
|
+
exports.renderBlogJsonLd = renderBlogJsonLd;
|
|
421
450
|
exports.verifyWebhookSignature = verifyWebhookSignature;
|
package/dist/index.d.cts
CHANGED
|
@@ -245,6 +245,8 @@ interface BlogHeading {
|
|
|
245
245
|
/** A post in the list: everything but the text. */
|
|
246
246
|
interface PublicBlogPostSummary {
|
|
247
247
|
slug: string;
|
|
248
|
+
/** The post's page on your website (from the blog's post address), or null. */
|
|
249
|
+
url: string | null;
|
|
248
250
|
title: string;
|
|
249
251
|
description: string;
|
|
250
252
|
/** ISO time readers see as the publication date. */
|
|
@@ -292,8 +294,53 @@ interface PublicBlogBlockDefinition {
|
|
|
292
294
|
fields: PublicBlogBlockField[];
|
|
293
295
|
text: string;
|
|
294
296
|
}
|
|
297
|
+
/** A `<meta>` tag: `name` (description, robots, twitter:…) or `property` (og:…, article:…). */
|
|
298
|
+
type PublicBlogMetaTag = {
|
|
299
|
+
name: string;
|
|
300
|
+
content: string;
|
|
301
|
+
} | {
|
|
302
|
+
property: string;
|
|
303
|
+
content: string;
|
|
304
|
+
};
|
|
305
|
+
/** One question the post answers. */
|
|
306
|
+
interface PublicBlogFaqItem {
|
|
307
|
+
question: string;
|
|
308
|
+
answer: string;
|
|
309
|
+
}
|
|
310
|
+
/** The post in another language (`hreflang`); `x-default` is the main language. */
|
|
311
|
+
interface PublicBlogAlternate {
|
|
312
|
+
locale: string;
|
|
313
|
+
url: string;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Everything for the page's `<head>`: the texts with their fallbacks applied
|
|
317
|
+
* and ready-made meta tags and structured data (schema.org JSON-LD). Render it
|
|
318
|
+
* with `renderBlogHead`, or pick the parts your framework wants.
|
|
319
|
+
*/
|
|
320
|
+
interface PublicBlogSeo {
|
|
321
|
+
/** For `<title>`: the search title, else the title. */
|
|
322
|
+
title: string;
|
|
323
|
+
/** For `<meta name="description">`: the search description, else the description. */
|
|
324
|
+
description: string;
|
|
325
|
+
/** For link previews: the own image, else the cover. */
|
|
326
|
+
image: PublicBlogImage | null;
|
|
327
|
+
/** Search engines should not list the post. */
|
|
328
|
+
noindex: boolean;
|
|
329
|
+
/** For `<link rel="canonical">`, or null when the blog has no post address. */
|
|
330
|
+
canonicalUrl: string | null;
|
|
331
|
+
/** For `<link rel="alternate" hreflang>` (empty unless the post address has `{lang}`). */
|
|
332
|
+
alternates: PublicBlogAlternate[];
|
|
333
|
+
meta: PublicBlogMetaTag[];
|
|
334
|
+
/** Each one a `<script type="application/ld+json">`: `BlogPosting`, and `FAQPage` with a FAQ. */
|
|
335
|
+
jsonLd: Record<string, unknown>[];
|
|
336
|
+
}
|
|
295
337
|
/** One post with its text. */
|
|
296
338
|
interface PublicBlogPost extends PublicBlogPostSummary {
|
|
339
|
+
/** The key points in a few sentences, or null. */
|
|
340
|
+
summary: string | null;
|
|
341
|
+
/** Questions and answers (empty when the post has none). */
|
|
342
|
+
faq: PublicBlogFaqItem[];
|
|
343
|
+
seo: PublicBlogSeo;
|
|
297
344
|
/** The post's Markdown, the blog's own building blocks as written (see `blocks`). */
|
|
298
345
|
markdown: string;
|
|
299
346
|
/**
|
|
@@ -435,4 +482,18 @@ declare function createBlogClient(options: BlogClientOptions): BlogClient;
|
|
|
435
482
|
*/
|
|
436
483
|
declare function parseBlogWebhook(secret: string, rawBody: WebhookBody, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): Promise<BlogWebhookBody | null>;
|
|
437
484
|
|
|
438
|
-
|
|
485
|
+
/**
|
|
486
|
+
* JSON for a `<script type="application/ld+json">`: `<`, `>` and `&` as
|
|
487
|
+
* Unicode escapes, so no text in it can close the script element.
|
|
488
|
+
*/
|
|
489
|
+
declare function renderBlogJsonLd(data: Record<string, unknown>): string;
|
|
490
|
+
/**
|
|
491
|
+
* A post's `<head>` as HTML: `<title>`, the canonical link, language
|
|
492
|
+
* alternates, every meta tag (description, robots, Open Graph, Twitter) and
|
|
493
|
+
* the structured data scripts — everything in `post.seo`, escaped. For
|
|
494
|
+
* frameworks with their own head API (Next.js `generateMetadata`, Astro,
|
|
495
|
+
* Nuxt), read `post.seo` directly instead.
|
|
496
|
+
*/
|
|
497
|
+
declare function renderBlogHead(post: Pick<PublicBlogPost, "seo">): string;
|
|
498
|
+
|
|
499
|
+
export { type BaseClientOptions, type BlogBlockElement, type BlogBlockRenderer, type BlogBlockValue, type BlogClient, type BlogClientOptions, type BlogHeading, type BlogPostMoved, type BlogPostResult, type BlogWebhookBody, type BlogWebhookEvent, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPostParams, IMAGE_WIDTHS, type ImageWidth, type LangParams, type ListPostsParams, type PublicBlogAlternate, type PublicBlogAuthor, type PublicBlogAuthorsPayload, type PublicBlogBlockDefinition, type PublicBlogBlockField, type PublicBlogBlockFieldType, type PublicBlogClosing, type PublicBlogFaqItem, type PublicBlogImage, type PublicBlogMetaTag, type PublicBlogPost, type PublicBlogPostPayload, type PublicBlogPostSummary, type PublicBlogPostsPayload, type PublicBlogSeo, type PublicBlogTag, type PublicBlogTagsPayload, type RequestOptions, SIGNATURE_HEADER, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createBlogClient, createMemoryEtagCache, escapeBlogHtml, imageUrl, imageWidthFor, parseBlogWebhook, renderBlogBlocks, renderBlogHead, renderBlogJsonLd, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -245,6 +245,8 @@ interface BlogHeading {
|
|
|
245
245
|
/** A post in the list: everything but the text. */
|
|
246
246
|
interface PublicBlogPostSummary {
|
|
247
247
|
slug: string;
|
|
248
|
+
/** The post's page on your website (from the blog's post address), or null. */
|
|
249
|
+
url: string | null;
|
|
248
250
|
title: string;
|
|
249
251
|
description: string;
|
|
250
252
|
/** ISO time readers see as the publication date. */
|
|
@@ -292,8 +294,53 @@ interface PublicBlogBlockDefinition {
|
|
|
292
294
|
fields: PublicBlogBlockField[];
|
|
293
295
|
text: string;
|
|
294
296
|
}
|
|
297
|
+
/** A `<meta>` tag: `name` (description, robots, twitter:…) or `property` (og:…, article:…). */
|
|
298
|
+
type PublicBlogMetaTag = {
|
|
299
|
+
name: string;
|
|
300
|
+
content: string;
|
|
301
|
+
} | {
|
|
302
|
+
property: string;
|
|
303
|
+
content: string;
|
|
304
|
+
};
|
|
305
|
+
/** One question the post answers. */
|
|
306
|
+
interface PublicBlogFaqItem {
|
|
307
|
+
question: string;
|
|
308
|
+
answer: string;
|
|
309
|
+
}
|
|
310
|
+
/** The post in another language (`hreflang`); `x-default` is the main language. */
|
|
311
|
+
interface PublicBlogAlternate {
|
|
312
|
+
locale: string;
|
|
313
|
+
url: string;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Everything for the page's `<head>`: the texts with their fallbacks applied
|
|
317
|
+
* and ready-made meta tags and structured data (schema.org JSON-LD). Render it
|
|
318
|
+
* with `renderBlogHead`, or pick the parts your framework wants.
|
|
319
|
+
*/
|
|
320
|
+
interface PublicBlogSeo {
|
|
321
|
+
/** For `<title>`: the search title, else the title. */
|
|
322
|
+
title: string;
|
|
323
|
+
/** For `<meta name="description">`: the search description, else the description. */
|
|
324
|
+
description: string;
|
|
325
|
+
/** For link previews: the own image, else the cover. */
|
|
326
|
+
image: PublicBlogImage | null;
|
|
327
|
+
/** Search engines should not list the post. */
|
|
328
|
+
noindex: boolean;
|
|
329
|
+
/** For `<link rel="canonical">`, or null when the blog has no post address. */
|
|
330
|
+
canonicalUrl: string | null;
|
|
331
|
+
/** For `<link rel="alternate" hreflang>` (empty unless the post address has `{lang}`). */
|
|
332
|
+
alternates: PublicBlogAlternate[];
|
|
333
|
+
meta: PublicBlogMetaTag[];
|
|
334
|
+
/** Each one a `<script type="application/ld+json">`: `BlogPosting`, and `FAQPage` with a FAQ. */
|
|
335
|
+
jsonLd: Record<string, unknown>[];
|
|
336
|
+
}
|
|
295
337
|
/** One post with its text. */
|
|
296
338
|
interface PublicBlogPost extends PublicBlogPostSummary {
|
|
339
|
+
/** The key points in a few sentences, or null. */
|
|
340
|
+
summary: string | null;
|
|
341
|
+
/** Questions and answers (empty when the post has none). */
|
|
342
|
+
faq: PublicBlogFaqItem[];
|
|
343
|
+
seo: PublicBlogSeo;
|
|
297
344
|
/** The post's Markdown, the blog's own building blocks as written (see `blocks`). */
|
|
298
345
|
markdown: string;
|
|
299
346
|
/**
|
|
@@ -435,4 +482,18 @@ declare function createBlogClient(options: BlogClientOptions): BlogClient;
|
|
|
435
482
|
*/
|
|
436
483
|
declare function parseBlogWebhook(secret: string, rawBody: WebhookBody, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): Promise<BlogWebhookBody | null>;
|
|
437
484
|
|
|
438
|
-
|
|
485
|
+
/**
|
|
486
|
+
* JSON for a `<script type="application/ld+json">`: `<`, `>` and `&` as
|
|
487
|
+
* Unicode escapes, so no text in it can close the script element.
|
|
488
|
+
*/
|
|
489
|
+
declare function renderBlogJsonLd(data: Record<string, unknown>): string;
|
|
490
|
+
/**
|
|
491
|
+
* A post's `<head>` as HTML: `<title>`, the canonical link, language
|
|
492
|
+
* alternates, every meta tag (description, robots, Open Graph, Twitter) and
|
|
493
|
+
* the structured data scripts — everything in `post.seo`, escaped. For
|
|
494
|
+
* frameworks with their own head API (Next.js `generateMetadata`, Astro,
|
|
495
|
+
* Nuxt), read `post.seo` directly instead.
|
|
496
|
+
*/
|
|
497
|
+
declare function renderBlogHead(post: Pick<PublicBlogPost, "seo">): string;
|
|
498
|
+
|
|
499
|
+
export { type BaseClientOptions, type BlogBlockElement, type BlogBlockRenderer, type BlogBlockValue, type BlogClient, type BlogClientOptions, type BlogHeading, type BlogPostMoved, type BlogPostResult, type BlogWebhookBody, type BlogWebhookEvent, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPostParams, IMAGE_WIDTHS, type ImageWidth, type LangParams, type ListPostsParams, type PublicBlogAlternate, type PublicBlogAuthor, type PublicBlogAuthorsPayload, type PublicBlogBlockDefinition, type PublicBlogBlockField, type PublicBlogBlockFieldType, type PublicBlogClosing, type PublicBlogFaqItem, type PublicBlogImage, type PublicBlogMetaTag, type PublicBlogPost, type PublicBlogPostPayload, type PublicBlogPostSummary, type PublicBlogPostsPayload, type PublicBlogSeo, type PublicBlogTag, type PublicBlogTagsPayload, type RequestOptions, SIGNATURE_HEADER, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createBlogClient, createMemoryEtagCache, escapeBlogHtml, imageUrl, imageWidthFor, parseBlogWebhook, renderBlogBlocks, renderBlogHead, renderBlogJsonLd, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -404,4 +404,31 @@ function parseBlogWebhook(secret, rawBody, signatureHeader, options) {
|
|
|
404
404
|
return parseWebhook(secret, rawBody, signatureHeader, options);
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
|
|
407
|
+
// src/head.ts
|
|
408
|
+
function renderBlogJsonLd(data) {
|
|
409
|
+
return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
410
|
+
}
|
|
411
|
+
function renderBlogHead(post) {
|
|
412
|
+
const { seo } = post;
|
|
413
|
+
const lines = [`<title>${escapeBlogHtml(seo.title)}</title>`];
|
|
414
|
+
if (seo.canonicalUrl) {
|
|
415
|
+
lines.push(`<link rel="canonical" href="${escapeBlogHtml(seo.canonicalUrl)}">`);
|
|
416
|
+
}
|
|
417
|
+
for (const alternate of seo.alternates) {
|
|
418
|
+
lines.push(
|
|
419
|
+
`<link rel="alternate" hreflang="${escapeBlogHtml(alternate.locale)}" href="${escapeBlogHtml(alternate.url)}">`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
for (const tag of seo.meta) {
|
|
423
|
+
const [attribute, key] = "property" in tag ? ["property", tag.property] : ["name", tag.name];
|
|
424
|
+
lines.push(
|
|
425
|
+
`<meta ${attribute}="${escapeBlogHtml(key)}" content="${escapeBlogHtml(tag.content)}">`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
for (const data of seo.jsonLd) {
|
|
429
|
+
lines.push(`<script type="application/ld+json">${renderBlogJsonLd(data)}</script>`);
|
|
430
|
+
}
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export { DEFAULT_BASE_URL, EVENT_HEADER, IMAGE_WIDTHS, SIGNATURE_HEADER, SolutionDeskError, createBlogClient, createMemoryEtagCache, escapeBlogHtml, imageUrl, imageWidthFor, parseBlogWebhook, renderBlogBlocks, renderBlogHead, renderBlogJsonLd, verifyWebhookSignature };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solutiondesk/blog",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-5e7e6d3",
|
|
4
4
|
"description": "Typed, zero-dependency client for the SolutionDesk public blog API: posts, tags, authors, previews and webhook verification.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"solutiondesk",
|