@next-md-blog/core 1.0.7 → 1.1.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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * llms.txt / llms-full.txt generators — see https://llmstxt.org
3
+ *
4
+ * These return plain markdown strings. App code wraps them in a route handler:
5
+ *
6
+ * // app/llms.txt/route.ts
7
+ * import { generateLlmsTxt } from '@next-md-blog/core';
8
+ * export const dynamic = 'force-static';
9
+ * export const revalidate = 3600;
10
+ * export async function GET() {
11
+ * const body = await generateLlmsTxt({ config, locales: ['en','fr'] });
12
+ * return new Response(body, { headers: { 'content-type': 'text/plain' }});
13
+ * }
14
+ */
15
+ import type { Config } from './types.js';
16
+ export interface LlmsTxtOptions {
17
+ config?: Config;
18
+ /** Locales to enumerate. Defaults to `[config.defaultLang ?? 'en']`. */
19
+ locales?: readonly string[];
20
+ /** Marks one locale as default (added to the heading). Defaults to `locales[0]`. */
21
+ defaultLocale?: string;
22
+ /** Override the site summary (defaults to siteName + siteUrl). */
23
+ summary?: string;
24
+ }
25
+ /**
26
+ * Generate the index `llms.txt` markdown: one section per locale listing posts.
27
+ */
28
+ export declare function generateLlmsTxt(options?: LlmsTxtOptions): Promise<string>;
29
+ /**
30
+ * Generate `llms-full.txt`: concatenated post bodies (default locale only,
31
+ * unless `locales` is overridden). Designed for LLM ingestion.
32
+ */
33
+ export declare function generateLlmsFullTxt(options?: LlmsTxtOptions): Promise<string>;
34
+ //# sourceMappingURL=llms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms.d.ts","sourceRoot":"","sources":["../../src/core/llms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAoB,MAAM,EAAE,MAAM,YAAY,CAAC;AAO3D,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,oFAAoF;IACpF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAuBD;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CA6CjB;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,MAAM,CAAC,CA+CjB"}
@@ -0,0 +1,109 @@
1
+ import { getAllBlogPosts, getBlogPost } from './file-utils.js';
2
+ import { resolveFrontmatterField } from './type-guards.js';
3
+ import { getConfig } from './config.js';
4
+ import { DEFAULT_SITE_NAME } from './constants.js';
5
+ import { resolvePostUrlWithConfig } from './seo-utils.js';
6
+ function postLine(post, siteUrl, config, locale) {
7
+ const title = post.frontmatter.title ?? post.slug;
8
+ const description = resolveFrontmatterField(['description', 'excerpt'], post.frontmatter) ?? '';
9
+ const url = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, config, locale);
10
+ return `- [${title}](${url})${description ? `: ${description}` : ''}`;
11
+ }
12
+ /**
13
+ * Generate the index `llms.txt` markdown: one section per locale listing posts.
14
+ */
15
+ export async function generateLlmsTxt(options = {}) {
16
+ const config = options.config || getConfig();
17
+ const siteName = config.siteName ?? DEFAULT_SITE_NAME;
18
+ const siteUrl = (config.siteUrl ?? '').replace(/\/$/, '');
19
+ const locales = options.locales ?? [config.defaultLang ?? 'en'];
20
+ const defaultLocale = options.defaultLocale ?? locales[0];
21
+ const lines = [];
22
+ lines.push(`# ${siteName}`);
23
+ lines.push('');
24
+ if (options.summary) {
25
+ lines.push(`> ${options.summary}`);
26
+ }
27
+ else if (siteUrl) {
28
+ lines.push(`> Canonical site: ${siteUrl}`);
29
+ }
30
+ lines.push('');
31
+ for (const locale of locales) {
32
+ let posts;
33
+ try {
34
+ posts = await getAllBlogPosts({ config, locale });
35
+ }
36
+ catch {
37
+ posts = [];
38
+ }
39
+ if (!posts.length)
40
+ continue;
41
+ const isDefault = locale === defaultLocale;
42
+ lines.push(`## Blog (${locale})${isDefault ? ' — default locale' : ''}`);
43
+ lines.push('');
44
+ for (const p of posts) {
45
+ lines.push(postLine(p, siteUrl, config, locale));
46
+ }
47
+ lines.push('');
48
+ }
49
+ if (siteUrl) {
50
+ lines.push('## Feeds');
51
+ lines.push('');
52
+ for (const locale of locales) {
53
+ lines.push(`- [RSS — ${locale}](${siteUrl}/${locale}/feed.xml)`);
54
+ }
55
+ lines.push('');
56
+ lines.push(`- [Full prose](${siteUrl}/llms-full.txt)`);
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+ /**
61
+ * Generate `llms-full.txt`: concatenated post bodies (default locale only,
62
+ * unless `locales` is overridden). Designed for LLM ingestion.
63
+ */
64
+ export async function generateLlmsFullTxt(options = {}) {
65
+ const config = options.config || getConfig();
66
+ const siteName = config.siteName ?? DEFAULT_SITE_NAME;
67
+ const siteUrl = (config.siteUrl ?? '').replace(/\/$/, '');
68
+ const locales = options.locales ?? [config.defaultLang ?? 'en'];
69
+ const out = [];
70
+ out.push(`# ${siteName}`);
71
+ out.push('');
72
+ if (options.summary)
73
+ out.push(`> ${options.summary}`);
74
+ if (siteUrl)
75
+ out.push(`> Source: ${siteUrl}`);
76
+ out.push('');
77
+ for (const locale of locales) {
78
+ let metadata;
79
+ try {
80
+ metadata = await getAllBlogPosts({ config, locale });
81
+ }
82
+ catch {
83
+ metadata = [];
84
+ }
85
+ for (const m of metadata) {
86
+ const post = await getBlogPost(m.slug, { config, locale });
87
+ if (!post)
88
+ continue;
89
+ const url = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, config, locale);
90
+ const title = post.frontmatter.title ?? post.slug;
91
+ out.push('---');
92
+ out.push('');
93
+ out.push(`# ${title}`);
94
+ out.push('');
95
+ if (url)
96
+ out.push(`Source: ${url}`);
97
+ const published = resolveFrontmatterField(['date'], post.frontmatter);
98
+ if (published)
99
+ out.push(`Published: ${published}`);
100
+ const updated = resolveFrontmatterField(['updated', 'modifiedDate'], post.frontmatter);
101
+ if (updated)
102
+ out.push(`Updated: ${updated}`);
103
+ out.push('');
104
+ out.push(post.content.trim());
105
+ out.push('');
106
+ }
107
+ }
108
+ return out.join('\n');
109
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"organization-schema.d.ts","sourceRoot":"","sources":["../../src/core/organization-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAIzC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAQvE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOzE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAoC1F;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE/F;AAED,sDAAsD;AACtD,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAM3F;AAED,kDAAkD;AAClD,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE/F"}
1
+ {"version":3,"file":"organization-schema.d.ts","sourceRoot":"","sources":["../../src/core/organization-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAIzC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAQvE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOzE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAmD1F;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE/F;AAED,sDAAsD;AACtD,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAM3F;AAED,kDAAkD;AAClD,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAE/F"}
@@ -57,8 +57,24 @@ export function buildOrganizationNode(config) {
57
57
  url: org.logo,
58
58
  };
59
59
  }
60
- if (org?.sameAs && org.sameAs.length > 0) {
61
- node.sameAs = org.sameAs.length === 1 ? org.sameAs[0] : org.sameAs;
60
+ if (org?.foundingDate)
61
+ node.foundingDate = org.foundingDate;
62
+ if (org?.founder) {
63
+ node.founder = { '@type': 'Person', name: org.founder };
64
+ }
65
+ if (org?.address && Object.values(org.address).some(Boolean)) {
66
+ node.address = { '@type': 'PostalAddress', ...org.address };
67
+ }
68
+ if (org?.contactPoint && Object.values(org.contactPoint).some(Boolean)) {
69
+ node.contactPoint = { '@type': 'ContactPoint', ...org.contactPoint };
70
+ }
71
+ // Merge wikidata into sameAs (deduplicated).
72
+ const sameAsList = [
73
+ ...(org?.sameAs ?? []),
74
+ ...(org?.wikidata ? [org.wikidata] : []),
75
+ ].filter((v, i, arr) => arr.indexOf(v) === i);
76
+ if (sameAsList.length > 0) {
77
+ node.sameAs = sameAsList.length === 1 ? sameAsList[0] : sameAsList;
62
78
  }
63
79
  return node;
64
80
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * High-level query helpers built on top of getBlogPost / getAllBlogPosts.
3
+ *
4
+ * These cover the patterns app code keeps reinventing:
5
+ * - looking up the same slug across every locale (for hreflang)
6
+ * - filtering posts by author or series
7
+ * - enumerating slugs for `generateStaticParams`
8
+ */
9
+ import type { BlogPost, BlogPostMetadata, GetBlogPostOptions } from './types.js';
10
+ export declare function slugifyAuthor(name: string): string;
11
+ export declare function slugifySeries(value: string): string;
12
+ /**
13
+ * Collect every author name referenced in a post's frontmatter (`author` and `authors`).
14
+ */
15
+ export declare function authorNamesFromFrontmatter(fm: Record<string, unknown>): string[];
16
+ /**
17
+ * Look up the same slug across every locale. Returned map preserves locale insertion order.
18
+ * Useful for building `alternates.languages` in `generateMetadata` without writing a loop.
19
+ */
20
+ export declare function getPostInAllLocales(slug: string, locales: readonly string[], options?: Omit<GetBlogPostOptions, 'locale'>): Promise<Map<string, BlogPost | null>>;
21
+ /**
22
+ * Returns posts authored (or co-authored) by `authorSlug`. Slugification matches
23
+ * `slugifyAuthor` so the same convention is used everywhere.
24
+ */
25
+ export declare function getPostsByAuthor(authorSlug: string, options?: GetBlogPostOptions): Promise<BlogPostMetadata[]>;
26
+ /**
27
+ * Returns posts that share a `series` frontmatter value. Sorted by `seriesOrder`,
28
+ * falling back to publish date (ascending).
29
+ */
30
+ export declare function getPostsBySeries(seriesSlug: string, options?: GetBlogPostOptions): Promise<BlogPostMetadata[]>;
31
+ /** Enumerate every author slug across the provided locales (or the default locale only). */
32
+ export declare function getAllAuthorSlugs(options?: GetBlogPostOptions, locales?: readonly string[]): Promise<string[]>;
33
+ /** Enumerate every series slug across the provided locales. */
34
+ export declare function getAllSeriesSlugs(options?: GetBlogPostOptions, locales?: readonly string[]): Promise<string[]>;
35
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/core/queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EACV,QAAQ,EACR,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAIpB,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAWD;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC1B,MAAM,EAAE,CAoBV;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,GAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,CAAM,GAC/C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC,CAAC,CAWvC;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAO7B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAgB7B;AAED,4FAA4F;AAC5F,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,kBAAuB,EAChC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBnB;AAED,+DAA+D;AAC/D,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,kBAAuB,EAChC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,OAAO,CAAC,MAAM,EAAE,CAAC,CAanB"}
@@ -0,0 +1,138 @@
1
+ import { getAllBlogPosts, getBlogPost } from './file-utils.js';
2
+ import { resolveFrontmatterField } from './type-guards.js';
3
+ export function slugifyAuthor(name) {
4
+ return name
5
+ .toLowerCase()
6
+ .trim()
7
+ .replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
8
+ .replace(/\s+/g, '-')
9
+ .replace(/-+/g, '-');
10
+ }
11
+ export function slugifySeries(value) {
12
+ return slugifyAuthor(value);
13
+ }
14
+ function authorNameFrom(value) {
15
+ if (typeof value === 'string')
16
+ return value;
17
+ if (value && typeof value === 'object' && 'name' in value) {
18
+ const name = value.name;
19
+ if (typeof name === 'string')
20
+ return name;
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * Collect every author name referenced in a post's frontmatter (`author` and `authors`).
26
+ */
27
+ export function authorNamesFromFrontmatter(fm) {
28
+ const out = new Set();
29
+ const author = fm.author;
30
+ if (Array.isArray(author)) {
31
+ for (const a of author) {
32
+ const n = authorNameFrom(a);
33
+ if (n)
34
+ out.add(n);
35
+ }
36
+ }
37
+ else {
38
+ const n = authorNameFrom(author);
39
+ if (n)
40
+ out.add(n);
41
+ }
42
+ const authors = fm.authors;
43
+ if (Array.isArray(authors)) {
44
+ for (const a of authors) {
45
+ const n = authorNameFrom(a);
46
+ if (n)
47
+ out.add(n);
48
+ }
49
+ }
50
+ return [...out];
51
+ }
52
+ /**
53
+ * Look up the same slug across every locale. Returned map preserves locale insertion order.
54
+ * Useful for building `alternates.languages` in `generateMetadata` without writing a loop.
55
+ */
56
+ export async function getPostInAllLocales(slug, locales, options = {}) {
57
+ const entries = await Promise.all(locales.map(async (locale) => {
58
+ try {
59
+ return [locale, await getBlogPost(slug, { ...options, locale })];
60
+ }
61
+ catch {
62
+ return [locale, null];
63
+ }
64
+ }));
65
+ return new Map(entries);
66
+ }
67
+ /**
68
+ * Returns posts authored (or co-authored) by `authorSlug`. Slugification matches
69
+ * `slugifyAuthor` so the same convention is used everywhere.
70
+ */
71
+ export async function getPostsByAuthor(authorSlug, options = {}) {
72
+ const posts = await getAllBlogPosts(options);
73
+ return posts.filter((p) => authorNamesFromFrontmatter(p.frontmatter).some((name) => slugifyAuthor(name) === authorSlug));
74
+ }
75
+ /**
76
+ * Returns posts that share a `series` frontmatter value. Sorted by `seriesOrder`,
77
+ * falling back to publish date (ascending).
78
+ */
79
+ export async function getPostsBySeries(seriesSlug, options = {}) {
80
+ const posts = await getAllBlogPosts(options);
81
+ const inSeries = posts.filter((p) => {
82
+ const raw = resolveFrontmatterField(['series'], p.frontmatter);
83
+ return raw ? slugifySeries(raw) === seriesSlug : false;
84
+ });
85
+ return inSeries.sort((a, b) => {
86
+ const oa = resolveFrontmatterField(['seriesOrder'], a.frontmatter);
87
+ const ob = resolveFrontmatterField(['seriesOrder'], b.frontmatter);
88
+ if (typeof oa === 'number' && typeof ob === 'number')
89
+ return oa - ob;
90
+ if (typeof oa === 'number')
91
+ return -1;
92
+ if (typeof ob === 'number')
93
+ return 1;
94
+ const da = a.frontmatter.date || '';
95
+ const db = b.frontmatter.date || '';
96
+ return da.localeCompare(db);
97
+ });
98
+ }
99
+ /** Enumerate every author slug across the provided locales (or the default locale only). */
100
+ export async function getAllAuthorSlugs(options = {}, locales) {
101
+ const localeList = locales && locales.length > 0 ? locales : [options.locale ?? undefined];
102
+ const out = new Set();
103
+ for (const locale of localeList) {
104
+ const opts = { ...options };
105
+ if (locale)
106
+ opts.locale = locale;
107
+ const posts = await getAllBlogPosts(opts);
108
+ for (const p of posts) {
109
+ for (const name of authorNamesFromFrontmatter(p.frontmatter)) {
110
+ out.add(slugifyAuthor(name));
111
+ }
112
+ }
113
+ }
114
+ // Also include authors defined in config but with no posts yet — useful for
115
+ // dedicated author landing pages.
116
+ for (const a of options.config?.authors ?? []) {
117
+ if (a.name)
118
+ out.add(slugifyAuthor(a.name));
119
+ }
120
+ return [...out];
121
+ }
122
+ /** Enumerate every series slug across the provided locales. */
123
+ export async function getAllSeriesSlugs(options = {}, locales) {
124
+ const localeList = locales && locales.length > 0 ? locales : [options.locale ?? undefined];
125
+ const out = new Set();
126
+ for (const locale of localeList) {
127
+ const opts = { ...options };
128
+ if (locale)
129
+ opts.locale = locale;
130
+ const posts = await getAllBlogPosts(opts);
131
+ for (const p of posts) {
132
+ const raw = resolveFrontmatterField(['series'], p.frontmatter);
133
+ if (raw)
134
+ out.add(slugifySeries(raw));
135
+ }
136
+ }
137
+ return [...out];
138
+ }
@@ -1,12 +1,45 @@
1
1
  import type { BlogPost, BlogPostMetadata, Config } from './types.js';
2
2
  import type { Metadata } from 'next';
3
+ /** Optional behaviour switches for `generateBlogPostMetadata`. */
4
+ export interface GenerateBlogPostMetadataOptions {
5
+ /**
6
+ * Locale segment to inject into the canonical URL (`/{locale}/{segment}/{slug}`).
7
+ * Also sets `openGraph.locale` and is used to scope hreflang fallbacks.
8
+ */
9
+ locale?: string;
10
+ /**
11
+ * Controls the final `<title>` value.
12
+ * - `"site-suffix"` (default): `"{title} | {siteName}"` — same as 1.0.
13
+ * - `"absolute"`: returns `{ absolute: "{title} | {siteName}" }` so a parent
14
+ * layout's `title.template` does not double up.
15
+ * - `"bare"`: just the post title; let a parent layout's template handle the suffix.
16
+ * - A function: receives `{ title, siteName }` and returns the final string.
17
+ */
18
+ titleTemplate?: 'site-suffix' | 'absolute' | 'bare' | ((args: {
19
+ title: string;
20
+ siteName: string;
21
+ }) => string);
22
+ /**
23
+ * Custom URL builder. Overrides the default `/{locale}/{segment}/{slug}` logic.
24
+ */
25
+ urlBuilder?: (args: {
26
+ canonicalUrl: string | undefined;
27
+ slug: string;
28
+ siteUrl: string;
29
+ locale?: string;
30
+ config: Config;
31
+ }) => string;
32
+ /** Locale-aware hreflang map override. Useful when sibling URLs are computed externally. */
33
+ alternateLanguages?: Record<string, string>;
34
+ }
3
35
  /**
4
- * Generates comprehensive metadata for a blog post
36
+ * Generates comprehensive metadata for a blog post.
5
37
  * @param post - The blog post
6
38
  * @param config - SEO configuration
39
+ * @param options - Locale + URL + title overrides (see {@link GenerateBlogPostMetadataOptions})
7
40
  * @returns Metadata object for Next.js
8
41
  */
9
- export declare function generateBlogPostMetadata(post: BlogPost, config?: Config): Metadata;
42
+ export declare function generateBlogPostMetadata(post: BlogPost, config?: Config, options?: GenerateBlogPostMetadataOptions): Metadata;
10
43
  /**
11
44
  * Generates metadata for the blog listing page
12
45
  * @param posts - Array of blog posts
@@ -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;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
+ {"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,kEAAkE;AAClE,MAAM,WAAW,+BAA+B;IAC9C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,aAAa,CAAC,EACV,aAAa,GACb,UAAU,GACV,MAAM,GACN,CAAC,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC,CAAC;IAC5D;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;QACjC,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,MAAM,CAAC;IACb,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,+BAA+B,GACxC,QAAQ,CAyNV;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,gBAAgB,EAAE,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,CAyBV"}
@@ -3,19 +3,35 @@ import { resolveFrontmatterField, isStringArray } from './type-guards.js';
3
3
  import { DEFAULT_SITE_NAME, DEFAULT_LANG } from './constants.js';
4
4
  import { normalizeKeywords, getAuthorNames, ensureAuthorsResolved, resolveDefaultAuthor, buildRobotsMeta, resolvePostUrlWithConfig, resolveHreflangMap, resolveBlogIndexUrl, resolveCanonicalUrl, } from './seo-utils.js';
5
5
  /**
6
- * Generates comprehensive metadata for a blog post
6
+ * Generates comprehensive metadata for a blog post.
7
7
  * @param post - The blog post
8
8
  * @param config - SEO configuration
9
+ * @param options - Locale + URL + title overrides (see {@link GenerateBlogPostMetadataOptions})
9
10
  * @returns Metadata object for Next.js
10
11
  */
11
- export function generateBlogPostMetadata(post, config) {
12
+ export function generateBlogPostMetadata(post, config, options) {
12
13
  const blogConfig = config || getConfig();
14
+ const opts = options ?? {};
13
15
  const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, authors: configAuthors, twitterHandle, defaultOgImage, defaultLang = DEFAULT_LANG, } = blogConfig;
14
16
  const fm = post.frontmatter;
15
17
  // Title resolution: seoTitle > title > slug
16
18
  const baseTitle = resolveFrontmatterField(['title'], fm, post.slug) || post.slug;
17
19
  const seoTitle = resolveFrontmatterField(['seoTitle', 'title'], fm, baseTitle) || baseTitle;
18
- const pageTitle = `${seoTitle} | ${siteName}`;
20
+ const titleMode = opts.titleTemplate ?? 'site-suffix';
21
+ const suffixed = `${seoTitle} | ${siteName}`;
22
+ let pageTitle;
23
+ if (typeof titleMode === 'function') {
24
+ pageTitle = titleMode({ title: seoTitle, siteName });
25
+ }
26
+ else if (titleMode === 'absolute') {
27
+ pageTitle = { absolute: suffixed };
28
+ }
29
+ else if (titleMode === 'bare') {
30
+ pageTitle = seoTitle;
31
+ }
32
+ else {
33
+ pageTitle = suffixed;
34
+ }
19
35
  // Description resolution: seoDescription > description > excerpt > empty
20
36
  const description = resolveFrontmatterField(['seoDescription', 'description', 'excerpt'], fm, '') || '';
21
37
  // Use normalized authors from post, or fallback to default author (resolved from config if available)
@@ -57,9 +73,18 @@ export function generateBlogPostMetadata(post, config) {
57
73
  const twitterDescription = resolveFrontmatterField(['twitterDescription'], fm) || ogDescription;
58
74
  // Canonical URL - supports both absolute and relative URLs
59
75
  // Relative URLs will be resolved against siteUrl
60
- const canonicalUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], fm), post.slug, siteUrl, blogConfig);
61
- // Language
62
- const lang = resolveFrontmatterField(['lang'], fm) || defaultLang;
76
+ const fmCanonical = resolveFrontmatterField(['canonicalUrl'], fm);
77
+ const canonicalUrl = opts.urlBuilder
78
+ ? opts.urlBuilder({
79
+ canonicalUrl: fmCanonical,
80
+ slug: post.slug,
81
+ siteUrl,
82
+ ...(opts.locale ? { locale: opts.locale } : {}),
83
+ config: blogConfig,
84
+ })
85
+ : resolvePostUrlWithConfig(fmCanonical, post.slug, siteUrl, blogConfig, opts.locale);
86
+ // Language: explicit option > frontmatter.lang > config.defaultLang
87
+ const lang = opts.locale || resolveFrontmatterField(['lang'], fm) || defaultLang;
63
88
  // Robots meta
64
89
  const robots = buildRobotsMeta(fm);
65
90
  const postUrl = canonicalUrl;
@@ -95,11 +120,11 @@ export function generateBlogPostMetadata(post, config) {
95
120
  }
96
121
  }
97
122
  });
98
- // Build alternates object
123
+ // Build alternates object — explicit option > frontmatter > config.
99
124
  const alternates = {};
100
125
  if (canonicalUrl)
101
126
  alternates.canonical = canonicalUrl;
102
- const hreflang = resolveHreflangMap(fm, blogConfig);
127
+ const hreflang = opts.alternateLanguages ?? resolveHreflangMap(fm, blogConfig);
103
128
  if (hreflang && Object.keys(hreflang).length > 0) {
104
129
  alternates.languages = siteUrl
105
130
  ? Object.fromEntries(Object.entries(hreflang).map(([k, v]) => [
@@ -1,13 +1,29 @@
1
1
  import type { BlogPost, Config } from './types.js';
2
- /** Options for `generateBlogPostSchema` */
2
+ /** Options for `generateBlogPostSchema`. */
3
3
  export interface BlogPostSchemaOptions {
4
4
  /** If true and an organization @id exists, publisher is `{ "@id": "..." }` only */
5
5
  publisherReference?: boolean;
6
+ /** Locale segment, used for URL building and `inLanguage`. */
7
+ locale?: string;
8
+ /**
9
+ * Speakable specification (https://schema.org/speakable). Pass `true` to use a
10
+ * sensible default (article header + first paragraph), or your own selectors.
11
+ */
12
+ speakable?: boolean | {
13
+ cssSelector?: string[];
14
+ xpath?: string[];
15
+ };
16
+ /**
17
+ * Free-form mutation of the BlogPosting node before serialization. Use this
18
+ * for HowTo extensions, custom fields, isPartOf overrides, etc.
19
+ */
20
+ extendArticle?: (node: Record<string, unknown>) => Record<string, unknown>;
6
21
  }
7
22
  /**
8
- * Generates JSON-LD structured data (Schema.org) for a blog post
23
+ * Generates JSON-LD structured data (Schema.org) for a blog post.
9
24
  * @param post - The blog post
10
25
  * @param config - SEO configuration
26
+ * @param options - Locale, speakable, publisher reference, extension hook
11
27
  * @returns JSON-LD schema object
12
28
  */
13
29
  export declare function generateBlogPostSchema(post: BlogPost, config?: Config, options?: BlogPostSchemaOptions): Record<string, unknown>;
@@ -15,18 +31,29 @@ export declare function generateBlogPostSchema(post: BlogPost, config?: Config,
15
31
  * Generates breadcrumbs schema for a blog post
16
32
  * @param post - The blog post
17
33
  * @param config - SEO configuration
18
- * @param breadcrumbs - Optional custom breadcrumb items (e.g., [{ name: 'Home', url: '/' }, { name: 'Blog', url: '/blog' }])
34
+ * @param breadcrumbs - Optional custom breadcrumb items
35
+ * @param locale - Optional locale segment for URL building
19
36
  * @returns Breadcrumbs JSON-LD schema object
20
37
  */
21
38
  export declare function generateBreadcrumbsSchema(post: BlogPost, config?: Config, breadcrumbs?: Array<{
22
39
  name: string;
23
40
  url: string;
24
- }>): Record<string, unknown>;
41
+ }>, locale?: string): Record<string, unknown>;
42
+ /** Options for `generateBlogPostSchemaGraph`. */
43
+ export interface BlogPostSchemaGraphOptions {
44
+ locale?: string;
45
+ speakable?: BlogPostSchemaOptions['speakable'];
46
+ extendArticle?: BlogPostSchemaOptions['extendArticle'];
47
+ }
25
48
  /**
26
49
  * Single JSON-LD `@graph` for Organization + BlogPosting + BreadcrumbList.
50
+ *
51
+ * Backwards-compatible: the 5th `options` parameter is optional. Passing
52
+ * `{ locale, speakable, extendArticle }` removes the need for app code to
53
+ * post-mutate the graph for locale-aware URLs and rich SEO fields.
27
54
  */
28
55
  export declare function generateBlogPostSchemaGraph(post: BlogPost, config?: Config, breadcrumbs?: Array<{
29
56
  name: string;
30
57
  url: string;
31
- }>, includeBreadcrumbs?: boolean): Record<string, unknown>;
58
+ }>, includeBreadcrumbs?: boolean, options?: BlogPostSchemaGraphOptions): Record<string, unknown>;
32
59
  //# 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;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"}
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,4CAA4C;AAC5C,MAAM,WAAW,qBAAqB;IACpC,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG;QAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACnE;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5E;AAgBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,qBAAqB,GAC9B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA8JzB;AAED;;;;;;;GAOG;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,EAClD,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqCzB;AAED,iDAAiD;AACjD,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAC;IAC/C,aAAa,CAAC,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAC;CACxD;AAED;;;;;;GAMG;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,EACzB,OAAO,CAAC,EAAE,0BAA0B,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB"}
@@ -4,31 +4,44 @@ import { resolveFrontmatterField, isStringArray } from './type-guards.js';
4
4
  import { DEFAULT_SITE_NAME } from './constants.js';
5
5
  import { ensureAuthorsResolved, resolveDefaultAuthor, resolvePostUrlWithConfig, resolveBlogIndexUrl, } from './seo-utils.js';
6
6
  import { buildPublisherEmbedded, buildOrganizationGraphNode, resolveOrganizationId, } from './organization-schema.js';
7
+ const DEFAULT_SPEAKABLE_SELECTORS = [
8
+ 'article > header h1',
9
+ 'article > header p',
10
+ ];
11
+ function slugifySeries(value) {
12
+ return value
13
+ .toLowerCase()
14
+ .trim()
15
+ .replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
16
+ .replace(/\s+/g, '-')
17
+ .replace(/-+/g, '-');
18
+ }
7
19
  /**
8
- * Generates JSON-LD structured data (Schema.org) for a blog post
20
+ * Generates JSON-LD structured data (Schema.org) for a blog post.
9
21
  * @param post - The blog post
10
22
  * @param config - SEO configuration
23
+ * @param options - Locale, speakable, publisher reference, extension hook
11
24
  * @returns JSON-LD schema object
12
25
  */
13
26
  export function generateBlogPostSchema(post, config, options) {
14
27
  const blogConfig = config || getConfig();
15
28
  const { siteName = DEFAULT_SITE_NAME, siteUrl = '', defaultAuthor, authors: configAuthors, } = blogConfig;
16
29
  const publisherReference = options?.publisherReference === true;
30
+ const locale = options?.locale;
17
31
  const fm = post.frontmatter;
18
32
  const title = resolveFrontmatterField(['seoTitle', 'title'], fm, post.slug) || post.slug;
19
33
  const description = resolveFrontmatterField(['seoDescription', 'description', 'excerpt'], fm, '') || '';
20
- // Use normalized authors from post, or fallback to default author (resolved from config if available)
21
- // Note: post.authors should already be resolved from config.authors when the post was loaded (in file-utils.ts)
22
- // However, we ensure they are resolved here as a safety net in case resolution didn't happen earlier
34
+ // Use normalized authors from post, or fallback to default author (resolved from config if available).
23
35
  const resolvedDefaultAuthor = resolveDefaultAuthor(defaultAuthor, configAuthors);
24
36
  const postAuthors = (post.authors && post.authors.length > 0)
25
37
  ? post.authors
26
38
  : (resolvedDefaultAuthor ? [resolvedDefaultAuthor] : []);
27
- // Ensure all authors are resolved from config (safety net)
28
39
  const authors = ensureAuthorsResolved(postAuthors, configAuthors);
29
40
  const publishedDate = resolveFrontmatterField(['publishedDate', 'date'], fm);
30
- const modifiedDate = resolveFrontmatterField(['modifiedDate'], fm) || publishedDate;
31
- const postUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], fm), post.slug, siteUrl, blogConfig);
41
+ // `updated` is a first-class alias for `modifiedDate`.
42
+ const modifiedDate = resolveFrontmatterField(['modifiedDate', 'updated'], fm) ||
43
+ publishedDate;
44
+ const postUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], fm), post.slug, siteUrl, blogConfig, locale);
32
45
  const ogImageUrl = resolveFrontmatterField(['ogImage', 'image'], fm);
33
46
  // Calculate reading time and word count if not provided
34
47
  const readingTime = resolveFrontmatterField(['readingTime'], fm) || calculateReadingTime(post.content);
@@ -49,6 +62,7 @@ export function generateBlogPostSchema(post, config, options) {
49
62
  ...(author.avatar && { image: author.avatar }),
50
63
  };
51
64
  };
65
+ const lang = locale ?? resolveFrontmatterField(['lang'], fm);
52
66
  // Base schema
53
67
  const schema = {
54
68
  '@context': 'https://schema.org',
@@ -89,33 +103,68 @@ export function generateBlogPostSchema(post, config, options) {
89
103
  ...(isStringArray(fm.tags) && fm.tags.length > 0 && {
90
104
  keywords: fm.tags.join(', '),
91
105
  }),
92
- ...(resolveFrontmatterField(['lang'], fm) ? { inLanguage: resolveFrontmatterField(['lang'], fm) } : {}),
106
+ ...(lang ? { inLanguage: lang } : {}),
93
107
  ...(wordCount > 0 && { wordCount }),
94
108
  ...(readingTime > 0 && {
95
109
  timeRequired: `PT${readingTime}M`,
96
110
  }),
97
111
  };
98
- // Merge with custom schema from frontmatter
99
- if (fm.schema && typeof fm.schema === 'object') {
100
- return {
101
- ...schema,
102
- ...fm.schema,
112
+ // Speakable spec (voice-assistant friendly).
113
+ if (options?.speakable) {
114
+ const sp = options.speakable === true
115
+ ? { cssSelector: DEFAULT_SPEAKABLE_SELECTORS }
116
+ : options.speakable;
117
+ schema.speakable = { '@type': 'SpeakableSpecification', ...sp };
118
+ }
119
+ // E-E-A-T: reviewer / fact-checker / last reviewed.
120
+ const reviewedBy = resolveFrontmatterField(['reviewedBy'], fm);
121
+ if (reviewedBy) {
122
+ schema.reviewedBy = { '@type': 'Person', name: reviewedBy };
123
+ }
124
+ const factCheckedBy = resolveFrontmatterField(['factCheckedBy'], fm);
125
+ if (factCheckedBy) {
126
+ schema.factCheckedBy = { '@type': 'Person', name: factCheckedBy };
127
+ }
128
+ const lastReviewed = resolveFrontmatterField(['lastReviewed'], fm);
129
+ if (lastReviewed) {
130
+ schema.lastReviewed = lastReviewed;
131
+ }
132
+ // Series → isPartOf the pillar CollectionPage.
133
+ const series = resolveFrontmatterField(['series'], fm);
134
+ if (series && siteUrl) {
135
+ const localeSeg = locale ? `/${locale}/` : '/';
136
+ const seriesSlug = slugifySeries(series);
137
+ const seriesTitle = resolveFrontmatterField(['seriesTitle'], fm) || series;
138
+ schema.isPartOf = {
139
+ '@type': 'CollectionPage',
140
+ '@id': `${siteUrl.replace(/\/$/, '')}${localeSeg}topics/${seriesSlug}`,
141
+ name: seriesTitle,
103
142
  };
104
143
  }
105
- return schema;
144
+ // Merge with custom schema from frontmatter (frontmatter wins).
145
+ let merged = schema;
146
+ if (fm.schema && typeof fm.schema === 'object') {
147
+ merged = { ...schema, ...fm.schema };
148
+ }
149
+ // Free-form mutation hook (runs last).
150
+ if (options?.extendArticle) {
151
+ return options.extendArticle({ ...merged });
152
+ }
153
+ return merged;
106
154
  }
107
155
  /**
108
156
  * Generates breadcrumbs schema for a blog post
109
157
  * @param post - The blog post
110
158
  * @param config - SEO configuration
111
- * @param breadcrumbs - Optional custom breadcrumb items (e.g., [{ name: 'Home', url: '/' }, { name: 'Blog', url: '/blog' }])
159
+ * @param breadcrumbs - Optional custom breadcrumb items
160
+ * @param locale - Optional locale segment for URL building
112
161
  * @returns Breadcrumbs JSON-LD schema object
113
162
  */
114
- export function generateBreadcrumbsSchema(post, config, breadcrumbs) {
163
+ export function generateBreadcrumbsSchema(post, config, breadcrumbs, locale) {
115
164
  const blogConfig = config || getConfig();
116
165
  const { siteUrl = '' } = blogConfig;
117
166
  const title = resolveFrontmatterField(['seoTitle', 'title'], post.frontmatter, post.slug) || post.slug;
118
- const postUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, blogConfig);
167
+ const postUrl = resolvePostUrlWithConfig(resolveFrontmatterField(['canonicalUrl'], post.frontmatter), post.slug, siteUrl, blogConfig, locale);
119
168
  const blogIndexUrl = resolveBlogIndexUrl(siteUrl, blogConfig);
120
169
  // Default breadcrumbs: Home > Blog > Post
121
170
  const defaultBreadcrumbs = [
@@ -137,10 +186,19 @@ export function generateBreadcrumbsSchema(post, config, breadcrumbs) {
137
186
  }
138
187
  /**
139
188
  * Single JSON-LD `@graph` for Organization + BlogPosting + BreadcrumbList.
189
+ *
190
+ * Backwards-compatible: the 5th `options` parameter is optional. Passing
191
+ * `{ locale, speakable, extendArticle }` removes the need for app code to
192
+ * post-mutate the graph for locale-aware URLs and rich SEO fields.
140
193
  */
141
- export function generateBlogPostSchemaGraph(post, config, breadcrumbs, includeBreadcrumbs = true) {
194
+ export function generateBlogPostSchemaGraph(post, config, breadcrumbs, includeBreadcrumbs = true, options) {
142
195
  const orgNode = buildOrganizationGraphNode(config);
143
- const article = generateBlogPostSchema(post, config, { publisherReference: true });
196
+ const article = generateBlogPostSchema(post, config, {
197
+ publisherReference: true,
198
+ ...(options?.locale !== undefined && { locale: options.locale }),
199
+ ...(options?.speakable !== undefined && { speakable: options.speakable }),
200
+ ...(options?.extendArticle !== undefined && { extendArticle: options.extendArticle }),
201
+ });
144
202
  const articleBody = { ...article };
145
203
  delete articleBody['@context'];
146
204
  const graph = [];
@@ -148,7 +206,7 @@ export function generateBlogPostSchemaGraph(post, config, breadcrumbs, includeBr
148
206
  graph.push(orgNode);
149
207
  graph.push(articleBody);
150
208
  if (includeBreadcrumbs) {
151
- const crumbs = generateBreadcrumbsSchema(post, config, breadcrumbs);
209
+ const crumbs = generateBreadcrumbsSchema(post, config, breadcrumbs, options?.locale);
152
210
  const crumbsBody = { ...crumbs };
153
211
  delete crumbsBody['@context'];
154
212
  graph.push(crumbsBody);
@@ -55,12 +55,14 @@ export declare function resolveCanonicalUrl(url: string, siteUrl: string): strin
55
55
  export declare function getBlogPostPathSegment(config?: Config): string;
56
56
  /**
57
57
  * Resolves a post URL from canonical URL or generates default `/{segment}/{slug}` under siteUrl.
58
+ * When `locale` is provided, builds `/{locale}/{segment}/{slug}` (skipped when a canonicalUrl is set).
58
59
  */
59
- export declare function resolvePostUrl(canonicalUrl: string | undefined, slug: string, siteUrl: string, blogPostPathSegment?: string): string;
60
+ export declare function resolvePostUrl(canonicalUrl: string | undefined, slug: string, siteUrl: string, blogPostPathSegment?: string, locale?: string): string;
60
61
  /**
61
62
  * Resolves post URL using optional blog config for path segment.
63
+ * Pass `locale` to prefix the URL with the locale segment.
62
64
  */
63
- export declare function resolvePostUrlWithConfig(canonicalUrl: string | undefined, slug: string, siteUrl: string, config?: Config): string;
65
+ export declare function resolvePostUrlWithConfig(canonicalUrl: string | undefined, slug: string, siteUrl: string, config?: Config, locale?: string): string;
64
66
  /**
65
67
  * Default blog listing URL for breadcrumbs (config.blogIndexPath or `{siteUrl}/blogs`).
66
68
  */
@@ -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,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"}
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;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,mBAAmB,SAAS,EAC5B,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAQR;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"}
@@ -120,18 +120,22 @@ export function getBlogPostPathSegment(config) {
120
120
  }
121
121
  /**
122
122
  * Resolves a post URL from canonical URL or generates default `/{segment}/{slug}` under siteUrl.
123
+ * When `locale` is provided, builds `/{locale}/{segment}/{slug}` (skipped when a canonicalUrl is set).
123
124
  */
124
- export function resolvePostUrl(canonicalUrl, slug, siteUrl, blogPostPathSegment = 'blog') {
125
+ export function resolvePostUrl(canonicalUrl, slug, siteUrl, blogPostPathSegment = 'blog', locale) {
125
126
  const seg = blogPostPathSegment.replace(/^\/+|\/+$/g, '') || 'blog';
126
127
  const base = siteUrl.replace(/\/$/, '');
127
- const urlRaw = canonicalUrl || (base ? `${base}/${seg}/${slug}` : `/${seg}/${slug}`);
128
+ const localePrefix = locale ? `/${locale.replace(/^\/+|\/+$/g, '')}` : '';
129
+ const path = `${localePrefix}/${seg}/${slug}`;
130
+ const urlRaw = canonicalUrl || (base ? `${base}${path}` : path);
128
131
  return siteUrl ? resolveCanonicalUrl(urlRaw, siteUrl) : urlRaw;
129
132
  }
130
133
  /**
131
134
  * Resolves post URL using optional blog config for path segment.
135
+ * Pass `locale` to prefix the URL with the locale segment.
132
136
  */
133
- export function resolvePostUrlWithConfig(canonicalUrl, slug, siteUrl, config) {
134
- return resolvePostUrl(canonicalUrl, slug, siteUrl, getBlogPostPathSegment(config));
137
+ export function resolvePostUrlWithConfig(canonicalUrl, slug, siteUrl, config, locale) {
138
+ return resolvePostUrl(canonicalUrl, slug, siteUrl, getBlogPostPathSegment(config), locale);
135
139
  }
136
140
  /**
137
141
  * Default blog listing URL for breadcrumbs (config.blogIndexPath or `{siteUrl}/blogs`).
@@ -5,8 +5,9 @@
5
5
  * RSS feeds, and sitemap entry builders for blog posts.
6
6
  */
7
7
  export { generateBlogPostMetadata, generateBlogListMetadata, } from './seo-metadata.js';
8
+ export type { GenerateBlogPostMetadataOptions } from './seo-metadata.js';
8
9
  export { generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, } from './seo-schema.js';
9
- export type { BlogPostSchemaOptions } from './seo-schema.js';
10
+ export type { BlogPostSchemaOptions, BlogPostSchemaGraphOptions, } from './seo-schema.js';
10
11
  export { generateRSSFeed } from './seo-feeds.js';
11
12
  export { generateOrganizationSchema } from './organization-schema.js';
12
13
  export { getBlogSitemapEntries } from './sitemap-data.js';
@@ -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,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"}
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;AAC3B,YAAY,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,qBAAqB,EACrB,0BAA0B,GAC3B,MAAM,iBAAiB,CAAC;AAEzB,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"}
@@ -1,3 +1,26 @@
1
+ /**
2
+ * Postal address (Schema.org PostalAddress).
3
+ */
4
+ export interface PostalAddress {
5
+ streetAddress?: string;
6
+ addressLocality?: string;
7
+ addressRegion?: string;
8
+ postalCode?: string;
9
+ addressCountry?: string;
10
+ }
11
+ /**
12
+ * Contact point (Schema.org ContactPoint). Used for Organization JSON-LD.
13
+ */
14
+ export interface ContactPoint {
15
+ email?: string;
16
+ telephone?: string;
17
+ /** e.g. "customer support", "sales", "press". */
18
+ contactType?: string;
19
+ /** ISO country codes (e.g. ["US", "FR"]) or region names. */
20
+ areaServed?: string | string[];
21
+ /** BCP-47 codes (e.g. ["en", "fr"]). */
22
+ availableLanguage?: string | string[];
23
+ }
1
24
  /**
2
25
  * Publisher / Organization fields for JSON-LD (extends siteName + siteUrl).
3
26
  */
@@ -10,6 +33,16 @@ export interface SiteOrganization {
10
33
  logo?: string;
11
34
  /** Official profiles (Schema.org sameAs) */
12
35
  sameAs?: string[];
36
+ /** ISO date string. */
37
+ foundingDate?: string;
38
+ /** Founder name (rendered as Person). */
39
+ founder?: string;
40
+ /** PostalAddress fields. */
41
+ address?: PostalAddress;
42
+ /** ContactPoint fields. */
43
+ contactPoint?: ContactPoint;
44
+ /** Wikidata entity URL (e.g. https://www.wikidata.org/wiki/Q1234567). Auto-merged into sameAs. */
45
+ wikidata?: string;
13
46
  }
14
47
  /**
15
48
  * Blog configuration
@@ -85,6 +118,20 @@ export interface BlogPostFrontmatter {
85
118
  readingTime?: number;
86
119
  /** Per-post hreflang map (overrides config.alternateLanguages for this post) */
87
120
  alternateLanguages?: Record<string, string>;
121
+ /** Last-updated date. Drives `dateModified` in JSON-LD and `lastmod` in sitemaps. */
122
+ updated?: string;
123
+ /** Topic-cluster identifier. Posts sharing the same series belong to the same pillar. */
124
+ series?: string;
125
+ /** Display name for the series pillar page (defaults to humanized series slug). */
126
+ seriesTitle?: string;
127
+ /** Sort order within a series (lower first). Falls back to date when absent. */
128
+ seriesOrder?: number;
129
+ /** E-E-A-T: editor / reviewer who validated the post. */
130
+ reviewedBy?: string;
131
+ /** E-E-A-T: fact-checker. */
132
+ factCheckedBy?: string;
133
+ /** E-E-A-T: date of last editorial review (yyyy-mm-dd). */
134
+ lastReviewed?: string;
88
135
  [key: string]: unknown;
89
136
  }
90
137
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -6,11 +6,14 @@ 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 { getPostInAllLocales, getPostsByAuthor, getPostsBySeries, getAllAuthorSlugs, getAllSeriesSlugs, slugifyAuthor, slugifySeries, authorNamesFromFrontmatter, } from './core/queries.js';
10
+ export { generateLlmsTxt, generateLlmsFullTxt } from './core/llms.js';
11
+ export type { LlmsTxtOptions } from './core/llms.js';
9
12
  export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, generateRSSFeed, generateOrganizationSchema, getBlogSitemapEntries, } from './core/seo.js';
10
- export type { BlogPostSchemaOptions, BlogSitemapEntry } from './core/seo.js';
13
+ export type { BlogPostSchemaOptions, BlogPostSchemaGraphOptions, BlogSitemapEntry, GenerateBlogPostMetadataOptions, } from './core/seo.js';
11
14
  export { getBlogSitemap, getBlogRobots, createRssFeedResponse, } from './next.js';
12
15
  export { createConfig, loadConfig, getConfig } from './core/config.js';
13
- export type { BlogPost, BlogPostMetadata, BlogPostFrontmatter, GetBlogPostOptions, Author, Config, SiteOrganization, } from './core/types.js';
16
+ export type { BlogPost, BlogPostMetadata, BlogPostFrontmatter, GetBlogPostOptions, Author, Config, SiteOrganization, PostalAddress, ContactPoint, } from './core/types.js';
14
17
  export { MdxBlogError, BlogPostNotFoundError, FileReadError, DirectoryError, } from './core/errors.js';
15
18
  export { POSTS_DIR_NAME, getPostsDirectory } from './core/constants.js';
16
19
  export { calculateReadingTime, calculateWordCount, normalizeAuthors } from './core/utils.js';
@@ -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,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"}
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,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,0BAA0B,GAC3B,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGrD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,sBAAsB,EACtB,yBAAyB,EACzB,2BAA2B,EAC3B,eAAe,EACf,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,qBAAqB,EACrB,0BAA0B,EAC1B,gBAAgB,EAChB,+BAA+B,GAChC,MAAM,eAAe,CAAC;AAEvB,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,EAChB,aAAa,EACb,YAAY,GACb,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
@@ -5,6 +5,10 @@ export { OgImage } from './components/OgImage.js';
5
5
  export { BlogPostSEO } from './components/BlogPostSEO.js';
6
6
  // Utilities
7
7
  export { getBlogPost, getAllBlogPosts, getAllBlogPostSlugs } from './core/file-utils.js';
8
+ // Query helpers (locale-aware, series, authors)
9
+ export { getPostInAllLocales, getPostsByAuthor, getPostsBySeries, getAllAuthorSlugs, getAllSeriesSlugs, slugifyAuthor, slugifySeries, authorNamesFromFrontmatter, } from './core/queries.js';
10
+ // llms.txt generators
11
+ export { generateLlmsTxt, generateLlmsFullTxt } from './core/llms.js';
8
12
  // SEO
9
13
  export { generateBlogPostMetadata, generateBlogListMetadata, generateBlogPostSchema, generateBreadcrumbsSchema, generateBlogPostSchemaGraph, generateRSSFeed, generateOrganizationSchema, getBlogSitemapEntries, } from './core/seo.js';
10
14
  export { getBlogSitemap, getBlogRobots, createRssFeedResponse, } from './next.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next-md-blog/core",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "A React library for parsing and displaying markdown blog posts in Next.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",