@next-md-blog/core 1.0.2 → 1.0.4
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/LICENSE +22 -0
- package/README.md +44 -207
- package/dist/components/BlogPostSEO.d.ts +3 -1
- package/dist/components/BlogPostSEO.d.ts.map +1 -1
- package/dist/components/BlogPostSEO.js +6 -4
- package/dist/components/MarkdownContent.d.ts +5 -6
- package/dist/components/MarkdownContent.d.ts.map +1 -1
- package/dist/components/MarkdownContent.js +8 -4
- package/dist/components/markdown/img.d.ts.map +1 -1
- package/dist/components/markdown/img.js +4 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/organization-schema.d.ts +22 -0
- package/dist/core/organization-schema.d.ts.map +1 -0
- package/dist/core/organization-schema.js +83 -0
- package/dist/core/seo-feeds.d.ts +2 -12
- package/dist/core/seo-feeds.d.ts.map +1 -1
- package/dist/core/seo-feeds.js +4 -34
- package/dist/core/seo-metadata.d.ts.map +1 -1
- package/dist/core/seo-metadata.js +16 -12
- package/dist/core/seo-schema.d.ts +13 -1
- package/dist/core/seo-schema.d.ts.map +1 -1
- package/dist/core/seo-schema.js +42 -13
- package/dist/core/seo-utils.d.ts +19 -7
- package/dist/core/seo-utils.d.ts.map +1 -1
- package/dist/core/seo-utils.js +65 -7
- package/dist/core/seo.d.ts +8 -4
- package/dist/core/seo.d.ts.map +1 -1
- package/dist/core/seo.js +6 -4
- package/dist/core/sitemap-data.d.ts +18 -0
- package/dist/core/sitemap-data.d.ts.map +1 -0
- package/dist/core/sitemap-data.js +42 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/utils.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/next.d.ts +19 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +44 -0
- package/package.json +33 -28
package/dist/core/seo-feeds.js
CHANGED
|
@@ -1,52 +1,22 @@
|
|
|
1
1
|
import { getConfig } from './config.js';
|
|
2
2
|
import { resolveFrontmatterField } from './type-guards.js';
|
|
3
3
|
import { DEFAULT_SITE_NAME, RSS_POST_LIMIT } from './constants.js';
|
|
4
|
-
import {
|
|
4
|
+
import { resolvePostUrlWithConfig, escapeXml, getAuthorName, } from './seo-utils.js';
|
|
5
5
|
/**
|
|
6
|
-
* Generates
|
|
7
|
-
* @param posts - Array of blog post metadata
|
|
8
|
-
* @param config - SEO configuration
|
|
9
|
-
* @returns Sitemap XML string
|
|
10
|
-
*/
|
|
11
|
-
export function generateSitemap(posts, config) {
|
|
12
|
-
const blogConfig = config || getConfig();
|
|
13
|
-
const { siteUrl = '' } = blogConfig;
|
|
14
|
-
const urls = posts
|
|
15
|
-
.map((post) => {
|
|
16
|
-
const lastmod = resolveFrontmatterField(['modifiedDate', 'date'], post.frontmatter) || new Date().toISOString().split('T')[0];
|
|
17
|
-
const url = `${siteUrl}/blog/${post.slug}`;
|
|
18
|
-
return ` <url>
|
|
19
|
-
<loc>${escapeXml(url)}</loc>
|
|
20
|
-
<lastmod>${lastmod}</lastmod>
|
|
21
|
-
<changefreq>monthly</changefreq>
|
|
22
|
-
<priority>0.8</priority>
|
|
23
|
-
</url>`;
|
|
24
|
-
})
|
|
25
|
-
.join('\n');
|
|
26
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
27
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
28
|
-
${urls}
|
|
29
|
-
</urlset>`;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Generates RSS feed XML for blog posts
|
|
33
|
-
* @param posts - Array of blog posts
|
|
34
|
-
* @param config - SEO configuration
|
|
35
|
-
* @returns RSS XML string
|
|
6
|
+
* Generates RSS feed XML for blog posts (used by `createRssFeedResponse` in `@next-md-blog/core/next`).
|
|
36
7
|
*/
|
|
37
8
|
export function generateRSSFeed(posts, config) {
|
|
38
9
|
const blogConfig = config || getConfig();
|
|
39
10
|
const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, } = blogConfig;
|
|
40
11
|
const items = posts
|
|
41
|
-
.slice(0, RSS_POST_LIMIT)
|
|
12
|
+
.slice(0, RSS_POST_LIMIT)
|
|
42
13
|
.map((post) => {
|
|
43
14
|
const title = resolveFrontmatterField(['title'], post.frontmatter, post.slug) || post.slug;
|
|
44
15
|
const description = resolveFrontmatterField(['description', 'excerpt'], post.frontmatter, '') || '';
|
|
45
16
|
const authorObj = post.authors[0];
|
|
46
17
|
const author = authorObj ? getAuthorName(authorObj) : (defaultAuthor || '');
|
|
47
18
|
const pubDate = resolveFrontmatterField(['publishedDate', 'date'], post.frontmatter) || new Date().toISOString();
|
|
48
|
-
const url =
|
|
49
|
-
// Format date for RSS (RFC 822)
|
|
19
|
+
const url = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, blogConfig);
|
|
50
20
|
const rssDate = new Date(pubDate).toUTCString();
|
|
51
21
|
return ` <item>
|
|
52
22
|
<title>${escapeXml(title)}</title>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-metadata.d.ts","sourceRoot":"","sources":["../../src/core/seo-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"seo-metadata.d.ts","sourceRoot":"","sources":["../../src/core/seo-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAgBrC;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,CAwMV;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,gBAAgB,EAAE,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,CAyBV"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { calculateReadingTime, calculateWordCount } from './utils.js';
|
|
2
1
|
import { getConfig } from './config.js';
|
|
3
2
|
import { resolveFrontmatterField, isStringArray } from './type-guards.js';
|
|
4
3
|
import { DEFAULT_SITE_NAME, DEFAULT_LANG } from './constants.js';
|
|
5
|
-
import { normalizeKeywords, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta,
|
|
4
|
+
import { normalizeKeywords, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolvePostUrlWithConfig, resolveHreflangMap, resolveBlogIndexUrl, resolveCanonicalUrl, } from './seo-utils.js';
|
|
6
5
|
/**
|
|
7
6
|
* Generates comprehensive metadata for a blog post
|
|
8
7
|
* @param post - The blog post
|
|
@@ -11,7 +10,7 @@ import { normalizeKeywords, getAuthorNames, ensureAuthorsResolved, resolveDefaul
|
|
|
11
10
|
*/
|
|
12
11
|
export function generateBlogPostMetadata(post, config) {
|
|
13
12
|
const blogConfig = config || getConfig();
|
|
14
|
-
const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, authors: configAuthors, twitterHandle, defaultOgImage, defaultLang = DEFAULT_LANG,
|
|
13
|
+
const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, authors: configAuthors, twitterHandle, defaultOgImage, defaultLang = DEFAULT_LANG, } = blogConfig;
|
|
15
14
|
const fm = post.frontmatter;
|
|
16
15
|
// Title resolution: seoTitle > title > slug
|
|
17
16
|
const baseTitle = resolveFrontmatterField(['title'], fm, post.slug) || post.slug;
|
|
@@ -45,9 +44,6 @@ export function generateBlogPostMetadata(post, config) {
|
|
|
45
44
|
const tags = isStringArray(fm.tags) ? fm.tags : [];
|
|
46
45
|
const keywords = normalizeKeywords(fm.keywords);
|
|
47
46
|
const allKeywords = [...tags, ...keywords].filter((k, i, arr) => arr.indexOf(k) === i); // Remove duplicates
|
|
48
|
-
// Calculate reading time if not provided
|
|
49
|
-
const readingTime = resolveFrontmatterField(['readingTime'], fm) || calculateReadingTime(post.content);
|
|
50
|
-
const wordCount = calculateWordCount(post.content);
|
|
51
47
|
// OG image resolution: ogImage > image > defaultOgImage
|
|
52
48
|
// If not provided, Next.js will automatically use opengraph-image.tsx file convention
|
|
53
49
|
const ogImageUrl = resolveFrontmatterField(['ogImage', 'image'], fm) ||
|
|
@@ -61,13 +57,12 @@ export function generateBlogPostMetadata(post, config) {
|
|
|
61
57
|
const twitterDescription = resolveFrontmatterField(['twitterDescription'], fm) || ogDescription;
|
|
62
58
|
// Canonical URL - supports both absolute and relative URLs
|
|
63
59
|
// Relative URLs will be resolved against siteUrl
|
|
64
|
-
const canonicalUrl =
|
|
60
|
+
const canonicalUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], fm), post.slug, siteUrl, blogConfig);
|
|
65
61
|
// Language
|
|
66
62
|
const lang = resolveFrontmatterField(['lang'], fm) || defaultLang;
|
|
67
63
|
// Robots meta
|
|
68
64
|
const robots = buildRobotsMeta(fm);
|
|
69
|
-
|
|
70
|
-
const postUrl = `${siteUrl}/blog/${post.slug}`;
|
|
65
|
+
const postUrl = canonicalUrl;
|
|
71
66
|
// Article meta tags (for better SEO)
|
|
72
67
|
const articleMeta = {};
|
|
73
68
|
if (publishedDate)
|
|
@@ -104,8 +99,16 @@ export function generateBlogPostMetadata(post, config) {
|
|
|
104
99
|
const alternates = {};
|
|
105
100
|
if (canonicalUrl)
|
|
106
101
|
alternates.canonical = canonicalUrl;
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const hreflang = resolveHreflangMap(fm, blogConfig);
|
|
103
|
+
if (hreflang && Object.keys(hreflang).length > 0) {
|
|
104
|
+
alternates.languages = siteUrl
|
|
105
|
+
? Object.fromEntries(Object.entries(hreflang).map(([k, v]) => [
|
|
106
|
+
k,
|
|
107
|
+
v.startsWith('http://') || v.startsWith('https://')
|
|
108
|
+
? v
|
|
109
|
+
: resolveCanonicalUrl(v, siteUrl),
|
|
110
|
+
]))
|
|
111
|
+
: hreflang;
|
|
109
112
|
}
|
|
110
113
|
// Merge all custom meta tags into the 'other' field
|
|
111
114
|
const otherMeta = {};
|
|
@@ -177,6 +180,7 @@ export function generateBlogListMetadata(posts, config) {
|
|
|
177
180
|
const { siteName = DEFAULT_SITE_NAME, siteUrl = '' } = blogConfig;
|
|
178
181
|
const title = 'Blog Posts';
|
|
179
182
|
const description = `Browse all ${posts.length} blog posts`;
|
|
183
|
+
const listUrl = resolveBlogIndexUrl(siteUrl, blogConfig);
|
|
180
184
|
const metadata = {
|
|
181
185
|
title: `${title} | ${siteName}`,
|
|
182
186
|
description,
|
|
@@ -184,7 +188,7 @@ export function generateBlogListMetadata(posts, config) {
|
|
|
184
188
|
title,
|
|
185
189
|
description,
|
|
186
190
|
type: 'website',
|
|
187
|
-
url:
|
|
191
|
+
url: listUrl,
|
|
188
192
|
siteName,
|
|
189
193
|
},
|
|
190
194
|
twitter: {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { BlogPost, Config } from './types.js';
|
|
2
|
+
/** Options for `generateBlogPostSchema` */
|
|
3
|
+
export interface BlogPostSchemaOptions {
|
|
4
|
+
/** If true and an organization @id exists, publisher is `{ "@id": "..." }` only */
|
|
5
|
+
publisherReference?: boolean;
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
8
|
* Generates JSON-LD structured data (Schema.org) for a blog post
|
|
4
9
|
* @param post - The blog post
|
|
5
10
|
* @param config - SEO configuration
|
|
6
11
|
* @returns JSON-LD schema object
|
|
7
12
|
*/
|
|
8
|
-
export declare function generateBlogPostSchema(post: BlogPost, config?: Config): Record<string, unknown>;
|
|
13
|
+
export declare function generateBlogPostSchema(post: BlogPost, config?: Config, options?: BlogPostSchemaOptions): Record<string, unknown>;
|
|
9
14
|
/**
|
|
10
15
|
* Generates breadcrumbs schema for a blog post
|
|
11
16
|
* @param post - The blog post
|
|
@@ -17,4 +22,11 @@ export declare function generateBreadcrumbsSchema(post: BlogPost, config?: Confi
|
|
|
17
22
|
name: string;
|
|
18
23
|
url: string;
|
|
19
24
|
}>): Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Single JSON-LD `@graph` for Organization + BlogPosting + BreadcrumbList.
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateBlogPostSchemaGraph(post: BlogPost, config?: Config, breadcrumbs?: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
url: string;
|
|
31
|
+
}>, includeBreadcrumbs?: boolean): Record<string, unknown>;
|
|
20
32
|
//# sourceMappingURL=seo-schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-schema.d.ts","sourceRoot":"","sources":["../../src/core/seo-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAU,MAAM,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"seo-schema.d.ts","sourceRoot":"","sources":["../../src/core/seo-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAU,MAAM,EAAE,MAAM,YAAY,CAAC;AAiB3D,2CAA2C;AAC3C,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,qBAAqB,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmHzB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GACjD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoCzB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,kBAAkB,UAAO,GACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBzB"}
|
package/dist/core/seo-schema.js
CHANGED
|
@@ -2,16 +2,18 @@ import { calculateReadingTime, calculateWordCount } from './utils.js';
|
|
|
2
2
|
import { getConfig } from './config.js';
|
|
3
3
|
import { resolveFrontmatterField, isStringArray } from './type-guards.js';
|
|
4
4
|
import { DEFAULT_SITE_NAME } from './constants.js';
|
|
5
|
-
import { ensureAuthorsResolved, resolveDefaultAuthor,
|
|
5
|
+
import { ensureAuthorsResolved, resolveDefaultAuthor, resolvePostUrlWithConfig, resolveBlogIndexUrl, } from './seo-utils.js';
|
|
6
|
+
import { buildPublisherEmbedded, buildOrganizationGraphNode, resolveOrganizationId, } from './organization-schema.js';
|
|
6
7
|
/**
|
|
7
8
|
* Generates JSON-LD structured data (Schema.org) for a blog post
|
|
8
9
|
* @param post - The blog post
|
|
9
10
|
* @param config - SEO configuration
|
|
10
11
|
* @returns JSON-LD schema object
|
|
11
12
|
*/
|
|
12
|
-
export function generateBlogPostSchema(post, config) {
|
|
13
|
+
export function generateBlogPostSchema(post, config, options) {
|
|
13
14
|
const blogConfig = config || getConfig();
|
|
14
15
|
const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, authors: configAuthors, } = blogConfig;
|
|
16
|
+
const publisherReference = options?.publisherReference === true;
|
|
15
17
|
const fm = post.frontmatter;
|
|
16
18
|
const title = resolveFrontmatterField(['seoTitle', 'title'], fm, post.slug) || post.slug;
|
|
17
19
|
const description = resolveFrontmatterField(['seoDescription', 'description', 'excerpt'], fm, '') || '';
|
|
@@ -24,10 +26,9 @@ export function generateBlogPostSchema(post, config) {
|
|
|
24
26
|
: (resolvedDefaultAuthor ? [resolvedDefaultAuthor] : []);
|
|
25
27
|
// Ensure all authors are resolved from config (safety net)
|
|
26
28
|
const authors = ensureAuthorsResolved(postAuthors, configAuthors);
|
|
27
|
-
const authorNames = getAuthorNames(authors);
|
|
28
29
|
const publishedDate = resolveFrontmatterField(['publishedDate', 'date'], fm);
|
|
29
30
|
const modifiedDate = resolveFrontmatterField(['modifiedDate'], fm) || publishedDate;
|
|
30
|
-
const postUrl =
|
|
31
|
+
const postUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], fm), post.slug, siteUrl, blogConfig);
|
|
31
32
|
const ogImageUrl = resolveFrontmatterField(['ogImage', 'image'], fm);
|
|
32
33
|
// Calculate reading time and word count if not provided
|
|
33
34
|
const readingTime = resolveFrontmatterField(['readingTime'], fm) || calculateReadingTime(post.content);
|
|
@@ -66,13 +67,17 @@ export function generateBlogPostSchema(post, config) {
|
|
|
66
67
|
? buildAuthorSchema(authors[0])
|
|
67
68
|
: authors.map(buildAuthorSchema),
|
|
68
69
|
}),
|
|
69
|
-
...(siteName &&
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
...(siteName &&
|
|
71
|
+
(() => {
|
|
72
|
+
if (publisherReference) {
|
|
73
|
+
const orgId = resolveOrganizationId(blogConfig);
|
|
74
|
+
if (orgId) {
|
|
75
|
+
return { publisher: { '@id': orgId } };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const pub = buildPublisherEmbedded(blogConfig);
|
|
79
|
+
return pub ? { publisher: pub } : {};
|
|
80
|
+
})()),
|
|
76
81
|
...(ogImageUrl && {
|
|
77
82
|
image: {
|
|
78
83
|
'@type': 'ImageObject',
|
|
@@ -110,11 +115,12 @@ export function generateBreadcrumbsSchema(post, config, breadcrumbs) {
|
|
|
110
115
|
const blogConfig = config || getConfig();
|
|
111
116
|
const { siteUrl = '' } = blogConfig;
|
|
112
117
|
const title = resolveFrontmatterField(['seoTitle', 'title'], post.frontmatter, post.slug) || post.slug;
|
|
113
|
-
const postUrl =
|
|
118
|
+
const postUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, blogConfig);
|
|
119
|
+
const blogIndexUrl = resolveBlogIndexUrl(siteUrl, blogConfig);
|
|
114
120
|
// Default breadcrumbs: Home > Blog > Post
|
|
115
121
|
const defaultBreadcrumbs = [
|
|
116
122
|
{ name: 'Home', url: siteUrl || '/' },
|
|
117
|
-
{ name: 'Blog', url:
|
|
123
|
+
{ name: 'Blog', url: blogIndexUrl },
|
|
118
124
|
{ name: title, url: postUrl },
|
|
119
125
|
];
|
|
120
126
|
const items = breadcrumbs || defaultBreadcrumbs;
|
|
@@ -129,3 +135,26 @@ export function generateBreadcrumbsSchema(post, config, breadcrumbs) {
|
|
|
129
135
|
})),
|
|
130
136
|
};
|
|
131
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Single JSON-LD `@graph` for Organization + BlogPosting + BreadcrumbList.
|
|
140
|
+
*/
|
|
141
|
+
export function generateBlogPostSchemaGraph(post, config, breadcrumbs, includeBreadcrumbs = true) {
|
|
142
|
+
const orgNode = buildOrganizationGraphNode(config);
|
|
143
|
+
const article = generateBlogPostSchema(post, config, { publisherReference: true });
|
|
144
|
+
const articleBody = { ...article };
|
|
145
|
+
delete articleBody['@context'];
|
|
146
|
+
const graph = [];
|
|
147
|
+
if (orgNode)
|
|
148
|
+
graph.push(orgNode);
|
|
149
|
+
graph.push(articleBody);
|
|
150
|
+
if (includeBreadcrumbs) {
|
|
151
|
+
const crumbs = generateBreadcrumbsSchema(post, config, breadcrumbs);
|
|
152
|
+
const crumbsBody = { ...crumbs };
|
|
153
|
+
delete crumbsBody['@context'];
|
|
154
|
+
graph.push(crumbsBody);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
'@context': 'https://schema.org',
|
|
158
|
+
'@graph': graph,
|
|
159
|
+
};
|
|
160
|
+
}
|
package/dist/core/seo-utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlogPost, Author } from './types.js';
|
|
1
|
+
import type { BlogPost, Author, Config, BlogPostFrontmatter } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Normalizes keywords to an array of strings
|
|
4
4
|
* Handles both string (comma-separated) and array formats
|
|
@@ -50,13 +50,25 @@ export declare function buildRobotsMeta(frontmatter: BlogPost['frontmatter']): s
|
|
|
50
50
|
*/
|
|
51
51
|
export declare function resolveCanonicalUrl(url: string, siteUrl: string): string;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
54
|
-
* @param canonicalUrl - Optional canonical URL from frontmatter
|
|
55
|
-
* @param slug - Post slug
|
|
56
|
-
* @param siteUrl - Base site URL
|
|
57
|
-
* @returns Resolved absolute URL
|
|
53
|
+
* Default path segment for post URLs (no leading/trailing slashes).
|
|
58
54
|
*/
|
|
59
|
-
export declare function
|
|
55
|
+
export declare function getBlogPostPathSegment(config?: Config): string;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves a post URL from canonical URL or generates default `/{segment}/{slug}` under siteUrl.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolvePostUrl(canonicalUrl: string | undefined, slug: string, siteUrl: string, blogPostPathSegment?: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Resolves post URL using optional blog config for path segment.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolvePostUrlWithConfig(canonicalUrl: string | undefined, slug: string, siteUrl: string, config?: Config): string;
|
|
64
|
+
/**
|
|
65
|
+
* Default blog listing URL for breadcrumbs (config.blogIndexPath or `{siteUrl}/blogs`).
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Per-post hreflang map: frontmatter.alternateLanguages overrides config.alternateLanguages.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveHreflangMap(fm: BlogPostFrontmatter, config?: Config): Record<string, string> | undefined;
|
|
71
|
+
export declare function resolveBlogIndexUrl(siteUrl: string, config?: Config): string;
|
|
60
72
|
/**
|
|
61
73
|
* Escapes XML special characters to prevent injection attacks
|
|
62
74
|
* @param unsafe - String that may contain XML special characters
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-utils.d.ts","sourceRoot":"","sources":["../../src/core/seo-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"seo-utils.d.ts","sourceRoot":"","sources":["../../src/core/seo-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGhF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAOxE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAE7D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,EAAE,CAErE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAcrB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,GAAG,MAAM,GAAG,SAAS,CAI7B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,GAAG,SAAS,CAqBxF;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgBxE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,mBAAmB,SAAS,GAC3B,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAOR;AAED;;GAEG;AACH;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,mBAAmB,EACvB,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAkBpC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAe5E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOhD"}
|
package/dist/core/seo-utils.js
CHANGED
|
@@ -110,16 +110,74 @@ export function resolveCanonicalUrl(url, siteUrl) {
|
|
|
110
110
|
return `${baseUrl}/${url}`;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
114
|
-
* @param canonicalUrl - Optional canonical URL from frontmatter
|
|
115
|
-
* @param slug - Post slug
|
|
116
|
-
* @param siteUrl - Base site URL
|
|
117
|
-
* @returns Resolved absolute URL
|
|
113
|
+
* Default path segment for post URLs (no leading/trailing slashes).
|
|
118
114
|
*/
|
|
119
|
-
export function
|
|
120
|
-
const
|
|
115
|
+
export function getBlogPostPathSegment(config) {
|
|
116
|
+
const raw = config?.blogPostPathSegment?.trim();
|
|
117
|
+
if (!raw)
|
|
118
|
+
return 'blog';
|
|
119
|
+
return raw.replace(/^\/+|\/+$/g, '') || 'blog';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolves a post URL from canonical URL or generates default `/{segment}/{slug}` under siteUrl.
|
|
123
|
+
*/
|
|
124
|
+
export function resolvePostUrl(canonicalUrl, slug, siteUrl, blogPostPathSegment = 'blog') {
|
|
125
|
+
const seg = blogPostPathSegment.replace(/^\/+|\/+$/g, '') || 'blog';
|
|
126
|
+
const base = siteUrl.replace(/\/$/, '');
|
|
127
|
+
const urlRaw = canonicalUrl || (base ? `${base}/${seg}/${slug}` : `/${seg}/${slug}`);
|
|
121
128
|
return siteUrl ? resolveCanonicalUrl(urlRaw, siteUrl) : urlRaw;
|
|
122
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolves post URL using optional blog config for path segment.
|
|
132
|
+
*/
|
|
133
|
+
export function resolvePostUrlWithConfig(canonicalUrl, slug, siteUrl, config) {
|
|
134
|
+
return resolvePostUrl(canonicalUrl, slug, siteUrl, getBlogPostPathSegment(config));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Default blog listing URL for breadcrumbs (config.blogIndexPath or `{siteUrl}/blogs`).
|
|
138
|
+
*/
|
|
139
|
+
/**
|
|
140
|
+
* Per-post hreflang map: frontmatter.alternateLanguages overrides config.alternateLanguages.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveHreflangMap(fm, config) {
|
|
143
|
+
const fmAlt = fm.alternateLanguages;
|
|
144
|
+
if (fmAlt && typeof fmAlt === 'object' && !Array.isArray(fmAlt)) {
|
|
145
|
+
const out = {};
|
|
146
|
+
for (const [k, v] of Object.entries(fmAlt)) {
|
|
147
|
+
if (typeof v === 'string' && v.trim())
|
|
148
|
+
out[k] = v;
|
|
149
|
+
}
|
|
150
|
+
if (Object.keys(out).length > 0)
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
const cfgAlt = config?.alternateLanguages;
|
|
154
|
+
if (cfgAlt && typeof cfgAlt === 'object') {
|
|
155
|
+
const out = {};
|
|
156
|
+
for (const [k, v] of Object.entries(cfgAlt)) {
|
|
157
|
+
if (typeof v === 'string' && v.trim())
|
|
158
|
+
out[k] = v;
|
|
159
|
+
}
|
|
160
|
+
if (Object.keys(out).length > 0)
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
export function resolveBlogIndexUrl(siteUrl, config) {
|
|
166
|
+
const custom = config?.blogIndexPath?.trim();
|
|
167
|
+
if (!custom) {
|
|
168
|
+
const base = siteUrl.replace(/\/$/, '');
|
|
169
|
+
return base ? `${base}/blogs` : '/blogs';
|
|
170
|
+
}
|
|
171
|
+
if (custom.startsWith('http://') || custom.startsWith('https://')) {
|
|
172
|
+
return custom;
|
|
173
|
+
}
|
|
174
|
+
if (custom.startsWith('/')) {
|
|
175
|
+
const base = siteUrl.replace(/\/$/, '');
|
|
176
|
+
return base ? `${base}${custom}` : custom;
|
|
177
|
+
}
|
|
178
|
+
const base = siteUrl.replace(/\/$/, '');
|
|
179
|
+
return base ? `${base}/${custom.replace(/^\/+|\/+$/g, '')}` : `/${custom.replace(/^\/+|\/+$/g, '')}`;
|
|
180
|
+
}
|
|
123
181
|
/**
|
|
124
182
|
* Escapes XML special characters to prevent injection attacks
|
|
125
183
|
* @param unsafe - String that may contain XML special characters
|
package/dist/core/seo.d.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* SEO utilities and generators for next-md-blog
|
|
3
3
|
*
|
|
4
4
|
* This module provides functions for generating metadata, schema.org structured data,
|
|
5
|
-
* and
|
|
5
|
+
* RSS feeds, and sitemap entry builders for blog posts.
|
|
6
6
|
*/
|
|
7
7
|
export { generateBlogPostMetadata, generateBlogListMetadata, } from './seo-metadata.js';
|
|
8
|
-
export { generateBlogPostSchema, generateBreadcrumbsSchema, } from './seo-schema.js';
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
8
|
+
export { generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, } from './seo-schema.js';
|
|
9
|
+
export type { BlogPostSchemaOptions } from './seo-schema.js';
|
|
10
|
+
export { generateRSSFeed } from './seo-feeds.js';
|
|
11
|
+
export { generateOrganizationSchema } from './organization-schema.js';
|
|
12
|
+
export { getBlogSitemapEntries } from './sitemap-data.js';
|
|
13
|
+
export type { BlogSitemapEntry } from './sitemap-data.js';
|
|
14
|
+
export { normalizeKeywords, getAuthorName, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolveCanonicalUrl, resolvePostUrl, resolvePostUrlWithConfig, getBlogPostPathSegment, resolveHreflangMap, resolveBlogIndexUrl, escapeXml, } from './seo-utils.js';
|
|
11
15
|
//# sourceMappingURL=seo.d.ts.map
|
package/dist/core/seo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/core/seo.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/core/seo.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAEtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAG1D,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,SAAS,GACV,MAAM,gBAAgB,CAAC"}
|
package/dist/core/seo.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* SEO utilities and generators for next-md-blog
|
|
3
3
|
*
|
|
4
4
|
* This module provides functions for generating metadata, schema.org structured data,
|
|
5
|
-
* and
|
|
5
|
+
* RSS feeds, and sitemap entry builders for blog posts.
|
|
6
6
|
*/
|
|
7
7
|
// Re-export all SEO functions from their respective modules
|
|
8
8
|
export { generateBlogPostMetadata, generateBlogListMetadata, } from './seo-metadata.js';
|
|
9
|
-
export { generateBlogPostSchema, generateBreadcrumbsSchema, } from './seo-schema.js';
|
|
10
|
-
export {
|
|
9
|
+
export { generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, } from './seo-schema.js';
|
|
10
|
+
export { generateRSSFeed } from './seo-feeds.js';
|
|
11
|
+
export { generateOrganizationSchema } from './organization-schema.js';
|
|
12
|
+
export { getBlogSitemapEntries } from './sitemap-data.js';
|
|
11
13
|
// Re-export utility functions that might be useful
|
|
12
|
-
export { normalizeKeywords, getAuthorName, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolveCanonicalUrl, resolvePostUrl, escapeXml, } from './seo-utils.js';
|
|
14
|
+
export { normalizeKeywords, getAuthorName, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolveCanonicalUrl, resolvePostUrl, resolvePostUrlWithConfig, getBlogPostPathSegment, resolveHreflangMap, resolveBlogIndexUrl, escapeXml, } from './seo-utils.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BlogPostMetadata, Config } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One sitemap row compatible with Next.js `MetadataRoute.Sitemap` entries.
|
|
4
|
+
*/
|
|
5
|
+
export type BlogSitemapEntry = {
|
|
6
|
+
url: string;
|
|
7
|
+
lastModified?: string | Date;
|
|
8
|
+
changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
9
|
+
priority?: number;
|
|
10
|
+
alternates?: {
|
|
11
|
+
languages?: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Builds sitemap entries for blog posts (use with `app/sitemap.ts` via `@next-md-blog/core/next`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function getBlogSitemapEntries(posts: BlogPostMetadata[], config?: Config): BlogSitemapEntry[];
|
|
18
|
+
//# sourceMappingURL=sitemap-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap-data.d.ts","sourceRoot":"","sources":["../../src/core/sitemap-data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAwB3D;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CACrD,CAAC;AAEF;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,gBAAgB,EAAE,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,gBAAgB,EAAE,CAmCpB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getConfig } from './config.js';
|
|
2
|
+
import { resolveFrontmatterField } from './type-guards.js';
|
|
3
|
+
import { resolvePostUrlWithConfig, resolveHreflangMap, resolveCanonicalUrl, } from './seo-utils.js';
|
|
4
|
+
function absolutizeLanguageMap(langs, siteUrl) {
|
|
5
|
+
if (!siteUrl)
|
|
6
|
+
return langs;
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const [k, v] of Object.entries(langs)) {
|
|
9
|
+
out[k] =
|
|
10
|
+
v.startsWith('http://') || v.startsWith('https://')
|
|
11
|
+
? v
|
|
12
|
+
: resolveCanonicalUrl(v, siteUrl);
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds sitemap entries for blog posts (use with `app/sitemap.ts` via `@next-md-blog/core/next`).
|
|
18
|
+
*/
|
|
19
|
+
export function getBlogSitemapEntries(posts, config) {
|
|
20
|
+
const blogConfig = config || getConfig();
|
|
21
|
+
const { siteUrl = '' } = blogConfig;
|
|
22
|
+
return posts.map((post) => {
|
|
23
|
+
const url = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, blogConfig);
|
|
24
|
+
const dateStr = resolveFrontmatterField(['modifiedDate', 'date'], post.frontmatter) ||
|
|
25
|
+
new Date().toISOString().split('T')[0];
|
|
26
|
+
const parsed = new Date(dateStr);
|
|
27
|
+
const lastModified = Number.isNaN(parsed.getTime()) ? dateStr : parsed;
|
|
28
|
+
const languagesRaw = resolveHreflangMap(post.frontmatter, blogConfig);
|
|
29
|
+
const entry = {
|
|
30
|
+
url,
|
|
31
|
+
lastModified,
|
|
32
|
+
changeFrequency: 'monthly',
|
|
33
|
+
priority: 0.8,
|
|
34
|
+
};
|
|
35
|
+
if (languagesRaw && Object.keys(languagesRaw).length > 0) {
|
|
36
|
+
entry.alternates = {
|
|
37
|
+
languages: absolutizeLanguageMap(languagesRaw, siteUrl),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return entry;
|
|
41
|
+
});
|
|
42
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publisher / Organization fields for JSON-LD (extends siteName + siteUrl).
|
|
3
|
+
*/
|
|
4
|
+
export interface SiteOrganization {
|
|
5
|
+
/** Override JSON-LD @id (default: `${origin}/#organization`) */
|
|
6
|
+
id?: string;
|
|
7
|
+
legalName?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Absolute URL of the logo image */
|
|
10
|
+
logo?: string;
|
|
11
|
+
/** Official profiles (Schema.org sameAs) */
|
|
12
|
+
sameAs?: string[];
|
|
13
|
+
}
|
|
1
14
|
/**
|
|
2
15
|
* Blog configuration
|
|
3
16
|
*/
|
|
@@ -18,6 +31,12 @@ export interface Config {
|
|
|
18
31
|
defaultLang?: string;
|
|
19
32
|
/** Alternate language URLs for hreflang (e.g., { 'en': 'https://example.com/blog/post', 'fr': 'https://example.com/fr/blog/post' }) */
|
|
20
33
|
alternateLanguages?: Record<string, string>;
|
|
34
|
+
/** Path segment for post URLs when no canonicalUrl (default: `blog` → `/blog/{slug}`) */
|
|
35
|
+
blogPostPathSegment?: string;
|
|
36
|
+
/** Absolute or site-relative URL for the blog index in default breadcrumbs (default: `{siteUrl}/blogs`) */
|
|
37
|
+
blogIndexPath?: string;
|
|
38
|
+
/** Rich Organization / publisher JSON-LD */
|
|
39
|
+
organization?: SiteOrganization;
|
|
21
40
|
}
|
|
22
41
|
/**
|
|
23
42
|
* Author information
|
|
@@ -64,6 +83,8 @@ export interface BlogPostFrontmatter {
|
|
|
64
83
|
nofollow?: boolean;
|
|
65
84
|
/** Reading time in minutes (auto-calculated if not provided) */
|
|
66
85
|
readingTime?: number;
|
|
86
|
+
/** Per-post hreflang map (overrides config.alternateLanguages for this post) */
|
|
87
|
+
alternateLanguages?: Record<string, string>;
|
|
67
88
|
[key: string]: unknown;
|
|
68
89
|
}
|
|
69
90
|
/**
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,qBAAqB;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4BAA4B;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uIAAuI;IACvI,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
package/dist/core/utils.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Regex patterns for markdown stripping (compiled once at module level)
|
|
2
2
|
const CODE_BLOCK_REGEX = /```[\s\S]*?```/g;
|
|
3
3
|
const INLINE_CODE_REGEX = /`[^`]*`/g;
|
|
4
|
-
const LINK_REGEX = /\[([^\]]*)
|
|
5
|
-
const IMAGE_REGEX = /!\[([^\]]*)
|
|
4
|
+
const LINK_REGEX = /\[([^\]]*)]\([^)]*\)/g;
|
|
5
|
+
const IMAGE_REGEX = /!\[([^\]]*)]\([^)]*\)/g;
|
|
6
6
|
const HTML_TAG_REGEX = /<[^>]*>/g;
|
|
7
7
|
const HEADER_REGEX = /#+\s+/g;
|
|
8
8
|
const HORIZONTAL_RULE_REGEX = /---+/g;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ export type { OgImageProps } from './components/OgImage.js';
|
|
|
6
6
|
export { BlogPostSEO } from './components/BlogPostSEO.js';
|
|
7
7
|
export type { BlogPostSEOProps } from './components/BlogPostSEO.js';
|
|
8
8
|
export { getBlogPost, getAllBlogPosts, getAllBlogPostSlugs } from './core/file-utils.js';
|
|
9
|
-
export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema,
|
|
9
|
+
export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, generateRSSFeed, generateOrganizationSchema, getBlogSitemapEntries, } from './core/seo.js';
|
|
10
|
+
export type { BlogPostSchemaOptions, BlogSitemapEntry } from './core/seo.js';
|
|
11
|
+
export { getBlogSitemap, getBlogRobots, createRssFeedResponse, } from './next.js';
|
|
10
12
|
export { createConfig, loadConfig, getConfig } from './core/config.js';
|
|
11
|
-
export type { BlogPost, BlogPostMetadata, BlogPostFrontmatter, GetBlogPostOptions, Author, Config, } from './core/types.js';
|
|
13
|
+
export type { BlogPost, BlogPostMetadata, BlogPostFrontmatter, GetBlogPostOptions, Author, Config, SiteOrganization, } from './core/types.js';
|
|
12
14
|
export { MdxBlogError, BlogPostNotFoundError, FileReadError, DirectoryError, } from './core/errors.js';
|
|
13
15
|
export { POSTS_DIR_NAME, getPostsDirectory } from './core/constants.js';
|
|
14
16
|
export { calculateReadingTime, calculateWordCount, normalizeAuthors } from './core/utils.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChG,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAGpE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,eAAe,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChG,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAGpE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGzF,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,yBAAyB,EACzB,2BAA2B,EAC3B,eAAe,EACf,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE7E,OAAO,EACL,cAAc,EACd,aAAa,EACb,qBAAqB,GACtB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGvE,YAAY,EACV,QAAQ,EACR,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,MAAM,EACN,MAAM,EACN,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxE,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@ export { BlogPostSEO } from './components/BlogPostSEO.js';
|
|
|
6
6
|
// Utilities
|
|
7
7
|
export { getBlogPost, getAllBlogPosts, getAllBlogPostSlugs } from './core/file-utils.js';
|
|
8
8
|
// SEO
|
|
9
|
-
export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema,
|
|
9
|
+
export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, generateRSSFeed, generateOrganizationSchema, getBlogSitemapEntries, } from './core/seo.js';
|
|
10
|
+
export { getBlogSitemap, getBlogRobots, createRssFeedResponse, } from './next.js';
|
|
10
11
|
// Config
|
|
11
12
|
export { createConfig, loadConfig, getConfig } from './core/config.js';
|
|
12
13
|
// Errors
|