@vdaluz/astro-blog 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,7 +97,7 @@ Peer dependency: `astro` >= 6. For post body styling you'll also want `@tailwind
97
97
 
98
98
  Components that build post URLs (`PostCard`, `RelatedPosts`, `Pagination`) accept an optional `base` prop (default `/blog`).
99
99
 
100
- `PostCard`, `RelatedPosts`, `Pagination`, and `BlogPostMeta` accept an optional `locale` prop (`'en' | 'es'`, default `'en'`) that localizes their built-in UI strings (dates, "Read More", pagination labels) and `BlogPostMeta`'s JSON-LD `inLanguage` field. It does not affect the post URLs those components build - a locale-specific `base` still needs passing separately if the consuming app routes translated posts under a different prefix (e.g. `/es/blog`).
100
+ `PostCard`, `RelatedPosts`, `Pagination`, and `BlogPostMeta` accept an optional `locale` prop (default `'en'`) that localizes their built-in UI strings (dates, "Read More", pagination labels) and `BlogPostMeta`'s JSON-LD `inLanguage` field. `Locale` ships built-in strings for `'en' | 'es' | 'pt'` (autocompleted in editors) but accepts any string - an unrecognized locale falls back to the `en` strings and passes the raw string through to `Intl.DateTimeFormat` for date formatting. Call `t(locale, overrides)` directly with a `Partial<Strings>` to supply your own strings for a locale the package doesn't ship. It does not affect the post URLs those components build - a locale-specific `base` still needs passing separately if the consuming app routes translated posts under a different prefix (e.g. `/es/blog`).
101
101
 
102
102
  `PostCard` accepts an optional `categoryLabel` prop to override the category badge text (default `post.data.category`). `RelatedPosts` accepts the same override as a `(post) => string` function, since it renders a badge per post. Use these when `category` is a canonical/English taxonomy value that the consuming app translates for display - the package has no built-in category translation since the taxonomy itself is app-defined.
103
103
 
@@ -119,6 +119,10 @@ import HeroImageCredit from '@vdaluz/astro-blog/HeroImageCredit.astro';
119
119
 
120
120
  Set `updatedDate` in a post's frontmatter when you substantively edit it after publishing. `buildBlogPostingSchema` uses it for the JSON-LD `dateModified` field, falling back to `pubDate` when unset - so an edited post can signal freshness without every post needing the field.
121
121
 
122
+ ### Trailing slash
123
+
124
+ `buildBlogPostingSchema`/`BlogPostMeta` build the JSON-LD `url`/`mainEntityOfPage.@id` fields without a trailing slash by default. If your site's actual canonical post URL is slash-terminated, pass `trailingSlash={true}` - otherwise the JSON-LD `url` disagrees with your page's own `<link rel="canonical">`, which can cause search engines to pick the wrong canonical form. Check your real canonical output before setting this, not just your app's `trailingSlash` config: prerendered routes on some hosts are served slash-terminated regardless of that config (confirm with `curl -sI` on a bare post URL - a `307`/`308` to the slash form means you need `trailingSlash={true}`).
125
+
122
126
  ### Table of contents + reading time
123
127
 
124
128
  `TableOfContents` reads the `headings` array Astro's own `render()` already returns - no separate parsing step. It renders nothing if the post has fewer than `minHeadings` (default 3) h2/h3 headings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vdaluz/astro-blog",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Token-driven Astro blog components, related-posts scoring, a schema factory, and Shiki config - proven in production on vdaluz.com and imperfectsystems.com.",
5
5
  "keywords": [
6
6
  "astro",
@@ -13,10 +13,19 @@ interface Props {
13
13
  publisherName?: string;
14
14
  /** Locale for the JSON-LD `inLanguage` field. Omitted if unset. */
15
15
  locale?: Locale;
16
+ /** Whether this site's canonical post URL ends in a trailing slash. Defaults to false. */
17
+ trailingSlash?: boolean;
16
18
  }
17
19
 
18
- const { post, siteUrl, basePath = '/blog', publisherName, locale } = Astro.props;
19
- const schema = buildBlogPostingSchema({ post, siteUrl, basePath, publisherName, locale });
20
+ const { post, siteUrl, basePath = '/blog', publisherName, locale, trailingSlash } = Astro.props;
21
+ const schema = buildBlogPostingSchema({
22
+ post,
23
+ siteUrl,
24
+ basePath,
25
+ publisherName,
26
+ locale,
27
+ trailingSlash,
28
+ });
20
29
  ---
21
30
 
22
31
  {/* JSON-LD is valid in <body>; render this anywhere inside the post page. */}
package/src/index.ts CHANGED
@@ -7,5 +7,5 @@ export { shikiConfig } from './lib/shiki';
7
7
  export { buildRssItems } from './lib/rss';
8
8
  export type { RssItem } from './lib/rss';
9
9
  export type { BlogPostData, BlogPostLike, HeroImageCredit } from './lib/types';
10
- export { t, formatDate } from './lib/i18n';
11
- export type { Locale } from './lib/i18n';
10
+ export { t, formatDate, BUILT_IN_LOCALES } from './lib/i18n';
11
+ export type { Locale, Strings } from './lib/i18n';
package/src/lib/i18n.ts CHANGED
@@ -1,6 +1,10 @@
1
- export type Locale = 'en' | 'es' | 'pt';
1
+ export const BUILT_IN_LOCALES = ['en', 'es', 'pt'] as const;
2
+ type BuiltInLocale = (typeof BUILT_IN_LOCALES)[number];
2
3
 
3
- interface Strings {
4
+ /** Any string is accepted; 'en' | 'es' | 'pt' still autocomplete since they're valid members. */
5
+ export type Locale = BuiltInLocale | (string & {});
6
+
7
+ export interface Strings {
4
8
  readMore: string;
5
9
  read: string;
6
10
  relatedReading: string;
@@ -17,7 +21,7 @@ interface Strings {
17
21
  via: string;
18
22
  }
19
23
 
20
- const STRINGS: Record<Locale, Strings> = {
24
+ const STRINGS: Record<BuiltInLocale, Strings> = {
21
25
  en: {
22
26
  readMore: 'Read More',
23
27
  read: 'Read',
@@ -68,16 +72,18 @@ const STRINGS: Record<Locale, Strings> = {
68
72
  },
69
73
  };
70
74
 
71
- export function t(locale: Locale = 'en'): Strings {
72
- return STRINGS[locale];
75
+ export function t(locale: Locale = 'en', overrides?: Partial<Strings>): Strings {
76
+ const base = STRINGS[locale as BuiltInLocale] ?? STRINGS.en;
77
+ return overrides ? { ...base, ...overrides } : base;
73
78
  }
74
79
 
75
- const DATE_LOCALE: Record<Locale, string> = {
80
+ const DATE_LOCALE: Record<BuiltInLocale, string> = {
76
81
  en: 'en-US',
77
82
  es: 'es',
78
83
  pt: 'pt-BR',
79
84
  };
80
85
 
81
86
  export function formatDate(date: Date, locale: Locale = 'en', options?: Intl.DateTimeFormatOptions): string {
82
- return date.toLocaleDateString(DATE_LOCALE[locale], options ?? { year: 'numeric', month: 'long', day: 'numeric' });
87
+ const dateLocale = DATE_LOCALE[locale as BuiltInLocale] ?? locale;
88
+ return date.toLocaleDateString(dateLocale, options ?? { year: 'numeric', month: 'long', day: 'numeric' });
83
89
  }
package/src/lib/schema.ts CHANGED
@@ -49,6 +49,16 @@ export interface BlogPostingSchemaOptions {
49
49
  publisherName?: string;
50
50
  /** BCP 47 language tag for the `inLanguage` field, e.g. "en" or "es". Omitted if unset. */
51
51
  locale?: string;
52
+ /**
53
+ * Whether this site's actual served/canonical post URL ends in a trailing slash.
54
+ * Defaults to false. Check your own `<link rel="canonical">` output (and served-URL
55
+ * behavior - prerendered routes on some hosts are slash-terminated regardless of the
56
+ * app's own trailingSlash config) before setting this; don't assume it from the app
57
+ * config alone. Mismatching this from the real canonical produces a self-inconsistent
58
+ * page (JSON-LD `url` disagreeing with the declared canonical), which can cause search
59
+ * engines to pick the wrong canonical form.
60
+ */
61
+ trailingSlash?: boolean;
52
62
  }
53
63
 
54
64
  /**
@@ -61,10 +71,11 @@ export function buildBlogPostingSchema({
61
71
  basePath = '/blog',
62
72
  publisherName,
63
73
  locale,
74
+ trailingSlash = false,
64
75
  }: BlogPostingSchemaOptions) {
65
76
  const origin = siteUrl.replace(/\/$/, '');
66
77
  const prefix = basePath.replace(/\/$/, '');
67
- const postUrl = `${origin}${prefix}/${post.id}`;
78
+ const postUrl = `${origin}${prefix}/${post.id}${trailingSlash ? '/' : ''}`;
68
79
  const authorName = post.data.author || publisherName || '';
69
80
  return {
70
81
  '@context': 'https://schema.org',