@xexr/blog-platform 1.1.5 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +21 -0
  3. package/dist/app/api/blog/publish/route.test.ts +55 -0
  4. package/dist/app/api/blog/publish/route.ts +7 -2
  5. package/dist/app/blog/[slug]/page.tsx +41 -28
  6. package/dist/components/ArchiveStructuredData.tsx +61 -0
  7. package/dist/components/ArticleVideo.tsx +136 -0
  8. package/dist/components/SeriesArchivePages.tsx +10 -0
  9. package/dist/components/TopicArchivePages.test.ts +52 -4
  10. package/dist/components/TopicArchivePages.tsx +14 -4
  11. package/dist/{index-CKWrb9a4.d.mts → index-CmjKzrH-.d.mts} +30 -1
  12. package/dist/{index-CKWrb9a4.d.ts → index-CmjKzrH-.d.ts} +30 -1
  13. package/dist/index.d.mts +52 -5
  14. package/dist/index.d.ts +52 -5
  15. package/dist/index.js +14 -14
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +14 -14
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/lib/related-articles.d.mts +3 -2
  20. package/dist/lib/related-articles.d.ts +3 -2
  21. package/dist/lib/sitemap.ts +2 -42
  22. package/dist/lib/storage/drizzle.ts +9 -0
  23. package/dist/lib/structured-data.ts +62 -11
  24. package/dist/lib/video.ts +163 -0
  25. package/dist/{sitemap-BDT3d496.d.ts → sitemap-CEjqxRaQ.d.ts} +5 -4
  26. package/dist/{sitemap-D3rS0iAF.d.mts → sitemap-D1eInRA1.d.mts} +5 -4
  27. package/dist/sitemap.d.mts +2 -2
  28. package/dist/sitemap.d.ts +2 -2
  29. package/dist/sitemap.js +4 -4
  30. package/dist/sitemap.js.map +1 -1
  31. package/dist/sitemap.mjs +4 -4
  32. package/dist/sitemap.mjs.map +1 -1
  33. package/dist/types/contracts.ts +38 -0
  34. package/dist/types/index.d.mts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.ts +1 -0
  37. package/package.json +17 -13
@@ -44,6 +44,8 @@ interface PublishedAuthor {
44
44
  readonly pronouns?: string;
45
45
  /** Whether this is a guest author */
46
46
  readonly isGuest: boolean;
47
+ /** Areas of subject expertise. */
48
+ readonly expertise?: readonly string[];
47
49
  /** Tone adjectives that describe this author's writing style */
48
50
  readonly toneAdjectives: readonly string[];
49
51
  }
@@ -162,6 +164,31 @@ interface PublishedSeriesContext {
162
164
  /** Ordered article list for compact navigation and archive pages */
163
165
  readonly articles: readonly PublishedSeriesArticleLink[];
164
166
  }
167
+ /**
168
+ * Optional video shown at the top of an article.
169
+ *
170
+ * Only YouTube is supported. The page renders a click-to-play preview and
171
+ * loads the privacy-enhanced player only after the reader presses play.
172
+ */
173
+ interface PublishedArticleVideo {
174
+ /** Video host. Only "youtube" is supported. */
175
+ readonly provider: "youtube";
176
+ /** 11-character YouTube video id of the landscape video */
177
+ readonly id: string;
178
+ /** Video title, as on YouTube (used for the player label and VideoObject) */
179
+ readonly title: string;
180
+ /** Optional video description (VideoObject falls back to the article excerpt) */
181
+ readonly description?: string;
182
+ /** Upload date as an ISO 8601 date or date-time */
183
+ readonly uploadDate: string;
184
+ /** Optional ISO 8601 duration, e.g. "PT4M13S" */
185
+ readonly duration?: string;
186
+ /**
187
+ * Reserved: YouTube id of a vertical (Shorts) cut of the same video.
188
+ * Validated and stored, but not rendered yet.
189
+ */
190
+ readonly verticalId?: string;
191
+ }
165
192
  /**
166
193
  * Represents a published article with all necessary metadata for rendering
167
194
  * on external blog platforms.
@@ -195,6 +222,8 @@ interface PublishedArticle {
195
222
  readonly topics?: readonly PublishedTopic[];
196
223
  /** Optional ordered series context for article navigation */
197
224
  readonly series?: PublishedSeriesContext;
225
+ /** Optional video shown in place of the featured image at the top */
226
+ readonly video?: PublishedArticleVideo;
198
227
  /** Featured image URL (optional) */
199
228
  readonly featuredImage?: string;
200
229
  /** Alt text for featured image */
@@ -268,4 +297,4 @@ interface PublishResult {
268
297
  readonly idempotencyKey: string;
269
298
  }
270
299
 
271
- export type { PublishedTopic as P, PublishedArticle as a, PublishedSite as b, PublishedAuthor as c, PublishedNewsletterSettings as d, PublishedArticleCta as e, PublishResult as f, PublishedSeriesContext as g, PublishPayload as h, PublishedAuthorSocialLinks as i };
300
+ export type { PublishedTopic as P, PublishedArticle as a, PublishedSite as b, PublishedAuthor as c, PublishedNewsletterSettings as d, PublishedArticleCta as e, PublishResult as f, PublishedSeriesContext as g, PublishPayload as h, PublishedArticleVideo as i, PublishedAuthorSocialLinks as j };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as PublishedArticle, b as PublishedSite, d as PublishedNewsletterSettings, e as PublishedArticleCta, c as PublishedAuthor, f as PublishResult, P as PublishedTopic, g as PublishedSeriesContext } from './index-CKWrb9a4.mjs';
2
- export { h as PublishPayload, i as PublishedAuthorSocialLinks } from './index-CKWrb9a4.mjs';
1
+ import { a as PublishedArticle, b as PublishedSite, d as PublishedNewsletterSettings, e as PublishedArticleCta, c as PublishedAuthor, f as PublishResult, P as PublishedTopic, g as PublishedSeriesContext } from './index-CmjKzrH-.mjs';
2
+ export { h as PublishPayload, i as PublishedArticleVideo, j as PublishedAuthorSocialLinks } from './index-CmjKzrH-.mjs';
3
3
  import * as React from 'react';
4
4
  import { CSSProperties, ReactElement } from 'react';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
@@ -10,8 +10,8 @@ export { RelatedArticleCandidate, RelatedFeaturedArticlesOptions, getRelatedFeat
10
10
  import { NextRequest, NextResponse } from 'next/server.js';
11
11
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
12
12
  import { LibSQLDatabase } from 'drizzle-orm/libsql';
13
- import { C as CanonicalUrlOptions } from './sitemap-D3rS0iAF.mjs';
14
- 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, f as generateCanonicalUrl, h as generateCanonicalUrlFromSlug, i as generateSitemap, j as generateSitemapIndex, k as getBaseUrl, l as isValidCanonicalUrl, n as normalizeDomain, m as normalizeSlug, s as sitemapToXml, u as updateArticleCanonicalUrl, v as validateSitemap } from './sitemap-D3rS0iAF.mjs';
13
+ import { C as CanonicalUrlOptions } from './sitemap-D1eInRA1.mjs';
14
+ 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, f as generateCanonicalUrl, h as generateCanonicalUrlFromSlug, i as generateSitemap, j as generateSitemapIndex, k as getBaseUrl, l as isValidCanonicalUrl, n as normalizeDomain, m as normalizeSlug, s as sitemapToXml, u as updateArticleCanonicalUrl, v as validateSitemap } from './sitemap-D1eInRA1.mjs';
15
15
 
16
16
  interface BlogTheme {
17
17
  accentColor?: string;
@@ -452,6 +452,34 @@ declare function getAllSeries(articles: PublishedArticle[]): PublishedSeriesCont
452
452
  declare function getSeriesArchive(articles: PublishedArticle[], seriesSlug: string): SeriesArchive | null;
453
453
  declare function getArticleSeriesNavigation(article: PublishedArticle): PublishedSeriesContext | null;
454
454
 
455
+ /**
456
+ * Helpers for article videos (YouTube only).
457
+ *
458
+ * Every URL built here goes through `isYouTubeVideoId` first, so a stored id
459
+ * can never inject markup or a different host into an embed.
460
+ */
461
+ declare const ARTICLE_VIDEO_TITLE_MAX_LENGTH = 200;
462
+ declare const ARTICLE_VIDEO_DESCRIPTION_MAX_LENGTH = 5000;
463
+ declare function isYouTubeVideoId(value: unknown): value is string;
464
+ declare function isIsoDuration(value: unknown): value is string;
465
+ /**
466
+ * Extract a YouTube video id from a bare id or a YouTube URL
467
+ * (watch, youtu.be, embed, nocookie embed, shorts, live).
468
+ */
469
+ declare function parseYouTubeVideoId(input: string): string | null;
470
+ /**
471
+ * Validate an untrusted `video` value from a publish payload.
472
+ * Returns a list of human-readable errors; empty when valid.
473
+ */
474
+ declare function validatePublishedArticleVideo(value: unknown): string[];
475
+ /** Privacy-enhanced player URL, loaded only after the reader presses play. */
476
+ declare function buildYouTubeEmbedUrl(id: string): string;
477
+ /** Canonical player URL for structured data. */
478
+ declare function buildYouTubeStructuredDataEmbedUrl(id: string): string;
479
+ declare function buildYouTubeWatchUrl(id: string): string;
480
+ /** Thumbnail URL for structured data only; the page never requests it. */
481
+ declare function buildYouTubeThumbnailUrl(id: string): string;
482
+
455
483
  interface BlogTopicsIndexPageProps {
456
484
  articles: PublishedArticle[];
457
485
  basePath?: string;
@@ -2048,6 +2076,20 @@ interface BlogPostingSchema extends BaseStructuredData {
2048
2076
  "@type": "Thing";
2049
2077
  name: string;
2050
2078
  }[];
2079
+ video?: VideoObjectSchema;
2080
+ }
2081
+ /**
2082
+ * VideoObject schema for an article's embedded video
2083
+ * @see https://schema.org/VideoObject
2084
+ */
2085
+ interface VideoObjectSchema {
2086
+ "@type": "VideoObject";
2087
+ name: string;
2088
+ description: string;
2089
+ thumbnailUrl: string[];
2090
+ uploadDate: string;
2091
+ duration?: string;
2092
+ embedUrl: string;
2051
2093
  }
2052
2094
  /**
2053
2095
  * WebSite schema for the blog home page
@@ -2103,6 +2145,10 @@ interface StructuredDataOptions {
2103
2145
  * Generate comprehensive structured data for a blog article
2104
2146
  */
2105
2147
  declare function generateArticleStructuredData(article: PublishedArticle, options?: StructuredDataOptions): BlogPostingSchema;
2148
+ /**
2149
+ * Generate the VideoObject for an article's video, if it has a valid one
2150
+ */
2151
+ declare function generateVideoStructuredData(article: PublishedArticle): VideoObjectSchema | undefined;
2106
2152
  /**
2107
2153
  * Generate structured data for an author
2108
2154
  */
@@ -2283,4 +2329,5 @@ declare function OPTIONS(request: NextRequest): Promise<NextResponse>;
2283
2329
 
2284
2330
  declare const PACKAGE_VERSION: string;
2285
2331
 
2286
- export { ApiKeyAuth, type AuthConfig, type AuthResult, type AuthorArchive, type AuthorArchiveOptions, BLOG_PLATFORM_SCHEMA_VERSION, BLOG_PLATFORM_TABLE_NAMES, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, type BlogPlatformConfig, type BlogPlatformSchemaCheckResult, BlogPlatformSchemaError, type BlogPlatformStorage, type BlogPlatformTableName, type BlogPostingSchema, BlogSeriesArchivePage, BlogSeriesArchiveRoutePage, BlogSeriesIndexPage, BlogSeriesPage, type BlogTheme, BlogTopicArchivePage, BlogTopicsIndexPage, type BreadcrumbListSchema, Button, CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, type GeneratedMetadata, InMemoryStorage, type LinkTag, type MetaTag, type MetadataOptions, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, type NewsletterSignupProps, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, type RateLimitConfig, type RateLimitResult, type SeriesArchive, SeriesNavigation, Pagination as ShadcnPagination, StructuredDataError, type StructuredDataOptions, type TopicArchive, UninitializedAuth, type WebSiteSchema, autoConfigureRateLimit, blogPlatformDrizzleSchema, bpArticles, bpMetadata, bpOperations, buildAuthorPath, buildBlogPath, buildSeriesPath, buttonVariants, checkDrizzleAdapterSchema, checkRateLimit, cn, POST as configHandler, OPTIONS$1 as configOptionsHandler, configureRateLimit, createRateLimitErrorResponse, createStructuredDataScript, createTokenFingerprint, enforceRateLimit, ensureDrizzleAdapterSchema, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBreadcrumbStructuredData, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSeriesArchiveMetadata, generateSeriesIndexMetadata, generateSiteMetadata, generateWebSiteStructuredData, getAllSeries, getAllTopics, getArticlePrimaryTopic, getArticleSeriesNavigation, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getSeriesArchive, getStorage, getTopicArchive, GET$1 as indexNowKeyRoute, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, POST$2 as publishHandler, safeGenerateArticleStructuredData, setAuthConfig, setStorage, shouldSkipAuth, GET as slugExistsHandler, OPTIONS as slugExistsOptionsHandler, structuredDataToJsonLd, POST$1 as unpublishHandler, usePagination, validateMetadata, validateStructuredData, GET$2 as versionHandler };
2332
+ export { ARTICLE_VIDEO_DESCRIPTION_MAX_LENGTH, ARTICLE_VIDEO_TITLE_MAX_LENGTH, ApiKeyAuth, BLOG_PLATFORM_SCHEMA_VERSION, BLOG_PLATFORM_TABLE_NAMES, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, BlogPlatformSchemaError, BlogSeriesArchivePage, BlogSeriesArchiveRoutePage, BlogSeriesIndexPage, BlogSeriesPage, BlogTopicArchivePage, BlogTopicsIndexPage, Button, CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, InMemoryStorage, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, NoAuth, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, SeriesNavigation, Pagination as ShadcnPagination, StructuredDataError, UninitializedAuth, autoConfigureRateLimit, blogPlatformDrizzleSchema, bpArticles, bpMetadata, bpOperations, buildAuthorPath, buildBlogPath, buildSeriesPath, buildYouTubeEmbedUrl, buildYouTubeStructuredDataEmbedUrl, buildYouTubeThumbnailUrl, buildYouTubeWatchUrl, buttonVariants, checkDrizzleAdapterSchema, checkRateLimit, cn, POST as configHandler, OPTIONS$1 as configOptionsHandler, configureRateLimit, createRateLimitErrorResponse, createStructuredDataScript, createTokenFingerprint, enforceRateLimit, ensureDrizzleAdapterSchema, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBreadcrumbStructuredData, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSeriesArchiveMetadata, generateSeriesIndexMetadata, generateSiteMetadata, generateVideoStructuredData, generateWebSiteStructuredData, getAllSeries, getAllTopics, getArticlePrimaryTopic, getArticleSeriesNavigation, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getSeriesArchive, getStorage, getTopicArchive, GET$1 as indexNowKeyRoute, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, isIsoDuration, isYouTubeVideoId, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, parseYouTubeVideoId, POST$2 as publishHandler, safeGenerateArticleStructuredData, setAuthConfig, setStorage, shouldSkipAuth, GET as slugExistsHandler, OPTIONS as slugExistsOptionsHandler, structuredDataToJsonLd, POST$1 as unpublishHandler, usePagination, validateMetadata, validatePublishedArticleVideo, validateStructuredData, GET$2 as versionHandler };
2333
+ export type { AuthConfig, AuthResult, AuthorArchive, AuthorArchiveOptions, BlogPlatformConfig, BlogPlatformSchemaCheckResult, BlogPlatformStorage, BlogPlatformTableName, BlogPostingSchema, BlogTheme, BreadcrumbListSchema, GeneratedMetadata, LinkTag, MetaTag, MetadataOptions, NewsletterSignupProps, OrganizationSchema, PersonSchema, RateLimitConfig, RateLimitResult, SeriesArchive, StructuredDataOptions, TopicArchive, VideoObjectSchema, WebSiteSchema };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as PublishedArticle, b as PublishedSite, d as PublishedNewsletterSettings, e as PublishedArticleCta, c as PublishedAuthor, f as PublishResult, P as PublishedTopic, g as PublishedSeriesContext } from './index-CKWrb9a4.js';
2
- export { h as PublishPayload, i as PublishedAuthorSocialLinks } from './index-CKWrb9a4.js';
1
+ import { a as PublishedArticle, b as PublishedSite, d as PublishedNewsletterSettings, e as PublishedArticleCta, c as PublishedAuthor, f as PublishResult, P as PublishedTopic, g as PublishedSeriesContext } from './index-CmjKzrH-.js';
2
+ export { h as PublishPayload, i as PublishedArticleVideo, j as PublishedAuthorSocialLinks } from './index-CmjKzrH-.js';
3
3
  import * as React from 'react';
4
4
  import { CSSProperties, ReactElement } from 'react';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
@@ -10,8 +10,8 @@ export { RelatedArticleCandidate, RelatedFeaturedArticlesOptions, getRelatedFeat
10
10
  import { NextRequest, NextResponse } from 'next/server.js';
11
11
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
12
12
  import { LibSQLDatabase } from 'drizzle-orm/libsql';
13
- import { C as CanonicalUrlOptions } from './sitemap-BDT3d496.js';
14
- 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, f as generateCanonicalUrl, h as generateCanonicalUrlFromSlug, i as generateSitemap, j as generateSitemapIndex, k as getBaseUrl, l as isValidCanonicalUrl, n as normalizeDomain, m as normalizeSlug, s as sitemapToXml, u as updateArticleCanonicalUrl, v as validateSitemap } from './sitemap-BDT3d496.js';
13
+ import { C as CanonicalUrlOptions } from './sitemap-CEjqxRaQ.js';
14
+ 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, f as generateCanonicalUrl, h as generateCanonicalUrlFromSlug, i as generateSitemap, j as generateSitemapIndex, k as getBaseUrl, l as isValidCanonicalUrl, n as normalizeDomain, m as normalizeSlug, s as sitemapToXml, u as updateArticleCanonicalUrl, v as validateSitemap } from './sitemap-CEjqxRaQ.js';
15
15
 
16
16
  interface BlogTheme {
17
17
  accentColor?: string;
@@ -452,6 +452,34 @@ declare function getAllSeries(articles: PublishedArticle[]): PublishedSeriesCont
452
452
  declare function getSeriesArchive(articles: PublishedArticle[], seriesSlug: string): SeriesArchive | null;
453
453
  declare function getArticleSeriesNavigation(article: PublishedArticle): PublishedSeriesContext | null;
454
454
 
455
+ /**
456
+ * Helpers for article videos (YouTube only).
457
+ *
458
+ * Every URL built here goes through `isYouTubeVideoId` first, so a stored id
459
+ * can never inject markup or a different host into an embed.
460
+ */
461
+ declare const ARTICLE_VIDEO_TITLE_MAX_LENGTH = 200;
462
+ declare const ARTICLE_VIDEO_DESCRIPTION_MAX_LENGTH = 5000;
463
+ declare function isYouTubeVideoId(value: unknown): value is string;
464
+ declare function isIsoDuration(value: unknown): value is string;
465
+ /**
466
+ * Extract a YouTube video id from a bare id or a YouTube URL
467
+ * (watch, youtu.be, embed, nocookie embed, shorts, live).
468
+ */
469
+ declare function parseYouTubeVideoId(input: string): string | null;
470
+ /**
471
+ * Validate an untrusted `video` value from a publish payload.
472
+ * Returns a list of human-readable errors; empty when valid.
473
+ */
474
+ declare function validatePublishedArticleVideo(value: unknown): string[];
475
+ /** Privacy-enhanced player URL, loaded only after the reader presses play. */
476
+ declare function buildYouTubeEmbedUrl(id: string): string;
477
+ /** Canonical player URL for structured data. */
478
+ declare function buildYouTubeStructuredDataEmbedUrl(id: string): string;
479
+ declare function buildYouTubeWatchUrl(id: string): string;
480
+ /** Thumbnail URL for structured data only; the page never requests it. */
481
+ declare function buildYouTubeThumbnailUrl(id: string): string;
482
+
455
483
  interface BlogTopicsIndexPageProps {
456
484
  articles: PublishedArticle[];
457
485
  basePath?: string;
@@ -2048,6 +2076,20 @@ interface BlogPostingSchema extends BaseStructuredData {
2048
2076
  "@type": "Thing";
2049
2077
  name: string;
2050
2078
  }[];
2079
+ video?: VideoObjectSchema;
2080
+ }
2081
+ /**
2082
+ * VideoObject schema for an article's embedded video
2083
+ * @see https://schema.org/VideoObject
2084
+ */
2085
+ interface VideoObjectSchema {
2086
+ "@type": "VideoObject";
2087
+ name: string;
2088
+ description: string;
2089
+ thumbnailUrl: string[];
2090
+ uploadDate: string;
2091
+ duration?: string;
2092
+ embedUrl: string;
2051
2093
  }
2052
2094
  /**
2053
2095
  * WebSite schema for the blog home page
@@ -2103,6 +2145,10 @@ interface StructuredDataOptions {
2103
2145
  * Generate comprehensive structured data for a blog article
2104
2146
  */
2105
2147
  declare function generateArticleStructuredData(article: PublishedArticle, options?: StructuredDataOptions): BlogPostingSchema;
2148
+ /**
2149
+ * Generate the VideoObject for an article's video, if it has a valid one
2150
+ */
2151
+ declare function generateVideoStructuredData(article: PublishedArticle): VideoObjectSchema | undefined;
2106
2152
  /**
2107
2153
  * Generate structured data for an author
2108
2154
  */
@@ -2283,4 +2329,5 @@ declare function OPTIONS(request: NextRequest): Promise<NextResponse>;
2283
2329
 
2284
2330
  declare const PACKAGE_VERSION: string;
2285
2331
 
2286
- export { ApiKeyAuth, type AuthConfig, type AuthResult, type AuthorArchive, type AuthorArchiveOptions, BLOG_PLATFORM_SCHEMA_VERSION, BLOG_PLATFORM_TABLE_NAMES, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, type BlogPlatformConfig, type BlogPlatformSchemaCheckResult, BlogPlatformSchemaError, type BlogPlatformStorage, type BlogPlatformTableName, type BlogPostingSchema, BlogSeriesArchivePage, BlogSeriesArchiveRoutePage, BlogSeriesIndexPage, BlogSeriesPage, type BlogTheme, BlogTopicArchivePage, BlogTopicsIndexPage, type BreadcrumbListSchema, Button, CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, type GeneratedMetadata, InMemoryStorage, type LinkTag, type MetaTag, type MetadataOptions, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, type NewsletterSignupProps, NoAuth, type OrganizationSchema, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, type PersonSchema, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, type RateLimitConfig, type RateLimitResult, type SeriesArchive, SeriesNavigation, Pagination as ShadcnPagination, StructuredDataError, type StructuredDataOptions, type TopicArchive, UninitializedAuth, type WebSiteSchema, autoConfigureRateLimit, blogPlatformDrizzleSchema, bpArticles, bpMetadata, bpOperations, buildAuthorPath, buildBlogPath, buildSeriesPath, buttonVariants, checkDrizzleAdapterSchema, checkRateLimit, cn, POST as configHandler, OPTIONS$1 as configOptionsHandler, configureRateLimit, createRateLimitErrorResponse, createStructuredDataScript, createTokenFingerprint, enforceRateLimit, ensureDrizzleAdapterSchema, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBreadcrumbStructuredData, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSeriesArchiveMetadata, generateSeriesIndexMetadata, generateSiteMetadata, generateWebSiteStructuredData, getAllSeries, getAllTopics, getArticlePrimaryTopic, getArticleSeriesNavigation, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getSeriesArchive, getStorage, getTopicArchive, GET$1 as indexNowKeyRoute, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, POST$2 as publishHandler, safeGenerateArticleStructuredData, setAuthConfig, setStorage, shouldSkipAuth, GET as slugExistsHandler, OPTIONS as slugExistsOptionsHandler, structuredDataToJsonLd, POST$1 as unpublishHandler, usePagination, validateMetadata, validateStructuredData, GET$2 as versionHandler };
2332
+ export { ARTICLE_VIDEO_DESCRIPTION_MAX_LENGTH, ARTICLE_VIDEO_TITLE_MAX_LENGTH, ApiKeyAuth, BLOG_PLATFORM_SCHEMA_VERSION, BLOG_PLATFORM_TABLE_NAMES, ArticlePage as BlogArticlePage, AuthorPage as BlogAuthorPage, BlogAuthorsIndexPage, BlogHomePage, BlogPlatformSchemaError, BlogSeriesArchivePage, BlogSeriesArchiveRoutePage, BlogSeriesIndexPage, BlogSeriesPage, BlogTopicArchivePage, BlogTopicsIndexPage, Button, CanonicalUrlOptions, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DrizzleStorage, EnvAuth, InMemoryStorage, NEWSLETTER_SUCCESS_MESSAGE, NewsletterSignup, NoAuth, PACKAGE_VERSION, Pagination$1 as Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PublishResult, PublishedArticle, PublishedAuthor, PublishedNewsletterSettings, PublishedSite, PublishedTopic, SeriesNavigation, Pagination as ShadcnPagination, StructuredDataError, UninitializedAuth, autoConfigureRateLimit, blogPlatformDrizzleSchema, bpArticles, bpMetadata, bpOperations, buildAuthorPath, buildBlogPath, buildSeriesPath, buildYouTubeEmbedUrl, buildYouTubeStructuredDataEmbedUrl, buildYouTubeThumbnailUrl, buildYouTubeWatchUrl, buttonVariants, checkDrizzleAdapterSchema, checkRateLimit, cn, POST as configHandler, OPTIONS$1 as configOptionsHandler, configureRateLimit, createRateLimitErrorResponse, createStructuredDataScript, createTokenFingerprint, enforceRateLimit, ensureDrizzleAdapterSchema, extractKeywordsFromArticle, extractTableOfContents, extractTokenFromHeader, formatArticleDateMeta, generateArticleMetadata, generateArticleStructuredData, generateAuthorPageMetadata, generateAuthorStructuredData, generateBlogHomeMetadata, generateBlogHomeStructuredData, generateBreadcrumbStructuredData, generateCompleteArticleStructuredData, generateOrganizationStructuredData, generatePaginationMetadata, generateSeriesArchiveMetadata, generateSeriesIndexMetadata, generateSiteMetadata, generateVideoStructuredData, generateWebSiteStructuredData, getAllSeries, getAllTopics, getArticlePrimaryTopic, getArticleSeriesNavigation, getAuthConfig, getAuthorArchive, getAuthorsFromArticles, getBlogThemeStyle, getDrizzleAdapterSql, getPathPrefixFromBlogUrl, getRateLimitHeaders, getSeriesArchive, getStorage, getTopicArchive, GET$1 as indexNowKeyRoute, initBlogPlatform, initBlogPlatformDev, initBlogPlatformOnce, initBlogPlatformProd, initEnvAuth, isIsoDuration, isYouTubeVideoId, loadAuthorArchive, metadataToHtml, normalizeBlogBasePath, parseYouTubeVideoId, POST$2 as publishHandler, safeGenerateArticleStructuredData, setAuthConfig, setStorage, shouldSkipAuth, GET as slugExistsHandler, OPTIONS as slugExistsOptionsHandler, structuredDataToJsonLd, POST$1 as unpublishHandler, usePagination, validateMetadata, validatePublishedArticleVideo, validateStructuredData, GET$2 as versionHandler };
2333
+ export type { AuthConfig, AuthResult, AuthorArchive, AuthorArchiveOptions, BlogPlatformConfig, BlogPlatformSchemaCheckResult, BlogPlatformStorage, BlogPlatformTableName, BlogPostingSchema, BlogTheme, BreadcrumbListSchema, GeneratedMetadata, LinkTag, MetaTag, MetadataOptions, NewsletterSignupProps, OrganizationSchema, PersonSchema, RateLimitConfig, RateLimitResult, SeriesArchive, StructuredDataOptions, TopicArchive, VideoObjectSchema, WebSiteSchema };