@scalemule/news-publication 0.0.83 → 0.0.85
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 +7 -4
- package/bin/scalemule-article.mjs +3 -3
- package/dist/article-workflow-client.cjs +21 -2
- package/dist/article-workflow-client.d.cts +7 -1
- package/dist/article-workflow-client.d.ts +7 -1
- package/dist/article-workflow-client.js +21 -2
- package/dist/index.cjs +6 -14
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,7 +324,7 @@ The publisher-supplied `PawMark` preserves the approved SVG paths. Hosts may set
|
|
|
324
324
|
|
|
325
325
|
### Homepage image identity
|
|
326
326
|
|
|
327
|
-
`NewsItem.visual_identity` is a versioned public identity from the Blog feed. `composeEdition` allocates covers across the entire bounded inventory,
|
|
327
|
+
`NewsItem.visual_identity` is a versioned public identity from the Blog feed. `composeEdition` allocates covers across the entire bounded inventory and holds repeated covers as `null` without mutating inputs. Lead, secondary, compact and section image placements require a usable distinct cover, including pinned stories. A held lead yields to the first ready story; an entirely unready inventory has no visual lead. Intentional briefs retain their editorial selection rules. `pendingImages` and the complete `navigation` inventory retain unresolved stories for repair and discovery: excluding them from image placements does not mean their photos were repaired. Consumer planners must not refill image placements from raw inventory or reintroduce duplicate covers. Legacy Photo rendition URLs are normalized by tenant/asset ID; server-side Photo identity detects copies under different IDs and ordinary resize/re-encoding/crop variants. Do not fetch or decode images in page components. Preserve existing caption/credit policy. See platform `docs/HOMEPAGE-PHOTO-IDENTITY.md` for storage, rights, concurrency and rollout tests.
|
|
328
328
|
|
|
329
329
|
### Standard article submission
|
|
330
330
|
|
|
@@ -377,7 +377,7 @@ matches one declared slot. Credentials are normal project/member credentials.
|
|
|
377
377
|
The operation key is retained for 30 days. A legacy article must first have an
|
|
378
378
|
explicitly selected collaboration policy; this endpoint never downgrades review.
|
|
379
379
|
|
|
380
|
-
### Edit article prose, cards and
|
|
380
|
+
### Edit article prose, cards, citations and tags
|
|
381
381
|
|
|
382
382
|
```sh
|
|
383
383
|
npx --yes --package @scalemule/news-publication@0.0.81 scalemule-article edit \
|
|
@@ -385,7 +385,10 @@ npx --yes --package @scalemule/news-publication@0.0.81 scalemule-article edit \
|
|
|
385
385
|
```
|
|
386
386
|
|
|
387
387
|
The patch may contain title, subtitle, content_markdown, excerpt, section,
|
|
388
|
-
community, format, layout, cards and
|
|
388
|
+
community, format, layout, cards, citations and `tag_ids`. Tag edits require SDK
|
|
389
|
+
0.0.85+ and the deployed `versioned_editorial_tags` capability. Supply up to 32
|
|
390
|
+
existing tenant-owned tag UUIDs; an empty array clears assignments. Create or
|
|
391
|
+
select tags using the ordinary Blog tags API first. It preserves omitted fields and
|
|
389
392
|
media and saves one immutable revision. Default: save for review. Add
|
|
390
393
|
`--release` only to request release through the existing policy; the command
|
|
391
394
|
never issues APPROVE or downgrades review. On a legacy article, explicitly choose
|
|
@@ -402,7 +405,7 @@ Unknown save/release responses reconcile against the original revision; another
|
|
|
402
405
|
current revision stops rather than being overwritten. Completed replay and no-op
|
|
403
406
|
edits add no revision. `RELEASE_HELD`/exit 2 preserves the saved revision while
|
|
404
407
|
review is pending. With `--url --release`, success requires the rendered
|
|
405
|
-
headline, prose
|
|
408
|
+
headline, prose, actual card components and requested canonical tag links; public visibility pending is also
|
|
406
409
|
exit 2. A saved revision and a rendered published correction are separate states.
|
|
407
410
|
|
|
408
411
|
Use the same client method, `client.editArticle`, for a caller-supplied durable
|
|
@@ -18,7 +18,7 @@ Credentials: SCALEMULE_API_KEY and SCALEMULE_ACCESS_TOKEN (normal authorized mem
|
|
|
18
18
|
Platform admins may use their normal access token with --application-id ID.
|
|
19
19
|
Optional --api-url URL defaults to https://api.scalemule.com.
|
|
20
20
|
|
|
21
|
-
edit saves prose, excerpt, cards and
|
|
21
|
+
edit saves prose, excerpt, cards, citations and tenant-owned tag_ids in one immutable revision.
|
|
22
22
|
Unspecified fields and media are retained. Default: save for review.
|
|
23
23
|
--release explicitly requests release under the existing policy; it never issues
|
|
24
24
|
APPROVE or changes a policy. Legacy onboarding requires an explicit
|
|
@@ -157,11 +157,11 @@ try {
|
|
|
157
157
|
while(!verified){
|
|
158
158
|
const response=await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(5000,remaining()))});
|
|
159
159
|
const html=await response.text();
|
|
160
|
-
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title)});
|
|
160
|
+
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title),tags:checkpoint.document.tag_ids?article.tags:undefined});
|
|
161
161
|
if(!verified)await new Promise(r=>setTimeout(r,Math.min(300,remaining())));
|
|
162
162
|
}
|
|
163
163
|
}catch(error){reason=error.message;}
|
|
164
|
-
if(!verified){console.error(JSON.stringify({...result,state:'published_public_verification_pending',stage:'public_page',message:'The exact revision is published; rendered prose/cards remain unverified. Resume the same edit.',reason}));process.exitCode=2;}
|
|
164
|
+
if(!verified){console.error(JSON.stringify({...result,state:'published_public_verification_pending',stage:'public_page',message:'The exact revision is published; rendered prose/cards/tags remain unverified. Resume the same edit.',reason}));process.exitCode=2;}
|
|
165
165
|
else console.log(JSON.stringify({...result,public_page_verified:true,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
166
166
|
}else{
|
|
167
167
|
console.log(JSON.stringify({...result,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
@@ -43,6 +43,17 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
43
43
|
const all = text(visible);
|
|
44
44
|
if (!paragraphs.length || !paragraphs.every((p) => all.includes(p)))
|
|
45
45
|
return false;
|
|
46
|
+
if (expected.tags?.length) {
|
|
47
|
+
const links = [...visible.matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/giu)];
|
|
48
|
+
if (!expected.tags.every((tag) => links.some((link) => {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(decode(link[1]), "https://publication.example");
|
|
51
|
+
return url.pathname === "/news" && url.searchParams.get("tag") === tag.slug && text(link[2]).replace(/^#\s*/u, "") === tag.name;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}))) return false;
|
|
56
|
+
}
|
|
46
57
|
const titles = [
|
|
47
58
|
...visible.matchAll(
|
|
48
59
|
/<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
|
|
@@ -365,7 +376,8 @@ var patchKeys = /* @__PURE__ */ new Set([
|
|
|
365
376
|
"format",
|
|
366
377
|
"layout",
|
|
367
378
|
"cards",
|
|
368
|
-
"citations"
|
|
379
|
+
"citations",
|
|
380
|
+
"tag_ids"
|
|
369
381
|
]);
|
|
370
382
|
function validate(input) {
|
|
371
383
|
if (!input.operationKey || !/^[\w.:-]{1,128}$/u.test(input.operationKey) || !input.articleId || !input.patch || typeof input.patch !== "object" || Array.isArray(input.patch) || !Object.keys(input.patch).length)
|
|
@@ -374,7 +386,10 @@ function validate(input) {
|
|
|
374
386
|
);
|
|
375
387
|
for (const [key, value] of Object.entries(input.patch)) {
|
|
376
388
|
if (!patchKeys.has(key)) throw Error("Unsupported edit field: " + key);
|
|
377
|
-
if (
|
|
389
|
+
if (key === "tag_ids") {
|
|
390
|
+
if (!Array.isArray(value) || value.length > 32 || new Set(value).size !== value.length || value.some((id) => typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(id) || /^0{8}-0{4}-0{4}-0{4}-0{12}$/u.test(id)))
|
|
391
|
+
throw Error("tag_ids must contain at most 32 distinct, nonempty UUIDs");
|
|
392
|
+
} else if (["cards", "citations"].includes(key)) {
|
|
378
393
|
if (!Array.isArray(value)) throw Error(key + " must be an array");
|
|
379
394
|
} else if (key === "layout") {
|
|
380
395
|
if (!value || typeof value !== "object" || typeof value.id !== "string" || !Number.isInteger(value.version))
|
|
@@ -458,6 +473,8 @@ async function runArticleEdit(t, input) {
|
|
|
458
473
|
stage,
|
|
459
474
|
"The server must expose versioned_editorial_fields before this edit; no article was changed"
|
|
460
475
|
);
|
|
476
|
+
if (input.patch.tag_ids !== void 0 && options.capabilities?.versioned_editorial_tags !== true)
|
|
477
|
+
throw new ArticleEditOperationError("CAPABILITY_UNAVAILABLE", stage, "The server must expose versioned_editorial_tags before changing tags; no article was changed");
|
|
461
478
|
if (!identityMatches(source))
|
|
462
479
|
throw new ArticleEditOperationError(
|
|
463
480
|
"ARTICLE_IDENTITY_CHANGED",
|
|
@@ -621,6 +638,8 @@ async function runArticleEdit(t, input) {
|
|
|
621
638
|
409,
|
|
622
639
|
state.revisionId
|
|
623
640
|
);
|
|
641
|
+
if (state.document?.tag_ids && !equal([...state.document.tag_ids].sort(), (after.tags ?? []).map((tag) => tag.id).sort()))
|
|
642
|
+
throw new ArticleEditOperationError("VERIFICATION_CONFLICT", stage, "Published tags differ from the saved revision", 409, state.revisionId);
|
|
624
643
|
const normalizedCards = state.document?.cards;
|
|
625
644
|
if (normalizedCards && after.metadata?.cards?.length !== normalizedCards.length)
|
|
626
645
|
throw new ArticleEditOperationError(
|
|
@@ -8,6 +8,10 @@ declare function articleEditHtmlMatches(html: string, expected: {
|
|
|
8
8
|
title: string;
|
|
9
9
|
content_html: string;
|
|
10
10
|
card_titles: string[];
|
|
11
|
+
tags?: {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
}[];
|
|
11
15
|
}): boolean;
|
|
12
16
|
|
|
13
17
|
type PhotoOperationCheckpoint = {
|
|
@@ -48,7 +52,7 @@ declare class ArticlePhotoOperationError extends Error {
|
|
|
48
52
|
constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations">>;
|
|
55
|
+
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
|
|
52
56
|
type ArticleEditCheckpoint = {
|
|
53
57
|
version: 1;
|
|
54
58
|
identity: string;
|
|
@@ -93,6 +97,7 @@ type ArticleDocument = {
|
|
|
93
97
|
excerpt?: string;
|
|
94
98
|
cards?: unknown[];
|
|
95
99
|
citations?: unknown[];
|
|
100
|
+
tag_ids?: string[];
|
|
96
101
|
section: string;
|
|
97
102
|
community?: string;
|
|
98
103
|
format?: ArticleFormat;
|
|
@@ -195,6 +200,7 @@ type ArticleWorkflowOptions = {
|
|
|
195
200
|
prioritized_narration_processing?: boolean;
|
|
196
201
|
content_first_editor_document?: boolean;
|
|
197
202
|
versioned_editorial_fields?: boolean;
|
|
203
|
+
versioned_editorial_tags?: boolean;
|
|
198
204
|
managed_submission_version?: number | null;
|
|
199
205
|
multipart_submission?: boolean;
|
|
200
206
|
};
|
|
@@ -8,6 +8,10 @@ declare function articleEditHtmlMatches(html: string, expected: {
|
|
|
8
8
|
title: string;
|
|
9
9
|
content_html: string;
|
|
10
10
|
card_titles: string[];
|
|
11
|
+
tags?: {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
}[];
|
|
11
15
|
}): boolean;
|
|
12
16
|
|
|
13
17
|
type PhotoOperationCheckpoint = {
|
|
@@ -48,7 +52,7 @@ declare class ArticlePhotoOperationError extends Error {
|
|
|
48
52
|
constructor(code: string, stage: string, message: string, fileId?: string | undefined, status?: number | undefined);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations">>;
|
|
55
|
+
type ArticleEditPatch = Partial<Pick<ArticleDocument, "title" | "subtitle" | "content_markdown" | "excerpt" | "section" | "community" | "format" | "layout" | "cards" | "citations" | "tag_ids">>;
|
|
52
56
|
type ArticleEditCheckpoint = {
|
|
53
57
|
version: 1;
|
|
54
58
|
identity: string;
|
|
@@ -93,6 +97,7 @@ type ArticleDocument = {
|
|
|
93
97
|
excerpt?: string;
|
|
94
98
|
cards?: unknown[];
|
|
95
99
|
citations?: unknown[];
|
|
100
|
+
tag_ids?: string[];
|
|
96
101
|
section: string;
|
|
97
102
|
community?: string;
|
|
98
103
|
format?: ArticleFormat;
|
|
@@ -195,6 +200,7 @@ type ArticleWorkflowOptions = {
|
|
|
195
200
|
prioritized_narration_processing?: boolean;
|
|
196
201
|
content_first_editor_document?: boolean;
|
|
197
202
|
versioned_editorial_fields?: boolean;
|
|
203
|
+
versioned_editorial_tags?: boolean;
|
|
198
204
|
managed_submission_version?: number | null;
|
|
199
205
|
multipart_submission?: boolean;
|
|
200
206
|
};
|
|
@@ -41,6 +41,17 @@ function articleEditHtmlMatches(html, expected) {
|
|
|
41
41
|
const all = text(visible);
|
|
42
42
|
if (!paragraphs.length || !paragraphs.every((p) => all.includes(p)))
|
|
43
43
|
return false;
|
|
44
|
+
if (expected.tags?.length) {
|
|
45
|
+
const links = [...visible.matchAll(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/giu)];
|
|
46
|
+
if (!expected.tags.every((tag) => links.some((link) => {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(decode(link[1]), "https://publication.example");
|
|
49
|
+
return url.pathname === "/news" && url.searchParams.get("tag") === tag.slug && text(link[2]).replace(/^#\s*/u, "") === tag.name;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}))) return false;
|
|
54
|
+
}
|
|
44
55
|
const titles = [
|
|
45
56
|
...visible.matchAll(
|
|
46
57
|
/<aside\b[^>]*class=["'][^"']*\barticle-verified-card\b[^"']*["'][^>]*>([\s\S]*?)<\/aside>/giu
|
|
@@ -363,7 +374,8 @@ var patchKeys = /* @__PURE__ */ new Set([
|
|
|
363
374
|
"format",
|
|
364
375
|
"layout",
|
|
365
376
|
"cards",
|
|
366
|
-
"citations"
|
|
377
|
+
"citations",
|
|
378
|
+
"tag_ids"
|
|
367
379
|
]);
|
|
368
380
|
function validate(input) {
|
|
369
381
|
if (!input.operationKey || !/^[\w.:-]{1,128}$/u.test(input.operationKey) || !input.articleId || !input.patch || typeof input.patch !== "object" || Array.isArray(input.patch) || !Object.keys(input.patch).length)
|
|
@@ -372,7 +384,10 @@ function validate(input) {
|
|
|
372
384
|
);
|
|
373
385
|
for (const [key, value] of Object.entries(input.patch)) {
|
|
374
386
|
if (!patchKeys.has(key)) throw Error("Unsupported edit field: " + key);
|
|
375
|
-
if (
|
|
387
|
+
if (key === "tag_ids") {
|
|
388
|
+
if (!Array.isArray(value) || value.length > 32 || new Set(value).size !== value.length || value.some((id) => typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(id) || /^0{8}-0{4}-0{4}-0{4}-0{12}$/u.test(id)))
|
|
389
|
+
throw Error("tag_ids must contain at most 32 distinct, nonempty UUIDs");
|
|
390
|
+
} else if (["cards", "citations"].includes(key)) {
|
|
376
391
|
if (!Array.isArray(value)) throw Error(key + " must be an array");
|
|
377
392
|
} else if (key === "layout") {
|
|
378
393
|
if (!value || typeof value !== "object" || typeof value.id !== "string" || !Number.isInteger(value.version))
|
|
@@ -456,6 +471,8 @@ async function runArticleEdit(t, input) {
|
|
|
456
471
|
stage,
|
|
457
472
|
"The server must expose versioned_editorial_fields before this edit; no article was changed"
|
|
458
473
|
);
|
|
474
|
+
if (input.patch.tag_ids !== void 0 && options.capabilities?.versioned_editorial_tags !== true)
|
|
475
|
+
throw new ArticleEditOperationError("CAPABILITY_UNAVAILABLE", stage, "The server must expose versioned_editorial_tags before changing tags; no article was changed");
|
|
459
476
|
if (!identityMatches(source))
|
|
460
477
|
throw new ArticleEditOperationError(
|
|
461
478
|
"ARTICLE_IDENTITY_CHANGED",
|
|
@@ -619,6 +636,8 @@ async function runArticleEdit(t, input) {
|
|
|
619
636
|
409,
|
|
620
637
|
state.revisionId
|
|
621
638
|
);
|
|
639
|
+
if (state.document?.tag_ids && !equal([...state.document.tag_ids].sort(), (after.tags ?? []).map((tag) => tag.id).sort()))
|
|
640
|
+
throw new ArticleEditOperationError("VERIFICATION_CONFLICT", stage, "Published tags differ from the saved revision", 409, state.revisionId);
|
|
622
641
|
const normalizedCards = state.document?.cards;
|
|
623
642
|
if (normalizedCards && after.metadata?.cards?.length !== normalizedCards.length)
|
|
624
643
|
throw new ArticleEditOperationError(
|
package/dist/index.cjs
CHANGED
|
@@ -188,7 +188,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
188
188
|
return true;
|
|
189
189
|
});
|
|
190
190
|
const pins = !selection.pinned_until || Date.parse(selection.pinned_until) > now ? selection : {};
|
|
191
|
-
const selectedLead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory
|
|
191
|
+
const selectedLead = inventory.find((item) => item.slug === pins.lead_slug && coverIdentity(item)) ?? inventory.find((item) => coverIdentity(item));
|
|
192
192
|
const rest = inventory.filter((item) => item !== selectedLead);
|
|
193
193
|
const requested = new Set(pins.secondary_slugs ?? []);
|
|
194
194
|
const ordered = [
|
|
@@ -197,13 +197,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
197
197
|
];
|
|
198
198
|
const originalOrder = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
|
|
199
199
|
const seenImages = /* @__PURE__ */ new Set();
|
|
200
|
-
const duplicateImages = /* @__PURE__ */ new Set();
|
|
201
200
|
const display = /* @__PURE__ */ new Map();
|
|
202
201
|
for (const item of [selectedLead, ...originalOrder]) {
|
|
203
202
|
if (!item) continue;
|
|
204
203
|
const key = coverIdentity(item);
|
|
205
204
|
if (key && seenImages.has(key)) {
|
|
206
|
-
duplicateImages.add(item.id);
|
|
207
205
|
display.set(item.id, { ...item, cover_image_url: null });
|
|
208
206
|
} else {
|
|
209
207
|
display.set(item.id, item);
|
|
@@ -214,14 +212,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
214
212
|
const lead = selectedLead ? display.get(selectedLead.id) : void 0;
|
|
215
213
|
let remaining = rest.map((item) => display.get(item.id));
|
|
216
214
|
const uniqueOrdered = originalOrder.map((item) => display.get(item.id));
|
|
217
|
-
const secondary =
|
|
218
|
-
const deferred = [];
|
|
219
|
-
for (const item of uniqueOrdered) {
|
|
220
|
-
if (secondary.length >= 3) break;
|
|
221
|
-
if (duplicateImages.has(item.id)) deferred.push(item);
|
|
222
|
-
else secondary.push(item);
|
|
223
|
-
}
|
|
224
|
-
while (secondary.length < 3 && deferred.length > 0) secondary.push(deferred.shift());
|
|
215
|
+
const secondary = uniqueOrdered.filter((item) => coverIdentity(item)).slice(0, 3);
|
|
225
216
|
const secondaryIds = new Set(secondary.map((item) => item.id));
|
|
226
217
|
remaining = remaining.filter((item) => !secondaryIds.has(item.id));
|
|
227
218
|
const eligible2 = remaining.filter((item) => {
|
|
@@ -236,10 +227,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
236
227
|
briefIds.add(item.id);
|
|
237
228
|
return true;
|
|
238
229
|
});
|
|
239
|
-
const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
|
|
230
|
+
const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].filter((item) => coverIdentity(item)).slice(0, 4);
|
|
240
231
|
const compactIds = new Set(compact.map((item) => item.id));
|
|
241
|
-
const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
|
|
242
|
-
|
|
232
|
+
const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id) && coverIdentity(item)));
|
|
233
|
+
const pendingImages = inventory.filter((item) => !coverIdentity(item));
|
|
234
|
+
return { lead, secondary, briefs, compact, sections: sections2, pendingImages, navigation: editorialSections(inventory) };
|
|
243
235
|
}
|
|
244
236
|
function displayHeadline(article, selection = {}) {
|
|
245
237
|
return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
|
package/dist/index.d.cts
CHANGED
|
@@ -246,6 +246,7 @@ declare function composeEdition(items: NewsItem[], selection?: HomepageSelection
|
|
|
246
246
|
briefs: NewsItem[];
|
|
247
247
|
compact: NewsItem[];
|
|
248
248
|
sections: EditorialSection[];
|
|
249
|
+
pendingImages: NewsItem[];
|
|
249
250
|
navigation: EditorialSection[];
|
|
250
251
|
};
|
|
251
252
|
declare function displayHeadline(article: NewsItem, selection?: HomepageSelection): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -246,6 +246,7 @@ declare function composeEdition(items: NewsItem[], selection?: HomepageSelection
|
|
|
246
246
|
briefs: NewsItem[];
|
|
247
247
|
compact: NewsItem[];
|
|
248
248
|
sections: EditorialSection[];
|
|
249
|
+
pendingImages: NewsItem[];
|
|
249
250
|
navigation: EditorialSection[];
|
|
250
251
|
};
|
|
251
252
|
declare function displayHeadline(article: NewsItem, selection?: HomepageSelection): string;
|
package/dist/index.js
CHANGED
|
@@ -132,7 +132,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
132
132
|
return true;
|
|
133
133
|
});
|
|
134
134
|
const pins = !selection.pinned_until || Date.parse(selection.pinned_until) > now ? selection : {};
|
|
135
|
-
const selectedLead = inventory.find((item) => item.slug === pins.lead_slug) ?? inventory
|
|
135
|
+
const selectedLead = inventory.find((item) => item.slug === pins.lead_slug && coverIdentity(item)) ?? inventory.find((item) => coverIdentity(item));
|
|
136
136
|
const rest = inventory.filter((item) => item !== selectedLead);
|
|
137
137
|
const requested = new Set(pins.secondary_slugs ?? []);
|
|
138
138
|
const ordered = [
|
|
@@ -141,13 +141,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
141
141
|
];
|
|
142
142
|
const originalOrder = Array.from(new Map(ordered.map((item) => [item.id, item])).values());
|
|
143
143
|
const seenImages = /* @__PURE__ */ new Set();
|
|
144
|
-
const duplicateImages = /* @__PURE__ */ new Set();
|
|
145
144
|
const display = /* @__PURE__ */ new Map();
|
|
146
145
|
for (const item of [selectedLead, ...originalOrder]) {
|
|
147
146
|
if (!item) continue;
|
|
148
147
|
const key = coverIdentity(item);
|
|
149
148
|
if (key && seenImages.has(key)) {
|
|
150
|
-
duplicateImages.add(item.id);
|
|
151
149
|
display.set(item.id, { ...item, cover_image_url: null });
|
|
152
150
|
} else {
|
|
153
151
|
display.set(item.id, item);
|
|
@@ -158,14 +156,7 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
158
156
|
const lead = selectedLead ? display.get(selectedLead.id) : void 0;
|
|
159
157
|
let remaining = rest.map((item) => display.get(item.id));
|
|
160
158
|
const uniqueOrdered = originalOrder.map((item) => display.get(item.id));
|
|
161
|
-
const secondary =
|
|
162
|
-
const deferred = [];
|
|
163
|
-
for (const item of uniqueOrdered) {
|
|
164
|
-
if (secondary.length >= 3) break;
|
|
165
|
-
if (duplicateImages.has(item.id)) deferred.push(item);
|
|
166
|
-
else secondary.push(item);
|
|
167
|
-
}
|
|
168
|
-
while (secondary.length < 3 && deferred.length > 0) secondary.push(deferred.shift());
|
|
159
|
+
const secondary = uniqueOrdered.filter((item) => coverIdentity(item)).slice(0, 3);
|
|
169
160
|
const secondaryIds = new Set(secondary.map((item) => item.id));
|
|
170
161
|
remaining = remaining.filter((item) => !secondaryIds.has(item.id));
|
|
171
162
|
const eligible2 = remaining.filter((item) => {
|
|
@@ -180,10 +171,11 @@ function composeEdition(items, selection = {}, now = Date.now()) {
|
|
|
180
171
|
briefIds.add(item.id);
|
|
181
172
|
return true;
|
|
182
173
|
});
|
|
183
|
-
const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].slice(0, 4);
|
|
174
|
+
const compact = [...briefs, ...remaining.filter((item) => !briefIds.has(item.id))].filter((item) => coverIdentity(item)).slice(0, 4);
|
|
184
175
|
const compactIds = new Set(compact.map((item) => item.id));
|
|
185
|
-
const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id)));
|
|
186
|
-
|
|
176
|
+
const sections2 = editorialSections(remaining.filter((item) => !compactIds.has(item.id) && coverIdentity(item)));
|
|
177
|
+
const pendingImages = inventory.filter((item) => !coverIdentity(item));
|
|
178
|
+
return { lead, secondary, briefs, compact, sections: sections2, pendingImages, navigation: editorialSections(inventory) };
|
|
187
179
|
}
|
|
188
180
|
function displayHeadline(article, selection = {}) {
|
|
189
181
|
return selection.stories?.[article.slug]?.display_title?.trim() || article.title;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.85",
|
|
4
4
|
"description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|