@riebeckite/plugin-recent-posts 0.0.18 → 0.0.20

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,10 +8,12 @@ frontmatter date.
8
8
  ## Overview
9
9
 
10
10
  `recentPosts()` provides a `RecentPosts` component that renders an ordered list
11
- of recently posted articles. `getRecentPosts()` filters unpublished notes,
12
- derives a date from the frontmatter (`date` falling back to `created`), sorts
13
- newest first, and truncates to `limit` items. Notes without a parseable date
14
- are dropped.
11
+ of recently posted articles. `getRecentPosts()` reads
12
+ `manifest.discoverableEntries`, so `unlisted`, `draft`, and scheduled notes are
13
+ excluded. It derives a date from the frontmatter (`date` falling back to
14
+ `created`), sorts newest first, and truncates to `limit` items. Notes without a
15
+ parseable date are dropped. Whether and where to place the list is the site's
16
+ decision.
15
17
 
16
18
  With no posts, the component renders nothing.
17
19
 
@@ -34,18 +36,9 @@ export default defineConfig({
34
36
 
35
37
  ```tsx
36
38
  import RecentPosts, { getRecentPosts } from "@riebeckite/plugin-recent-posts";
37
- import { config } from "../config";
38
39
  import { content } from "../content";
39
- import { getArticleTitle } from "../lib/article-title";
40
-
41
- const posts = await content.getAllPosts();
42
- const recentPosts = await getRecentPosts({
43
- posts,
44
- config,
45
- getProcessedContent: (slug) => content.getProcessedContent(slug),
46
- resolveTitle: getArticleTitle,
47
- limit: 5,
48
- });
40
+
41
+ const recentPosts = getRecentPosts({ manifest: await content.getManifest() });
49
42
 
50
43
  // ...in your route
51
44
  return <Article afterContent={<RecentPosts posts={recentPosts} />} />;
@@ -56,8 +49,8 @@ return <Article afterContent={<RecentPosts posts={recentPosts} />} />;
56
49
  ## Component
57
50
 
58
51
  `RecentPosts({ posts })` renders a
59
- `<section class="recent-posts rr-recent-posts">` with an eyebrow label and an
60
- ordered list. Each item links to the post and shows its date formatted for the
52
+ `<section class="rr-recent-posts">` with a heading and an ordered
53
+ list. Each item links to the post and shows its date formatted for the
61
54
  `en-US` locale. The `rr-recent-posts` root hook is the stable class themes may
62
55
  target.
63
56
 
@@ -65,16 +58,17 @@ target.
65
58
 
66
59
  | Option | Type | Default | Description |
67
60
  | ------ | ---- | ------- | ----------- |
61
+ | `manifest` | `Pick<ContentManifest, "discoverableEntries">` | — | Source of candidate posts |
68
62
  | `limit` | `number` | `5` | Maximum number of posts to return |
69
63
 
70
64
  ## Exports
71
65
 
72
66
  - `recentPostsPlugin()` — plugin factory
73
67
  - `RecentPosts` — list component (default export of `components/recent-posts.tsx`)
74
- - `getRecentPosts({ posts, config, getProcessedContent, resolveTitle, limit? })`
75
- — collects the latest published posts
68
+ - `getRecentPosts({ manifest, limit? })` — collects the latest discoverable
69
+ posts
76
70
  - Type: `RecentPost` (`{ slug, permalink, title, postedAt }`)
77
71
 
78
72
  ## See also
79
73
 
80
- - [Plugin guide](../../../docs/docs/reference/plugin-api.en.md)
74
+ - [Plugin guide](../../../docs/docs/reference/plugin-api.md)
package/README_ja.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## できること
8
8
 
9
- `getRecentPosts()` は非公開ノートと `index` ノートを除き、`frontmatter.date`、なければ `created` を基準に新しい順へ並べます。日付を解釈できないノートは一覧から外れます。`RecentPosts` は結果が空なら何も描画しません。
9
+ `getRecentPosts()` は `manifest.discoverableEntries` を読み、unlisted・draft・予約公開のノートと `index` ノートを除き、`frontmatter.date`、なければ `created` を基準に新しい順へ並べます。日付を解釈できないノートは一覧から外れます。`RecentPosts` は結果が空なら何も描画しません。一覧を置くかどうか、どこへ置くかは Site 側で決めます。
10
10
 
11
11
  ## 設定と配置
12
12
 
@@ -21,23 +21,25 @@ export default defineConfig({ plugins: [recentPostsPlugin()] });
21
21
 
22
22
  ```tsx
23
23
  import RecentPosts, { getRecentPosts } from "@riebeckite/plugin-recent-posts";
24
+ import { content } from "../content";
24
25
 
25
- const posts = await getRecentPosts({ posts: await content.getAllPosts(), config, getProcessedContent: content.getProcessedContent, resolveTitle, limit: 5 });
26
+ const posts = getRecentPosts({ manifest: await content.getManifest() });
26
27
  return <Article afterContent={<RecentPosts posts={posts} />} />;
27
28
  ```
28
29
 
29
30
  | オプション | 既定値 | 内容 |
30
31
  | --- | --- | --- |
32
+ | `manifest` | 必須 | 候補ノートの取得元(`discoverableEntries`) |
31
33
  | `limit` | `5` | 返す記事数の上限 |
32
34
 
33
35
  ## 公開 API
34
36
 
35
37
  - `recentPostsPlugin()` — プラグインファクトリ
36
38
  - `RecentPosts` — 最新記事一覧コンポーネント
37
- - `getRecentPosts({ posts, config, getProcessedContent, resolveTitle, limit? })`
39
+ - `getRecentPosts({ manifest, limit? })`
38
40
  - `RecentPost` — `{ slug, permalink, title, postedAt }` の型
39
41
 
40
42
  ## 関連資料
41
43
 
42
- - [プラグインシステム](../../../docs/docs/reference/plugin-api.md)
44
+ - [プラグインシステム](../../../docs/docs/reference/plugin-api.ja.md)
43
45
 
@@ -2,34 +2,24 @@
2
2
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
3
3
  function RecentPosts(props) {
4
4
  if (props.posts.length === 0) return null;
5
- return /* @__PURE__ */ jsxs(
6
- "section",
7
- {
8
- class: "recent-posts rr-recent-posts",
9
- "aria-labelledby": "recent-posts-title",
10
- children: [
11
- /* @__PURE__ */ jsxs("div", { class: "recent-posts__header", children: [
12
- /* @__PURE__ */ jsx("p", { class: "recent-posts__eyebrow", children: "RECENT POSTS" }),
13
- /* @__PURE__ */ jsx("h2", { class: "recent-posts__title", id: "recent-posts-title", children: "Recent Posts" })
14
- ] }),
15
- /* @__PURE__ */ jsx("ol", { class: "recent-posts__list", children: props.posts.map((post) => {
16
- const formattedDate = formatPostedDate(post.postedAt);
17
- return /* @__PURE__ */ jsx("li", { class: "recent-posts__item", children: /* @__PURE__ */ jsxs("a", { class: "recent-posts__link", href: post.permalink, children: [
18
- /* @__PURE__ */ jsx(
19
- "time",
20
- {
21
- class: "recent-posts__date",
22
- dateTime: formattedDate.isoDate,
23
- title: formattedDate.fullDate,
24
- children: formattedDate.displayDate
25
- }
26
- ),
27
- /* @__PURE__ */ jsx("span", { class: "recent-posts__post-title", children: post.title })
28
- ] }) }, post.slug);
29
- }) })
30
- ]
31
- }
32
- );
5
+ return /* @__PURE__ */ jsxs("section", { class: "rr-recent-posts", "aria-labelledby": "recent-posts-title", children: [
6
+ /* @__PURE__ */ jsx("div", { class: "rr-recent-posts__header", children: /* @__PURE__ */ jsx("h2", { class: "rr-recent-posts__title", id: "recent-posts-title", children: "Recent Posts" }) }),
7
+ /* @__PURE__ */ jsx("ol", { class: "rr-recent-posts__list", children: props.posts.map((post) => {
8
+ const formattedDate = formatPostedDate(post.postedAt);
9
+ return /* @__PURE__ */ jsx("li", { class: "rr-recent-posts__item", children: /* @__PURE__ */ jsxs("a", { class: "rr-recent-posts__link", href: post.permalink, children: [
10
+ /* @__PURE__ */ jsx(
11
+ "time",
12
+ {
13
+ class: "rr-recent-posts__date",
14
+ dateTime: formattedDate.isoDate,
15
+ title: formattedDate.fullDate,
16
+ children: formattedDate.displayDate
17
+ }
18
+ ),
19
+ /* @__PURE__ */ jsx("span", { class: "rr-recent-posts__post-title", children: post.title })
20
+ ] }) }, post.slug);
21
+ }) })
22
+ ] });
33
23
  }
34
24
  function formatPostedDate(date) {
35
25
  const isoDate = date.toISOString();
package/dist/index.js CHANGED
@@ -5,34 +5,24 @@ import { definePlugin } from "@riebeckite/core";
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  function RecentPosts(props) {
7
7
  if (props.posts.length === 0) return null;
8
- return /* @__PURE__ */ jsxs(
9
- "section",
10
- {
11
- class: "recent-posts rr-recent-posts",
12
- "aria-labelledby": "recent-posts-title",
13
- children: [
14
- /* @__PURE__ */ jsxs("div", { class: "recent-posts__header", children: [
15
- /* @__PURE__ */ jsx("p", { class: "recent-posts__eyebrow", children: "RECENT POSTS" }),
16
- /* @__PURE__ */ jsx("h2", { class: "recent-posts__title", id: "recent-posts-title", children: "Recent Posts" })
17
- ] }),
18
- /* @__PURE__ */ jsx("ol", { class: "recent-posts__list", children: props.posts.map((post) => {
19
- const formattedDate = formatPostedDate(post.postedAt);
20
- return /* @__PURE__ */ jsx("li", { class: "recent-posts__item", children: /* @__PURE__ */ jsxs("a", { class: "recent-posts__link", href: post.permalink, children: [
21
- /* @__PURE__ */ jsx(
22
- "time",
23
- {
24
- class: "recent-posts__date",
25
- dateTime: formattedDate.isoDate,
26
- title: formattedDate.fullDate,
27
- children: formattedDate.displayDate
28
- }
29
- ),
30
- /* @__PURE__ */ jsx("span", { class: "recent-posts__post-title", children: post.title })
31
- ] }) }, post.slug);
32
- }) })
33
- ]
34
- }
35
- );
8
+ return /* @__PURE__ */ jsxs("section", { class: "rr-recent-posts", "aria-labelledby": "recent-posts-title", children: [
9
+ /* @__PURE__ */ jsx("div", { class: "rr-recent-posts__header", children: /* @__PURE__ */ jsx("h2", { class: "rr-recent-posts__title", id: "recent-posts-title", children: "Recent Posts" }) }),
10
+ /* @__PURE__ */ jsx("ol", { class: "rr-recent-posts__list", children: props.posts.map((post) => {
11
+ const formattedDate = formatPostedDate(post.postedAt);
12
+ return /* @__PURE__ */ jsx("li", { class: "rr-recent-posts__item", children: /* @__PURE__ */ jsxs("a", { class: "rr-recent-posts__link", href: post.permalink, children: [
13
+ /* @__PURE__ */ jsx(
14
+ "time",
15
+ {
16
+ class: "rr-recent-posts__date",
17
+ dateTime: formattedDate.isoDate,
18
+ title: formattedDate.fullDate,
19
+ children: formattedDate.displayDate
20
+ }
21
+ ),
22
+ /* @__PURE__ */ jsx("span", { class: "rr-recent-posts__post-title", children: post.title })
23
+ ] }) }, post.slug);
24
+ }) })
25
+ ] });
36
26
  }
37
27
  function formatPostedDate(date) {
38
28
  const isoDate = date.toISOString();
@@ -49,24 +39,23 @@ function formatPostedDate(date) {
49
39
  }
50
40
 
51
41
  // src/recent-posts.server.ts
52
- async function getRecentPosts(args) {
53
- const limit = args.limit ?? 5;
54
- const recentPosts = await Promise.all(
55
- args.posts.filter((post) => post.slug !== "index").map(async (post) => {
56
- const processed = await args.getProcessedContent(post.slug);
57
- const postedAt = parseFrontmatterDate(
58
- processed.frontmatter.date ?? processed.frontmatter.created
59
- );
60
- if (!postedAt) return null;
61
- return {
62
- slug: post.slug,
63
- permalink: post.permalink,
64
- title: args.resolveTitle(post.slug, processed.frontmatter.title),
42
+ var DEFAULT_RECENT_POSTS_LIMIT = 5;
43
+ function getRecentPosts(args) {
44
+ const limit = Math.max(0, args.limit ?? DEFAULT_RECENT_POSTS_LIMIT);
45
+ return args.manifest.discoverableEntries.filter((entry) => entry.slug !== "index").flatMap((entry) => {
46
+ const postedAt = parseFrontmatterDate(
47
+ entry.frontmatter.date ?? entry.frontmatter.created
48
+ );
49
+ if (!postedAt) return [];
50
+ return [
51
+ {
52
+ slug: entry.slug,
53
+ permalink: entry.permalink,
54
+ title: entry.title,
65
55
  postedAt
66
- };
67
- })
68
- );
69
- return recentPosts.filter((post) => post !== null).toSorted((a, b) => b.postedAt.getTime() - a.postedAt.getTime()).slice(0, limit);
56
+ }
57
+ ];
58
+ }).toSorted((a, b) => b.postedAt.getTime() - a.postedAt.getTime()).slice(0, limit);
70
59
  }
71
60
  function parseFrontmatterDate(value) {
72
61
  if (value instanceof Date) return value;
@@ -1,18 +1,16 @@
1
- import type { ResolvedRiebeckiteConfig } from "@riebeckite/core";
1
+ import type { ContentManifest } from "@riebeckite/core";
2
2
  import type { RecentPost } from "./recent-posts.js";
3
- type PostRef = {
4
- slug: string;
5
- permalink: string;
6
- };
7
- type ProcessedPost = {
8
- frontmatter: Record<string, unknown>;
9
- };
10
- type TitleResolver = (slug: string, title: unknown) => string;
3
+ export declare const DEFAULT_RECENT_POSTS_LIMIT = 5;
4
+ /**
5
+ * Builds the recent-posts list from the manifest.
6
+ *
7
+ * Reads `manifest.discoverableEntries` on purpose: discovery UI must never
8
+ * surface `unlisted`, `draft`, or future-`publishAt` notes that are not
9
+ * discoverable, even though some of them are routable. The plugin owns the
10
+ * entry selection, the frontmatter date extraction, the sort, and the default
11
+ * title so a Site only decides whether and where to render the list.
12
+ */
11
13
  export declare function getRecentPosts(args: {
12
- posts: PostRef[];
13
- config: ResolvedRiebeckiteConfig;
14
- getProcessedContent: (slug: string) => Promise<ProcessedPost>;
15
- resolveTitle: TitleResolver;
14
+ manifest: Pick<ContentManifest, "discoverableEntries">;
16
15
  limit?: number;
17
- }): Promise<RecentPost[]>;
18
- export {};
16
+ }): RecentPost[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-recent-posts",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "hono": "^4.12.25",
25
- "@riebeckite/core": "0.0.18"
25
+ "@riebeckite/core": "0.0.20"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsx": "^4.22.4"
package/style.css CHANGED
@@ -1,46 +1,35 @@
1
1
  @layer components {
2
- .recent-posts {
2
+ .rr-recent-posts {
3
3
  margin-top: 4rem;
4
4
  padding-top: 1.5rem;
5
5
  border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent);
6
6
  }
7
7
 
8
- .recent-posts__header {
9
- display: grid;
10
- gap: 0.25rem;
8
+ .rr-recent-posts__header {
11
9
  margin-bottom: 1.25rem;
12
10
  }
13
11
 
14
- .recent-posts__eyebrow {
15
- margin: 0;
16
- font-size: 0.75rem;
17
- font-weight: 700;
18
- letter-spacing: 0.12em;
19
- color: color-mix(in srgb, currentColor 54%, transparent);
20
- }
21
-
22
- .recent-posts__title {
12
+ .rr-recent-posts__title {
23
13
  margin: 0;
24
14
  font-size: clamp(1.5rem, 4vw, 2rem);
25
15
  line-height: 1.2;
26
16
  letter-spacing: -0.02em;
27
17
  }
28
18
 
29
- .recent-posts__list {
19
+ .rr-recent-posts__list {
30
20
  display: grid;
31
21
  margin: 0;
32
22
  padding: 0;
33
- border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
34
23
  list-style: none;
35
24
  }
36
25
 
37
- .recent-posts__item {
26
+ .rr-recent-posts__item {
38
27
  margin: 0;
39
28
  padding: 0;
40
29
  border-bottom: 1px solid color-mix(in srgb, currentColor 14%, transparent);
41
30
  }
42
31
 
43
- .recent-posts__link {
32
+ .rr-recent-posts__link {
44
33
  display: grid;
45
34
  gap: 0.375rem;
46
35
  padding: 1rem 0;
@@ -51,30 +40,30 @@
51
40
  padding-inline 160ms ease;
52
41
  }
53
42
 
54
- .recent-posts__link:hover,
55
- .recent-posts__link:focus-visible {
43
+ .rr-recent-posts__link:hover,
44
+ .rr-recent-posts__link:focus-visible {
56
45
  color: var(--rb-color-accent);
57
46
  }
58
47
 
59
- .recent-posts__link:focus-visible {
48
+ .rr-recent-posts__link:focus-visible {
60
49
  outline: 2px solid var(--rb-color-accent);
61
50
  outline-offset: 4px;
62
51
  }
63
52
 
64
- .recent-posts__date {
53
+ .rr-recent-posts__date {
65
54
  font-size: 0.75rem;
66
55
  font-weight: 700;
67
56
  letter-spacing: 0.08em;
68
57
  color: color-mix(in srgb, currentColor 62%, transparent);
69
58
  }
70
59
 
71
- .recent-posts__post-title {
60
+ .rr-recent-posts__post-title {
72
61
  font-size: 1rem;
73
62
  line-height: 1.5;
74
63
  }
75
64
 
76
65
  @media (min-width: 40rem) {
77
- .recent-posts__link {
66
+ .rr-recent-posts__link {
78
67
  grid-template-columns: 8rem minmax(0, 1fr);
79
68
  align-items: baseline;
80
69
  gap: 1.5rem;