@jxsuite/feed 0.1.0 → 0.3.1

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 ADDED
@@ -0,0 +1,144 @@
1
+ # `@jxsuite/feed`
2
+
3
+ > Atom and JSON Feed documents generated from a content collection at build time.
4
+
5
+ ## Overview
6
+
7
+ `@jxsuite/feed` is the Jx extension that turns a content collection into something readers
8
+ subscribe to. It contributes one class (`Feed`) and one project.json section, both reached through
9
+ `jx-extension.json` — which names the class descriptor (`src/Feed.class.json`, whose `project` block
10
+ declares the section key) and the schema fragment that gives the section its shape:
11
+
12
+ - **`feed`** — a map of feed name → feed object. Each entry names its `collection`, the `basePath`
13
+ its entries are served under, and optional metadata (`title`, `description`, `author`), output
14
+ shape (`formats`, `output`, `pageSize`, `archive`, `contentMode`), frontmatter field names
15
+ (`dateField`, `updatedField`) and `language`. The schema
16
+ (`schemas/project.fragment.schema.json`, `$id` `https://jxsuite.com/schema/ext/feed/project/v1`)
17
+ declares the same defaults `normalizeFeedConfig` applies at runtime (`DEFAULTS` in
18
+ `src/shared.ts`), two lists kept in step by hand; it requires at least one feed and rejects
19
+ unknown keys.
20
+
21
+ The behavior is specified in specs/site-architecture.md §6.7 (`Status: Implemented`), which also
22
+ records **why this is an extension** rather than a compiler built-in: a feed is derived from a
23
+ content collection, and wiring the compiler to one extension's section is the coupling
24
+ specs/extensions.md §1 exists to prevent. The capability contracts are specs/extensions.md §8.4
25
+ (`emit`) and §8.6 (`head`). User docs:
26
+ [/docs/framework/site/feeds](https://jxsuite.com/docs/framework/site/feeds).
27
+
28
+ **Atom 1.0 (RFC 4287) and JSON Feed 1.1. RSS 2.0 is deliberately not offered** — no standards body,
29
+ unsettled `<guid>` semantics, and every reader handles Atom. All three standards this package binds
30
+ (RFC 4287, JSON Feed 1.1, RFC 5005) are recorded as **Subset** in the Standards Alignment table of
31
+ specs/site-architecture.md, each with an explicit list of what is not implemented. Read those rows
32
+ before claiming conformance.
33
+
34
+ ## Enable it
35
+
36
+ ```json
37
+ {
38
+ "url": "https://example.com",
39
+ "extensions": ["@jxsuite/parser", "@jxsuite/feed"],
40
+ "content": {
41
+ "posts": { "format": "Markdown", "source": "./content/posts/" }
42
+ },
43
+ "feed": {
44
+ "blog": {
45
+ "collection": "posts",
46
+ "basePath": "/blog/",
47
+ "title": "Example Blog",
48
+ "archive": true
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ `@jxsuite/parser` is what loads the content collections this publishes; a feed names one of them by
55
+ its collection key. A default build writes `dist/feed.xml` and `dist/feed.json`, plus
56
+ `dist/feed/archive/<n>.xml` when `archive` is on, and adds the discovery links to every page's
57
+ `<head>`.
58
+
59
+ **`url` is not optional.** A feed's entry identities are absolute URLs, so without it `head` returns
60
+ `[]` silently, and `emit` returns `[]` after warning that `url` is not set in project.json.
61
+
62
+ ## The `Feed` class
63
+
64
+ `src/Feed.class.json` declares three capabilities, all implemented by the plain object exported as
65
+ `Feed` from `src/feed.ts`:
66
+
67
+ - **`projectData(sectionValue, ctx?)`** — timing `["compiler", "server"]`, the only one that also
68
+ runs on the server. Normalizes the section (defaults applied) into `_project.feed`.
69
+ - **`head(sectionValue, ctx)`** — timing `["compiler"]`. Returns one `<link rel="alternate">` per
70
+ configured format — one per format _and_ locale when the collection is localized — typed
71
+ `application/atom+xml` / `application/feed+json` and titled with the feed's `title` (or
72
+ `"Feed"`). Both links survive `<head>` dedup because the merger keys a link on `rel` plus `href`
73
+ plus whichever of `hreflang`, `type`, `media` or `sizes` is present
74
+ (specs/site-architecture.md §8.3) — for two formats that qualifier is `type`; for a localized
75
+ feed it is `hreflang`, and the links differ by `href` anyway.
76
+ - **`emit(sectionValue, ctx)`** — timing `["compiler"]`. Returns `{ path, content }[]`; the host
77
+ writes them.
78
+
79
+ `head` exists separately from `emit` because the two answer different questions at different times
80
+ (specs/extensions.md §8.6): `head` derives entries from **configuration** and runs once before the
81
+ first page is built, while `emit` derives files from loaded content and runs after the last page has
82
+ been written — too late to reach any `<head>`.
83
+
84
+ ## Emission model
85
+
86
+ Everything happens inside the compiler's site build. `emit` runs at step 6e — after routes,
87
+ components and the worker are generated, and before redirects and the `public/` copy; the **host**
88
+ writes the returned records under `outDir` (paths are outDir-relative, a leading `/` is tolerated,
89
+ and a path escaping `outDir` is a build error). The package never touches the filesystem, which is
90
+ what makes both serializers testable against a literal array of items.
91
+
92
+ Consequences worth knowing:
93
+
94
+ - The `public/` copy runs after `emit`, so a same-named file in `public/` shadows an emitted feed.
95
+ - The compiler writes `_headers` Content-Type rules — `application/atom+xml; charset=utf-8` for
96
+ `/feed.xml` and `/feed/archive/*`, `application/feed+json; charset=utf-8` for `/feed.json` — but
97
+ only for files the build produced, and the names it looks for are the **default** ones
98
+ (`packages/compiler/src/site/headers-emitter.ts`). A custom `output` gets no rule.
99
+
100
+ Subpath exports (`@jxsuite/feed/atom`, `/json-feed`, `/shared`) resolve to TypeScript source, so the
101
+ serializers and the pure feed model — `normalizeFeedConfig`, `entryToItem`, `sortItems`,
102
+ `feedUpdated`, `paginate`, `feedPath` — are importable on their own.
103
+
104
+ ## Surprises
105
+
106
+ - **`pageSize` (default 20) trims the subscription document even when `archive` is `false`.** The
107
+ older entries are then published nowhere. `archive: true` is what preserves them.
108
+ - **Archives are chunked from the oldest end.** Chunking from the newest would reshuffle every
109
+ boundary on each new entry; counting from the oldest means archive 1 keeps its contents forever
110
+ and only the newest archive changes — RFC 5005 §2 asks that a published archive not change.
111
+ - **`<fh:complete/>` needs no archives _and_ nothing trimmed.** A document trimmed by `pageSize`
112
+ with archives off is not complete, and says so by omission.
113
+ - **RFC 5005 archives are Atom-only.** The JSON Feed branch writes exactly one document and never
114
+ sets `next_url`, rather than mixing two pagination conventions in one feed.
115
+ - **Dates are RFC 3339 or nothing.** Only `YYYY-MM-DD` (expanded to `T00:00:00Z`) and a full
116
+ timestamp with `Z` or a numeric offset are accepted; anything else becomes `null` rather than a
117
+ guess. Give the collection schema `"format": "date"` so the parser's date coercion
118
+ (`coerceEntryDates`, specs/parser.md §9.3) normalizes the field first. An entry with no readable
119
+ date falls back to `_meta.mtime`.
120
+ - **The feed-level `<updated>` is the newest item, never the build time** — a feed stamped with the
121
+ build re-notifies every subscriber on every deploy. An empty feed renders
122
+ `<updated>1970-01-01T00:00:00Z</updated>`.
123
+ - **A localized collection is several feeds, not one, and this is not configurable.** When the named
124
+ collection's `source` contains `{locale}`, each language is published in its own URL prefix
125
+ (`/feed.xml`, `/fr-ca/feed.xml`) holding only that language's entries, stating its language with
126
+ `xml:lang` / JSON Feed `language`. A locale with no matching entries is not written at all.
127
+ Discovery advertises every language with `hreflang`, because `head` runs before routing and cannot
128
+ know which locale its page is in.
129
+ - **`contentMode` branches only on `"full"`** (`src/shared.ts`), which is what adds
130
+ `<content type="html">` / `content_html`; the summary is emitted either way. A third value,
131
+ `"none"`, was accepted until 0.3.0 and did nothing — JSON Feed 1.1 requires one of
132
+ `content_html`/`content_text`, so "omit the content" was never expressible.
133
+ - **A feed naming a collection that is not loaded is skipped with a console warning**, not a build
134
+ error.
135
+ - **The schema requires `collection` and `basePath`, but the runtime tolerates their absence** —
136
+ `collection` falls back to the section key and `basePath` to `"/"`. Validation, not
137
+ `normalizeFeedConfig`, is what enforces them.
138
+
139
+ ## Versioning
140
+
141
+ Published to npm as `@jxsuite/feed` — TypeScript source, like every `@jxsuite` package, following
142
+ the monorepo's release train. Its only runtime dependency is `@jxsuite/schema`; extensions may
143
+ depend on core packages but never the reverse — no core package lists this one as a runtime
144
+ dependency or imports it from `src/`, and `scripts/check-dep-rules.ts` enforces that.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/feed",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "Syndication feeds for Jx: Atom (RFC 4287) and JSON Feed emitted from content collections at build time",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "./Feed.class.json": "./src/Feed.class.json"
26
26
  },
27
27
  "publishConfig": {
28
- "provenance": false
28
+ "provenance": true
29
29
  },
30
30
  "scripts": {
31
31
  "prepare": "bun build src/feed.ts --outdir=dist --format=esm --target=node",
@@ -35,7 +35,7 @@
35
35
  "upgrade": "bunx npm-check-updates -u && bun install"
36
36
  },
37
37
  "dependencies": {
38
- "@jxsuite/schema": "workspace:^"
38
+ "@jxsuite/schema": "^1.8.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "ajv": "^8.20.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://jxsuite.com/schema/extensions/feed/v1",
3
+ "$id": "https://jxsuite.com/schema/ext/feed/project/v1",
4
4
  "title": "@jxsuite/feed project section",
5
5
  "type": "object",
6
6
  "properties": {
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dateField": { "type": "string", "default": "date" },
55
55
  "updatedField": { "type": "string", "default": "updated" },
56
- "contentMode": { "enum": ["full", "summary", "none"], "default": "summary" },
56
+ "contentMode": { "enum": ["full", "summary"], "default": "summary" },
57
57
  "language": {
58
58
  "type": "string",
59
59
  "description": "BCP 47 language tag; defaults to defaults.lang"
package/src/shared.ts CHANGED
@@ -20,7 +20,7 @@ export interface FeedConfig {
20
20
  author?: { name?: string; uri?: string; email?: string };
21
21
  dateField?: string;
22
22
  updatedField?: string;
23
- contentMode?: "full" | "summary" | "none";
23
+ contentMode?: "full" | "summary";
24
24
  language?: string;
25
25
  }
26
26