@riebeckite/plugin-recent-posts 0.0.19 → 0.0.21

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
@@ -7,11 +7,13 @@ frontmatter date.
7
7
 
8
8
  ## Overview
9
9
 
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.
10
+ `RecentPosts` renders an ordered list of recently posted articles.
11
+ `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. Official `starter` and `showcase` sites place the
16
+ list explicitly in their homepage route after the page content.
15
17
 
16
18
  With no posts, the component renders nothing.
17
19
 
@@ -28,35 +30,30 @@ export default defineConfig({
28
30
  ```
29
31
 
30
32
  `recentPostsPlugin()` registers the plugin in the plugin list and bundles
31
- `style.css` into the app stylesheet.
33
+ `style.css` into the app stylesheet; it never renders UI itself. The generated
34
+ `starter` and `showcase` demonstrate the recommended homepage placement.
32
35
 
33
- ### Render the component
36
+ ### Custom placement
34
37
 
35
38
  ```tsx
36
- import RecentPosts, { getRecentPosts } from "@riebeckite/plugin-recent-posts";
37
- import { config } from "../config";
38
- 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
- });
39
+ import { RecentPosts, getRecentPosts } from "@riebeckite/plugin-recent-posts";
40
+ import { content } from "virtual:riebeckite/content";
41
+
42
+ const recentPosts = getRecentPosts({ manifest: await content.getManifest() });
49
43
 
50
- // ...in your route
51
- return <Article afterContent={<RecentPosts posts={recentPosts} />} />;
44
+ // ...in a homepage route or site-owned layout
45
+ return <RecentPosts posts={recentPosts} />;
52
46
  ```
53
47
 
54
48
  `getRecentPosts()` filters out the `index` note before collecting posts.
49
+ To disable the generated homepage list, remove its `RecentPosts` import and
50
+ `afterContent` prop. To reposition it, move that same element to the desired
51
+ existing route or layout slot.
55
52
 
56
53
  ## Component
57
54
 
58
55
  `RecentPosts({ posts })` renders a
59
- `<section class="recent-posts rr-recent-posts">` with a heading and an ordered
56
+ `<section class="rr-recent-posts">` with a heading and an ordered
60
57
  list. Each item links to the post and shows its date formatted for the
61
58
  `en-US` locale. The `rr-recent-posts` root hook is the stable class themes may
62
59
  target.
@@ -65,16 +62,17 @@ target.
65
62
 
66
63
  | Option | Type | Default | Description |
67
64
  | ------ | ---- | ------- | ----------- |
65
+ | `manifest` | `Pick<ContentManifest, "discoverableEntries">` | — | Source of candidate posts |
68
66
  | `limit` | `number` | `5` | Maximum number of posts to return |
69
67
 
70
68
  ## Exports
71
69
 
72
70
  - `recentPostsPlugin()` — plugin factory
73
71
  - `RecentPosts` — list component (default export of `components/recent-posts.tsx`)
74
- - `getRecentPosts({ posts, config, getProcessedContent, resolveTitle, limit? })`
75
- — collects the latest published posts
72
+ - `getRecentPosts({ manifest, limit? })` — collects the latest discoverable
73
+ posts
76
74
  - Type: `RecentPost` (`{ slug, permalink, title, postedAt }`)
77
75
 
78
76
  ## See also
79
77
 
80
- - [Plugin guide](../../../docs/docs/reference/plugin-api.en.md)
78
+ - [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` は結果が空なら何も描画しません。公式の `starter` と `showcase` は、ホームページ route で一覧を本文の後に明示的に配置します。
10
10
 
11
11
  ## 設定と配置
12
12
 
@@ -17,27 +17,30 @@ import { recentPostsPlugin } from "@riebeckite/plugin-recent-posts";
17
17
  export default defineConfig({ plugins: [recentPostsPlugin()] });
18
18
  ```
19
19
 
20
- 一覧を置く場所はアプリ側で決めます。
20
+ `recentPostsPlugin()` は UI を自動描画しません。生成された `starter` と `showcase` は推奨するホームページ配置を実装しています。独自の配置では、アプリ側で表示位置を決めます。
21
21
 
22
22
  ```tsx
23
- import RecentPosts, { getRecentPosts } from "@riebeckite/plugin-recent-posts";
23
+ import { RecentPosts, getRecentPosts } from "@riebeckite/plugin-recent-posts";
24
+ import { content } from "virtual:riebeckite/content";
24
25
 
25
- const posts = await getRecentPosts({ posts: await content.getAllPosts(), config, getProcessedContent: content.getProcessedContent, resolveTitle, limit: 5 });
26
- return <Article afterContent={<RecentPosts posts={posts} />} />;
26
+ const posts = getRecentPosts({ manifest: await content.getManifest() });
27
+ return <RecentPosts posts={posts} />;
27
28
  ```
28
29
 
30
+ 生成されたホームページの一覧を無効にするには、`RecentPosts` の import と `afterContent` prop を削除します。位置を変える場合は、同じ要素を既存の route または layout の目的の slot へ移動します。
31
+
29
32
  | オプション | 既定値 | 内容 |
30
33
  | --- | --- | --- |
34
+ | `manifest` | 必須 | 候補ノートの取得元(`discoverableEntries`) |
31
35
  | `limit` | `5` | 返す記事数の上限 |
32
36
 
33
37
  ## 公開 API
34
38
 
35
39
  - `recentPostsPlugin()` — プラグインファクトリ
36
40
  - `RecentPosts` — 最新記事一覧コンポーネント
37
- - `getRecentPosts({ posts, config, getProcessedContent, resolveTitle, limit? })`
41
+ - `getRecentPosts({ manifest, limit? })`
38
42
  - `RecentPost` — `{ slug, permalink, title, postedAt }` の型
39
43
 
40
44
  ## 関連資料
41
45
 
42
- - [プラグインシステム](../../../docs/docs/reference/plugin-api.md)
43
-
46
+ - [プラグインシステム](../../../docs/docs/reference/plugin-api.ja.md)
@@ -1,6 +1,6 @@
1
1
  import type { RecentPost } from "../src/recent-posts.js";
2
2
  type Props = {
3
- posts: RecentPost[];
3
+ posts?: RecentPost[];
4
4
  };
5
5
  export default function RecentPosts(props: Props): import("hono/jsx/jsx-dev-runtime").JSX.Element;
6
6
  export {};
@@ -1,32 +1,26 @@
1
1
  // components/recent-posts.tsx
2
2
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
3
3
  function RecentPosts(props) {
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__ */ jsx("div", { class: "recent-posts__header", children: /* @__PURE__ */ jsx("h2", { class: "recent-posts__title", id: "recent-posts-title", children: "Recent Posts" }) }),
12
- /* @__PURE__ */ jsx("ol", { class: "recent-posts__list", children: props.posts.map((post) => {
13
- const formattedDate = formatPostedDate(post.postedAt);
14
- return /* @__PURE__ */ jsx("li", { class: "recent-posts__item", children: /* @__PURE__ */ jsxs("a", { class: "recent-posts__link", href: post.permalink, children: [
15
- /* @__PURE__ */ jsx(
16
- "time",
17
- {
18
- class: "recent-posts__date",
19
- dateTime: formattedDate.isoDate,
20
- title: formattedDate.fullDate,
21
- children: formattedDate.displayDate
22
- }
23
- ),
24
- /* @__PURE__ */ jsx("span", { class: "recent-posts__post-title", children: post.title })
25
- ] }) }, post.slug);
26
- }) })
27
- ]
28
- }
29
- );
4
+ const posts = props.posts ?? [];
5
+ if (posts.length === 0) return null;
6
+ return /* @__PURE__ */ jsxs("section", { class: "rr-recent-posts", "aria-labelledby": "recent-posts-title", children: [
7
+ /* @__PURE__ */ jsx("div", { class: "rr-recent-posts__header", children: /* @__PURE__ */ jsx("h2", { class: "rr-recent-posts__title", id: "recent-posts-title", children: "Recent Posts" }) }),
8
+ /* @__PURE__ */ jsx("ol", { class: "rr-recent-posts__list", children: posts.map((post) => {
9
+ const formattedDate = formatPostedDate(post.postedAt);
10
+ return /* @__PURE__ */ jsx("li", { class: "rr-recent-posts__item", children: /* @__PURE__ */ jsxs("a", { class: "rr-recent-posts__link", href: post.permalink, children: [
11
+ /* @__PURE__ */ jsx(
12
+ "time",
13
+ {
14
+ class: "rr-recent-posts__date",
15
+ dateTime: formattedDate.isoDate,
16
+ title: formattedDate.fullDate,
17
+ children: formattedDate.displayDate
18
+ }
19
+ ),
20
+ /* @__PURE__ */ jsx("span", { class: "rr-recent-posts__post-title", children: post.title })
21
+ ] }) }, post.slug);
22
+ }) })
23
+ ] });
30
24
  }
31
25
  function formatPostedDate(date) {
32
26
  const isoDate = date.toISOString();
package/dist/index.js CHANGED
@@ -4,32 +4,26 @@ import { definePlugin } from "@riebeckite/core";
4
4
  // components/recent-posts.tsx
5
5
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
6
  function RecentPosts(props) {
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__ */ jsx("div", { class: "recent-posts__header", children: /* @__PURE__ */ jsx("h2", { class: "recent-posts__title", id: "recent-posts-title", children: "Recent Posts" }) }),
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
- );
7
+ const posts = props.posts ?? [];
8
+ if (posts.length === 0) return null;
9
+ return /* @__PURE__ */ jsxs("section", { class: "rr-recent-posts", "aria-labelledby": "recent-posts-title", children: [
10
+ /* @__PURE__ */ jsx("div", { class: "rr-recent-posts__header", children: /* @__PURE__ */ jsx("h2", { class: "rr-recent-posts__title", id: "recent-posts-title", children: "Recent Posts" }) }),
11
+ /* @__PURE__ */ jsx("ol", { class: "rr-recent-posts__list", children: posts.map((post) => {
12
+ const formattedDate = formatPostedDate(post.postedAt);
13
+ return /* @__PURE__ */ jsx("li", { class: "rr-recent-posts__item", children: /* @__PURE__ */ jsxs("a", { class: "rr-recent-posts__link", href: post.permalink, children: [
14
+ /* @__PURE__ */ jsx(
15
+ "time",
16
+ {
17
+ class: "rr-recent-posts__date",
18
+ dateTime: formattedDate.isoDate,
19
+ title: formattedDate.fullDate,
20
+ children: formattedDate.displayDate
21
+ }
22
+ ),
23
+ /* @__PURE__ */ jsx("span", { class: "rr-recent-posts__post-title", children: post.title })
24
+ ] }) }, post.slug);
25
+ }) })
26
+ ] });
33
27
  }
34
28
  function formatPostedDate(date) {
35
29
  const isoDate = date.toISOString();
@@ -46,24 +40,23 @@ function formatPostedDate(date) {
46
40
  }
47
41
 
48
42
  // src/recent-posts.server.ts
49
- async function getRecentPosts(args) {
50
- const limit = args.limit ?? 5;
51
- const recentPosts = await Promise.all(
52
- args.posts.filter((post) => post.slug !== "index").map(async (post) => {
53
- const processed = await args.getProcessedContent(post.slug);
54
- const postedAt = parseFrontmatterDate(
55
- processed.frontmatter.date ?? processed.frontmatter.created
56
- );
57
- if (!postedAt) return null;
58
- return {
59
- slug: post.slug,
60
- permalink: post.permalink,
61
- title: args.resolveTitle(post.slug, processed.frontmatter.title),
43
+ var DEFAULT_RECENT_POSTS_LIMIT = 5;
44
+ function getRecentPosts(args) {
45
+ const limit = Math.max(0, args.limit ?? DEFAULT_RECENT_POSTS_LIMIT);
46
+ return args.manifest.discoverableEntries.filter((entry) => entry.slug !== "index").flatMap((entry) => {
47
+ const postedAt = parseFrontmatterDate(
48
+ entry.frontmatter.date ?? entry.frontmatter.created
49
+ );
50
+ if (!postedAt) return [];
51
+ return [
52
+ {
53
+ slug: entry.slug,
54
+ permalink: entry.permalink,
55
+ title: entry.title,
62
56
  postedAt
63
- };
64
- })
65
- );
66
- return recentPosts.filter((post) => post !== null).toSorted((a, b) => b.postedAt.getTime() - a.postedAt.getTime()).slice(0, limit);
57
+ }
58
+ ];
59
+ }).toSorted((a, b) => b.postedAt.getTime() - a.postedAt.getTime()).slice(0, limit);
67
60
  }
68
61
  function parseFrontmatterDate(value) {
69
62
  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.19",
3
+ "version": "0.0.21",
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.19"
25
+ "@riebeckite/core": "0.0.21"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsx": "^4.22.4"
package/style.css CHANGED
@@ -1,35 +1,37 @@
1
1
  @layer components {
2
- .recent-posts {
3
- margin-top: 4rem;
2
+ .rr-recent-posts {
3
+ margin-top: var(--rb-space-6, 3rem);
4
4
  padding-top: 1.5rem;
5
- border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent);
5
+ border-top: 1px solid
6
+ var(--rb-color-border, color-mix(in srgb, currentColor 18%, transparent));
6
7
  }
7
8
 
8
- .recent-posts__header {
9
+ .rr-recent-posts__header {
9
10
  margin-bottom: 1.25rem;
10
11
  }
11
12
 
12
- .recent-posts__title {
13
+ .rr-recent-posts__title {
13
14
  margin: 0;
14
- font-size: clamp(1.5rem, 4vw, 2rem);
15
+ font-size: 1.125rem;
16
+ font-weight: 700;
15
17
  line-height: 1.2;
16
18
  letter-spacing: -0.02em;
17
19
  }
18
20
 
19
- .recent-posts__list {
21
+ .rr-recent-posts__list {
20
22
  display: grid;
21
23
  margin: 0;
22
24
  padding: 0;
23
25
  list-style: none;
24
26
  }
25
27
 
26
- .recent-posts__item {
28
+ .rr-recent-posts__item {
27
29
  margin: 0;
28
30
  padding: 0;
29
31
  border-bottom: 1px solid color-mix(in srgb, currentColor 14%, transparent);
30
32
  }
31
33
 
32
- .recent-posts__link {
34
+ .rr-recent-posts__link {
33
35
  display: grid;
34
36
  gap: 0.375rem;
35
37
  padding: 1rem 0;
@@ -40,30 +42,30 @@
40
42
  padding-inline 160ms ease;
41
43
  }
42
44
 
43
- .recent-posts__link:hover,
44
- .recent-posts__link:focus-visible {
45
+ .rr-recent-posts__link:hover,
46
+ .rr-recent-posts__link:focus-visible {
45
47
  color: var(--rb-color-accent);
46
48
  }
47
49
 
48
- .recent-posts__link:focus-visible {
50
+ .rr-recent-posts__link:focus-visible {
49
51
  outline: 2px solid var(--rb-color-accent);
50
52
  outline-offset: 4px;
51
53
  }
52
54
 
53
- .recent-posts__date {
55
+ .rr-recent-posts__date {
54
56
  font-size: 0.75rem;
55
57
  font-weight: 700;
56
58
  letter-spacing: 0.08em;
57
59
  color: color-mix(in srgb, currentColor 62%, transparent);
58
60
  }
59
61
 
60
- .recent-posts__post-title {
62
+ .rr-recent-posts__post-title {
61
63
  font-size: 1rem;
62
64
  line-height: 1.5;
63
65
  }
64
66
 
65
67
  @media (min-width: 40rem) {
66
- .recent-posts__link {
68
+ .rr-recent-posts__link {
67
69
  grid-template-columns: 8rem minmax(0, 1fr);
68
70
  align-items: baseline;
69
71
  gap: 1.5rem;