@xexr/blog-platform 1.1.7 → 1.2.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 +6 -1
- package/README.md +21 -0
- package/dist/app/api/blog/publish/route.test.ts +55 -0
- package/dist/app/api/blog/publish/route.ts +7 -2
- package/dist/app/blog/[slug]/page.tsx +41 -28
- package/dist/components/ArticleVideo.tsx +136 -0
- package/dist/{index-DsXjuPn4.d.mts → index-CmjKzrH-.d.mts} +28 -1
- package/dist/{index-DsXjuPn4.d.ts → index-CmjKzrH-.d.ts} +28 -1
- package/dist/index.d.mts +52 -6
- package/dist/index.d.ts +52 -6
- package/dist/index.js +14 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -14
- package/dist/index.mjs.map +1 -1
- package/dist/lib/storage/drizzle.ts +9 -0
- package/dist/lib/structured-data.ts +52 -0
- package/dist/lib/video.ts +163 -0
- package/dist/{sitemap-B64QGT4l.d.ts → sitemap-CEjqxRaQ.d.ts} +1 -1
- package/dist/{sitemap-Dy7Q4ATM.d.mts → sitemap-D1eInRA1.d.mts} +1 -1
- package/dist/sitemap.d.mts +2 -2
- package/dist/sitemap.d.ts +2 -2
- package/dist/types/contracts.ts +35 -0
- package/dist/types/index.d.mts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.ts +1 -0
- package/package.json +6 -1
|
@@ -100,6 +100,7 @@ function toRow(article: PublishedArticle): ArticleRow {
|
|
|
100
100
|
__bloggyTopics: article.topics ?? [],
|
|
101
101
|
__bloggySeries: article.series ?? null,
|
|
102
102
|
__bloggyCta: article.cta ?? null,
|
|
103
|
+
...(article.video && { __bloggyVideo: article.video }),
|
|
103
104
|
}),
|
|
104
105
|
version: article.version,
|
|
105
106
|
};
|
|
@@ -128,6 +129,7 @@ function fromRow(row: ArticleRow): PublishedArticle {
|
|
|
128
129
|
cta: extractCta(row.metaJson),
|
|
129
130
|
topics: extractTopics(row.metaJson),
|
|
130
131
|
series: extractSeries(row.metaJson),
|
|
132
|
+
...withVideo(row.metaJson),
|
|
131
133
|
meta: extractMeta(row.metaJson),
|
|
132
134
|
version: row.version,
|
|
133
135
|
};
|
|
@@ -137,6 +139,7 @@ type StoredArticleMeta = PublishedArticle["meta"] & {
|
|
|
137
139
|
__bloggyTopics?: PublishedArticle["topics"];
|
|
138
140
|
__bloggySeries?: PublishedArticle["series"] | null;
|
|
139
141
|
__bloggyCta?: PublishedArticle["cta"] | null;
|
|
142
|
+
__bloggyVideo?: PublishedArticle["video"];
|
|
140
143
|
};
|
|
141
144
|
|
|
142
145
|
function parseStoredMeta(metaJson: string): StoredArticleMeta {
|
|
@@ -158,8 +161,14 @@ function extractSeries(metaJson: string): PublishedArticle["series"] {
|
|
|
158
161
|
return parsed.__bloggySeries ?? undefined;
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
function withVideo(metaJson: string): Pick<PublishedArticle, "video"> {
|
|
165
|
+
const video = parseStoredMeta(metaJson).__bloggyVideo;
|
|
166
|
+
return video ? { video } : {};
|
|
167
|
+
}
|
|
168
|
+
|
|
161
169
|
function extractMeta(metaJson: string): PublishedArticle["meta"] {
|
|
162
170
|
const parsed = parseStoredMeta(metaJson);
|
|
171
|
+
delete parsed.__bloggyVideo;
|
|
163
172
|
delete parsed.__bloggyTopics;
|
|
164
173
|
delete parsed.__bloggySeries;
|
|
165
174
|
delete parsed.__bloggyCta;
|
|
@@ -15,6 +15,11 @@ import type {
|
|
|
15
15
|
PublishedSite,
|
|
16
16
|
} from "../types/contracts";
|
|
17
17
|
import { buildAuthorPath } from "./urls";
|
|
18
|
+
import {
|
|
19
|
+
buildYouTubeStructuredDataEmbedUrl,
|
|
20
|
+
buildYouTubeThumbnailUrl,
|
|
21
|
+
isYouTubeVideoId,
|
|
22
|
+
} from "./video";
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* Base schema.org context and type
|
|
@@ -95,6 +100,21 @@ export interface BlogPostingSchema extends BaseStructuredData {
|
|
|
95
100
|
"@type": "Thing";
|
|
96
101
|
name: string;
|
|
97
102
|
}[];
|
|
103
|
+
video?: VideoObjectSchema;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* VideoObject schema for an article's embedded video
|
|
108
|
+
* @see https://schema.org/VideoObject
|
|
109
|
+
*/
|
|
110
|
+
export interface VideoObjectSchema {
|
|
111
|
+
"@type": "VideoObject";
|
|
112
|
+
name: string;
|
|
113
|
+
description: string;
|
|
114
|
+
thumbnailUrl: string[];
|
|
115
|
+
uploadDate: string;
|
|
116
|
+
duration?: string;
|
|
117
|
+
embedUrl: string;
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
/**
|
|
@@ -205,6 +225,11 @@ export function generateArticleStructuredData(
|
|
|
205
225
|
isAccessibleForFree,
|
|
206
226
|
};
|
|
207
227
|
|
|
228
|
+
const videoSchema = generateVideoStructuredData(article);
|
|
229
|
+
if (videoSchema) {
|
|
230
|
+
schema.video = videoSchema;
|
|
231
|
+
}
|
|
232
|
+
|
|
208
233
|
// Add optional properties if enabled
|
|
209
234
|
if (includeOptionalProperties) {
|
|
210
235
|
// Featured image
|
|
@@ -239,6 +264,33 @@ export function generateArticleStructuredData(
|
|
|
239
264
|
return schema;
|
|
240
265
|
}
|
|
241
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Generate the VideoObject for an article's video, if it has a valid one
|
|
269
|
+
*/
|
|
270
|
+
export function generateVideoStructuredData(
|
|
271
|
+
article: PublishedArticle,
|
|
272
|
+
): VideoObjectSchema | undefined {
|
|
273
|
+
const video = article.video;
|
|
274
|
+
if (!video || !isYouTubeVideoId(video.id)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const description = video.description?.trim() ?? "";
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"@type": "VideoObject",
|
|
282
|
+
name: video.title,
|
|
283
|
+
description: description.length > 0 ? description : article.excerpt,
|
|
284
|
+
thumbnailUrl: [
|
|
285
|
+
buildYouTubeThumbnailUrl(video.id),
|
|
286
|
+
...(article.featuredImage ? [article.featuredImage] : []),
|
|
287
|
+
],
|
|
288
|
+
uploadDate: video.uploadDate,
|
|
289
|
+
...(video.duration && { duration: video.duration }),
|
|
290
|
+
embedUrl: buildYouTubeStructuredDataEmbedUrl(video.id),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
242
294
|
/**
|
|
243
295
|
* Generate structured data for an author
|
|
244
296
|
*/
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for article videos (YouTube only).
|
|
3
|
+
*
|
|
4
|
+
* Every URL built here goes through `isYouTubeVideoId` first, so a stored id
|
|
5
|
+
* can never inject markup or a different host into an embed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const YOUTUBE_VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/;
|
|
9
|
+
const ISO_DURATION_PATTERN = /^PT(?=\d)(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?$/;
|
|
10
|
+
const ISO_DATE_PREFIX_PATTERN = /^\d{4}-\d{2}-\d{2}(?:$|T)/;
|
|
11
|
+
const YOUTUBE_HOSTS = new Set([
|
|
12
|
+
"youtube.com",
|
|
13
|
+
"www.youtube.com",
|
|
14
|
+
"m.youtube.com",
|
|
15
|
+
"music.youtube.com",
|
|
16
|
+
"youtube-nocookie.com",
|
|
17
|
+
"www.youtube-nocookie.com",
|
|
18
|
+
"youtu.be",
|
|
19
|
+
]);
|
|
20
|
+
const YOUTUBE_PATH_PREFIXES = ["embed", "shorts", "live", "v"];
|
|
21
|
+
|
|
22
|
+
export const ARTICLE_VIDEO_TITLE_MAX_LENGTH = 200;
|
|
23
|
+
export const ARTICLE_VIDEO_DESCRIPTION_MAX_LENGTH = 5000;
|
|
24
|
+
|
|
25
|
+
export function isYouTubeVideoId(value: unknown): value is string {
|
|
26
|
+
return typeof value === "string" && YOUTUBE_VIDEO_ID_PATTERN.test(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isIsoDuration(value: unknown): value is string {
|
|
30
|
+
return (
|
|
31
|
+
typeof value === "string" &&
|
|
32
|
+
value !== "PT" &&
|
|
33
|
+
ISO_DURATION_PATTERN.test(value)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** ISO 8601 date or date-time whose calendar date exists (no 31 February). */
|
|
38
|
+
function isIsoDate(value: unknown): value is string {
|
|
39
|
+
if (typeof value !== "string" || !ISO_DATE_PREFIX_PATTERN.test(value)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const datePart = value.slice(0, 10);
|
|
43
|
+
const parsed = new Date(`${datePart}T00:00:00Z`);
|
|
44
|
+
return (
|
|
45
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
46
|
+
parsed.toISOString().slice(0, 10) === datePart &&
|
|
47
|
+
!Number.isNaN(Date.parse(value))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract a YouTube video id from a bare id or a YouTube URL
|
|
53
|
+
* (watch, youtu.be, embed, nocookie embed, shorts, live).
|
|
54
|
+
*/
|
|
55
|
+
export function parseYouTubeVideoId(input: string): string | null {
|
|
56
|
+
const trimmed = input.trim();
|
|
57
|
+
if (YOUTUBE_VIDEO_ID_PATTERN.test(trimmed)) return trimmed;
|
|
58
|
+
if (!trimmed) return null;
|
|
59
|
+
|
|
60
|
+
let url: URL;
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(
|
|
63
|
+
/^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`,
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const host = url.hostname.toLowerCase();
|
|
70
|
+
if (!YOUTUBE_HOSTS.has(host)) return null;
|
|
71
|
+
|
|
72
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
73
|
+
let candidate: string | null | undefined;
|
|
74
|
+
if (host === "youtu.be") {
|
|
75
|
+
candidate = segments[0];
|
|
76
|
+
} else if (segments[0] === "watch") {
|
|
77
|
+
candidate = url.searchParams.get("v");
|
|
78
|
+
} else if (segments[0] && YOUTUBE_PATH_PREFIXES.includes(segments[0])) {
|
|
79
|
+
candidate = segments[1];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return isYouTubeVideoId(candidate) ? candidate : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate an untrusted `video` value from a publish payload.
|
|
87
|
+
* Returns a list of human-readable errors; empty when valid.
|
|
88
|
+
*/
|
|
89
|
+
export function validatePublishedArticleVideo(value: unknown): string[] {
|
|
90
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
+
return ["Article video must be an object"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const video = value as Record<string, unknown>;
|
|
95
|
+
const errors: string[] = [];
|
|
96
|
+
|
|
97
|
+
if (video.provider !== "youtube") {
|
|
98
|
+
errors.push('Article video provider must be "youtube"');
|
|
99
|
+
}
|
|
100
|
+
if (!isYouTubeVideoId(video.id)) {
|
|
101
|
+
errors.push("Article video id must be an 11-character YouTube id");
|
|
102
|
+
}
|
|
103
|
+
if (typeof video.title !== "string" || !video.title.trim()) {
|
|
104
|
+
errors.push("Article video title is required");
|
|
105
|
+
} else if (video.title.length > ARTICLE_VIDEO_TITLE_MAX_LENGTH) {
|
|
106
|
+
errors.push(
|
|
107
|
+
`Article video title must be ${ARTICLE_VIDEO_TITLE_MAX_LENGTH} characters or fewer`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (!isIsoDate(video.uploadDate)) {
|
|
111
|
+
errors.push("Article video uploadDate must be an ISO 8601 date");
|
|
112
|
+
}
|
|
113
|
+
if (video.duration !== undefined && !isIsoDuration(video.duration)) {
|
|
114
|
+
errors.push(
|
|
115
|
+
"Article video duration must be an ISO 8601 duration such as PT4M13S",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (video.verticalId !== undefined && !isYouTubeVideoId(video.verticalId)) {
|
|
119
|
+
errors.push("Article video verticalId must be an 11-character YouTube id");
|
|
120
|
+
}
|
|
121
|
+
if (video.description !== undefined) {
|
|
122
|
+
if (typeof video.description !== "string") {
|
|
123
|
+
errors.push("Article video description must be a string");
|
|
124
|
+
} else if (
|
|
125
|
+
video.description.length > ARTICLE_VIDEO_DESCRIPTION_MAX_LENGTH
|
|
126
|
+
) {
|
|
127
|
+
errors.push(
|
|
128
|
+
`Article video description must be ${ARTICLE_VIDEO_DESCRIPTION_MAX_LENGTH} characters or fewer`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertYouTubeVideoId(id: string): void {
|
|
137
|
+
if (!isYouTubeVideoId(id)) {
|
|
138
|
+
throw new Error("Invalid YouTube video id");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Privacy-enhanced player URL, loaded only after the reader presses play. */
|
|
143
|
+
export function buildYouTubeEmbedUrl(id: string): string {
|
|
144
|
+
assertYouTubeVideoId(id);
|
|
145
|
+
return `https://www.youtube-nocookie.com/embed/${id}?rel=0&autoplay=1`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Canonical player URL for structured data. */
|
|
149
|
+
export function buildYouTubeStructuredDataEmbedUrl(id: string): string {
|
|
150
|
+
assertYouTubeVideoId(id);
|
|
151
|
+
return `https://www.youtube.com/embed/${id}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function buildYouTubeWatchUrl(id: string): string {
|
|
155
|
+
assertYouTubeVideoId(id);
|
|
156
|
+
return `https://www.youtube.com/watch?v=${id}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Thumbnail URL for structured data only; the page never requests it. */
|
|
160
|
+
export function buildYouTubeThumbnailUrl(id: string): string {
|
|
161
|
+
assertYouTubeVideoId(id);
|
|
162
|
+
return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`;
|
|
163
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as PublishedArticle, b as PublishedSite, c as PublishedAuthor } from './index-
|
|
1
|
+
import { a as PublishedArticle, b as PublishedSite, c as PublishedAuthor } from './index-CmjKzrH-.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SEO utilities for canonical URL generation and validation.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as PublishedArticle, b as PublishedSite, c as PublishedAuthor } from './index-
|
|
1
|
+
import { a as PublishedArticle, b as PublishedSite, c as PublishedAuthor } from './index-CmjKzrH-.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SEO utilities for canonical URL generation and validation.
|
package/dist/sitemap.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import './index-
|
|
2
|
-
export { B as BlogSitemapConfig, N as NextjsSitemapEntry, S as SitemapChangeFreq, a as SitemapData, b as SitemapEntry, c as SitemapOptions, d as createBlogSitemapRoute, e as createDynamicBlogSitemap, g as generateBlogSitemapEntries, i as generateSitemap, j as generateSitemapIndex, s as sitemapToXml, v as validateSitemap } from './sitemap-
|
|
1
|
+
import './index-CmjKzrH-.mjs';
|
|
2
|
+
export { B as BlogSitemapConfig, N as NextjsSitemapEntry, S as SitemapChangeFreq, a as SitemapData, b as SitemapEntry, c as SitemapOptions, d as createBlogSitemapRoute, e as createDynamicBlogSitemap, g as generateBlogSitemapEntries, i as generateSitemap, j as generateSitemapIndex, s as sitemapToXml, v as validateSitemap } from './sitemap-D1eInRA1.mjs';
|
package/dist/sitemap.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import './index-
|
|
2
|
-
export { B as BlogSitemapConfig, N as NextjsSitemapEntry, S as SitemapChangeFreq, a as SitemapData, b as SitemapEntry, c as SitemapOptions, d as createBlogSitemapRoute, e as createDynamicBlogSitemap, g as generateBlogSitemapEntries, i as generateSitemap, j as generateSitemapIndex, s as sitemapToXml, v as validateSitemap } from './sitemap-
|
|
1
|
+
import './index-CmjKzrH-.js';
|
|
2
|
+
export { B as BlogSitemapConfig, N as NextjsSitemapEntry, S as SitemapChangeFreq, a as SitemapData, b as SitemapEntry, c as SitemapOptions, d as createBlogSitemapRoute, e as createDynamicBlogSitemap, g as generateBlogSitemapEntries, i as generateSitemap, j as generateSitemapIndex, s as sitemapToXml, v as validateSitemap } from './sitemap-CEjqxRaQ.js';
|
package/dist/types/contracts.ts
CHANGED
|
@@ -221,6 +221,38 @@ export interface PublishedSeriesContext {
|
|
|
221
221
|
readonly articles: readonly PublishedSeriesArticleLink[];
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Optional video shown at the top of an article.
|
|
226
|
+
*
|
|
227
|
+
* Only YouTube is supported. The page renders a click-to-play preview and
|
|
228
|
+
* loads the privacy-enhanced player only after the reader presses play.
|
|
229
|
+
*/
|
|
230
|
+
export interface PublishedArticleVideo {
|
|
231
|
+
/** Video host. Only "youtube" is supported. */
|
|
232
|
+
readonly provider: "youtube";
|
|
233
|
+
|
|
234
|
+
/** 11-character YouTube video id of the landscape video */
|
|
235
|
+
readonly id: string;
|
|
236
|
+
|
|
237
|
+
/** Video title, as on YouTube (used for the player label and VideoObject) */
|
|
238
|
+
readonly title: string;
|
|
239
|
+
|
|
240
|
+
/** Optional video description (VideoObject falls back to the article excerpt) */
|
|
241
|
+
readonly description?: string;
|
|
242
|
+
|
|
243
|
+
/** Upload date as an ISO 8601 date or date-time */
|
|
244
|
+
readonly uploadDate: string;
|
|
245
|
+
|
|
246
|
+
/** Optional ISO 8601 duration, e.g. "PT4M13S" */
|
|
247
|
+
readonly duration?: string;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reserved: YouTube id of a vertical (Shorts) cut of the same video.
|
|
251
|
+
* Validated and stored, but not rendered yet.
|
|
252
|
+
*/
|
|
253
|
+
readonly verticalId?: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
224
256
|
/**
|
|
225
257
|
* Represents a published article with all necessary metadata for rendering
|
|
226
258
|
* on external blog platforms.
|
|
@@ -268,6 +300,9 @@ export interface PublishedArticle {
|
|
|
268
300
|
/** Optional ordered series context for article navigation */
|
|
269
301
|
readonly series?: PublishedSeriesContext;
|
|
270
302
|
|
|
303
|
+
/** Optional video shown in place of the featured image at the top */
|
|
304
|
+
readonly video?: PublishedArticleVideo;
|
|
305
|
+
|
|
271
306
|
/** Featured image URL (optional) */
|
|
272
307
|
readonly featuredImage?: string;
|
|
273
308
|
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { h as PublishPayload, f as PublishResult, a as PublishedArticle, c as PublishedAuthor,
|
|
1
|
+
export { h as PublishPayload, f as PublishResult, a as PublishedArticle, i as PublishedArticleVideo, c as PublishedAuthor, j as PublishedAuthorSocialLinks, d as PublishedNewsletterSettings, b as PublishedSite, P as PublishedTopic } from '../index-CmjKzrH-.mjs';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { h as PublishPayload, f as PublishResult, a as PublishedArticle, c as PublishedAuthor,
|
|
1
|
+
export { h as PublishPayload, f as PublishResult, a as PublishedArticle, i as PublishedArticleVideo, c as PublishedAuthor, j as PublishedAuthorSocialLinks, d as PublishedNewsletterSettings, b as PublishedSite, P as PublishedTopic } from '../index-CmjKzrH-.js';
|
package/dist/types/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xexr/blog-platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "SEO-friendly blog platform for Next.js 15 with Bloggy integration",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Xexr/bloggy.git",
|
|
8
|
+
"directory": "packages/blog-platform"
|
|
9
|
+
},
|
|
5
10
|
"main": "./dist/index.js",
|
|
6
11
|
"types": "./dist/index.d.ts",
|
|
7
12
|
"exports": {
|