@vdaluz/astro-blog 0.9.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
@@ -8,12 +8,16 @@ A blog needs a listing page, pagination, related posts, tag filters, JSON-LD, RS
8
8
 
9
9
  ## Install
10
10
 
11
- Pinned https tarball from a tag (no registry needed):
11
+ ```
12
+ npm install @vdaluz/astro-blog
13
+ ```
14
+
15
+ Alternatively, a pinned https tarball from a tag works too, with no registry involved:
12
16
 
13
17
  ```jsonc
14
18
  // package.json
15
19
  "dependencies": {
16
- "@vdaluz/astro-blog": "https://github.com/vdaluz/astro-blog/archive/refs/tags/v0.7.0.tar.gz"
20
+ "@vdaluz/astro-blog": "https://github.com/vdaluz/astro-blog/archive/refs/tags/v0.9.0.tar.gz"
17
21
  }
18
22
  ```
19
23
 
@@ -21,9 +25,7 @@ Pinned https tarball from a tag (no registry needed):
21
25
  > shorthand (and even an explicit `git+https://` URL) to `git+ssh://` in the lockfile.
22
26
  > CI runners (e.g. Cloudflare Pages/Workers) have no SSH key, so `npm ci` would fail to
23
27
  > clone it. The `/archive/refs/tags/<tag>.tar.gz` URL is anonymous https with an integrity
24
- > hash in the lockfile - it just works in CI. Bump the tag in the URL to upgrade. This is the
25
- > only supported install path; there's no npm registry package (tag-tarball works for anyone,
26
- > no registry auth needed).
28
+ > hash in the lockfile - it just works in CI. Bump the tag in the URL to upgrade.
27
29
 
28
30
  Peer dependency: `astro` >= 6. For post body styling you'll also want `@tailwindcss/typography` in the app.
29
31
 
@@ -95,7 +97,7 @@ Peer dependency: `astro` >= 6. For post body styling you'll also want `@tailwind
95
97
 
96
98
  Components that build post URLs (`PostCard`, `RelatedPosts`, `Pagination`) accept an optional `base` prop (default `/blog`).
97
99
 
98
- `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`).
99
101
 
100
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.
101
103
 
@@ -117,6 +119,10 @@ import HeroImageCredit from '@vdaluz/astro-blog/HeroImageCredit.astro';
117
119
 
118
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.
119
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
+
120
126
  ### Table of contents + reading time
121
127
 
122
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.9.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';
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',
@@ -50,17 +54,36 @@ const STRINGS: Record<Locale, Strings> = {
50
54
  photoCredit: 'Foto:',
51
55
  via: 'vía',
52
56
  },
57
+ pt: {
58
+ readMore: 'Leia Mais',
59
+ read: 'Ler',
60
+ relatedReading: 'Leitura relacionada',
61
+ goToFirstPage: 'Ir para a primeira página',
62
+ goToPreviousPage: 'Ir para a página anterior',
63
+ goToNextPage: 'Ir para a próxima página',
64
+ goToLastPage: 'Ir para a última página',
65
+ pageOf: (current, last) => `Página ${current} de ${last}`,
66
+ onThisPage: 'Nesta página',
67
+ minRead: (minutes) => `${minutes} min de leitura`,
68
+ blogPagination: 'Paginação do blog',
69
+ filterPosts: 'Filtrar publicações',
70
+ photoCredit: 'Foto:',
71
+ via: 'via',
72
+ },
53
73
  };
54
74
 
55
- export function t(locale: Locale = 'en'): Strings {
56
- 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;
57
78
  }
58
79
 
59
- const DATE_LOCALE: Record<Locale, string> = {
80
+ const DATE_LOCALE: Record<BuiltInLocale, string> = {
60
81
  en: 'en-US',
61
82
  es: 'es',
83
+ pt: 'pt-BR',
62
84
  };
63
85
 
64
86
  export function formatDate(date: Date, locale: Locale = 'en', options?: Intl.DateTimeFormatOptions): string {
65
- 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' });
66
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',