@xexr/blog-platform 1.1.5 → 1.1.7

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/dist/sitemap.mjs CHANGED
@@ -1,6 +1,6 @@
1
- function O(t){let e=new Map;for(let r of t)r.author?.slug&&e.set(r.author.slug,L(e.get(r.author.slug),r.author));return Array.from(e.values()).sort((r,i)=>r.name.localeCompare(i.name))}function L(t,e){return t?{...t,bio:t.bio??e.bio,avatarUrl:t.avatarUrl??e.avatarUrl,pronouns:t.pronouns??e.pronouns,socialLinks:t.socialLinks??e.socialLinks,toneAdjectives:t.toneAdjectives.length>0?t.toneAdjectives:e.toneAdjectives}:e}function R(t,e=true){let r=t.trim();return r=r.replace(/\/+$/,""),/^https?:\/\//.exec(r)||(r=`${e?"https":"http"}://${r}`),e&&r.startsWith("http://")&&(r=r.replace("http://","https://")),r}function k(t){let e=t.trim();if(!e)throw new Error("Slug cannot be empty");if(!/^[a-z0-9-]+$/.test(e))throw new Error("Slug can only contain lowercase letters, numbers, and hyphens");let i=e.replace(/-+/g,"-").replace(/^-+|-+$/g,"");if(!i)throw new Error("Slug cannot consist only of hyphens");return i}function _(t,e={}){let{forceHttps:r=true,removeTrailingSlash:i=true,domain:n=t.site.domain,pathPrefix:a="/blog"}=e;if(!n)throw new Error("Domain is required for canonical URL generation");if(!t.slug)throw new Error("Article slug is required for canonical URL generation");let l=R(n,r),P=k(t.slug),b=a.replace(/^(?!\/)/,"/").replace(/\/+$/,""),o=`${l}${b}/${P}`;return i&&(o=o.replace(/\/+$/,"")),o}function U(t){let e=new Map;for(let r of t)r.series&&(!e.has(r.series.slug)||r.series.articles.length>(e.get(r.series.slug)?.articles.length??0))&&e.set(r.series.slug,r.series);return Array.from(e.values()).sort((r,i)=>r.title.localeCompare(i.title))}function q(t){let e=new Map;for(let r of t)for(let i of r.topics??[])e.has(i.slug)||e.set(i.slug,i);return Array.from(e.values()).sort((r,i)=>r.name.localeCompare(i.name))}function D(t="/blog"){let e=t.trim();return !e||e==="/"?"":e.replace(/^(?!\/)/,"/").replace(/\/+$/,"")}function y(t,e){let r=D(t),i=e?.replace(/^\/+/,"").replace(/\/+$/,"");return i?`${r}/${i}`.replace(/\/+/g,"/"):r||"/"}function B(t,e){return y(t,`authors/${e}`)}function F(t,e){return y(t,`series/${e}`)}async function C(t,e,r=[],i={}){let{includeAuthorPages:n=true,includeTopicPages:a=true,includeSeriesPages:l=true,includePaginationPages:P=true,articlesPerPage:b=10,maxEntries:o=5e4,changeFrequency:u={},priority:c={}}=i,s=[],h=R(i.canonical?.domain??t.domain,i.canonical?.forceHttps??true),m=i.canonical?.pathPrefix??"/blog",v=r.length>0?r:O(e),S=q([...e]),x=U([...e]);if(s.push({url:`${h}${m}`,lastModified:f(e),changeFrequency:u.homePage??"daily",priority:c.homePage??1}),e.forEach(p=>{if(s.length>=o)return;let d=_(p,i.canonical);s.push({url:d,lastModified:p.updatedAt,changeFrequency:u.articles??"monthly",priority:c.articles??.8});}),a&&S.length>0&&(s.length<o&&s.push({url:`${h}${y(m,"topics")}`,lastModified:f(e),changeFrequency:u.topicPages??"weekly",priority:c.topicPages??.6}),S.forEach(p=>{if(s.length>=o)return;let d=e.filter(w=>(w.topics??[]).some(g=>g.slug===p.slug));s.push({url:`${h}${y(m,`topics/${p.slug}`)}`,lastModified:f(d),changeFrequency:u.topicPages??"weekly",priority:c.topicPages??.6});})),l&&x.length>0&&(s.length<o&&s.push({url:`${h}${y(m,"series")}`,lastModified:f(e),changeFrequency:u.seriesPages??"weekly",priority:c.seriesPages??.6}),x.forEach(p=>{if(s.length>=o)return;let d=e.filter(w=>w.series?.slug===p.slug);s.push({url:`${h}${F(m,p.slug)}`,lastModified:f(d),changeFrequency:u.seriesPages??"weekly",priority:c.seriesPages??.6});})),n&&v.length>0&&(s.length<o&&s.push({url:`${h}${y(m,"authors")}`,lastModified:f(e),changeFrequency:u.authorPages??"weekly",priority:c.authorPages??.6}),v.forEach(p=>{if(s.length>=o)return;let d=`${h}${B(m,p.slug)}`,w=e.filter(A=>A.author?.id===p.id),g=f(w);s.push({url:d,lastModified:g,changeFrequency:u.authorPages??"weekly",priority:c.authorPages??.6});})),P&&e.length>b){let p=Math.ceil(e.length/b);for(let d=2;d<=p&&!(s.length>=o);d++)s.push({url:`${h}${m}?page=${d}`,lastModified:f(e),changeFrequency:u.paginationPages??"daily",priority:c.paginationPages??.5});}return s}function te(t,e,r=[],i={}){let{includeAuthorPages:n=true,includeTopicPages:a=true,includeSeriesPages:l=true,includePaginationPages:P=true,articlesPerPage:b=10,maxEntries:o=5e4,changeFrequency:u={},priority:c={}}=i,s=[],h=R(i.canonical?.domain??t.domain,i.canonical?.forceHttps??true),m=i.canonical?.pathPrefix??"/blog",v=new Date().toISOString(),S=r.length>0?r:O(e),x=q([...e]),p=U([...e]),d=1;if(d+=e.length,a&&x.length>0&&(d+=x.length+1),l&&p.length>0&&(d+=p.length+1),n&&(d+=S.length>0?S.length+1:0),P&&e.length>b){let g=Math.ceil(e.length/b);d+=g-1;}let w=d>o;if(s.push({loc:`${h}${m}`,lastmod:f(e),changefreq:u.homePage??"daily",priority:c.homePage??1}),e.forEach(g=>{if(s.length>=o)return;let A=_(g,i.canonical);s.push({loc:A,lastmod:g.updatedAt,changefreq:u.articles??"monthly",priority:c.articles??.8});}),a&&x.length>0&&(s.length<o&&s.push({loc:`${h}${y(m,"topics")}`,lastmod:f(e),changefreq:u.topicPages??"weekly",priority:c.topicPages??.6}),x.forEach(g=>{if(s.length>=o)return;let A=e.filter($=>($.topics??[]).some(T=>T.slug===g.slug));s.push({loc:`${h}${y(m,`topics/${g.slug}`)}`,lastmod:f(A),changefreq:u.topicPages??"weekly",priority:c.topicPages??.6});})),l&&p.length>0&&(s.length<o&&s.push({loc:`${h}${y(m,"series")}`,lastmod:f(e),changefreq:u.seriesPages??"weekly",priority:c.seriesPages??.6}),p.forEach(g=>{if(s.length>=o)return;let A=e.filter($=>$.series?.slug===g.slug);s.push({loc:`${h}${F(m,g.slug)}`,lastmod:f(A),changefreq:u.seriesPages??"weekly",priority:c.seriesPages??.6});})),n&&S.length>0&&(s.length<o&&s.push({loc:`${h}${y(m,"authors")}`,lastmod:f(e),changefreq:u.authorPages??"weekly",priority:c.authorPages??.6}),S.forEach(g=>{if(s.length>=o)return;let A=`${h}${B(m,g.slug)}`,$=e.filter(M=>M.author?.id===g.id),T=f($);s.push({loc:A,lastmod:T,changefreq:u.authorPages??"weekly",priority:c.authorPages??.6});})),P&&e.length>b){let g=Math.ceil(e.length/b);for(let A=2;A<=g&&!(s.length>=o);A++)s.push({loc:`${h}${m}?page=${A}`,lastmod:f(e),changefreq:u.paginationPages??"daily",priority:c.paginationPages??.5});}return {entries:s,totalEntries:s.length,truncated:w,generatedAt:v,baseUrl:h}}function G(t){let e='<?xml version="1.0" encoding="UTF-8"?>',r='<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',i="</urlset>",n=t.entries.map(a=>{let l=[];return l.push(" <url>"),l.push(` <loc>${E(a.loc)}</loc>`),a.lastmod&&l.push(` <lastmod>${E(a.lastmod)}</lastmod>`),a.changefreq&&l.push(` <changefreq>${E(a.changefreq)}</changefreq>`),a.priority!==void 0&&l.push(` <priority>${a.priority.toFixed(1)}</priority>`),l.push(" </url>"),l.join(`
1
+ function v(t){let e=new Map;for(let r of t)r.author?.slug&&e.set(r.author.slug,F(e.get(r.author.slug),r.author));return Array.from(e.values()).sort((r,i)=>r.name.localeCompare(i.name))}function F(t,e){return t?{...t,bio:t.bio??e.bio,avatarUrl:t.avatarUrl??e.avatarUrl,pronouns:t.pronouns??e.pronouns,socialLinks:t.socialLinks??e.socialLinks,toneAdjectives:t.toneAdjectives.length>0?t.toneAdjectives:e.toneAdjectives}:e}function w(t,e=true){let r=t.trim();return r=r.replace(/\/+$/,""),/^https?:\/\//.exec(r)||(r=`${e?"https":"http"}://${r}`),e&&r.startsWith("http://")&&(r=r.replace("http://","https://")),r}function M(t){let e=t.trim();if(!e)throw new Error("Slug cannot be empty");if(!/^[a-z0-9-]+$/.test(e))throw new Error("Slug can only contain lowercase letters, numbers, and hyphens");let i=e.replace(/-+/g,"-").replace(/^-+|-+$/g,"");if(!i)throw new Error("Slug cannot consist only of hyphens");return i}function T(t,e={}){let{forceHttps:r=true,removeTrailingSlash:i=true,domain:n=t.site.domain,pathPrefix:o="/blog"}=e;if(!n)throw new Error("Domain is required for canonical URL generation");if(!t.slug)throw new Error("Article slug is required for canonical URL generation");let u=w(n,r),l=M(t.slug),c=o.replace(/^(?!\/)/,"/").replace(/\/+$/,""),a=`${u}${c}/${l}`;return i&&(a=a.replace(/\/+$/,"")),a}function O(t){let e=new Map;for(let r of t)r.series&&(!e.has(r.series.slug)||r.series.articles.length>(e.get(r.series.slug)?.articles.length??0))&&e.set(r.series.slug,r.series);return Array.from(e.values()).sort((r,i)=>r.title.localeCompare(i.title))}function _(t){let e=new Map;for(let r of t)for(let i of r.topics??[])e.has(i.slug)||e.set(i.slug,i);return Array.from(e.values()).sort((r,i)=>r.name.localeCompare(i.name))}function L(t="/blog"){let e=t.trim();return !e||e==="/"?"":e.replace(/^(?!\/)/,"/").replace(/\/+$/,"")}function P(t,e){let r=L(t),i=e?.replace(/^\/+/,"").replace(/\/+$/,"");return i?`${r}/${i}`.replace(/\/+/g,"/"):r||"/"}function U(t,e){return P(t,`authors/${e}`)}function B(t,e){return P(t,`series/${e}`)}async function q(t,e,r=[],i={}){let{includeAuthorPages:n=true,includeTopicPages:o=true,includeSeriesPages:u=true,maxEntries:l=5e4,changeFrequency:c={},priority:a={}}=i,s=[],h=w(i.canonical?.domain??t.domain,i.canonical?.forceHttps??true),p=i.canonical?.pathPrefix??"/blog",$=r.length>0?r:v(e),b=_([...e]),y=O([...e]);return s.push({url:`${h}${p}`,lastModified:m(e),changeFrequency:c.homePage??"daily",priority:a.homePage??1}),e.forEach(g=>{if(s.length>=l)return;let f=T(g,i.canonical);s.push({url:f,lastModified:g.updatedAt,changeFrequency:c.articles??"monthly",priority:a.articles??.8});}),o&&b.length>0&&(s.length<l&&s.push({url:`${h}${P(p,"topics")}`,lastModified:m(e),changeFrequency:c.topicPages??"weekly",priority:a.topicPages??.6}),b.forEach(g=>{if(s.length>=l)return;let f=e.filter(S=>(S.topics??[]).some(d=>d.slug===g.slug));s.push({url:`${h}${P(p,`topics/${g.slug}`)}`,lastModified:m(f),changeFrequency:c.topicPages??"weekly",priority:a.topicPages??.6});})),u&&y.length>0&&(s.length<l&&s.push({url:`${h}${P(p,"series")}`,lastModified:m(e),changeFrequency:c.seriesPages??"weekly",priority:a.seriesPages??.6}),y.forEach(g=>{if(s.length>=l)return;let f=e.filter(S=>S.series?.slug===g.slug);s.push({url:`${h}${B(p,g.slug)}`,lastModified:m(f),changeFrequency:c.seriesPages??"weekly",priority:a.seriesPages??.6});})),n&&$.length>0&&(s.length<l&&s.push({url:`${h}${P(p,"authors")}`,lastModified:m(e),changeFrequency:c.authorPages??"weekly",priority:a.authorPages??.6}),$.forEach(g=>{if(s.length>=l)return;let f=`${h}${U(p,g.slug)}`,S=e.filter(A=>A.author?.id===g.id),d=m(S);s.push({url:f,lastModified:d,changeFrequency:c.authorPages??"weekly",priority:a.authorPages??.6});})),s}function K(t,e,r=[],i={}){let{includeAuthorPages:n=true,includeTopicPages:o=true,includeSeriesPages:u=true,maxEntries:l=5e4,changeFrequency:c={},priority:a={}}=i,s=[],h=w(i.canonical?.domain??t.domain,i.canonical?.forceHttps??true),p=i.canonical?.pathPrefix??"/blog",$=new Date().toISOString(),b=r.length>0?r:v(e),y=_([...e]),g=O([...e]),f=1;f+=e.length,o&&y.length>0&&(f+=y.length+1),u&&g.length>0&&(f+=g.length+1),n&&(f+=b.length>0?b.length+1:0);let S=f>l;return s.push({loc:`${h}${p}`,lastmod:m(e),changefreq:c.homePage??"daily",priority:a.homePage??1}),e.forEach(d=>{if(s.length>=l)return;let A=T(d,i.canonical);s.push({loc:A,lastmod:d.updatedAt,changefreq:c.articles??"monthly",priority:a.articles??.8});}),o&&y.length>0&&(s.length<l&&s.push({loc:`${h}${P(p,"topics")}`,lastmod:m(e),changefreq:c.topicPages??"weekly",priority:a.topicPages??.6}),y.forEach(d=>{if(s.length>=l)return;let A=e.filter(x=>(x.topics??[]).some(E=>E.slug===d.slug));s.push({loc:`${h}${P(p,`topics/${d.slug}`)}`,lastmod:m(A),changefreq:c.topicPages??"weekly",priority:a.topicPages??.6});})),u&&g.length>0&&(s.length<l&&s.push({loc:`${h}${P(p,"series")}`,lastmod:m(e),changefreq:c.seriesPages??"weekly",priority:a.seriesPages??.6}),g.forEach(d=>{if(s.length>=l)return;let A=e.filter(x=>x.series?.slug===d.slug);s.push({loc:`${h}${B(p,d.slug)}`,lastmod:m(A),changefreq:c.seriesPages??"weekly",priority:a.seriesPages??.6});})),n&&b.length>0&&(s.length<l&&s.push({loc:`${h}${P(p,"authors")}`,lastmod:m(e),changefreq:c.authorPages??"weekly",priority:a.authorPages??.6}),b.forEach(d=>{if(s.length>=l)return;let A=`${h}${U(p,d.slug)}`,x=e.filter(C=>C.author?.id===d.id),E=m(x);s.push({loc:A,lastmod:E,changefreq:c.authorPages??"weekly",priority:a.authorPages??.6});})),{entries:s,totalEntries:s.length,truncated:S,generatedAt:$,baseUrl:h}}function k(t){let e='<?xml version="1.0" encoding="UTF-8"?>',r='<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',i="</urlset>",n=t.entries.map(o=>{let u=[];return u.push(" <url>"),u.push(` <loc>${R(o.loc)}</loc>`),o.lastmod&&u.push(` <lastmod>${R(o.lastmod)}</lastmod>`),o.changefreq&&u.push(` <changefreq>${R(o.changefreq)}</changefreq>`),o.priority!==void 0&&u.push(` <priority>${o.priority.toFixed(1)}</priority>`),u.push(" </url>"),u.join(`
2
2
  `)});return [e,r,...n,i].join(`
3
- `)}function re(t,e){let r='<?xml version="1.0" encoding="UTF-8"?>',i='<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',n="</sitemapindex>",a=t.map(l=>{let P=[];return P.push(" <sitemap>"),P.push(` <loc>${E(l.url)}</loc>`),l.lastmod&&P.push(` <lastmod>${E(l.lastmod)}</lastmod>`),P.push(" </sitemap>"),P.join(`
4
- `)});return [r,i,...a,n].join(`
5
- `)}function ie(t){let e=[],r=[];t.entries.length===0?e.push("Sitemap must contain at least one URL"):t.entries.length>5e4&&e.push("Sitemap contains more than 50,000 URLs (Google limit)"),t.truncated&&r.push("Sitemap was truncated due to size limits"),t.entries.forEach((n,a)=>{try{new URL(n.loc);}catch{e.push(`Invalid URL at entry ${a+1}: ${n.loc}`);}n.priority!==void 0&&(n.priority<0||n.priority>1)&&e.push(`Invalid priority at entry ${a+1}: must be between 0.0 and 1.0`),n.lastmod&&!I(n.lastmod)&&e.push(`Invalid lastmod date at entry ${a+1}: ${n.lastmod}`),n.loc.length>2048&&r.push(`URL at entry ${a+1} exceeds 2048 character limit`);});let i=new Map;return t.entries.forEach(n=>{let a=i.get(n.loc)??0;i.set(n.loc,a+1);}),i.forEach((n,a)=>{n>1&&e.push(`Duplicate URL found: ${a} (appears ${n} times)`);}),{valid:e.length===0,errors:e,warnings:r}}function f(t){return t.length===0?new Date().toISOString():t.reduce((r,i)=>{let n=new Date(i.updatedAt);return n>r?n:r},new Date(t[0].updatedAt)).toISOString()}function se(t,e,r=[],i={}){return async function(){let l=(await C(t,e,r,i)).map(o=>({loc:o.url,lastmod:typeof o.lastModified=="string"?o.lastModified:o.lastModified?.toISOString(),changefreq:o.changeFrequency,priority:o.priority})),P={entries:l,totalEntries:l.length,generatedAt:new Date().toISOString(),baseUrl:R(i.canonical?.domain??t.domain,i.canonical?.forceHttps??true)},b=G(P);return new Response(b,{headers:{"Content-Type":"application/xml","Cache-Control":"public, max-age=3600, s-maxage=3600"}})}}function ne(t){return async function(){let r=[...t.staticRoutes??[],...t.getStaticRoutes?await t.getStaticRoutes():[]],i=await t.getSite(),n=await t.getArticles(),a=t.getAuthors?await t.getAuthors():[];return [...r,...await C(i,n,a,t.options)]}}function E(t){let e={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;"};return t.replace(/[&<>"']/g,r=>e[r]??r)}function I(t){try{let e=new Date(t),r=e.toISOString(),i=t.endsWith("Z")&&!t.includes(".")?t.replace("Z",".000Z"):t;return r===i&&!isNaN(e.getTime())}catch{return false}}export{se as createBlogSitemapRoute,ne as createDynamicBlogSitemap,C as generateBlogSitemapEntries,te as generateSitemap,re as generateSitemapIndex,G as sitemapToXml,ie as validateSitemap};//# sourceMappingURL=sitemap.mjs.map
3
+ `)}function ee(t,e){let r='<?xml version="1.0" encoding="UTF-8"?>',i='<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',n="</sitemapindex>",o=t.map(u=>{let l=[];return l.push(" <sitemap>"),l.push(` <loc>${R(u.url)}</loc>`),u.lastmod&&l.push(` <lastmod>${R(u.lastmod)}</lastmod>`),l.push(" </sitemap>"),l.join(`
4
+ `)});return [r,i,...o,n].join(`
5
+ `)}function te(t){let e=[],r=[];t.entries.length===0?e.push("Sitemap must contain at least one URL"):t.entries.length>5e4&&e.push("Sitemap contains more than 50,000 URLs (Google limit)"),t.truncated&&r.push("Sitemap was truncated due to size limits"),t.entries.forEach((n,o)=>{try{new URL(n.loc);}catch{e.push(`Invalid URL at entry ${o+1}: ${n.loc}`);}n.priority!==void 0&&(n.priority<0||n.priority>1)&&e.push(`Invalid priority at entry ${o+1}: must be between 0.0 and 1.0`),n.lastmod&&!D(n.lastmod)&&e.push(`Invalid lastmod date at entry ${o+1}: ${n.lastmod}`),n.loc.length>2048&&r.push(`URL at entry ${o+1} exceeds 2048 character limit`);});let i=new Map;return t.entries.forEach(n=>{let o=i.get(n.loc)??0;i.set(n.loc,o+1);}),i.forEach((n,o)=>{n>1&&e.push(`Duplicate URL found: ${o} (appears ${n} times)`);}),{valid:e.length===0,errors:e,warnings:r}}function m(t){return t.length===0?new Date().toISOString():t.reduce((r,i)=>{let n=new Date(i.updatedAt);return n>r?n:r},new Date(t[0].updatedAt)).toISOString()}function re(t,e,r=[],i={}){return async function(){let u=(await q(t,e,r,i)).map(a=>({loc:a.url,lastmod:typeof a.lastModified=="string"?a.lastModified:a.lastModified?.toISOString(),changefreq:a.changeFrequency,priority:a.priority})),l={entries:u,totalEntries:u.length,generatedAt:new Date().toISOString(),baseUrl:w(i.canonical?.domain??t.domain,i.canonical?.forceHttps??true)},c=k(l);return new Response(c,{headers:{"Content-Type":"application/xml","Cache-Control":"public, max-age=3600, s-maxage=3600"}})}}function ie(t){return async function(){let r=[...t.staticRoutes??[],...t.getStaticRoutes?await t.getStaticRoutes():[]],i=await t.getSite(),n=await t.getArticles(),o=t.getAuthors?await t.getAuthors():[];return [...r,...await q(i,n,o,t.options)]}}function R(t){let e={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;"};return t.replace(/[&<>"']/g,r=>e[r]??r)}function D(t){try{let e=new Date(t),r=e.toISOString(),i=t.endsWith("Z")&&!t.includes(".")?t.replace("Z",".000Z"):t;return r===i&&!isNaN(e.getTime())}catch{return false}}export{re as createBlogSitemapRoute,ie as createDynamicBlogSitemap,q as generateBlogSitemapEntries,K as generateSitemap,ee as generateSitemapIndex,k as sitemapToXml,te as validateSitemap};//# sourceMappingURL=sitemap.mjs.map
6
6
  //# sourceMappingURL=sitemap.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/authors.ts","../src/lib/seo.ts","../src/lib/series.ts","../src/lib/topics.ts","../src/lib/urls.ts","../src/lib/sitemap.ts"],"names":["getAuthorsFromArticles","articles","authorsBySlug","article","mergeAuthorProfile","a","b","current","next","normalizeDomain","domain","forceHttps","normalized","normalizeSlug","slug","trimmed","clean","generateCanonicalUrl","options","removeTrailingSlash","pathPrefix","normalizedDomain","normalizedSlug","cleanPrefix","canonicalUrl","getAllSeries","seriesBySlug","getAllTopics","topicsBySlug","topic","normalizeBlogBasePath","basePath","buildBlogPath","normalizedBasePath","cleanSlug","buildAuthorPath","authorSlug","buildSeriesPath","seriesSlug","generateBlogSitemapEntries","site","authors","includeAuthorPages","includeTopicPages","includeSeriesPages","includePaginationPages","articlesPerPage","maxEntries","changeFrequency","priority","entries","baseUrl","sitemapAuthors","sitemapTopics","sitemapSeries","getLatestModificationDate","articleUrl","topicArticles","candidate","series","seriesArticles","author","authorUrl","authorArticles","latestAuthorMod","totalPages","page","generateSitemap","generatedAt","potentialEntries","willTruncate","sitemapToXml","sitemapData","xmlHeader","urlsetOpen","urlsetClose","urlEntries","entry","elements","escapeXml","generateSitemapIndex","sitemaps","_baseUrl","indexOpen","indexClose","sitemapEntries","sitemap","validateSitemap","errors","warnings","index","isValidISODate","urlCounts","count","url","latest","articleDate","createBlogSitemapRoute","xmlEntries","xml","createDynamicBlogSitemap","config","staticRoutes","text","xmlEscapes","char","dateString","date","isoString","normalizedInput"],"mappings":"AAcO,SAASA,EACdC,CAAAA,CACmB,CACnB,IAAMC,CAAAA,CAAgB,IAAI,IAE1B,IAAA,IAAWC,CAAAA,IAAWF,EAChBE,CAAAA,CAAQ,MAAA,EAAQ,MAClBD,CAAAA,CAAc,GAAA,CACZC,EAAQ,MAAA,CAAO,IAAA,CACfC,EACEF,CAAAA,CAAc,GAAA,CAAIC,EAAQ,MAAA,CAAO,IAAI,EACrCA,CAAAA,CAAQ,MACV,CACF,CAAA,CAIJ,OAAO,MAAM,IAAA,CAAKD,CAAAA,CAAc,QAAQ,CAAA,CAAE,KAAK,CAACG,CAAAA,CAAGC,IACjDD,CAAAA,CAAE,IAAA,CAAK,cAAcC,CAAAA,CAAE,IAAI,CAC7B,CACF,CA8CA,SAASF,CAAAA,CACPG,EACAC,CAAAA,CACiB,CACjB,OAAKD,CAAAA,CAIE,CACL,GAAGA,CAAAA,CACH,GAAA,CAAKA,EAAQ,GAAA,EAAOC,CAAAA,CAAK,IACzB,SAAA,CAAWD,CAAAA,CAAQ,WAAaC,CAAAA,CAAK,SAAA,CACrC,SAAUD,CAAAA,CAAQ,QAAA,EAAYC,EAAK,QAAA,CACnC,WAAA,CAAaD,EAAQ,WAAA,EAAeC,CAAAA,CAAK,YACzC,cAAA,CACED,CAAAA,CAAQ,eAAe,MAAA,CAAS,CAAA,CAC5BA,EAAQ,cAAA,CACRC,CAAAA,CAAK,cACb,CAAA,CAbSA,CAcX,CC3DO,SAASC,CAAAA,CAAgBC,CAAAA,CAAgBC,CAAAA,CAAa,KAAc,CACzE,IAAIC,EAAaF,CAAAA,CAAO,IAAA,GAGxB,OAAAE,CAAAA,CAAaA,EAAW,OAAA,CAAQ,MAAA,CAAQ,EAAE,CAAA,CAGrC,cAAA,CAAe,KAAKA,CAAU,CAAA,GACjCA,EAAa,CAAA,EAAGD,CAAAA,CAAa,QAAU,MAAM,CAAA,GAAA,EAAMC,CAAU,CAAA,CAAA,CAAA,CAI3DD,CAAAA,EAAcC,EAAW,UAAA,CAAW,SAAS,IAC/CA,CAAAA,CAAaA,CAAAA,CAAW,QAAQ,SAAA,CAAW,UAAU,GAGhDA,CACT,CAgBO,SAASC,CAAAA,CAAcC,CAAAA,CAAsB,CAClD,IAAMC,CAAAA,CAAUD,EAAK,IAAA,EAAK,CAE1B,GAAI,CAACC,CAAAA,CACH,MAAM,IAAI,KAAA,CAAM,sBAAsB,CAAA,CAIxC,GAAI,CAAC,cAAA,CAAe,IAAA,CAAKA,CAAO,CAAA,CAC9B,MAAM,IAAI,KAAA,CAAM,+DAA+D,EAOjF,IAAMC,CAAAA,CAHaD,EAAQ,OAAA,CAAQ,KAAA,CAAO,GAAG,CAAA,CAGpB,OAAA,CAAQ,WAAY,EAAE,CAAA,CAE/C,GAAI,CAACC,CAAAA,CACH,MAAM,IAAI,KAAA,CAAM,qCAAqC,CAAA,CAGvD,OAAOA,CACT,CAyBO,SAASC,EACdd,CAAAA,CACAe,CAAAA,CAA+B,EAAC,CACxB,CACR,GAAM,CACJ,WAAAP,CAAAA,CAAa,IAAA,CACb,oBAAAQ,CAAAA,CAAsB,IAAA,CACtB,OAAAT,CAAAA,CAASP,CAAAA,CAAQ,KAAK,MAAA,CACtB,UAAA,CAAAiB,EAAa,OACf,CAAA,CAAIF,EAEJ,GAAI,CAACR,EACH,MAAM,IAAI,MAAM,iDAAiD,CAAA,CAGnE,GAAI,CAACP,CAAAA,CAAQ,KACX,MAAM,IAAI,MAAM,uDAAuD,CAAA,CAIzE,IAAMkB,CAAAA,CAAmBZ,CAAAA,CAAgBC,EAAQC,CAAU,CAAA,CACrDW,EAAiBT,CAAAA,CAAcV,CAAAA,CAAQ,IAAI,CAAA,CAG3CoB,EAAcH,CAAAA,CACjB,OAAA,CAAQ,UAAW,GAAG,CAAA,CACtB,QAAQ,MAAA,CAAQ,EAAE,EAGjBI,CAAAA,CAAe,CAAA,EAAGH,CAAgB,CAAA,EAAGE,CAAW,IAAID,CAAc,CAAA,CAAA,CAGtE,OAAIH,CAAAA,GACFK,CAAAA,CAAeA,EAAa,OAAA,CAAQ,MAAA,CAAQ,EAAE,CAAA,CAAA,CAGzCA,CACT,CCpJO,SAASC,CAAAA,CACdxB,EAC0B,CAC1B,IAAMyB,EAAe,IAAI,GAAA,CAEzB,QAAWvB,CAAAA,IAAWF,CAAAA,CAElBE,EAAQ,MAAA,GACP,CAACuB,EAAa,GAAA,CAAIvB,CAAAA,CAAQ,MAAA,CAAO,IAAI,GACpCA,CAAAA,CAAQ,MAAA,CAAO,SAAS,MAAA,EACrBuB,CAAAA,CAAa,IAAIvB,CAAAA,CAAQ,MAAA,CAAO,IAAI,CAAA,EAAG,QAAA,CAAS,QAAU,CAAA,CAAA,CAAA,EAE/DuB,CAAAA,CAAa,IAAIvB,CAAAA,CAAQ,MAAA,CAAO,KAAMA,CAAAA,CAAQ,MAAM,EAIxD,OAAO,KAAA,CAAM,KAAKuB,CAAAA,CAAa,MAAA,EAAQ,CAAA,CAAE,IAAA,CAAK,CAACrB,CAAAA,CAAGC,CAAAA,GAChDD,EAAE,KAAA,CAAM,aAAA,CAAcC,EAAE,KAAK,CAC/B,CACF,CCfO,SAASqB,EAAa1B,CAAAA,CAAgD,CAC3E,IAAM2B,CAAAA,CAAe,IAAI,IAEzB,IAAA,IAAWzB,CAAAA,IAAWF,EACpB,IAAA,IAAW4B,CAAAA,IAAS1B,EAAQ,MAAA,EAAU,GAC/ByB,CAAAA,CAAa,GAAA,CAAIC,EAAM,IAAI,CAAA,EAC9BD,EAAa,GAAA,CAAIC,CAAAA,CAAM,KAAMA,CAAK,CAAA,CAKxC,OAAO,KAAA,CAAM,IAAA,CAAKD,EAAa,MAAA,EAAQ,EAAE,IAAA,CAAK,CAACvB,EAAGC,CAAAA,GAChDD,CAAAA,CAAE,KAAK,aAAA,CAAcC,CAAAA,CAAE,IAAI,CAC7B,CACF,CCxBO,SAASwB,CAAAA,CAAsBC,EAAW,OAAA,CAAiB,CAChE,IAAMhB,CAAAA,CAAUgB,EAAS,IAAA,EAAK,CAC9B,OAAI,CAAChB,CAAAA,EAAWA,IAAY,GAAA,CAAY,EAAA,CAEjCA,EAAQ,OAAA,CAAQ,SAAA,CAAW,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAQ,EAAE,CAC3D,CAEO,SAASiB,CAAAA,CACdD,EACAjB,CAAAA,CACQ,CACR,IAAMmB,CAAAA,CAAqBH,CAAAA,CAAsBC,CAAQ,CAAA,CACnDG,CAAAA,CAAYpB,GAAM,OAAA,CAAQ,MAAA,CAAQ,EAAE,CAAA,CAAE,OAAA,CAAQ,OAAQ,EAAE,CAAA,CAE9D,OAAKoB,CAAAA,CAIE,CAAA,EAAGD,CAAkB,CAAA,CAAA,EAAIC,CAAS,CAAA,CAAA,CAAG,OAAA,CAAQ,OAAQ,GAAG,CAAA,CAHtDD,GAAsB,GAIjC,CAEO,SAASE,CAAAA,CACdJ,CAAAA,CACAK,EACQ,CACR,OAAOJ,EAAcD,CAAAA,CAAU,CAAA,QAAA,EAAWK,CAAU,CAAA,CAAE,CACxD,CAEO,SAASC,CAAAA,CACdN,EACAO,CAAAA,CACQ,CACR,OAAON,CAAAA,CAAcD,CAAAA,CAAU,UAAUO,CAAU,CAAA,CAAE,CACvD,CCwKA,eAAsBC,EACpBC,CAAAA,CACAvC,CAAAA,CACAwC,EAAsC,EAAC,CACvCvB,EAA0B,EAAC,CACI,CAC/B,GAAM,CACJ,kBAAA,CAAAwB,CAAAA,CAAqB,KACrB,iBAAA,CAAAC,CAAAA,CAAoB,KACpB,kBAAA,CAAAC,CAAAA,CAAqB,KACrB,sBAAA,CAAAC,CAAAA,CAAyB,KACzB,eAAA,CAAAC,CAAAA,CAAkB,GAClB,UAAA,CAAAC,CAAAA,CAAa,IACb,eAAA,CAAAC,CAAAA,CAAkB,EAAC,CACnB,QAAA,CAAAC,EAAW,EACb,EAAI/B,CAAAA,CAEEgC,CAAAA,CAAgC,EAAC,CACjCC,CAAAA,CAAU1C,EACdS,CAAAA,CAAQ,SAAA,EAAW,QAAUsB,CAAAA,CAAK,MAAA,CAClCtB,EAAQ,SAAA,EAAW,UAAA,EAAc,IACnC,CAAA,CAEME,CAAAA,CAAaF,EAAQ,SAAA,EAAW,UAAA,EAAc,QAC9CkC,CAAAA,CACJX,CAAAA,CAAQ,OAAS,CAAA,CAAIA,CAAAA,CAAUzC,EAAuBC,CAAQ,CAAA,CAC1DoD,EAAgB1B,CAAAA,CAAa,CAAC,GAAG1B,CAAQ,CAAC,EAC1CqD,CAAAA,CAAgB7B,CAAAA,CAAa,CAAC,GAAGxB,CAAQ,CAAC,CAAA,CA6GhD,GA1GAiD,EAAQ,IAAA,CAAK,CACX,IAAK,CAAA,EAAGC,CAAO,GAAG/B,CAAU,CAAA,CAAA,CAC5B,aAAcmC,CAAAA,CAA0BtD,CAAQ,EAChD,eAAA,CAAiB+C,CAAAA,CAAgB,UAAY,OAAA,CAC7C,QAAA,CAAUC,EAAS,QAAA,EAAY,CACjC,CAAC,CAAA,CAGDhD,CAAAA,CAAS,OAAA,CAASE,CAAAA,EAAY,CAC5B,GAAI+C,CAAAA,CAAQ,QAAUH,CAAAA,CAAY,OAElC,IAAMS,CAAAA,CAAavC,CAAAA,CAAqBd,EAASe,CAAAA,CAAQ,SAAS,EAClEgC,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAKM,CAAAA,CACL,aAAcrD,CAAAA,CAAQ,SAAA,CACtB,gBAAiB6C,CAAAA,CAAgB,QAAA,EAAY,UAC7C,QAAA,CAAUC,CAAAA,CAAS,UAAY,EACjC,CAAC,EACH,CAAC,CAAA,CAEGN,GAAqBU,CAAAA,CAAc,MAAA,CAAS,IAC1CH,CAAAA,CAAQ,MAAA,CAASH,GACnBG,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAK,CAAA,EAAGC,CAAO,CAAA,EAAGnB,EAAcZ,CAAAA,CAAY,QAAQ,CAAC,CAAA,CAAA,CACrD,YAAA,CAAcmC,EAA0BtD,CAAQ,CAAA,CAChD,gBAAiB+C,CAAAA,CAAgB,UAAA,EAAc,SAC/C,QAAA,CAAUC,CAAAA,CAAS,YAAc,EACnC,CAAC,EAGHI,CAAAA,CAAc,OAAA,CAASxB,GAAU,CAC/B,GAAIqB,EAAQ,MAAA,EAAUH,CAAAA,CAAY,OAElC,IAAMU,CAAAA,CAAgBxD,EAAS,MAAA,CAAQE,CAAAA,EAAAA,CACpCA,EAAQ,MAAA,EAAU,IAAI,IAAA,CACpBuD,CAAAA,EAAcA,EAAU,IAAA,GAAS7B,CAAAA,CAAM,IAC1C,CACF,CAAA,CAEAqB,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAK,CAAA,EAAGC,CAAO,CAAA,EAAGnB,CAAAA,CAAcZ,EAAY,CAAA,OAAA,EAAUS,CAAAA,CAAM,IAAI,CAAA,CAAE,CAAC,GACnE,YAAA,CAAc0B,CAAAA,CAA0BE,CAAa,CAAA,CACrD,eAAA,CAAiBT,EAAgB,UAAA,EAAc,QAAA,CAC/C,SAAUC,CAAAA,CAAS,UAAA,EAAc,EACnC,CAAC,EACH,CAAC,CAAA,CAAA,CAGCL,CAAAA,EAAsBU,EAAc,MAAA,CAAS,CAAA,GAC3CJ,EAAQ,MAAA,CAASH,CAAAA,EACnBG,EAAQ,IAAA,CAAK,CACX,IAAK,CAAA,EAAGC,CAAO,GAAGnB,CAAAA,CAAcZ,CAAAA,CAAY,QAAQ,CAAC,CAAA,CAAA,CACrD,aAAcmC,CAAAA,CAA0BtD,CAAQ,EAChD,eAAA,CAAiB+C,CAAAA,CAAgB,aAAe,QAAA,CAChD,QAAA,CAAUC,EAAS,WAAA,EAAe,EACpC,CAAC,CAAA,CAGHK,CAAAA,CAAc,QAASK,CAAAA,EAAW,CAChC,GAAIT,CAAAA,CAAQ,MAAA,EAAUH,EAAY,OAElC,IAAMa,EAAiB3D,CAAAA,CAAS,MAAA,CAC7BE,GAAYA,CAAAA,CAAQ,MAAA,EAAQ,OAASwD,CAAAA,CAAO,IAC/C,EAEAT,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAK,CAAA,EAAGC,CAAO,CAAA,EAAGd,CAAAA,CAAgBjB,EAAYuC,CAAAA,CAAO,IAAI,CAAC,CAAA,CAAA,CAC1D,aAAcJ,CAAAA,CAA0BK,CAAc,EACtD,eAAA,CAAiBZ,CAAAA,CAAgB,aAAe,QAAA,CAChD,QAAA,CAAUC,EAAS,WAAA,EAAe,EACpC,CAAC,EACH,CAAC,GAICP,CAAAA,EAAsBU,CAAAA,CAAe,OAAS,CAAA,GAC5CF,CAAAA,CAAQ,OAASH,CAAAA,EACnBG,CAAAA,CAAQ,KAAK,CACX,GAAA,CAAK,GAAGC,CAAO,CAAA,EAAGnB,EAAcZ,CAAAA,CAAY,SAAS,CAAC,CAAA,CAAA,CACtD,YAAA,CAAcmC,EAA0BtD,CAAQ,CAAA,CAChD,gBAAiB+C,CAAAA,CAAgB,WAAA,EAAe,SAChD,QAAA,CAAUC,CAAAA,CAAS,WAAA,EAAe,EACpC,CAAC,CAAA,CAGHG,CAAAA,CAAe,QAASS,CAAAA,EAAW,CACjC,GAAIX,CAAAA,CAAQ,MAAA,EAAUH,EAAY,OAElC,IAAMe,EAAY,CAAA,EAAGX,CAAO,GAAGhB,CAAAA,CAAgBf,CAAAA,CAAYyC,EAAO,IAAI,CAAC,GAGjEE,CAAAA,CAAiB9D,CAAAA,CAAS,OAC7BE,CAAAA,EAAYA,CAAAA,CAAQ,QAAQ,EAAA,GAAO0D,CAAAA,CAAO,EAC7C,CAAA,CACMG,CAAAA,CAAkBT,EAA0BQ,CAAc,CAAA,CAEhEb,EAAQ,IAAA,CAAK,CACX,IAAKY,CAAAA,CACL,YAAA,CAAcE,EACd,eAAA,CAAiBhB,CAAAA,CAAgB,WAAA,EAAe,QAAA,CAChD,SAAUC,CAAAA,CAAS,WAAA,EAAe,EACpC,CAAC,EACH,CAAC,CAAA,CAAA,CAICJ,CAAAA,EAA0B5C,EAAS,MAAA,CAAS6C,CAAAA,CAAiB,CAC/D,IAAMmB,CAAAA,CAAa,KAAK,IAAA,CAAKhE,CAAAA,CAAS,OAAS6C,CAAe,CAAA,CAE9D,QAASoB,CAAAA,CAAO,CAAA,CAAGA,GAAQD,CAAAA,EACrB,EAAAf,EAAQ,MAAA,EAAUH,CAAAA,CAAAA,CADemB,IAGrChB,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAK,CAAA,EAAGC,CAAO,CAAA,EAAG/B,CAAU,SAAS8C,CAAI,CAAA,CAAA,CACzC,aAAcX,CAAAA,CAA0BtD,CAAQ,EAChD,eAAA,CAAiB+C,CAAAA,CAAgB,iBAAmB,OAAA,CACpD,QAAA,CAAUC,EAAS,eAAA,EAAmB,EACxC,CAAC,EAEL,CAEA,OAAOC,CACT,CAYO,SAASiB,EAAAA,CACd3B,CAAAA,CACAvC,EACAwC,CAAAA,CAAsC,GACtCvB,CAAAA,CAA0B,GACb,CACb,GAAM,CACJ,kBAAA,CAAAwB,CAAAA,CAAqB,KACrB,iBAAA,CAAAC,CAAAA,CAAoB,KACpB,kBAAA,CAAAC,CAAAA,CAAqB,KACrB,sBAAA,CAAAC,CAAAA,CAAyB,KACzB,eAAA,CAAAC,CAAAA,CAAkB,GAClB,UAAA,CAAAC,CAAAA,CAAa,IACb,eAAA,CAAAC,CAAAA,CAAkB,EAAC,CACnB,SAAAC,CAAAA,CAAW,EACb,CAAA,CAAI/B,CAAAA,CAEEgC,EAA0B,EAAC,CAC3BC,EAAU1C,CAAAA,CACdS,CAAAA,CAAQ,WAAW,MAAA,EAAUsB,CAAAA,CAAK,OAClCtB,CAAAA,CAAQ,SAAA,EAAW,YAAc,IACnC,CAAA,CAEME,EAAaF,CAAAA,CAAQ,SAAA,EAAW,YAAc,OAAA,CAC9CkD,CAAAA,CAAc,IAAI,IAAA,EAAK,CAAE,aAAY,CACrChB,CAAAA,CACJX,EAAQ,MAAA,CAAS,CAAA,CAAIA,EAAUzC,CAAAA,CAAuBC,CAAQ,EAC1DoD,CAAAA,CAAgB1B,CAAAA,CAAa,CAAC,GAAG1B,CAAQ,CAAC,CAAA,CAC1CqD,EAAgB7B,CAAAA,CAAa,CAAC,GAAGxB,CAAQ,CAAC,EAG5CoE,CAAAA,CAAmB,CAAA,CAYvB,GAXAA,CAAAA,EAAoBpE,CAAAA,CAAS,OACzB0C,CAAAA,EAAqBU,CAAAA,CAAc,OAAS,CAAA,GAC9CgB,CAAAA,EAAoBhB,EAAc,MAAA,CAAS,CAAA,CAAA,CAEzCT,GAAsBU,CAAAA,CAAc,MAAA,CAAS,IAC/Ce,CAAAA,EAAoBf,CAAAA,CAAc,OAAS,CAAA,CAAA,CAEzCZ,CAAAA,GACF2B,GACEjB,CAAAA,CAAe,MAAA,CAAS,EAAIA,CAAAA,CAAe,MAAA,CAAS,EAAI,CAAA,CAAA,CAExDP,CAAAA,EAA0B5C,EAAS,MAAA,CAAS6C,CAAAA,CAAiB,CAC/D,IAAMmB,CAAAA,CAAa,IAAA,CAAK,IAAA,CAAKhE,EAAS,MAAA,CAAS6C,CAAe,EAC9DuB,CAAAA,EAAoBJ,CAAAA,CAAa,EACnC,CAEA,IAAMK,EAAeD,CAAAA,CAAmBtB,CAAAA,CA6GxC,GA1GAG,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAK,CAAA,EAAGC,CAAO,CAAA,EAAG/B,CAAU,GAC5B,OAAA,CAASmC,CAAAA,CAA0BtD,CAAQ,CAAA,CAC3C,UAAA,CAAY+C,EAAgB,QAAA,EAAY,OAAA,CACxC,SAAUC,CAAAA,CAAS,QAAA,EAAY,CACjC,CAAC,CAAA,CAGDhD,EAAS,OAAA,CAASE,CAAAA,EAAY,CAC5B,GAAI+C,CAAAA,CAAQ,QAAUH,CAAAA,CAAY,OAElC,IAAMS,CAAAA,CAAavC,CAAAA,CAAqBd,EAASe,CAAAA,CAAQ,SAAS,EAClEgC,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAKM,CAAAA,CACL,QAASrD,CAAAA,CAAQ,SAAA,CACjB,WAAY6C,CAAAA,CAAgB,QAAA,EAAY,UACxC,QAAA,CAAUC,CAAAA,CAAS,UAAY,EACjC,CAAC,EACH,CAAC,CAAA,CAEGN,GAAqBU,CAAAA,CAAc,MAAA,CAAS,IAC1CH,CAAAA,CAAQ,MAAA,CAASH,GACnBG,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAK,CAAA,EAAGC,CAAO,CAAA,EAAGnB,CAAAA,CAAcZ,EAAY,QAAQ,CAAC,GACrD,OAAA,CAASmC,CAAAA,CAA0BtD,CAAQ,CAAA,CAC3C,WAAY+C,CAAAA,CAAgB,UAAA,EAAc,SAC1C,QAAA,CAAUC,CAAAA,CAAS,YAAc,EACnC,CAAC,EAGHI,CAAAA,CAAc,OAAA,CAASxB,GAAU,CAC/B,GAAIqB,EAAQ,MAAA,EAAUH,CAAAA,CAAY,OAElC,IAAMU,CAAAA,CAAgBxD,EAAS,MAAA,CAAQE,CAAAA,EAAAA,CACpCA,EAAQ,MAAA,EAAU,IAAI,IAAA,CACpBuD,CAAAA,EAAcA,EAAU,IAAA,GAAS7B,CAAAA,CAAM,IAC1C,CACF,CAAA,CAEAqB,EAAQ,IAAA,CAAK,CACX,IAAK,CAAA,EAAGC,CAAO,GAAGnB,CAAAA,CAAcZ,CAAAA,CAAY,CAAA,OAAA,EAAUS,CAAAA,CAAM,IAAI,CAAA,CAAE,CAAC,GACnE,OAAA,CAAS0B,CAAAA,CAA0BE,CAAa,CAAA,CAChD,UAAA,CAAYT,EAAgB,UAAA,EAAc,QAAA,CAC1C,SAAUC,CAAAA,CAAS,UAAA,EAAc,EACnC,CAAC,EACH,CAAC,CAAA,CAAA,CAGCL,CAAAA,EAAsBU,EAAc,MAAA,CAAS,CAAA,GAC3CJ,EAAQ,MAAA,CAASH,CAAAA,EACnBG,EAAQ,IAAA,CAAK,CACX,IAAK,CAAA,EAAGC,CAAO,GAAGnB,CAAAA,CAAcZ,CAAAA,CAAY,QAAQ,CAAC,CAAA,CAAA,CACrD,QAASmC,CAAAA,CAA0BtD,CAAQ,EAC3C,UAAA,CAAY+C,CAAAA,CAAgB,WAAA,EAAe,QAAA,CAC3C,SAAUC,CAAAA,CAAS,WAAA,EAAe,EACpC,CAAC,CAAA,CAGHK,EAAc,OAAA,CAASK,CAAAA,EAAW,CAChC,GAAIT,CAAAA,CAAQ,QAAUH,CAAAA,CAAY,OAElC,IAAMa,CAAAA,CAAiB3D,CAAAA,CAAS,OAC7BE,CAAAA,EAAYA,CAAAA,CAAQ,QAAQ,IAAA,GAASwD,CAAAA,CAAO,IAC/C,CAAA,CAEAT,CAAAA,CAAQ,KAAK,CACX,GAAA,CAAK,GAAGC,CAAO,CAAA,EAAGd,EAAgBjB,CAAAA,CAAYuC,CAAAA,CAAO,IAAI,CAAC,CAAA,CAAA,CAC1D,QAASJ,CAAAA,CAA0BK,CAAc,EACjD,UAAA,CAAYZ,CAAAA,CAAgB,aAAe,QAAA,CAC3C,QAAA,CAAUC,EAAS,WAAA,EAAe,EACpC,CAAC,EACH,CAAC,GAICP,CAAAA,EAAsBU,CAAAA,CAAe,OAAS,CAAA,GAC5CF,CAAAA,CAAQ,OAASH,CAAAA,EACnBG,CAAAA,CAAQ,KAAK,CACX,GAAA,CAAK,GAAGC,CAAO,CAAA,EAAGnB,EAAcZ,CAAAA,CAAY,SAAS,CAAC,CAAA,CAAA,CACtD,OAAA,CAASmC,EAA0BtD,CAAQ,CAAA,CAC3C,WAAY+C,CAAAA,CAAgB,WAAA,EAAe,SAC3C,QAAA,CAAUC,CAAAA,CAAS,aAAe,EACpC,CAAC,EAGHG,CAAAA,CAAe,OAAA,CAASS,GAAW,CACjC,GAAIX,CAAAA,CAAQ,MAAA,EAAUH,EAAY,OAElC,IAAMe,EAAY,CAAA,EAAGX,CAAO,GAAGhB,CAAAA,CAAgBf,CAAAA,CAAYyC,EAAO,IAAI,CAAC,GAGjEE,CAAAA,CAAiB9D,CAAAA,CAAS,OAC7BE,CAAAA,EAAYA,CAAAA,CAAQ,QAAQ,EAAA,GAAO0D,CAAAA,CAAO,EAC7C,CAAA,CACMG,CAAAA,CAAkBT,EAA0BQ,CAAc,CAAA,CAEhEb,EAAQ,IAAA,CAAK,CACX,IAAKY,CAAAA,CACL,OAAA,CAASE,EACT,UAAA,CAAYhB,CAAAA,CAAgB,aAAe,QAAA,CAC3C,QAAA,CAAUC,EAAS,WAAA,EAAe,EACpC,CAAC,EACH,CAAC,CAAA,CAAA,CAICJ,CAAAA,EAA0B5C,EAAS,MAAA,CAAS6C,CAAAA,CAAiB,CAC/D,IAAMmB,CAAAA,CAAa,KAAK,IAAA,CAAKhE,CAAAA,CAAS,OAAS6C,CAAe,CAAA,CAE9D,QAASoB,CAAAA,CAAO,CAAA,CAAGA,GAAQD,CAAAA,EACrB,EAAAf,EAAQ,MAAA,EAAUH,CAAAA,CAAAA,CADemB,IAGrChB,CAAAA,CAAQ,IAAA,CAAK,CACX,GAAA,CAAK,CAAA,EAAGC,CAAO,CAAA,EAAG/B,CAAU,SAAS8C,CAAI,CAAA,CAAA,CACzC,QAASX,CAAAA,CAA0BtD,CAAQ,EAC3C,UAAA,CAAY+C,CAAAA,CAAgB,iBAAmB,OAAA,CAC/C,QAAA,CAAUC,EAAS,eAAA,EAAmB,EACxC,CAAC,EAEL,CAEA,OAAO,CACL,QAAAC,CAAAA,CACA,YAAA,CAAcA,EAAQ,MAAA,CACtB,SAAA,CAAWoB,EACX,WAAA,CAAAF,CAAAA,CACA,QAAAjB,CACF,CACF,CAeO,SAASoB,CAAAA,CAAaC,EAAkC,CAC7D,IAAMC,EAAY,wCAAA,CACZC,CAAAA,CACJ,+DACIC,CAAAA,CAAc,WAAA,CAEdC,EAAaJ,CAAAA,CAAY,OAAA,CAAQ,IAAKK,CAAAA,EAAU,CACpD,IAAMC,CAAAA,CAAqB,GAE3B,OAAAA,CAAAA,CAAS,KAAK,SAAS,CAAA,CACvBA,EAAS,IAAA,CAAK,CAAA,SAAA,EAAYC,EAAUF,CAAAA,CAAM,GAAG,CAAC,CAAA,MAAA,CAAQ,CAAA,CAElDA,EAAM,OAAA,EACRC,CAAAA,CAAS,KAAK,CAAA,aAAA,EAAgBC,CAAAA,CAAUF,EAAM,OAAO,CAAC,YAAY,CAAA,CAGhEA,CAAAA,CAAM,YACRC,CAAAA,CAAS,IAAA,CACP,mBAAmBC,CAAAA,CAAUF,CAAAA,CAAM,UAAU,CAAC,CAAA,aAAA,CAChD,EAGEA,CAAAA,CAAM,QAAA,GAAa,QACrBC,CAAAA,CAAS,IAAA,CAAK,iBAAiBD,CAAAA,CAAM,QAAA,CAAS,QAAQ,CAAC,CAAC,aAAa,CAAA,CAGvEC,CAAAA,CAAS,KAAK,UAAU,CAAA,CAEjBA,EAAS,IAAA,CAAK;AAAA,CAAI,CAC3B,CAAC,CAAA,CAED,OAAO,CAACL,CAAAA,CAAWC,CAAAA,CAAY,GAAGE,CAAAA,CAAYD,CAAW,CAAA,CAAE,IAAA,CAAK;AAAA,CAAI,CACtE,CAiBO,SAASK,EAAAA,CACdC,CAAAA,CACAC,EACQ,CACR,IAAMT,CAAAA,CAAY,wCAAA,CACZU,CAAAA,CACJ,oEAAA,CACIC,EAAa,iBAAA,CAEbC,CAAAA,CAAiBJ,CAAAA,CAAS,GAAA,CAAKK,CAAAA,EAAY,CAC/C,IAAMR,CAAAA,CAAqB,EAAC,CAE5B,OAAAA,CAAAA,CAAS,IAAA,CAAK,aAAa,CAAA,CAC3BA,CAAAA,CAAS,IAAA,CAAK,CAAA,SAAA,EAAYC,CAAAA,CAAUO,CAAAA,CAAQ,GAAG,CAAC,CAAA,MAAA,CAAQ,CAAA,CAEpDA,CAAAA,CAAQ,OAAA,EACVR,CAAAA,CAAS,KAAK,CAAA,aAAA,EAAgBC,CAAAA,CAAUO,CAAAA,CAAQ,OAAO,CAAC,CAAA,UAAA,CAAY,CAAA,CAGtER,CAAAA,CAAS,IAAA,CAAK,cAAc,CAAA,CAErBA,CAAAA,CAAS,IAAA,CAAK;AAAA,CAAI,CAC3B,CAAC,CAAA,CAED,OAAO,CAACL,CAAAA,CAAWU,CAAAA,CAAW,GAAGE,CAAAA,CAAgBD,CAAU,CAAA,CAAE,IAAA,CAAK;AAAA,CAAI,CACxE,CAkBO,SAASG,EAAAA,CAAgBf,EAI9B,CACA,IAAMgB,CAAAA,CAAmB,EAAC,CACpBC,CAAAA,CAAqB,EAAC,CAGxBjB,CAAAA,CAAY,OAAA,CAAQ,MAAA,GAAW,CAAA,CACjCgB,CAAAA,CAAO,KAAK,uCAAuC,CAAA,CAC1ChB,CAAAA,CAAY,OAAA,CAAQ,MAAA,CAAS,GAAA,EACtCgB,EAAO,IAAA,CAAK,uDAAuD,CAAA,CAIjEhB,CAAAA,CAAY,SAAA,EACdiB,CAAAA,CAAS,KAAK,0CAA0C,CAAA,CAI1DjB,CAAAA,CAAY,OAAA,CAAQ,OAAA,CAAQ,CAACK,EAAOa,CAAAA,GAAU,CAE5C,GAAI,CACF,IAAI,GAAA,CAAIb,EAAM,GAAG,EACnB,CAAA,KAAQ,CACNW,CAAAA,CAAO,IAAA,CAAK,wBAAwBE,CAAAA,CAAQ,CAAC,KAAKb,CAAAA,CAAM,GAAG,EAAE,EAC/D,CAIEA,CAAAA,CAAM,QAAA,GAAa,MAAA,GAClBA,CAAAA,CAAM,SAAW,CAAA,EAAKA,CAAAA,CAAM,QAAA,CAAW,CAAA,CAAA,EAExCW,CAAAA,CAAO,IAAA,CACL,6BAA6BE,CAAAA,CAAQ,CAAC,CAAA,6BAAA,CACxC,CAAA,CAIEb,CAAAA,CAAM,OAAA,EAAW,CAACc,CAAAA,CAAed,CAAAA,CAAM,OAAO,CAAA,EAChDW,CAAAA,CAAO,IAAA,CACL,iCAAiCE,CAAAA,CAAQ,CAAC,CAAA,EAAA,EAAKb,CAAAA,CAAM,OAAO,CAAA,CAC9D,EAIEA,CAAAA,CAAM,GAAA,CAAI,MAAA,CAAS,IAAA,EACrBY,CAAAA,CAAS,IAAA,CAAK,gBAAgBC,CAAAA,CAAQ,CAAC,CAAA,6BAAA,CAA+B,EAE1E,CAAC,CAAA,CAGD,IAAME,CAAAA,CAAY,IAAI,IACtB,OAAApB,CAAAA,CAAY,QAAQ,OAAA,CAASK,CAAAA,EAAU,CACrC,IAAMgB,CAAAA,CAAQD,CAAAA,CAAU,IAAIf,CAAAA,CAAM,GAAG,CAAA,EAAK,CAAA,CAC1Ce,CAAAA,CAAU,GAAA,CAAIf,EAAM,GAAA,CAAKgB,CAAAA,CAAQ,CAAC,EACpC,CAAC,CAAA,CAEDD,EAAU,OAAA,CAAQ,CAACC,CAAAA,CAAOC,CAAAA,GAAQ,CAC5BD,CAAAA,CAAQ,GACVL,CAAAA,CAAO,IAAA,CAAK,CAAA,qBAAA,EAAwBM,CAAG,CAAA,UAAA,EAAaD,CAAK,SAAS,EAEtE,CAAC,CAAA,CAEM,CACL,KAAA,CAAOL,CAAAA,CAAO,SAAW,CAAA,CACzB,MAAA,CAAAA,CAAAA,CACA,QAAA,CAAAC,CACF,CACF,CAKA,SAASlC,CAAAA,CACPtD,EACQ,CACR,OAAIA,EAAS,MAAA,GAAW,CAAA,CACf,IAAI,IAAA,EAAK,CAAE,WAAA,GAGDA,CAAAA,CAAS,MAAA,CAAO,CAAC8F,CAAAA,CAAQ5F,CAAAA,GAAY,CACtD,IAAM6F,CAAAA,CAAc,IAAI,IAAA,CAAK7F,CAAAA,CAAQ,SAAS,CAAA,CAC9C,OAAO6F,CAAAA,CAAcD,CAAAA,CAASC,CAAAA,CAAcD,CAC9C,CAAA,CAAG,IAAI,KAAK9F,CAAAA,CAAS,CAAC,CAAA,CAAG,SAAS,CAAC,CAAA,CAEjB,aACpB,CAuBO,SAASgG,EAAAA,CACdzD,CAAAA,CACAvC,CAAAA,CACAwC,EAAsC,EAAC,CACvCvB,CAAAA,CAA0B,EAAC,CAC3B,CACA,OAAO,gBAAqB,CAS1B,IAAMgF,CAAAA,CAAAA,CARU,MAAM3D,EACpBC,CAAAA,CACAvC,CAAAA,CACAwC,CAAAA,CACAvB,CACF,CAAA,EAG2B,GAAA,CAAK2D,IAAW,CACzC,GAAA,CAAKA,CAAAA,CAAM,GAAA,CACX,OAAA,CACE,OAAOA,EAAM,YAAA,EAAiB,QAAA,CAC1BA,CAAAA,CAAM,YAAA,CACNA,CAAAA,CAAM,YAAA,EAAc,aAAY,CACtC,UAAA,CAAYA,CAAAA,CAAM,eAAA,CAClB,QAAA,CAAUA,CAAAA,CAAM,QAClB,CAAA,CAAE,CAAA,CAEIL,CAAAA,CAAc,CAClB,OAAA,CAAS0B,CAAAA,CACT,aAAcA,CAAAA,CAAW,MAAA,CAEzB,WAAA,CAAa,IAAI,MAAK,CAAE,WAAA,EAAY,CACpC,OAAA,CAASzF,CAAAA,CACPS,CAAAA,CAAQ,WAAW,MAAA,EAAUsB,CAAAA,CAAK,OAClCtB,CAAAA,CAAQ,SAAA,EAAW,YAAc,IACnC,CACF,CAAA,CAEMiF,CAAAA,CAAM5B,CAAAA,CAAaC,CAAW,EAEpC,OAAO,IAAI,QAAA,CAAS2B,CAAAA,CAAK,CACvB,OAAA,CAAS,CACP,cAAA,CAAgB,iBAAA,CAChB,eAAA,CAAiB,qCACnB,CACF,CAAC,CACH,CACF,CAkDO,SAASC,EAAAA,CAAyBC,CAAAA,CAA2B,CAClE,OAAO,gBAAwD,CAC7D,IAAMC,CAAAA,CAAe,CACnB,GAAID,EAAO,YAAA,EAAgB,EAAC,CAC5B,GAAIA,CAAAA,CAAO,eAAA,CAAkB,MAAMA,CAAAA,CAAO,eAAA,EAAgB,CAAI,EAChE,CAAA,CACM7D,EAAO,MAAM6D,CAAAA,CAAO,SAAQ,CAC5BpG,CAAAA,CAAW,MAAMoG,CAAAA,CAAO,WAAA,EAAY,CACpC5D,CAAAA,CAAU4D,CAAAA,CAAO,UAAA,CAAa,MAAMA,CAAAA,CAAO,UAAA,EAAW,CAAI,EAAC,CAEjE,OAAO,CACL,GAAGC,CAAAA,CACH,GAAI,MAAM/D,CAAAA,CACRC,CAAAA,CACAvC,EACAwC,CAAAA,CACA4D,CAAAA,CAAO,OACT,CACF,CACF,CACF,CAKA,SAAStB,CAAAA,CAAUwB,CAAAA,CAAsB,CACvC,IAAMC,CAAAA,CAAqC,CACzC,GAAA,CAAK,OAAA,CACL,GAAA,CAAK,MAAA,CACL,GAAA,CAAK,MAAA,CACL,IAAK,QAAA,CACL,GAAA,CAAK,QACP,CAAA,CAEA,OAAOD,CAAAA,CAAK,QAAQ,UAAA,CAAaE,CAAAA,EAASD,CAAAA,CAAWC,CAAI,CAAA,EAAKA,CAAI,CACpE,CAKA,SAASd,CAAAA,CAAee,CAAAA,CAA6B,CACnD,GAAI,CACF,IAAMC,CAAAA,CAAO,IAAI,IAAA,CAAKD,CAAU,CAAA,CAC1BE,EAAYD,CAAAA,CAAK,WAAA,EAAY,CAI7BE,CAAAA,CACJH,CAAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EAAK,CAACA,CAAAA,CAAW,QAAA,CAAS,GAAG,CAAA,CAChDA,EAAW,OAAA,CAAQ,GAAA,CAAK,OAAO,CAAA,CAC/BA,CAAAA,CAEN,OAAOE,IAAcC,CAAAA,EAAmB,CAAC,KAAA,CAAMF,CAAAA,CAAK,OAAA,EAAS,CAC/D,CAAA,KAAQ,CACN,OAAO,MACT,CACF","file":"sitemap.mjs","sourcesContent":["import type { BlogPlatformStorage } from \"./database\";\nimport { getStorage } from \"./database\";\nimport type { PublishedArticle, PublishedAuthor } from \"../types/contracts\";\n\nexport interface AuthorArchive {\n author: PublishedAuthor;\n articles: PublishedArticle[];\n}\n\nexport interface AuthorArchiveOptions {\n limit?: number;\n offset?: number;\n}\n\nexport function getAuthorsFromArticles(\n articles: readonly PublishedArticle[],\n): PublishedAuthor[] {\n const authorsBySlug = new Map<string, PublishedAuthor>();\n\n for (const article of articles) {\n if (article.author?.slug) {\n authorsBySlug.set(\n article.author.slug,\n mergeAuthorProfile(\n authorsBySlug.get(article.author.slug),\n article.author,\n ),\n );\n }\n }\n\n return Array.from(authorsBySlug.values()).sort((a, b) =>\n a.name.localeCompare(b.name),\n );\n}\n\nexport function getAuthorArchive(\n slug: string,\n articles: readonly PublishedArticle[],\n): AuthorArchive | null {\n const authorArticles = articles\n .filter((article) => article.author?.slug === slug)\n .sort(\n (a, b) =>\n new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(),\n );\n\n const author = authorArticles.reduce<PublishedAuthor | null>(\n (preferredAuthor, article) =>\n article.author\n ? mergeAuthorProfile(preferredAuthor, article.author)\n : preferredAuthor,\n null,\n );\n\n if (!author) {\n return null;\n }\n\n return {\n author,\n articles: authorArticles,\n };\n}\n\nexport async function loadAuthorArchive(\n slug: string,\n options: AuthorArchiveOptions = {},\n storage: BlogPlatformStorage = getStorage(),\n): Promise<AuthorArchive | null> {\n const { articles } = await storage.listArticles({\n limit: options.limit ?? 1000,\n offset: options.offset ?? 0,\n sortBy: \"publishedAt\",\n order: \"desc\",\n });\n\n return getAuthorArchive(slug, articles);\n}\n\nfunction mergeAuthorProfile(\n current: PublishedAuthor | null | undefined,\n next: PublishedAuthor,\n): PublishedAuthor {\n if (!current) {\n return next;\n }\n\n return {\n ...current,\n bio: current.bio ?? next.bio,\n avatarUrl: current.avatarUrl ?? next.avatarUrl,\n pronouns: current.pronouns ?? next.pronouns,\n socialLinks: current.socialLinks ?? next.socialLinks,\n toneAdjectives:\n current.toneAdjectives.length > 0\n ? current.toneAdjectives\n : next.toneAdjectives,\n };\n}\n","/**\n * SEO utilities for canonical URL generation and validation.\n *\n * These utilities ensure proper canonical URL structure for SEO optimization,\n * preventing duplicate content issues and improving search engine indexing.\n */\n\nimport type { PublishedArticle, PublishedSite } from '../types/contracts';\n\n/**\n * Configuration options for canonical URL generation.\n */\nexport interface CanonicalUrlOptions {\n /** Whether to force HTTPS protocol (default: true) */\n forceHttps?: boolean;\n\n /** Whether to remove trailing slashes (default: true) */\n removeTrailingSlash?: boolean;\n\n /** Custom domain override (uses site.domain if not provided) */\n domain?: string;\n\n /** Custom path prefix (default: '/blog') */\n pathPrefix?: string;\n}\n\n/**\n * Normalizes a domain by ensuring proper protocol and removing trailing slashes.\n *\n * @param domain - The domain to normalize\n * @param forceHttps - Whether to force HTTPS protocol\n * @returns The normalized domain\n *\n * @example\n * ```typescript\n * normalizeDomain('example.com') // 'https://example.com'\n * normalizeDomain('http://example.com/') // 'https://example.com'\n * normalizeDomain('https://example.com', false) // 'https://example.com'\n * ```\n */\nexport function normalizeDomain(domain: string, forceHttps = true): string {\n let normalized = domain.trim();\n\n // Remove trailing slashes\n normalized = normalized.replace(/\\/+$/, '');\n\n // Add protocol if missing\n if (!/^https?:\\/\\//.exec(normalized)) {\n normalized = `${forceHttps ? 'https' : 'http'}://${normalized}`;\n }\n\n // Force HTTPS if requested\n if (forceHttps && normalized.startsWith('http://')) {\n normalized = normalized.replace('http://', 'https://');\n }\n\n return normalized;\n}\n\n/**\n * Validates and normalizes a slug for URL usage.\n *\n * @param slug - The slug to validate and normalize\n * @returns The normalized slug\n * @throws Error if slug is invalid\n *\n * @example\n * ```typescript\n * normalizeSlug('hello-world') // 'hello-world'\n * normalizeSlug('Hello World!') // throws Error\n * normalizeSlug('hello--world') // 'hello-world'\n * ```\n */\nexport function normalizeSlug(slug: string): string {\n const trimmed = slug.trim();\n\n if (!trimmed) {\n throw new Error('Slug cannot be empty');\n }\n\n // Check if slug contains only valid characters\n if (!/^[a-z0-9-]+$/.test(trimmed)) {\n throw new Error('Slug can only contain lowercase letters, numbers, and hyphens');\n }\n\n // Remove multiple consecutive hyphens\n const normalized = trimmed.replace(/-+/g, '-');\n\n // Remove leading/trailing hyphens\n const clean = normalized.replace(/^-+|-+$/g, '');\n\n if (!clean) {\n throw new Error('Slug cannot consist only of hyphens');\n }\n\n return clean;\n}\n\n/**\n * Generates a canonical URL for an article.\n *\n * @param article - The published article\n * @param options - Configuration options for URL generation\n * @returns The canonical URL\n * @throws Error if article or site data is invalid\n *\n * @example\n * ```typescript\n * const article = {\n * slug: 'hello-world',\n * site: { domain: 'example.com' },\n * // ... other fields\n * };\n *\n * generateCanonicalUrl(article)\n * // 'https://example.com/blog/hello-world'\n *\n * generateCanonicalUrl(article, { pathPrefix: '/posts' })\n * // 'https://example.com/posts/hello-world'\n * ```\n */\nexport function generateCanonicalUrl(\n article: PublishedArticle,\n options: CanonicalUrlOptions = {}\n): string {\n const {\n forceHttps = true,\n removeTrailingSlash = true,\n domain = article.site.domain,\n pathPrefix = '/blog'\n } = options;\n\n if (!domain) {\n throw new Error('Domain is required for canonical URL generation');\n }\n\n if (!article.slug) {\n throw new Error('Article slug is required for canonical URL generation');\n }\n\n // Validate and normalize inputs\n const normalizedDomain = normalizeDomain(domain, forceHttps);\n const normalizedSlug = normalizeSlug(article.slug);\n\n // Ensure path prefix starts with '/' and doesn't end with '/'\n const cleanPrefix = pathPrefix\n .replace(/^(?!\\/)/, '/') // Add leading slash if missing\n .replace(/\\/+$/, ''); // Remove trailing slashes\n\n // Construct the canonical URL\n let canonicalUrl = `${normalizedDomain}${cleanPrefix}/${normalizedSlug}`;\n\n // Remove trailing slash if requested\n if (removeTrailingSlash) {\n canonicalUrl = canonicalUrl.replace(/\\/+$/, '');\n }\n\n return canonicalUrl;\n}\n\n/**\n * Generates a canonical URL for an article from site and slug data.\n * Useful when you don't have a full PublishedArticle object.\n *\n * @param site - The published site\n * @param slug - The article slug\n * @param options - Configuration options for URL generation\n * @returns The canonical URL\n *\n * @example\n * ```typescript\n * const site = { domain: 'example.com', ... };\n * generateCanonicalUrlFromSlug(site, 'hello-world')\n * // 'https://example.com/blog/hello-world'\n * ```\n */\nexport function generateCanonicalUrlFromSlug(\n site: PublishedSite,\n slug: string,\n options: CanonicalUrlOptions = {}\n): string {\n return generateCanonicalUrl(\n {\n slug,\n site,\n // Minimal required fields for canonical URL generation\n id: '',\n title: '',\n excerpt: '',\n content: '',\n readingSeconds: 0,\n wordCount: 0,\n publishedAt: '',\n updatedAt: '',\n author: null,\n meta: { keywords: [] },\n version: 1\n },\n options\n );\n}\n\n/**\n * Validates if a URL is a valid canonical URL format.\n *\n * @param url - The URL to validate\n * @returns True if the URL is valid for canonical use\n *\n * @example\n * ```typescript\n * isValidCanonicalUrl('https://example.com/blog/hello-world') // true\n * isValidCanonicalUrl('http://example.com/blog/hello-world') // true\n * isValidCanonicalUrl('example.com/blog/hello-world') // false (missing protocol)\n * isValidCanonicalUrl('https://example.com/blog/hello world') // false (spaces)\n * ```\n */\nexport function isValidCanonicalUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n\n // Must be HTTP or HTTPS\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n return false;\n }\n\n // Must have a valid hostname (not empty or just slashes)\n if (!parsed.hostname || parsed.hostname.length === 0) {\n return false;\n }\n\n // Check for URLs with empty hostname like 'https:///path'\n if (/^https?:\\/\\/\\//.exec(url) && !/^https?:\\/\\/[^\\/]/.exec(url)) {\n return false;\n }\n\n // Path should not contain unencoded spaces or other problematic characters\n // Check the original URL string since URL constructor auto-encodes\n if (url.includes(' ')) {\n return false;\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Extracts the base URL (protocol + hostname + port) from a canonical URL.\n *\n * @param canonicalUrl - The canonical URL\n * @returns The base URL\n *\n * @example\n * ```typescript\n * getBaseUrl('https://example.com/blog/hello-world') // 'https://example.com'\n * getBaseUrl('https://blog.example.com:8080/posts/test') // 'https://blog.example.com:8080'\n * ```\n */\nexport function getBaseUrl(canonicalUrl: string): string {\n try {\n const parsed = new URL(canonicalUrl);\n return `${parsed.protocol}//${parsed.host}`;\n } catch {\n throw new Error('Invalid canonical URL provided');\n }\n}\n\n/**\n * Updates an article's canonical URL in its meta field.\n *\n * @param article - The article to update\n * @param options - Configuration options for URL generation\n * @returns A new article object with updated canonical URL\n *\n * @example\n * ```typescript\n * const updatedArticle = updateArticleCanonicalUrl(article);\n * console.log(updatedArticle.meta.canonicalUrl); // Generated canonical URL\n * ```\n */\nexport function updateArticleCanonicalUrl(\n article: PublishedArticle,\n options: CanonicalUrlOptions = {}\n): PublishedArticle {\n const canonicalUrl = generateCanonicalUrl(article, options);\n\n return {\n ...article,\n meta: {\n ...article.meta,\n canonicalUrl\n }\n };\n}","import type {\n PublishedArticle,\n PublishedSeriesContext,\n} from \"../types/contracts\";\n\nexport interface SeriesArchive {\n series: PublishedSeriesContext;\n articles: PublishedArticle[];\n}\n\nexport function getAllSeries(\n articles: PublishedArticle[],\n): PublishedSeriesContext[] {\n const seriesBySlug = new Map<string, PublishedSeriesContext>();\n\n for (const article of articles) {\n if (\n article.series &&\n (!seriesBySlug.has(article.series.slug) ||\n article.series.articles.length >\n (seriesBySlug.get(article.series.slug)?.articles.length ?? 0))\n ) {\n seriesBySlug.set(article.series.slug, article.series);\n }\n }\n\n return Array.from(seriesBySlug.values()).sort((a, b) =>\n a.title.localeCompare(b.title),\n );\n}\n\nexport function getSeriesArchive(\n articles: PublishedArticle[],\n seriesSlug: string,\n): SeriesArchive | null {\n const matchingArticles = articles\n .filter((article) => article.series?.slug === seriesSlug)\n .sort((a, b) => (a.series?.position ?? 0) - (b.series?.position ?? 0));\n\n if (matchingArticles.length === 0) return null;\n\n const series = matchingArticles[0]?.series;\n if (!series) return null;\n\n return {\n series,\n articles: matchingArticles,\n };\n}\n\nexport function getArticleSeriesNavigation(\n article: PublishedArticle,\n): PublishedSeriesContext | null {\n return article.series ?? null;\n}\n","import type { PublishedArticle, PublishedTopic } from \"../types/contracts\";\n\nexport interface TopicArchive {\n topic: PublishedTopic;\n articles: PublishedArticle[];\n}\n\nexport function getArticlePrimaryTopic(\n article: PublishedArticle,\n): PublishedTopic | null {\n const topics = article.topics ?? [];\n return topics.find((topic) => topic.isPrimary) ?? topics[0] ?? null;\n}\n\nexport function getAllTopics(articles: PublishedArticle[]): PublishedTopic[] {\n const topicsBySlug = new Map<string, PublishedTopic>();\n\n for (const article of articles) {\n for (const topic of article.topics ?? []) {\n if (!topicsBySlug.has(topic.slug)) {\n topicsBySlug.set(topic.slug, topic);\n }\n }\n }\n\n return Array.from(topicsBySlug.values()).sort((a, b) =>\n a.name.localeCompare(b.name),\n );\n}\n\nexport function getTopicArchive(\n articles: PublishedArticle[],\n topicSlug: string,\n): TopicArchive | null {\n const matchingArticles = articles.filter((article) =>\n (article.topics ?? []).some((topic) => topic.slug === topicSlug),\n );\n\n if (matchingArticles.length === 0) {\n return null;\n }\n\n const topic =\n matchingArticles\n .flatMap((article) => article.topics ?? [])\n .find((candidate) => candidate.slug === topicSlug) ?? null;\n\n if (!topic) {\n return null;\n }\n\n return {\n topic,\n articles: matchingArticles,\n };\n}\n","/**\n * URL helpers shared by package UI, metadata, and sitemap integrations.\n */\n\nexport function normalizeBlogBasePath(basePath = \"/blog\"): string {\n const trimmed = basePath.trim();\n if (!trimmed || trimmed === \"/\") return \"\";\n\n return trimmed.replace(/^(?!\\/)/, \"/\").replace(/\\/+$/, \"\");\n}\n\nexport function buildBlogPath(\n basePath: string | undefined,\n slug?: string,\n): string {\n const normalizedBasePath = normalizeBlogBasePath(basePath);\n const cleanSlug = slug?.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n\n if (!cleanSlug) {\n return normalizedBasePath || \"/\";\n }\n\n return `${normalizedBasePath}/${cleanSlug}`.replace(/\\/+/g, \"/\");\n}\n\nexport function buildAuthorPath(\n basePath: string | undefined,\n authorSlug: string,\n): string {\n return buildBlogPath(basePath, `authors/${authorSlug}`);\n}\n\nexport function buildSeriesPath(\n basePath: string | undefined,\n seriesSlug: string,\n): string {\n return buildBlogPath(basePath, `series/${seriesSlug}`);\n}\n\nexport function getPathPrefixFromBlogUrl(blogUrl: string): string {\n const parsed = new URL(blogUrl);\n return normalizeBlogBasePath(parsed.pathname);\n}\n","/**\n * Next.js-compatible sitemap generation utilities for blog platform integration.\n *\n * These utilities generate sitemap entries that follow Next.js MetadataRoute.Sitemap\n * conventions, allowing seamless integration with existing Next.js applications.\n *\n * @example Integration with existing Next.js sitemap:\n * ```typescript\n * // app/sitemap.ts\n * import { generateBlogSitemapEntries } from '@xexr/blog-platform';\n * import type { MetadataRoute } from 'next';\n *\n * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n * // Your existing routes\n * const staticRoutes = [\n * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },\n * { url: 'https://yoursite.com/about', lastModified: new Date(), priority: 0.8 }\n * ];\n *\n * // Blog platform entries\n * const blogEntries = await generateBlogSitemapEntries(blogConfig);\n *\n * return [...staticRoutes, ...blogEntries];\n * }\n * ```\n */\n\nimport type {\n PublishedArticle,\n PublishedSite,\n PublishedAuthor,\n} from \"../types/contracts\";\nimport { getAuthorsFromArticles } from \"./authors\";\nimport {\n generateCanonicalUrl,\n normalizeDomain,\n type CanonicalUrlOptions,\n} from \"./seo\";\nimport { getAllSeries } from \"./series\";\nimport { getAllTopics } from \"./topics\";\nimport { buildAuthorPath, buildBlogPath, buildSeriesPath } from \"./urls\";\n\n/**\n * Configuration options for sitemap generation.\n */\nexport interface SitemapOptions {\n /** Canonical URL configuration */\n canonical?: CanonicalUrlOptions;\n\n /** Whether to include author pages in sitemap */\n includeAuthorPages?: boolean;\n\n /** Whether to include topic pages in sitemap */\n includeTopicPages?: boolean;\n\n /** Whether to include series pages in sitemap */\n includeSeriesPages?: boolean;\n\n /** Whether to include pagination pages in sitemap */\n includePaginationPages?: boolean;\n\n /** Number of articles per page for pagination calculation */\n articlesPerPage?: number;\n\n /** Maximum number of sitemap entries (defaults to 50,000 per Google guidelines) */\n maxEntries?: number;\n\n /** Change frequency for different page types */\n changeFrequency?: {\n /** Change frequency for blog home page */\n homePage?: SitemapChangeFreq;\n /** Change frequency for article pages */\n articles?: SitemapChangeFreq;\n /** Change frequency for author pages */\n authorPages?: SitemapChangeFreq;\n /** Change frequency for topic index and archive pages */\n topicPages?: SitemapChangeFreq;\n /** Change frequency for series index and archive pages */\n seriesPages?: SitemapChangeFreq;\n /** Change frequency for pagination pages */\n paginationPages?: SitemapChangeFreq;\n };\n\n /** Priority values for different page types */\n priority?: {\n /** Priority for blog home page (0.0-1.0) */\n homePage?: number;\n /** Priority for article pages (0.0-1.0) */\n articles?: number;\n /** Priority for author pages (0.0-1.0) */\n authorPages?: number;\n /** Priority for topic index and archive pages (0.0-1.0) */\n topicPages?: number;\n /** Priority for series index and archive pages (0.0-1.0) */\n seriesPages?: number;\n /** Priority for pagination pages (0.0-1.0) */\n paginationPages?: number;\n };\n}\n\n/**\n * Valid sitemap change frequency values as per XML sitemap standard.\n */\nexport type SitemapChangeFreq =\n | \"always\"\n | \"hourly\"\n | \"daily\"\n | \"weekly\"\n | \"monthly\"\n | \"yearly\"\n | \"never\";\n\n/**\n * Next.js MetadataRoute.Sitemap compatible entry.\n * This matches the expected format for Next.js sitemap generation.\n */\nexport interface NextjsSitemapEntry {\n /** The URL of the page */\n url: string;\n\n /** Last modification date */\n lastModified?: string | Date;\n\n /** Change frequency hint for search engines */\n changeFrequency?: SitemapChangeFreq;\n\n /** Priority relative to other URLs on the site (0.0-1.0) */\n priority?: number;\n\n /** Alternate language versions (for i18n) */\n alternates?: {\n languages?: Record<string, string>;\n };\n}\n\n/**\n * Legacy XML sitemap entry format (for backward compatibility).\n * @deprecated Use NextjsSitemapEntry for new implementations\n */\nexport interface SitemapEntry {\n /** The URL of the page */\n loc: string;\n\n /** Last modification date as ISO 8601 string */\n lastmod?: string;\n\n /** Change frequency hint for search engines */\n changefreq?: SitemapChangeFreq;\n\n /** Priority relative to other URLs on the site (0.0-1.0) */\n priority?: number;\n}\n\n/**\n * Complete sitemap data structure.\n */\nexport interface SitemapData {\n /** All URL entries in the sitemap */\n entries: SitemapEntry[];\n\n /** Total number of entries */\n totalEntries: number;\n\n /** Whether the sitemap was truncated due to size limits */\n truncated: boolean;\n\n /** Generation timestamp */\n generatedAt: string;\n\n /** Base URL used for canonical URL generation */\n baseUrl: string;\n}\n\n/**\n * Generates Next.js-compatible sitemap entries for blog platform integration.\n *\n * @param site - The published site information\n * @param articles - Array of published articles\n * @param authors - Array of published authors (optional)\n * @param options - Sitemap generation options\n * @returns Array of Next.js MetadataRoute.Sitemap compatible entries\n *\n * @example\n * ```typescript\n * // In your Next.js app/sitemap.ts file:\n * import { generateBlogSitemapEntries } from '@xexr/blog-platform';\n * import type { MetadataRoute } from 'next';\n *\n * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n * const blogEntries = await generateBlogSitemapEntries(site, articles, authors, {\n * includeAuthorPages: true,\n * includePaginationPages: true,\n * changeFrequency: { articles: 'monthly' }\n * });\n *\n * return [\n * // Your existing site routes\n * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },\n * { url: 'https://yoursite.com/about', lastModified: new Date() },\n * // Blog entries\n * ...blogEntries\n * ];\n * }\n * ```\n */\nexport async function generateBlogSitemapEntries(\n site: PublishedSite,\n articles: readonly PublishedArticle[],\n authors: readonly PublishedAuthor[] = [],\n options: SitemapOptions = {},\n): Promise<NextjsSitemapEntry[]> {\n const {\n includeAuthorPages = true,\n includeTopicPages = true,\n includeSeriesPages = true,\n includePaginationPages = true,\n articlesPerPage = 10,\n maxEntries = 50000,\n changeFrequency = {},\n priority = {},\n } = options;\n\n const entries: NextjsSitemapEntry[] = [];\n const baseUrl = normalizeDomain(\n options.canonical?.domain ?? site.domain,\n options.canonical?.forceHttps ?? true,\n );\n\n const pathPrefix = options.canonical?.pathPrefix ?? \"/blog\";\n const sitemapAuthors =\n authors.length > 0 ? authors : getAuthorsFromArticles(articles);\n const sitemapTopics = getAllTopics([...articles]);\n const sitemapSeries = getAllSeries([...articles]);\n\n // Add blog home page\n entries.push({\n url: `${baseUrl}${pathPrefix}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.homePage ?? \"daily\",\n priority: priority.homePage ?? 1.0,\n });\n\n // Add individual article pages\n articles.forEach((article) => {\n if (entries.length >= maxEntries) return;\n\n const articleUrl = generateCanonicalUrl(article, options.canonical);\n entries.push({\n url: articleUrl,\n lastModified: article.updatedAt,\n changeFrequency: changeFrequency.articles ?? \"monthly\",\n priority: priority.articles ?? 0.8,\n });\n });\n\n if (includeTopicPages && sitemapTopics.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, \"topics\")}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n }\n\n sitemapTopics.forEach((topic) => {\n if (entries.length >= maxEntries) return;\n\n const topicArticles = articles.filter((article) =>\n (article.topics ?? []).some(\n (candidate) => candidate.slug === topic.slug,\n ),\n );\n\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, `topics/${topic.slug}`)}`,\n lastModified: getLatestModificationDate(topicArticles),\n changeFrequency: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n });\n }\n\n if (includeSeriesPages && sitemapSeries.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, \"series\")}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n }\n\n sitemapSeries.forEach((series) => {\n if (entries.length >= maxEntries) return;\n\n const seriesArticles = articles.filter(\n (article) => article.series?.slug === series.slug,\n );\n\n entries.push({\n url: `${baseUrl}${buildSeriesPath(pathPrefix, series.slug)}`,\n lastModified: getLatestModificationDate(seriesArticles),\n changeFrequency: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n });\n }\n\n // Add author pages if enabled\n if (includeAuthorPages && sitemapAuthors.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, \"authors\")}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n }\n\n sitemapAuthors.forEach((author) => {\n if (entries.length >= maxEntries) return;\n\n const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;\n\n // Find the latest article by this author for lastModified\n const authorArticles = articles.filter(\n (article) => article.author?.id === author.id,\n );\n const latestAuthorMod = getLatestModificationDate(authorArticles);\n\n entries.push({\n url: authorUrl,\n lastModified: latestAuthorMod,\n changeFrequency: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n });\n }\n\n // Add pagination pages if enabled\n if (includePaginationPages && articles.length > articlesPerPage) {\n const totalPages = Math.ceil(articles.length / articlesPerPage);\n\n for (let page = 2; page <= totalPages; page++) {\n if (entries.length >= maxEntries) break;\n\n entries.push({\n url: `${baseUrl}${pathPrefix}?page=${page}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.paginationPages ?? \"daily\",\n priority: priority.paginationPages ?? 0.5,\n });\n }\n }\n\n return entries;\n}\n\n/**\n * Legacy sitemap generation for backward compatibility.\n * @deprecated Use generateBlogSitemapEntries for Next.js integration\n *\n * @param site - The published site information\n * @param articles - Array of published articles\n * @param authors - Array of published authors (optional)\n * @param options - Sitemap generation options\n * @returns Complete sitemap data\n */\nexport function generateSitemap(\n site: PublishedSite,\n articles: readonly PublishedArticle[],\n authors: readonly PublishedAuthor[] = [],\n options: SitemapOptions = {},\n): SitemapData {\n const {\n includeAuthorPages = true,\n includeTopicPages = true,\n includeSeriesPages = true,\n includePaginationPages = true,\n articlesPerPage = 10,\n maxEntries = 50000,\n changeFrequency = {},\n priority = {},\n } = options;\n\n const entries: SitemapEntry[] = [];\n const baseUrl = normalizeDomain(\n options.canonical?.domain ?? site.domain,\n options.canonical?.forceHttps ?? true,\n );\n\n const pathPrefix = options.canonical?.pathPrefix ?? \"/blog\";\n const generatedAt = new Date().toISOString();\n const sitemapAuthors =\n authors.length > 0 ? authors : getAuthorsFromArticles(articles);\n const sitemapTopics = getAllTopics([...articles]);\n const sitemapSeries = getAllSeries([...articles]);\n\n // Calculate total potential entries to determine if truncation will occur\n let potentialEntries = 1; // Home page\n potentialEntries += articles.length; // Article pages\n if (includeTopicPages && sitemapTopics.length > 0) {\n potentialEntries += sitemapTopics.length + 1; // Topic index + topic pages\n }\n if (includeSeriesPages && sitemapSeries.length > 0) {\n potentialEntries += sitemapSeries.length + 1; // Series index + series pages\n }\n if (includeAuthorPages) {\n potentialEntries +=\n sitemapAuthors.length > 0 ? sitemapAuthors.length + 1 : 0; // Authors index + author pages\n }\n if (includePaginationPages && articles.length > articlesPerPage) {\n const totalPages = Math.ceil(articles.length / articlesPerPage);\n potentialEntries += totalPages - 1; // Pagination pages (excluding page 1 which is home)\n }\n\n const willTruncate = potentialEntries > maxEntries;\n\n // Add blog home page\n entries.push({\n loc: `${baseUrl}${pathPrefix}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.homePage ?? \"daily\",\n priority: priority.homePage ?? 1.0,\n });\n\n // Add individual article pages\n articles.forEach((article) => {\n if (entries.length >= maxEntries) return;\n\n const articleUrl = generateCanonicalUrl(article, options.canonical);\n entries.push({\n loc: articleUrl,\n lastmod: article.updatedAt,\n changefreq: changeFrequency.articles ?? \"monthly\",\n priority: priority.articles ?? 0.8,\n });\n });\n\n if (includeTopicPages && sitemapTopics.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, \"topics\")}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n }\n\n sitemapTopics.forEach((topic) => {\n if (entries.length >= maxEntries) return;\n\n const topicArticles = articles.filter((article) =>\n (article.topics ?? []).some(\n (candidate) => candidate.slug === topic.slug,\n ),\n );\n\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, `topics/${topic.slug}`)}`,\n lastmod: getLatestModificationDate(topicArticles),\n changefreq: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n });\n }\n\n if (includeSeriesPages && sitemapSeries.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, \"series\")}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n }\n\n sitemapSeries.forEach((series) => {\n if (entries.length >= maxEntries) return;\n\n const seriesArticles = articles.filter(\n (article) => article.series?.slug === series.slug,\n );\n\n entries.push({\n loc: `${baseUrl}${buildSeriesPath(pathPrefix, series.slug)}`,\n lastmod: getLatestModificationDate(seriesArticles),\n changefreq: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n });\n }\n\n // Add author pages if enabled\n if (includeAuthorPages && sitemapAuthors.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, \"authors\")}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n }\n\n sitemapAuthors.forEach((author) => {\n if (entries.length >= maxEntries) return;\n\n const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;\n\n // Find the latest article by this author for lastmod\n const authorArticles = articles.filter(\n (article) => article.author?.id === author.id,\n );\n const latestAuthorMod = getLatestModificationDate(authorArticles);\n\n entries.push({\n loc: authorUrl,\n lastmod: latestAuthorMod,\n changefreq: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n });\n }\n\n // Add pagination pages if enabled\n if (includePaginationPages && articles.length > articlesPerPage) {\n const totalPages = Math.ceil(articles.length / articlesPerPage);\n\n for (let page = 2; page <= totalPages; page++) {\n if (entries.length >= maxEntries) break;\n\n entries.push({\n loc: `${baseUrl}${pathPrefix}?page=${page}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.paginationPages ?? \"daily\",\n priority: priority.paginationPages ?? 0.5,\n });\n }\n }\n\n return {\n entries,\n totalEntries: entries.length,\n truncated: willTruncate,\n generatedAt,\n baseUrl,\n };\n}\n\n/**\n * Converts sitemap data to XML format.\n *\n * @param sitemapData - The sitemap data to convert\n * @returns XML string in sitemap format\n *\n * @example\n * ```typescript\n * const sitemap = generateSitemap(site, articles);\n * const xmlString = sitemapToXml(sitemap);\n * // Use in API route response\n * ```\n */\nexport function sitemapToXml(sitemapData: SitemapData): string {\n const xmlHeader = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>';\n const urlsetOpen =\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">';\n const urlsetClose = \"</urlset>\";\n\n const urlEntries = sitemapData.entries.map((entry) => {\n const elements: string[] = [];\n\n elements.push(` <url>`);\n elements.push(` <loc>${escapeXml(entry.loc)}</loc>`);\n\n if (entry.lastmod) {\n elements.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);\n }\n\n if (entry.changefreq) {\n elements.push(\n ` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`,\n );\n }\n\n if (entry.priority !== undefined) {\n elements.push(` <priority>${entry.priority.toFixed(1)}</priority>`);\n }\n\n elements.push(` </url>`);\n\n return elements.join(\"\\n\");\n });\n\n return [xmlHeader, urlsetOpen, ...urlEntries, urlsetClose].join(\"\\n\");\n}\n\n/**\n * Generates a sitemap index for large blogs that need multiple sitemaps.\n *\n * @param sitemaps - Array of sitemap information\n * @param baseUrl - Base URL for the site\n * @returns XML string for sitemap index\n *\n * @example\n * ```typescript\n * const sitemapIndex = generateSitemapIndex([\n * { url: 'https://example.com/sitemap-articles.xml', lastmod: '2023-01-01T00:00:00Z' },\n * { url: 'https://example.com/sitemap-authors.xml', lastmod: '2023-01-01T00:00:00Z' }\n * ], 'https://example.com');\n * ```\n */\nexport function generateSitemapIndex(\n sitemaps: readonly { url: string; lastmod?: string }[],\n _baseUrl: string,\n): string {\n const xmlHeader = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>';\n const indexOpen =\n '<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">';\n const indexClose = \"</sitemapindex>\";\n\n const sitemapEntries = sitemaps.map((sitemap) => {\n const elements: string[] = [];\n\n elements.push(` <sitemap>`);\n elements.push(` <loc>${escapeXml(sitemap.url)}</loc>`);\n\n if (sitemap.lastmod) {\n elements.push(` <lastmod>${escapeXml(sitemap.lastmod)}</lastmod>`);\n }\n\n elements.push(` </sitemap>`);\n\n return elements.join(\"\\n\");\n });\n\n return [xmlHeader, indexOpen, ...sitemapEntries, indexClose].join(\"\\n\");\n}\n\n/**\n * Validates sitemap data for compliance with search engine guidelines.\n *\n * @param sitemapData - The sitemap data to validate\n * @returns Validation results with errors and warnings\n *\n * @example\n * ```typescript\n * const sitemap = generateSitemap(site, articles);\n * const validation = validateSitemap(sitemap);\n *\n * if (validation.errors.length > 0) {\n * console.error('Sitemap errors:', validation.errors);\n * }\n * ```\n */\nexport function validateSitemap(sitemapData: SitemapData): {\n valid: boolean;\n errors: string[];\n warnings: string[];\n} {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n // Check entry count\n if (sitemapData.entries.length === 0) {\n errors.push(\"Sitemap must contain at least one URL\");\n } else if (sitemapData.entries.length > 50000) {\n errors.push(\"Sitemap contains more than 50,000 URLs (Google limit)\");\n }\n\n // Check for truncation\n if (sitemapData.truncated) {\n warnings.push(\"Sitemap was truncated due to size limits\");\n }\n\n // Validate individual entries\n sitemapData.entries.forEach((entry, index) => {\n // Validate URL format\n try {\n new URL(entry.loc);\n } catch {\n errors.push(`Invalid URL at entry ${index + 1}: ${entry.loc}`);\n }\n\n // Validate priority range\n if (\n entry.priority !== undefined &&\n (entry.priority < 0 || entry.priority > 1)\n ) {\n errors.push(\n `Invalid priority at entry ${index + 1}: must be between 0.0 and 1.0`,\n );\n }\n\n // Validate lastmod format\n if (entry.lastmod && !isValidISODate(entry.lastmod)) {\n errors.push(\n `Invalid lastmod date at entry ${index + 1}: ${entry.lastmod}`,\n );\n }\n\n // Check URL length (Google limit is 2048 characters)\n if (entry.loc.length > 2048) {\n warnings.push(`URL at entry ${index + 1} exceeds 2048 character limit`);\n }\n });\n\n // Check for duplicate URLs\n const urlCounts = new Map<string, number>();\n sitemapData.entries.forEach((entry) => {\n const count = urlCounts.get(entry.loc) ?? 0;\n urlCounts.set(entry.loc, count + 1);\n });\n\n urlCounts.forEach((count, url) => {\n if (count > 1) {\n errors.push(`Duplicate URL found: ${url} (appears ${count} times)`);\n }\n });\n\n return {\n valid: errors.length === 0,\n errors,\n warnings,\n };\n}\n\n/**\n * Gets the latest modification date from an array of articles.\n */\nfunction getLatestModificationDate(\n articles: readonly PublishedArticle[],\n): string {\n if (articles.length === 0) {\n return new Date().toISOString();\n }\n\n const latestDate = articles.reduce((latest, article) => {\n const articleDate = new Date(article.updatedAt);\n return articleDate > latest ? articleDate : latest;\n }, new Date(articles[0]!.updatedAt));\n\n return latestDate.toISOString();\n}\n\n/**\n * Creates a separate sitemap specifically for blog content.\n * Useful for large sites that need to split sitemaps by content type.\n *\n * @param site - The published site information\n * @param articles - Array of published articles\n * @param authors - Array of published authors (optional)\n * @param options - Sitemap generation options\n * @returns Next.js sitemap function\n *\n * @example\n * ```typescript\n * // app/blog-sitemap/route.ts - separate blog sitemap endpoint\n * import { createBlogSitemapRoute } from '@xexr/blog-platform';\n *\n * export const GET = createBlogSitemapRoute(site, articles, authors, {\n * includeAuthorPages: true,\n * articlesPerPage: 20\n * });\n * ```\n */\nexport function createBlogSitemapRoute(\n site: PublishedSite,\n articles: readonly PublishedArticle[],\n authors: readonly PublishedAuthor[] = [],\n options: SitemapOptions = {},\n) {\n return async function GET() {\n const entries = await generateBlogSitemapEntries(\n site,\n articles,\n authors,\n options,\n );\n\n // Convert to Next.js sitemap format and return as XML\n const xmlEntries = entries.map((entry) => ({\n loc: entry.url,\n lastmod:\n typeof entry.lastModified === \"string\"\n ? entry.lastModified\n : entry.lastModified?.toISOString(),\n changefreq: entry.changeFrequency,\n priority: entry.priority,\n }));\n\n const sitemapData = {\n entries: xmlEntries,\n totalEntries: xmlEntries.length,\n truncated: false,\n generatedAt: new Date().toISOString(),\n baseUrl: normalizeDomain(\n options.canonical?.domain ?? site.domain,\n options.canonical?.forceHttps ?? true,\n ),\n };\n\n const xml = sitemapToXml(sitemapData);\n\n return new Response(xml, {\n headers: {\n \"Content-Type\": \"application/xml\",\n \"Cache-Control\": \"public, max-age=3600, s-maxage=3600\",\n },\n });\n };\n}\n\n/**\n * Configuration interface for blog data fetching.\n * Used to fetch blog data for sitemap generation.\n */\nexport interface BlogSitemapConfig {\n /** Static host app routes to include before Bloggy routes */\n staticRoutes?: readonly NextjsSitemapEntry[];\n\n /** Function to fetch static host app routes */\n getStaticRoutes?: () => Promise<readonly NextjsSitemapEntry[]>;\n\n /** Function to fetch site data */\n getSite: () => Promise<PublishedSite>;\n\n /** Function to fetch published articles */\n getArticles: () => Promise<readonly PublishedArticle[]>;\n\n /** Function to fetch authors (optional) */\n getAuthors?: () => Promise<readonly PublishedAuthor[]>;\n\n /** Sitemap generation options */\n options?: SitemapOptions;\n}\n\n/**\n * Higher-order function that creates a Next.js sitemap function with data fetching.\n * Ideal for dynamic blog content that needs to be fetched at sitemap generation time.\n *\n * @param config - Configuration for fetching blog data\n * @returns Next.js sitemap function\n *\n * @example\n * ```typescript\n * // app/sitemap.ts\n * import { createDynamicBlogSitemap } from '@xexr/blog-platform';\n * import { getBlogData } from './lib/blog';\n *\n * export default createDynamicBlogSitemap({\n * getSite: () => getBlogData().site,\n * getArticles: () => getBlogData().articles,\n * getAuthors: () => getBlogData().authors,\n * options: {\n * includeAuthorPages: true,\n * changeFrequency: { articles: 'weekly' }\n * }\n * });\n * ```\n */\nexport function createDynamicBlogSitemap(config: BlogSitemapConfig) {\n return async function sitemap(): Promise<NextjsSitemapEntry[]> {\n const staticRoutes = [\n ...(config.staticRoutes ?? []),\n ...(config.getStaticRoutes ? await config.getStaticRoutes() : []),\n ];\n const site = await config.getSite();\n const articles = await config.getArticles();\n const authors = config.getAuthors ? await config.getAuthors() : [];\n\n return [\n ...staticRoutes,\n ...(await generateBlogSitemapEntries(\n site,\n articles,\n authors,\n config.options,\n )),\n ];\n };\n}\n\n/**\n * Escapes special XML characters for safe inclusion in XML content.\n */\nfunction escapeXml(text: string): string {\n const xmlEscapes: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#x27;\",\n };\n\n return text.replace(/[&<>\"']/g, (char) => xmlEscapes[char] ?? char);\n}\n\n/**\n * Validates if a string is a valid ISO 8601 date.\n */\nfunction isValidISODate(dateString: string): boolean {\n try {\n const date = new Date(dateString);\n const isoString = date.toISOString();\n\n // Handle both formats: with and without milliseconds\n // 2023-01-01T00:00:00.000Z and 2023-01-01T00:00:00Z are both valid\n const normalizedInput =\n dateString.endsWith(\"Z\") && !dateString.includes(\".\")\n ? dateString.replace(\"Z\", \".000Z\")\n : dateString;\n\n return isoString === normalizedInput && !isNaN(date.getTime());\n } catch {\n return false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/lib/authors.ts","../src/lib/seo.ts","../src/lib/series.ts","../src/lib/topics.ts","../src/lib/urls.ts","../src/lib/sitemap.ts"],"names":["getAuthorsFromArticles","articles","authorsBySlug","article","mergeAuthorProfile","a","b","current","next","normalizeDomain","domain","forceHttps","normalized","normalizeSlug","slug","trimmed","clean","generateCanonicalUrl","options","removeTrailingSlash","pathPrefix","normalizedDomain","normalizedSlug","cleanPrefix","canonicalUrl","getAllSeries","seriesBySlug","getAllTopics","topicsBySlug","topic","normalizeBlogBasePath","basePath","buildBlogPath","normalizedBasePath","cleanSlug","buildAuthorPath","authorSlug","buildSeriesPath","seriesSlug","generateBlogSitemapEntries","site","authors","includeAuthorPages","includeTopicPages","includeSeriesPages","maxEntries","changeFrequency","priority","entries","baseUrl","sitemapAuthors","sitemapTopics","sitemapSeries","getLatestModificationDate","articleUrl","topicArticles","candidate","series","seriesArticles","author","authorUrl","authorArticles","latestAuthorMod","generateSitemap","generatedAt","potentialEntries","willTruncate","sitemapToXml","sitemapData","xmlHeader","urlsetOpen","urlsetClose","urlEntries","entry","elements","escapeXml","generateSitemapIndex","sitemaps","_baseUrl","indexOpen","indexClose","sitemapEntries","sitemap","validateSitemap","errors","warnings","index","isValidISODate","urlCounts","count","url","latest","articleDate","createBlogSitemapRoute","xmlEntries","xml","createDynamicBlogSitemap","config","staticRoutes","text","xmlEscapes","char","dateString","date","isoString","normalizedInput"],"mappings":"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","file":"sitemap.mjs","sourcesContent":["import type { BlogPlatformStorage } from \"./database\";\nimport { getStorage } from \"./database\";\nimport type { PublishedArticle, PublishedAuthor } from \"../types/contracts\";\n\nexport interface AuthorArchive {\n author: PublishedAuthor;\n articles: PublishedArticle[];\n}\n\nexport interface AuthorArchiveOptions {\n limit?: number;\n offset?: number;\n}\n\nexport function getAuthorsFromArticles(\n articles: readonly PublishedArticle[],\n): PublishedAuthor[] {\n const authorsBySlug = new Map<string, PublishedAuthor>();\n\n for (const article of articles) {\n if (article.author?.slug) {\n authorsBySlug.set(\n article.author.slug,\n mergeAuthorProfile(\n authorsBySlug.get(article.author.slug),\n article.author,\n ),\n );\n }\n }\n\n return Array.from(authorsBySlug.values()).sort((a, b) =>\n a.name.localeCompare(b.name),\n );\n}\n\nexport function getAuthorArchive(\n slug: string,\n articles: readonly PublishedArticle[],\n): AuthorArchive | null {\n const authorArticles = articles\n .filter((article) => article.author?.slug === slug)\n .sort(\n (a, b) =>\n new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(),\n );\n\n const author = authorArticles.reduce<PublishedAuthor | null>(\n (preferredAuthor, article) =>\n article.author\n ? mergeAuthorProfile(preferredAuthor, article.author)\n : preferredAuthor,\n null,\n );\n\n if (!author) {\n return null;\n }\n\n return {\n author,\n articles: authorArticles,\n };\n}\n\nexport async function loadAuthorArchive(\n slug: string,\n options: AuthorArchiveOptions = {},\n storage: BlogPlatformStorage = getStorage(),\n): Promise<AuthorArchive | null> {\n const { articles } = await storage.listArticles({\n limit: options.limit ?? 1000,\n offset: options.offset ?? 0,\n sortBy: \"publishedAt\",\n order: \"desc\",\n });\n\n return getAuthorArchive(slug, articles);\n}\n\nfunction mergeAuthorProfile(\n current: PublishedAuthor | null | undefined,\n next: PublishedAuthor,\n): PublishedAuthor {\n if (!current) {\n return next;\n }\n\n return {\n ...current,\n bio: current.bio ?? next.bio,\n avatarUrl: current.avatarUrl ?? next.avatarUrl,\n pronouns: current.pronouns ?? next.pronouns,\n socialLinks: current.socialLinks ?? next.socialLinks,\n toneAdjectives:\n current.toneAdjectives.length > 0\n ? current.toneAdjectives\n : next.toneAdjectives,\n };\n}\n","/**\n * SEO utilities for canonical URL generation and validation.\n *\n * These utilities ensure proper canonical URL structure for SEO optimization,\n * preventing duplicate content issues and improving search engine indexing.\n */\n\nimport type { PublishedArticle, PublishedSite } from '../types/contracts';\n\n/**\n * Configuration options for canonical URL generation.\n */\nexport interface CanonicalUrlOptions {\n /** Whether to force HTTPS protocol (default: true) */\n forceHttps?: boolean;\n\n /** Whether to remove trailing slashes (default: true) */\n removeTrailingSlash?: boolean;\n\n /** Custom domain override (uses site.domain if not provided) */\n domain?: string;\n\n /** Custom path prefix (default: '/blog') */\n pathPrefix?: string;\n}\n\n/**\n * Normalizes a domain by ensuring proper protocol and removing trailing slashes.\n *\n * @param domain - The domain to normalize\n * @param forceHttps - Whether to force HTTPS protocol\n * @returns The normalized domain\n *\n * @example\n * ```typescript\n * normalizeDomain('example.com') // 'https://example.com'\n * normalizeDomain('http://example.com/') // 'https://example.com'\n * normalizeDomain('https://example.com', false) // 'https://example.com'\n * ```\n */\nexport function normalizeDomain(domain: string, forceHttps = true): string {\n let normalized = domain.trim();\n\n // Remove trailing slashes\n normalized = normalized.replace(/\\/+$/, '');\n\n // Add protocol if missing\n if (!/^https?:\\/\\//.exec(normalized)) {\n normalized = `${forceHttps ? 'https' : 'http'}://${normalized}`;\n }\n\n // Force HTTPS if requested\n if (forceHttps && normalized.startsWith('http://')) {\n normalized = normalized.replace('http://', 'https://');\n }\n\n return normalized;\n}\n\n/**\n * Validates and normalizes a slug for URL usage.\n *\n * @param slug - The slug to validate and normalize\n * @returns The normalized slug\n * @throws Error if slug is invalid\n *\n * @example\n * ```typescript\n * normalizeSlug('hello-world') // 'hello-world'\n * normalizeSlug('Hello World!') // throws Error\n * normalizeSlug('hello--world') // 'hello-world'\n * ```\n */\nexport function normalizeSlug(slug: string): string {\n const trimmed = slug.trim();\n\n if (!trimmed) {\n throw new Error('Slug cannot be empty');\n }\n\n // Check if slug contains only valid characters\n if (!/^[a-z0-9-]+$/.test(trimmed)) {\n throw new Error('Slug can only contain lowercase letters, numbers, and hyphens');\n }\n\n // Remove multiple consecutive hyphens\n const normalized = trimmed.replace(/-+/g, '-');\n\n // Remove leading/trailing hyphens\n const clean = normalized.replace(/^-+|-+$/g, '');\n\n if (!clean) {\n throw new Error('Slug cannot consist only of hyphens');\n }\n\n return clean;\n}\n\n/**\n * Generates a canonical URL for an article.\n *\n * @param article - The published article\n * @param options - Configuration options for URL generation\n * @returns The canonical URL\n * @throws Error if article or site data is invalid\n *\n * @example\n * ```typescript\n * const article = {\n * slug: 'hello-world',\n * site: { domain: 'example.com' },\n * // ... other fields\n * };\n *\n * generateCanonicalUrl(article)\n * // 'https://example.com/blog/hello-world'\n *\n * generateCanonicalUrl(article, { pathPrefix: '/posts' })\n * // 'https://example.com/posts/hello-world'\n * ```\n */\nexport function generateCanonicalUrl(\n article: PublishedArticle,\n options: CanonicalUrlOptions = {}\n): string {\n const {\n forceHttps = true,\n removeTrailingSlash = true,\n domain = article.site.domain,\n pathPrefix = '/blog'\n } = options;\n\n if (!domain) {\n throw new Error('Domain is required for canonical URL generation');\n }\n\n if (!article.slug) {\n throw new Error('Article slug is required for canonical URL generation');\n }\n\n // Validate and normalize inputs\n const normalizedDomain = normalizeDomain(domain, forceHttps);\n const normalizedSlug = normalizeSlug(article.slug);\n\n // Ensure path prefix starts with '/' and doesn't end with '/'\n const cleanPrefix = pathPrefix\n .replace(/^(?!\\/)/, '/') // Add leading slash if missing\n .replace(/\\/+$/, ''); // Remove trailing slashes\n\n // Construct the canonical URL\n let canonicalUrl = `${normalizedDomain}${cleanPrefix}/${normalizedSlug}`;\n\n // Remove trailing slash if requested\n if (removeTrailingSlash) {\n canonicalUrl = canonicalUrl.replace(/\\/+$/, '');\n }\n\n return canonicalUrl;\n}\n\n/**\n * Generates a canonical URL for an article from site and slug data.\n * Useful when you don't have a full PublishedArticle object.\n *\n * @param site - The published site\n * @param slug - The article slug\n * @param options - Configuration options for URL generation\n * @returns The canonical URL\n *\n * @example\n * ```typescript\n * const site = { domain: 'example.com', ... };\n * generateCanonicalUrlFromSlug(site, 'hello-world')\n * // 'https://example.com/blog/hello-world'\n * ```\n */\nexport function generateCanonicalUrlFromSlug(\n site: PublishedSite,\n slug: string,\n options: CanonicalUrlOptions = {}\n): string {\n return generateCanonicalUrl(\n {\n slug,\n site,\n // Minimal required fields for canonical URL generation\n id: '',\n title: '',\n excerpt: '',\n content: '',\n readingSeconds: 0,\n wordCount: 0,\n publishedAt: '',\n updatedAt: '',\n author: null,\n meta: { keywords: [] },\n version: 1\n },\n options\n );\n}\n\n/**\n * Validates if a URL is a valid canonical URL format.\n *\n * @param url - The URL to validate\n * @returns True if the URL is valid for canonical use\n *\n * @example\n * ```typescript\n * isValidCanonicalUrl('https://example.com/blog/hello-world') // true\n * isValidCanonicalUrl('http://example.com/blog/hello-world') // true\n * isValidCanonicalUrl('example.com/blog/hello-world') // false (missing protocol)\n * isValidCanonicalUrl('https://example.com/blog/hello world') // false (spaces)\n * ```\n */\nexport function isValidCanonicalUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n\n // Must be HTTP or HTTPS\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n return false;\n }\n\n // Must have a valid hostname (not empty or just slashes)\n if (!parsed.hostname || parsed.hostname.length === 0) {\n return false;\n }\n\n // Check for URLs with empty hostname like 'https:///path'\n if (/^https?:\\/\\/\\//.exec(url) && !/^https?:\\/\\/[^\\/]/.exec(url)) {\n return false;\n }\n\n // Path should not contain unencoded spaces or other problematic characters\n // Check the original URL string since URL constructor auto-encodes\n if (url.includes(' ')) {\n return false;\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Extracts the base URL (protocol + hostname + port) from a canonical URL.\n *\n * @param canonicalUrl - The canonical URL\n * @returns The base URL\n *\n * @example\n * ```typescript\n * getBaseUrl('https://example.com/blog/hello-world') // 'https://example.com'\n * getBaseUrl('https://blog.example.com:8080/posts/test') // 'https://blog.example.com:8080'\n * ```\n */\nexport function getBaseUrl(canonicalUrl: string): string {\n try {\n const parsed = new URL(canonicalUrl);\n return `${parsed.protocol}//${parsed.host}`;\n } catch {\n throw new Error('Invalid canonical URL provided');\n }\n}\n\n/**\n * Updates an article's canonical URL in its meta field.\n *\n * @param article - The article to update\n * @param options - Configuration options for URL generation\n * @returns A new article object with updated canonical URL\n *\n * @example\n * ```typescript\n * const updatedArticle = updateArticleCanonicalUrl(article);\n * console.log(updatedArticle.meta.canonicalUrl); // Generated canonical URL\n * ```\n */\nexport function updateArticleCanonicalUrl(\n article: PublishedArticle,\n options: CanonicalUrlOptions = {}\n): PublishedArticle {\n const canonicalUrl = generateCanonicalUrl(article, options);\n\n return {\n ...article,\n meta: {\n ...article.meta,\n canonicalUrl\n }\n };\n}","import type {\n PublishedArticle,\n PublishedSeriesContext,\n} from \"../types/contracts\";\n\nexport interface SeriesArchive {\n series: PublishedSeriesContext;\n articles: PublishedArticle[];\n}\n\nexport function getAllSeries(\n articles: PublishedArticle[],\n): PublishedSeriesContext[] {\n const seriesBySlug = new Map<string, PublishedSeriesContext>();\n\n for (const article of articles) {\n if (\n article.series &&\n (!seriesBySlug.has(article.series.slug) ||\n article.series.articles.length >\n (seriesBySlug.get(article.series.slug)?.articles.length ?? 0))\n ) {\n seriesBySlug.set(article.series.slug, article.series);\n }\n }\n\n return Array.from(seriesBySlug.values()).sort((a, b) =>\n a.title.localeCompare(b.title),\n );\n}\n\nexport function getSeriesArchive(\n articles: PublishedArticle[],\n seriesSlug: string,\n): SeriesArchive | null {\n const matchingArticles = articles\n .filter((article) => article.series?.slug === seriesSlug)\n .sort((a, b) => (a.series?.position ?? 0) - (b.series?.position ?? 0));\n\n if (matchingArticles.length === 0) return null;\n\n const series = matchingArticles[0]?.series;\n if (!series) return null;\n\n return {\n series,\n articles: matchingArticles,\n };\n}\n\nexport function getArticleSeriesNavigation(\n article: PublishedArticle,\n): PublishedSeriesContext | null {\n return article.series ?? null;\n}\n","import type { PublishedArticle, PublishedTopic } from \"../types/contracts\";\n\nexport interface TopicArchive {\n topic: PublishedTopic;\n articles: PublishedArticle[];\n}\n\nexport function getArticlePrimaryTopic(\n article: PublishedArticle,\n): PublishedTopic | null {\n const topics = article.topics ?? [];\n return topics.find((topic) => topic.isPrimary) ?? topics[0] ?? null;\n}\n\nexport function getAllTopics(articles: PublishedArticle[]): PublishedTopic[] {\n const topicsBySlug = new Map<string, PublishedTopic>();\n\n for (const article of articles) {\n for (const topic of article.topics ?? []) {\n if (!topicsBySlug.has(topic.slug)) {\n topicsBySlug.set(topic.slug, topic);\n }\n }\n }\n\n return Array.from(topicsBySlug.values()).sort((a, b) =>\n a.name.localeCompare(b.name),\n );\n}\n\nexport function getTopicArchive(\n articles: PublishedArticle[],\n topicSlug: string,\n): TopicArchive | null {\n const matchingArticles = articles.filter((article) =>\n (article.topics ?? []).some((topic) => topic.slug === topicSlug),\n );\n\n if (matchingArticles.length === 0) {\n return null;\n }\n\n const topic =\n matchingArticles\n .flatMap((article) => article.topics ?? [])\n .find((candidate) => candidate.slug === topicSlug) ?? null;\n\n if (!topic) {\n return null;\n }\n\n return {\n topic,\n articles: matchingArticles,\n };\n}\n","/**\n * URL helpers shared by package UI, metadata, and sitemap integrations.\n */\n\nexport function normalizeBlogBasePath(basePath = \"/blog\"): string {\n const trimmed = basePath.trim();\n if (!trimmed || trimmed === \"/\") return \"\";\n\n return trimmed.replace(/^(?!\\/)/, \"/\").replace(/\\/+$/, \"\");\n}\n\nexport function buildBlogPath(\n basePath: string | undefined,\n slug?: string,\n): string {\n const normalizedBasePath = normalizeBlogBasePath(basePath);\n const cleanSlug = slug?.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n\n if (!cleanSlug) {\n return normalizedBasePath || \"/\";\n }\n\n return `${normalizedBasePath}/${cleanSlug}`.replace(/\\/+/g, \"/\");\n}\n\nexport function buildAuthorPath(\n basePath: string | undefined,\n authorSlug: string,\n): string {\n return buildBlogPath(basePath, `authors/${authorSlug}`);\n}\n\nexport function buildSeriesPath(\n basePath: string | undefined,\n seriesSlug: string,\n): string {\n return buildBlogPath(basePath, `series/${seriesSlug}`);\n}\n\nexport function getPathPrefixFromBlogUrl(blogUrl: string): string {\n const parsed = new URL(blogUrl);\n return normalizeBlogBasePath(parsed.pathname);\n}\n","/**\n * Next.js-compatible sitemap generation utilities for blog platform integration.\n *\n * These utilities generate sitemap entries that follow Next.js MetadataRoute.Sitemap\n * conventions, allowing seamless integration with existing Next.js applications.\n *\n * @example Integration with existing Next.js sitemap:\n * ```typescript\n * // app/sitemap.ts\n * import { generateBlogSitemapEntries } from '@xexr/blog-platform';\n * import type { MetadataRoute } from 'next';\n *\n * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n * // Your existing routes\n * const staticRoutes = [\n * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },\n * { url: 'https://yoursite.com/about', lastModified: new Date(), priority: 0.8 }\n * ];\n *\n * // Blog platform entries\n * const blogEntries = await generateBlogSitemapEntries(blogConfig);\n *\n * return [...staticRoutes, ...blogEntries];\n * }\n * ```\n */\n\nimport type {\n PublishedArticle,\n PublishedSite,\n PublishedAuthor,\n} from \"../types/contracts\";\nimport { getAuthorsFromArticles } from \"./authors\";\nimport {\n generateCanonicalUrl,\n normalizeDomain,\n type CanonicalUrlOptions,\n} from \"./seo\";\nimport { getAllSeries } from \"./series\";\nimport { getAllTopics } from \"./topics\";\nimport { buildAuthorPath, buildBlogPath, buildSeriesPath } from \"./urls\";\n\n/**\n * Configuration options for sitemap generation.\n */\nexport interface SitemapOptions {\n /** Canonical URL configuration */\n canonical?: CanonicalUrlOptions;\n\n /** Whether to include author pages in sitemap */\n includeAuthorPages?: boolean;\n\n /** Whether to include topic pages in sitemap */\n includeTopicPages?: boolean;\n\n /** Whether to include series pages in sitemap */\n includeSeriesPages?: boolean;\n\n /** @deprecated Pagination pages are never included. */\n includePaginationPages?: boolean;\n\n /** @deprecated Pagination pages are never included. */\n articlesPerPage?: number;\n\n /** Maximum number of sitemap entries (defaults to 50,000 per Google guidelines) */\n maxEntries?: number;\n\n /** Change frequency for different page types */\n changeFrequency?: {\n /** Change frequency for blog home page */\n homePage?: SitemapChangeFreq;\n /** Change frequency for article pages */\n articles?: SitemapChangeFreq;\n /** Change frequency for author pages */\n authorPages?: SitemapChangeFreq;\n /** Change frequency for topic index and archive pages */\n topicPages?: SitemapChangeFreq;\n /** Change frequency for series index and archive pages */\n seriesPages?: SitemapChangeFreq;\n /** Change frequency for pagination pages */\n paginationPages?: SitemapChangeFreq;\n };\n\n /** Priority values for different page types */\n priority?: {\n /** Priority for blog home page (0.0-1.0) */\n homePage?: number;\n /** Priority for article pages (0.0-1.0) */\n articles?: number;\n /** Priority for author pages (0.0-1.0) */\n authorPages?: number;\n /** Priority for topic index and archive pages (0.0-1.0) */\n topicPages?: number;\n /** Priority for series index and archive pages (0.0-1.0) */\n seriesPages?: number;\n /** Priority for pagination pages (0.0-1.0) */\n paginationPages?: number;\n };\n}\n\n/**\n * Valid sitemap change frequency values as per XML sitemap standard.\n */\nexport type SitemapChangeFreq =\n | \"always\"\n | \"hourly\"\n | \"daily\"\n | \"weekly\"\n | \"monthly\"\n | \"yearly\"\n | \"never\";\n\n/**\n * Next.js MetadataRoute.Sitemap compatible entry.\n * This matches the expected format for Next.js sitemap generation.\n */\nexport interface NextjsSitemapEntry {\n /** The URL of the page */\n url: string;\n\n /** Last modification date */\n lastModified?: string | Date;\n\n /** Change frequency hint for search engines */\n changeFrequency?: SitemapChangeFreq;\n\n /** Priority relative to other URLs on the site (0.0-1.0) */\n priority?: number;\n\n /** Alternate language versions (for i18n) */\n alternates?: {\n languages?: Record<string, string>;\n };\n}\n\n/**\n * Legacy XML sitemap entry format (for backward compatibility).\n * @deprecated Use NextjsSitemapEntry for new implementations\n */\nexport interface SitemapEntry {\n /** The URL of the page */\n loc: string;\n\n /** Last modification date as ISO 8601 string */\n lastmod?: string;\n\n /** Change frequency hint for search engines */\n changefreq?: SitemapChangeFreq;\n\n /** Priority relative to other URLs on the site (0.0-1.0) */\n priority?: number;\n}\n\n/**\n * Complete sitemap data structure.\n */\nexport interface SitemapData {\n /** All URL entries in the sitemap */\n entries: SitemapEntry[];\n\n /** Total number of entries */\n totalEntries: number;\n\n /** Whether the sitemap was truncated due to size limits */\n truncated: boolean;\n\n /** Generation timestamp */\n generatedAt: string;\n\n /** Base URL used for canonical URL generation */\n baseUrl: string;\n}\n\n/**\n * Generates Next.js-compatible sitemap entries for blog platform integration.\n *\n * @param site - The published site information\n * @param articles - Array of published articles\n * @param authors - Array of published authors (optional)\n * @param options - Sitemap generation options\n * @returns Array of Next.js MetadataRoute.Sitemap compatible entries\n *\n * @example\n * ```typescript\n * // In your Next.js app/sitemap.ts file:\n * import { generateBlogSitemapEntries } from '@xexr/blog-platform';\n * import type { MetadataRoute } from 'next';\n *\n * export default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n * const blogEntries = await generateBlogSitemapEntries(site, articles, authors, {\n * includeAuthorPages: true,\n * includePaginationPages: true,\n * changeFrequency: { articles: 'monthly' }\n * });\n *\n * return [\n * // Your existing site routes\n * { url: 'https://yoursite.com', lastModified: new Date(), priority: 1 },\n * { url: 'https://yoursite.com/about', lastModified: new Date() },\n * // Blog entries\n * ...blogEntries\n * ];\n * }\n * ```\n */\nexport async function generateBlogSitemapEntries(\n site: PublishedSite,\n articles: readonly PublishedArticle[],\n authors: readonly PublishedAuthor[] = [],\n options: SitemapOptions = {},\n): Promise<NextjsSitemapEntry[]> {\n const {\n includeAuthorPages = true,\n includeTopicPages = true,\n includeSeriesPages = true,\n maxEntries = 50000,\n changeFrequency = {},\n priority = {},\n } = options;\n\n const entries: NextjsSitemapEntry[] = [];\n const baseUrl = normalizeDomain(\n options.canonical?.domain ?? site.domain,\n options.canonical?.forceHttps ?? true,\n );\n\n const pathPrefix = options.canonical?.pathPrefix ?? \"/blog\";\n const sitemapAuthors =\n authors.length > 0 ? authors : getAuthorsFromArticles(articles);\n const sitemapTopics = getAllTopics([...articles]);\n const sitemapSeries = getAllSeries([...articles]);\n\n // Add blog home page\n entries.push({\n url: `${baseUrl}${pathPrefix}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.homePage ?? \"daily\",\n priority: priority.homePage ?? 1.0,\n });\n\n // Add individual article pages\n articles.forEach((article) => {\n if (entries.length >= maxEntries) return;\n\n const articleUrl = generateCanonicalUrl(article, options.canonical);\n entries.push({\n url: articleUrl,\n lastModified: article.updatedAt,\n changeFrequency: changeFrequency.articles ?? \"monthly\",\n priority: priority.articles ?? 0.8,\n });\n });\n\n if (includeTopicPages && sitemapTopics.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, \"topics\")}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n }\n\n sitemapTopics.forEach((topic) => {\n if (entries.length >= maxEntries) return;\n\n const topicArticles = articles.filter((article) =>\n (article.topics ?? []).some(\n (candidate) => candidate.slug === topic.slug,\n ),\n );\n\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, `topics/${topic.slug}`)}`,\n lastModified: getLatestModificationDate(topicArticles),\n changeFrequency: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n });\n }\n\n if (includeSeriesPages && sitemapSeries.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, \"series\")}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n }\n\n sitemapSeries.forEach((series) => {\n if (entries.length >= maxEntries) return;\n\n const seriesArticles = articles.filter(\n (article) => article.series?.slug === series.slug,\n );\n\n entries.push({\n url: `${baseUrl}${buildSeriesPath(pathPrefix, series.slug)}`,\n lastModified: getLatestModificationDate(seriesArticles),\n changeFrequency: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n });\n }\n\n // Add author pages if enabled\n if (includeAuthorPages && sitemapAuthors.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n url: `${baseUrl}${buildBlogPath(pathPrefix, \"authors\")}`,\n lastModified: getLatestModificationDate(articles),\n changeFrequency: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n }\n\n sitemapAuthors.forEach((author) => {\n if (entries.length >= maxEntries) return;\n\n const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;\n\n // Find the latest article by this author for lastModified\n const authorArticles = articles.filter(\n (article) => article.author?.id === author.id,\n );\n const latestAuthorMod = getLatestModificationDate(authorArticles);\n\n entries.push({\n url: authorUrl,\n lastModified: latestAuthorMod,\n changeFrequency: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n });\n }\n\n return entries;\n}\n\n/**\n * Legacy sitemap generation for backward compatibility.\n * @deprecated Use generateBlogSitemapEntries for Next.js integration\n *\n * @param site - The published site information\n * @param articles - Array of published articles\n * @param authors - Array of published authors (optional)\n * @param options - Sitemap generation options\n * @returns Complete sitemap data\n */\nexport function generateSitemap(\n site: PublishedSite,\n articles: readonly PublishedArticle[],\n authors: readonly PublishedAuthor[] = [],\n options: SitemapOptions = {},\n): SitemapData {\n const {\n includeAuthorPages = true,\n includeTopicPages = true,\n includeSeriesPages = true,\n maxEntries = 50000,\n changeFrequency = {},\n priority = {},\n } = options;\n\n const entries: SitemapEntry[] = [];\n const baseUrl = normalizeDomain(\n options.canonical?.domain ?? site.domain,\n options.canonical?.forceHttps ?? true,\n );\n\n const pathPrefix = options.canonical?.pathPrefix ?? \"/blog\";\n const generatedAt = new Date().toISOString();\n const sitemapAuthors =\n authors.length > 0 ? authors : getAuthorsFromArticles(articles);\n const sitemapTopics = getAllTopics([...articles]);\n const sitemapSeries = getAllSeries([...articles]);\n\n // Calculate total potential entries to determine if truncation will occur\n let potentialEntries = 1; // Home page\n potentialEntries += articles.length; // Article pages\n if (includeTopicPages && sitemapTopics.length > 0) {\n potentialEntries += sitemapTopics.length + 1; // Topic index + topic pages\n }\n if (includeSeriesPages && sitemapSeries.length > 0) {\n potentialEntries += sitemapSeries.length + 1; // Series index + series pages\n }\n if (includeAuthorPages) {\n potentialEntries +=\n sitemapAuthors.length > 0 ? sitemapAuthors.length + 1 : 0; // Authors index + author pages\n }\n\n const willTruncate = potentialEntries > maxEntries;\n\n // Add blog home page\n entries.push({\n loc: `${baseUrl}${pathPrefix}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.homePage ?? \"daily\",\n priority: priority.homePage ?? 1.0,\n });\n\n // Add individual article pages\n articles.forEach((article) => {\n if (entries.length >= maxEntries) return;\n\n const articleUrl = generateCanonicalUrl(article, options.canonical);\n entries.push({\n loc: articleUrl,\n lastmod: article.updatedAt,\n changefreq: changeFrequency.articles ?? \"monthly\",\n priority: priority.articles ?? 0.8,\n });\n });\n\n if (includeTopicPages && sitemapTopics.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, \"topics\")}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n }\n\n sitemapTopics.forEach((topic) => {\n if (entries.length >= maxEntries) return;\n\n const topicArticles = articles.filter((article) =>\n (article.topics ?? []).some(\n (candidate) => candidate.slug === topic.slug,\n ),\n );\n\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, `topics/${topic.slug}`)}`,\n lastmod: getLatestModificationDate(topicArticles),\n changefreq: changeFrequency.topicPages ?? \"weekly\",\n priority: priority.topicPages ?? 0.6,\n });\n });\n }\n\n if (includeSeriesPages && sitemapSeries.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, \"series\")}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n }\n\n sitemapSeries.forEach((series) => {\n if (entries.length >= maxEntries) return;\n\n const seriesArticles = articles.filter(\n (article) => article.series?.slug === series.slug,\n );\n\n entries.push({\n loc: `${baseUrl}${buildSeriesPath(pathPrefix, series.slug)}`,\n lastmod: getLatestModificationDate(seriesArticles),\n changefreq: changeFrequency.seriesPages ?? \"weekly\",\n priority: priority.seriesPages ?? 0.6,\n });\n });\n }\n\n // Add author pages if enabled\n if (includeAuthorPages && sitemapAuthors.length > 0) {\n if (entries.length < maxEntries) {\n entries.push({\n loc: `${baseUrl}${buildBlogPath(pathPrefix, \"authors\")}`,\n lastmod: getLatestModificationDate(articles),\n changefreq: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n }\n\n sitemapAuthors.forEach((author) => {\n if (entries.length >= maxEntries) return;\n\n const authorUrl = `${baseUrl}${buildAuthorPath(pathPrefix, author.slug)}`;\n\n // Find the latest article by this author for lastmod\n const authorArticles = articles.filter(\n (article) => article.author?.id === author.id,\n );\n const latestAuthorMod = getLatestModificationDate(authorArticles);\n\n entries.push({\n loc: authorUrl,\n lastmod: latestAuthorMod,\n changefreq: changeFrequency.authorPages ?? \"weekly\",\n priority: priority.authorPages ?? 0.6,\n });\n });\n }\n\n return {\n entries,\n totalEntries: entries.length,\n truncated: willTruncate,\n generatedAt,\n baseUrl,\n };\n}\n\n/**\n * Converts sitemap data to XML format.\n *\n * @param sitemapData - The sitemap data to convert\n * @returns XML string in sitemap format\n *\n * @example\n * ```typescript\n * const sitemap = generateSitemap(site, articles);\n * const xmlString = sitemapToXml(sitemap);\n * // Use in API route response\n * ```\n */\nexport function sitemapToXml(sitemapData: SitemapData): string {\n const xmlHeader = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>';\n const urlsetOpen =\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">';\n const urlsetClose = \"</urlset>\";\n\n const urlEntries = sitemapData.entries.map((entry) => {\n const elements: string[] = [];\n\n elements.push(` <url>`);\n elements.push(` <loc>${escapeXml(entry.loc)}</loc>`);\n\n if (entry.lastmod) {\n elements.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);\n }\n\n if (entry.changefreq) {\n elements.push(\n ` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`,\n );\n }\n\n if (entry.priority !== undefined) {\n elements.push(` <priority>${entry.priority.toFixed(1)}</priority>`);\n }\n\n elements.push(` </url>`);\n\n return elements.join(\"\\n\");\n });\n\n return [xmlHeader, urlsetOpen, ...urlEntries, urlsetClose].join(\"\\n\");\n}\n\n/**\n * Generates a sitemap index for large blogs that need multiple sitemaps.\n *\n * @param sitemaps - Array of sitemap information\n * @param baseUrl - Base URL for the site\n * @returns XML string for sitemap index\n *\n * @example\n * ```typescript\n * const sitemapIndex = generateSitemapIndex([\n * { url: 'https://example.com/sitemap-articles.xml', lastmod: '2023-01-01T00:00:00Z' },\n * { url: 'https://example.com/sitemap-authors.xml', lastmod: '2023-01-01T00:00:00Z' }\n * ], 'https://example.com');\n * ```\n */\nexport function generateSitemapIndex(\n sitemaps: readonly { url: string; lastmod?: string }[],\n _baseUrl: string,\n): string {\n const xmlHeader = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>';\n const indexOpen =\n '<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">';\n const indexClose = \"</sitemapindex>\";\n\n const sitemapEntries = sitemaps.map((sitemap) => {\n const elements: string[] = [];\n\n elements.push(` <sitemap>`);\n elements.push(` <loc>${escapeXml(sitemap.url)}</loc>`);\n\n if (sitemap.lastmod) {\n elements.push(` <lastmod>${escapeXml(sitemap.lastmod)}</lastmod>`);\n }\n\n elements.push(` </sitemap>`);\n\n return elements.join(\"\\n\");\n });\n\n return [xmlHeader, indexOpen, ...sitemapEntries, indexClose].join(\"\\n\");\n}\n\n/**\n * Validates sitemap data for compliance with search engine guidelines.\n *\n * @param sitemapData - The sitemap data to validate\n * @returns Validation results with errors and warnings\n *\n * @example\n * ```typescript\n * const sitemap = generateSitemap(site, articles);\n * const validation = validateSitemap(sitemap);\n *\n * if (validation.errors.length > 0) {\n * console.error('Sitemap errors:', validation.errors);\n * }\n * ```\n */\nexport function validateSitemap(sitemapData: SitemapData): {\n valid: boolean;\n errors: string[];\n warnings: string[];\n} {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n // Check entry count\n if (sitemapData.entries.length === 0) {\n errors.push(\"Sitemap must contain at least one URL\");\n } else if (sitemapData.entries.length > 50000) {\n errors.push(\"Sitemap contains more than 50,000 URLs (Google limit)\");\n }\n\n // Check for truncation\n if (sitemapData.truncated) {\n warnings.push(\"Sitemap was truncated due to size limits\");\n }\n\n // Validate individual entries\n sitemapData.entries.forEach((entry, index) => {\n // Validate URL format\n try {\n new URL(entry.loc);\n } catch {\n errors.push(`Invalid URL at entry ${index + 1}: ${entry.loc}`);\n }\n\n // Validate priority range\n if (\n entry.priority !== undefined &&\n (entry.priority < 0 || entry.priority > 1)\n ) {\n errors.push(\n `Invalid priority at entry ${index + 1}: must be between 0.0 and 1.0`,\n );\n }\n\n // Validate lastmod format\n if (entry.lastmod && !isValidISODate(entry.lastmod)) {\n errors.push(\n `Invalid lastmod date at entry ${index + 1}: ${entry.lastmod}`,\n );\n }\n\n // Check URL length (Google limit is 2048 characters)\n if (entry.loc.length > 2048) {\n warnings.push(`URL at entry ${index + 1} exceeds 2048 character limit`);\n }\n });\n\n // Check for duplicate URLs\n const urlCounts = new Map<string, number>();\n sitemapData.entries.forEach((entry) => {\n const count = urlCounts.get(entry.loc) ?? 0;\n urlCounts.set(entry.loc, count + 1);\n });\n\n urlCounts.forEach((count, url) => {\n if (count > 1) {\n errors.push(`Duplicate URL found: ${url} (appears ${count} times)`);\n }\n });\n\n return {\n valid: errors.length === 0,\n errors,\n warnings,\n };\n}\n\n/**\n * Gets the latest modification date from an array of articles.\n */\nfunction getLatestModificationDate(\n articles: readonly PublishedArticle[],\n): string {\n if (articles.length === 0) {\n return new Date().toISOString();\n }\n\n const latestDate = articles.reduce((latest, article) => {\n const articleDate = new Date(article.updatedAt);\n return articleDate > latest ? articleDate : latest;\n }, new Date(articles[0]!.updatedAt));\n\n return latestDate.toISOString();\n}\n\n/**\n * Creates a separate sitemap specifically for blog content.\n * Useful for large sites that need to split sitemaps by content type.\n *\n * @param site - The published site information\n * @param articles - Array of published articles\n * @param authors - Array of published authors (optional)\n * @param options - Sitemap generation options\n * @returns Next.js sitemap function\n *\n * @example\n * ```typescript\n * // app/blog-sitemap/route.ts - separate blog sitemap endpoint\n * import { createBlogSitemapRoute } from '@xexr/blog-platform';\n *\n * export const GET = createBlogSitemapRoute(site, articles, authors, {\n * includeAuthorPages: true,\n * articlesPerPage: 20\n * });\n * ```\n */\nexport function createBlogSitemapRoute(\n site: PublishedSite,\n articles: readonly PublishedArticle[],\n authors: readonly PublishedAuthor[] = [],\n options: SitemapOptions = {},\n) {\n return async function GET() {\n const entries = await generateBlogSitemapEntries(\n site,\n articles,\n authors,\n options,\n );\n\n // Convert to Next.js sitemap format and return as XML\n const xmlEntries = entries.map((entry) => ({\n loc: entry.url,\n lastmod:\n typeof entry.lastModified === \"string\"\n ? entry.lastModified\n : entry.lastModified?.toISOString(),\n changefreq: entry.changeFrequency,\n priority: entry.priority,\n }));\n\n const sitemapData = {\n entries: xmlEntries,\n totalEntries: xmlEntries.length,\n truncated: false,\n generatedAt: new Date().toISOString(),\n baseUrl: normalizeDomain(\n options.canonical?.domain ?? site.domain,\n options.canonical?.forceHttps ?? true,\n ),\n };\n\n const xml = sitemapToXml(sitemapData);\n\n return new Response(xml, {\n headers: {\n \"Content-Type\": \"application/xml\",\n \"Cache-Control\": \"public, max-age=3600, s-maxage=3600\",\n },\n });\n };\n}\n\n/**\n * Configuration interface for blog data fetching.\n * Used to fetch blog data for sitemap generation.\n */\nexport interface BlogSitemapConfig {\n /** Static host app routes to include before Bloggy routes */\n staticRoutes?: readonly NextjsSitemapEntry[];\n\n /** Function to fetch static host app routes */\n getStaticRoutes?: () => Promise<readonly NextjsSitemapEntry[]>;\n\n /** Function to fetch site data */\n getSite: () => Promise<PublishedSite>;\n\n /** Function to fetch published articles */\n getArticles: () => Promise<readonly PublishedArticle[]>;\n\n /** Function to fetch authors (optional) */\n getAuthors?: () => Promise<readonly PublishedAuthor[]>;\n\n /** Sitemap generation options */\n options?: SitemapOptions;\n}\n\n/**\n * Higher-order function that creates a Next.js sitemap function with data fetching.\n * Ideal for dynamic blog content that needs to be fetched at sitemap generation time.\n *\n * @param config - Configuration for fetching blog data\n * @returns Next.js sitemap function\n *\n * @example\n * ```typescript\n * // app/sitemap.ts\n * import { createDynamicBlogSitemap } from '@xexr/blog-platform';\n * import { getBlogData } from './lib/blog';\n *\n * export default createDynamicBlogSitemap({\n * getSite: () => getBlogData().site,\n * getArticles: () => getBlogData().articles,\n * getAuthors: () => getBlogData().authors,\n * options: {\n * includeAuthorPages: true,\n * changeFrequency: { articles: 'weekly' }\n * }\n * });\n * ```\n */\nexport function createDynamicBlogSitemap(config: BlogSitemapConfig) {\n return async function sitemap(): Promise<NextjsSitemapEntry[]> {\n const staticRoutes = [\n ...(config.staticRoutes ?? []),\n ...(config.getStaticRoutes ? await config.getStaticRoutes() : []),\n ];\n const site = await config.getSite();\n const articles = await config.getArticles();\n const authors = config.getAuthors ? await config.getAuthors() : [];\n\n return [\n ...staticRoutes,\n ...(await generateBlogSitemapEntries(\n site,\n articles,\n authors,\n config.options,\n )),\n ];\n };\n}\n\n/**\n * Escapes special XML characters for safe inclusion in XML content.\n */\nfunction escapeXml(text: string): string {\n const xmlEscapes: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#x27;\",\n };\n\n return text.replace(/[&<>\"']/g, (char) => xmlEscapes[char] ?? char);\n}\n\n/**\n * Validates if a string is a valid ISO 8601 date.\n */\nfunction isValidISODate(dateString: string): boolean {\n try {\n const date = new Date(dateString);\n const isoString = date.toISOString();\n\n // Handle both formats: with and without milliseconds\n // 2023-01-01T00:00:00.000Z and 2023-01-01T00:00:00Z are both valid\n const normalizedInput =\n dateString.endsWith(\"Z\") && !dateString.includes(\".\")\n ? dateString.replace(\"Z\", \".000Z\")\n : dateString;\n\n return isoString === normalizedInput && !isNaN(date.getTime());\n } catch {\n return false;\n }\n}\n"]}
@@ -59,6 +59,9 @@ export interface PublishedAuthor {
59
59
  /** Whether this is a guest author */
60
60
  readonly isGuest: boolean;
61
61
 
62
+ /** Areas of subject expertise. */
63
+ readonly expertise?: readonly string[];
64
+
62
65
  /** Tone adjectives that describe this author's writing style */
63
66
  readonly toneAdjectives: readonly string[];
64
67
  }
@@ -1 +1 @@
1
- export { h as PublishPayload, f as PublishResult, a as PublishedArticle, c as PublishedAuthor, i as PublishedAuthorSocialLinks, d as PublishedNewsletterSettings, b as PublishedSite, P as PublishedTopic } from '../index-CKWrb9a4.mjs';
1
+ export { h as PublishPayload, f as PublishResult, a as PublishedArticle, c as PublishedAuthor, i as PublishedAuthorSocialLinks, d as PublishedNewsletterSettings, b as PublishedSite, P as PublishedTopic } from '../index-DsXjuPn4.mjs';
@@ -1 +1 @@
1
- export { h as PublishPayload, f as PublishResult, a as PublishedArticle, c as PublishedAuthor, i as PublishedAuthorSocialLinks, d as PublishedNewsletterSettings, b as PublishedSite, P as PublishedTopic } from '../index-CKWrb9a4.js';
1
+ export { h as PublishPayload, f as PublishResult, a as PublishedArticle, c as PublishedAuthor, i as PublishedAuthorSocialLinks, d as PublishedNewsletterSettings, b as PublishedSite, P as PublishedTopic } from '../index-DsXjuPn4.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xexr/blog-platform",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "SEO-friendly blog platform for Next.js 15 with Bloggy integration",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -45,17 +45,10 @@
45
45
  "CHANGELOG.md"
46
46
  ],
47
47
  "license": "MIT",
48
- "scripts": {
49
- "build": "tsup && node scripts/copy-dist-assets.mjs",
50
- "test": "vitest",
51
- "lint": "eslint src --max-warnings 0",
52
- "typecheck": "tsc --noEmit",
53
- "prepublishOnly": "pnpm run build"
54
- },
55
48
  "peerDependencies": {
56
49
  "@libsql/client": "^0.15.0",
57
50
  "drizzle-orm": "^0.45.2",
58
- "next": "^15.0.0 || ^16.0.0",
51
+ "next": "^15.5.28 || ^16.4.1",
59
52
  "react": "^18.0.0 || ^19.0.0",
60
53
  "react-dom": "^18.0.0 || ^19.0.0"
61
54
  },
@@ -79,9 +72,15 @@
79
72
  },
80
73
  "devDependencies": {
81
74
  "@types/react": "19.2.14",
82
- "next": "16.2.6",
83
- "tsup": "^8.5.1",
75
+ "next": "16.4.1",
76
+ "tsup": "^9.0.0",
84
77
  "typescript": "^6.0.3",
85
- "vitest": "^4.1.5"
78
+ "vitest": "^4.1.12"
79
+ },
80
+ "scripts": {
81
+ "build": "tsup && node scripts/copy-dist-assets.mjs",
82
+ "test": "vitest",
83
+ "lint": "eslint src --max-warnings 0",
84
+ "typecheck": "tsc --noEmit"
86
85
  }
87
- }
86
+ }