@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 +144 -0
- package/package.json +3 -3
- package/schemas/project.fragment.schema.json +2 -2
- package/src/shared.ts +1 -1
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
|
|
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":
|
|
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": "
|
|
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/
|
|
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"
|
|
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