@mindful-web/marko-web-digital-edition 1.95.0

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.
Files changed (41) hide show
  1. package/.eslintignore +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +121 -0
  4. package/components/ad.marko +81 -0
  5. package/components/ad.marko.js +102 -0
  6. package/components/article-body.marko +134 -0
  7. package/components/article-body.marko.js +186 -0
  8. package/components/author.marko +30 -0
  9. package/components/author.marko.js +59 -0
  10. package/components/gate.marko +158 -0
  11. package/components/gate.marko.js +147 -0
  12. package/components/ld-json.marko +5 -0
  13. package/components/ld-json.marko.js +29 -0
  14. package/components/marko.json +78 -0
  15. package/components/reflow.marko +67 -0
  16. package/components/reflow.marko.js +106 -0
  17. package/components/replica.marko +86 -0
  18. package/components/replica.marko.js +128 -0
  19. package/components/scroll.marko +134 -0
  20. package/components/scroll.marko.js +23 -0
  21. package/components/theme.marko +73 -0
  22. package/components/theme.marko.js +30 -0
  23. package/components/toc.marko +28 -0
  24. package/components/toc.marko.js +53 -0
  25. package/components/toolbar.marko +47 -0
  26. package/components/toolbar.marko.js +54 -0
  27. package/components/tracking.marko +211 -0
  28. package/components/tracking.marko.js +60 -0
  29. package/lib/authors.js +125 -0
  30. package/lib/entities.js +97 -0
  31. package/lib/index.js +453 -0
  32. package/marko.json +6 -0
  33. package/package.json +33 -0
  34. package/routes/index.js +129 -0
  35. package/scss/index.scss +679 -0
  36. package/templates/article.marko +108 -0
  37. package/templates/article.marko.js +238 -0
  38. package/templates/hub.marko +94 -0
  39. package/templates/hub.marko.js +219 -0
  40. package/templates/partial.marko +17 -0
  41. package/templates/partial.marko.js +44 -0
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ *.marko.js
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Parameter1 LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @mindful-web/marko-web-digital-edition
2
+
3
+ A native digital edition for a magazine issue: a **reflow** article reader and a
4
+ **replica** page view, served on the publisher's own site under the issue's URL,
5
+ gated with IdentityX, tracked with p1events, indexable with paywalled-content
6
+ structured data.
7
+
8
+ The edition is described entirely by a **manifest JSON** produced by the
9
+ digital-edition ingest pipeline from the issue's press PDF. This package reads that
10
+ file; it reads and writes no CMS fields. Page images and ad crops are imgix URLs built
11
+ against the PDF already attached to the magazine issue.
12
+
13
+ ## Install
14
+
15
+ ```js
16
+ // packages/global/routes/index.js
17
+ const digitalEdition = require('@mindful-web/marko-web-digital-edition/routes');
18
+
19
+ module.exports = (app, siteConfig) => {
20
+ // Mount BEFORE the magazine routes.
21
+ digitalEdition(app, {
22
+ configDir: path.resolve(__dirname, '../config/editions'),
23
+ // Optional: render bylines with the site's own author node.
24
+ authorNode: require('@mindful-web/marko-web-theme-monorail/components/nodes/author.marko'),
25
+ });
26
+ magazine(app);
27
+ // …
28
+ };
29
+ ```
30
+
31
+ ```scss
32
+ // packages/global/scss/core.scss
33
+ @import "@mindful-web/marko-web-digital-edition/scss/index";
34
+ ```
35
+
36
+ Drop each issue's manifest at `config/editions/<issueId>.json`. The manifest's `siteId`
37
+ must match the running site's id, or the routes 404 — a shared package ships to every
38
+ site in a repo, and only the issue's own site may serve it.
39
+
40
+ ## Options
41
+
42
+ | Option | Default | Purpose |
43
+ |---|---|---|
44
+ | `configDir` | required | Directory holding `<issueId>.json` manifests |
45
+ | `mountPoint` | `/magazine` | Issue URL prefix: `<mountPoint>/<issueId>/digital-edition` |
46
+ | `siteId` | `() => app.locals.config.website('id')` | Site guard |
47
+ | `noIndexKey` | `digitalEdition.native.noIndex` | Site-config path; truthy = robots noindex on every edition page (review hold) |
48
+ | `authorNode` | none | Compiled Marko template rendering `{ author }`; without it bylines are plain linked names |
49
+ | `templates` | `{}` | Overrides for `hub`, `article`, `partial` |
50
+ | `legacyRedirect` | `false` | 301 `/edition/:issueId` to the hub |
51
+
52
+ ## URLs
53
+
54
+ ```
55
+ <mountPoint>/32023/digital-edition hub: cover, contents, first article
56
+ <mountPoint>/32023/digital-edition?view=print replica page view
57
+ <mountPoint>/32023/digital-edition/<slug> one article, server-rendered
58
+ <mountPoint>/32023/digital-edition/<slug>?partial=1 fragment for the scroll reader
59
+ ```
60
+
61
+ ## Sign-in gate
62
+
63
+ Anonymous readers get the same IdentityX flow as the content meter bar and hard-gated
64
+ articles, with the source **`digitalEdition`** so conversions are attributable: the
65
+ email field is visible in the gate itself; a new user is then asked for the site's
66
+ `requiredCreateFieldRows`; an existing user receives a login link. A logged-in user
67
+ still missing required fields sees the profile form instead.
68
+
69
+ - **Hub / continuous reader**: a fixed, collapsible bottom bar
70
+ (`<marko-web-digital-edition-gate variant="bar">`).
71
+ - **Single article page**: an inline panel after the teaser
72
+ (`variant="inline"`).
73
+
74
+ Events carry `contentGateType: "digitalEdition"`, the issue id, publication and issue
75
+ name, plus the article slug and print page on article pages. The IdentityX login form
76
+ needs `req.identityX`, so the site must have `marko-web-identity-x` configured.
77
+
78
+ ## Tracking
79
+
80
+ Everything goes through the site's p1events queue (so it carries the reader identity) and
81
+ is gated on `p1events.enabled`. Entities are built server-side in `lib/entities.js` and
82
+ stamped on the elements as `data-edition-entity`; namespaces come from the p1events
83
+ `create-namespace` util (the tenant), never a literal.
84
+
85
+ | Entity | Namespace | Id | Refs |
86
+ |---|---|---|---|
87
+ | issue | `base.<tenant>.magazine-issue` | `<issueId>` | |
88
+ | article | `base.<tenant>.digital-edition-article` | `<issueId>.<slug>` | issue, `content` (the matched web article), `authors` |
89
+ | ad | `base.<tenant>.digital-edition-ad` | `<issueId>.<adId>` | issue, `company` (directory match) |
90
+
91
+ | Category | Action | When | Entity |
92
+ |---|---|---|---|
93
+ | Digital Edition | View | hub, replica and article page loads (`props.view`) | issue |
94
+ | Digital Edition | Page View | replica page 50% visible, once per page | issue |
95
+ | Digital Edition | Engaged View | first human gesture on the hub / replica | issue |
96
+ | Digital Edition Article | View | article page load, and each article entering the scroll reader | article |
97
+ | Digital Edition Article | Scroll | 25 / 50 / 75 / 90 percent of the article's own body, **full body only** (never a teaser) | article |
98
+ | Digital Edition Article | Engaged View | first human gesture on an article page | article |
99
+ | Digital Edition Article | Continue Reading | a teaser or its link is clicked | article |
100
+ | Digital Edition Ad Unit | Impression | unit ≥ 25% in view (served) | ad |
101
+ | Digital Edition Ad Unit | Active View | IAB: ≥ 50% in view for one continuous second (≥ 30% for units over 242,500 px²) | ad |
102
+ | Digital Edition Ad Unit | Click | inline, trailing and replica hotspot links (p1events link listener) | ad |
103
+
104
+ Ad categories mirror the Mindful ad server's so a later swap to real creatives is a rename.
105
+
106
+ ## Review hold
107
+
108
+ Set the site-config key (default `digitalEdition.native.noIndex`) to `true` while an
109
+ issue is under review. Every edition page then emits a single
110
+ `<meta name="robots" content="noindex">` and the hub's structured data omits nothing
111
+ else. Flip it to `false` to let search in. Individual articles the ingest marked
112
+ `noindex: true` stay held regardless.
113
+
114
+ ## Notes
115
+
116
+ - The page templates render the site document directly with
117
+ `marko-web-default-page-metadata` rather than through a theme page component, because
118
+ `no-index` must reach the metadata component, which owns the single canonical and
119
+ robots tags. Never hand-roll either tag in an override template.
120
+ - Articles need a non-empty reflow body to receive a URL (`isPublishable`).
121
+ - The SCSS consumes no theme variables.
@@ -0,0 +1,81 @@
1
+ import { cropUrl, cropSrcset } from "../lib";
2
+ import { adEntity, entityAttr } from "../lib/entities";
3
+
4
+ $ const { manifest, item, eager, inline } = input;
5
+ $ const entity = adEntity(manifest, item, { placement: inline ? "inline" : "after" });
6
+ $ const page = item.printPage;
7
+ $ const src = cropUrl(manifest, page, item.bbox, { w: inline ? 640 : 1200 });
8
+ $ const srcset = cropSrcset(manifest, page, item.bbox, inline ? [320, 480, 640, 800] : undefined);
9
+ $ const sizes = inline ? "(min-width: 75rem) 352px, (min-width: 48rem) 288px, (min-width: 34rem) 46vw, calc(100vw - 2rem)" : "(min-width: 768px) 700px, 100vw";
10
+ $ const alt = item.advertiserName ? `Advertisement: ${item.advertiserName}` : "Advertisement";
11
+ $ const label = item.kind === "advertorial" ? "Sponsored" : "Advertisement";
12
+ $ const linked = Boolean(item.landingUrl);
13
+ $ // A trailing ad narrower than a page in print (a directory strip, a fractional
14
+ $ // unit) renders at roughly print scale instead of stretching to the column.
15
+ $ const scaled = !inline && item.bbox && item.bbox[2] < 0.9 ? `max-width: min(100%, ${Math.round(item.bbox[2] * 44)}rem);` : null;
16
+
17
+ <!--
18
+ Print ad rendered as an imgix crop of the issue PDF.
19
+
20
+ Tracking contract: these attributes mirror the Mindful ad server's DOM shape
21
+ (data-unit-form / data-creative-id) so that when this graduates to real
22
+ ADVERTISING_CREATIVE records served through mindful-reporting/apis/advertising,
23
+ the browser tracker swaps over without a redesign.
24
+
25
+ data-p1-* is required because the p1events engagement tracker deliberately ignores
26
+ ad containers, and these ads are same-origin (not framed) so clicks never bubble
27
+ through an iframe boundary the way GAM's do.
28
+
29
+ `inline` floats the unit into the article copy at its print-adjacent position
30
+ (see edition/index.js inlineAdPlacements); the impression tracker reads
31
+ data-edition-placement to tell the two treatments apart.
32
+ -->
33
+ <div
34
+ class=`edition-ad edition-ad--${item.kind}${inline ? " edition-ad--inline" : ""}`
35
+ style=scaled
36
+ data-unit-form="DIGITAL_EDITION"
37
+ data-edition-ad-id=item.id
38
+ data-edition-issue-id=manifest.issueId
39
+ data-edition-page=page
40
+ data-edition-placement=(inline ? "inline" : "after")
41
+ data-advertiser=(item.advertiserName || "")
42
+ data-edition-entity=JSON.stringify(entity)
43
+ data-p1-category="Digital Edition Ad Unit"
44
+ data-p1-entity=entityAttr(entity)
45
+ >
46
+ <span class="edition-ad__label">${label}</span>
47
+ <if(linked)>
48
+ <a
49
+ class="edition-ad__link"
50
+ href=item.landingUrl
51
+ target="_blank"
52
+ rel="nofollow noopener sponsored"
53
+ data-p1-action="Click"
54
+ data-p1-label=(item.advertiserName || "")
55
+ >
56
+ <img
57
+ class="edition-ad__img"
58
+ src=src
59
+ srcset=srcset
60
+ sizes=sizes
61
+ alt=alt
62
+ loading=(eager ? "eager" : "lazy")
63
+ decoding="async"
64
+ />
65
+ </a>
66
+ </if>
67
+ <else>
68
+ <img
69
+ class="edition-ad__img"
70
+ src=src
71
+ srcset=srcset
72
+ sizes=sizes
73
+ alt=alt
74
+ loading=(eager ? "eager" : "lazy")
75
+ decoding="async"
76
+ />
77
+ </else>
78
+ <if(item.advertiserName)>
79
+ <span class="edition-ad__advertiser">${item.advertiserName}</span>
80
+ </if>
81
+ </div>
@@ -0,0 +1,102 @@
1
+ // Compiled using marko@4.20.2 - DO NOT EDIT
2
+ "use strict";
3
+
4
+ var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
+ marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/ad.marko",
6
+ marko_renderer = require("marko/dist/runtime/components/renderer"),
7
+ module_lib_module = require("../lib"),
8
+ lib_module = module_lib_module.default || module_lib_module,
9
+ cropUrl = module_lib_module.cropUrl,
10
+ cropSrcset = module_lib_module.cropSrcset,
11
+ module_entities_module = require("../lib/entities"),
12
+ entities_module = module_entities_module.default || module_entities_module,
13
+ adEntity = module_entities_module.adEntity,
14
+ entityAttr = module_entities_module.entityAttr,
15
+ helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
16
+ marko_escapeXml = helpers_escape_xml.x,
17
+ marko_attr = require("marko/dist/runtime/html/helpers/attr"),
18
+ marko_classAttr = require("marko/dist/runtime/html/helpers/class-attr"),
19
+ marko_styleAttr = require("marko/dist/runtime/html/helpers/style-attr");
20
+
21
+ function render(input, out, __component, component, state) {
22
+ var data = input;
23
+
24
+ const { manifest, item, eager, inline } = input;
25
+
26
+ const entity = adEntity(manifest, item, { placement: inline ? "inline" : "after" });
27
+
28
+ const page = item.printPage;
29
+
30
+ const src = cropUrl(manifest, page, item.bbox, { w: inline ? 640 : 1200 });
31
+
32
+ const srcset = cropSrcset(manifest, page, item.bbox, inline ? [320, 480, 640, 800] : undefined);
33
+
34
+ const sizes = inline ? "(min-width: 75rem) 352px, (min-width: 48rem) 288px, (min-width: 34rem) 46vw, calc(100vw - 2rem)" : "(min-width: 768px) 700px, 100vw";
35
+
36
+ const alt = item.advertiserName ? `Advertisement: ${item.advertiserName}` : "Advertisement";
37
+
38
+ const label = item.kind === "advertorial" ? "Sponsored" : "Advertisement";
39
+
40
+ const linked = Boolean(item.landingUrl);
41
+
42
+ // A trailing ad narrower than a page in print (a directory strip, a fractional
43
+
44
+ // unit) renders at roughly print scale instead of stretching to the column.
45
+
46
+ const scaled = !inline && item.bbox && item.bbox[2] < 0.9 ? `max-width: min(100%, ${Math.round(item.bbox[2] * 44)}rem);` : null;
47
+
48
+ out.w("<div" +
49
+ marko_classAttr(("edition-ad edition-ad--" + item.kind) + (inline ? " edition-ad--inline" : "")) +
50
+ marko_styleAttr(scaled) +
51
+ " data-unit-form=\"DIGITAL_EDITION\"" +
52
+ marko_attr("data-edition-ad-id", item.id) +
53
+ marko_attr("data-edition-issue-id", manifest.issueId) +
54
+ marko_attr("data-edition-page", page) +
55
+ marko_attr("data-edition-placement", inline ? "inline" : "after") +
56
+ marko_attr("data-advertiser", item.advertiserName || "") +
57
+ marko_attr("data-edition-entity", JSON.stringify(entity)) +
58
+ " data-p1-category=\"Digital Edition Ad Unit\"" +
59
+ marko_attr("data-p1-entity", entityAttr(entity)) +
60
+ "><span class=\"edition-ad__label\">" +
61
+ marko_escapeXml(label) +
62
+ "</span>");
63
+
64
+ if (linked) {
65
+ out.w("<a class=\"edition-ad__link\"" +
66
+ marko_attr("href", item.landingUrl) +
67
+ " target=\"_blank\" rel=\"nofollow noopener sponsored\" data-p1-action=\"Click\"" +
68
+ marko_attr("data-p1-label", item.advertiserName || "") +
69
+ "><img class=\"edition-ad__img\"" +
70
+ marko_attr("src", src) +
71
+ marko_attr("srcset", srcset) +
72
+ marko_attr("sizes", sizes) +
73
+ marko_attr("alt", alt) +
74
+ marko_attr("loading", eager ? "eager" : "lazy") +
75
+ " decoding=\"async\"></a>");
76
+ } else {
77
+ out.w("<img class=\"edition-ad__img\"" +
78
+ marko_attr("src", src) +
79
+ marko_attr("srcset", srcset) +
80
+ marko_attr("sizes", sizes) +
81
+ marko_attr("alt", alt) +
82
+ marko_attr("loading", eager ? "eager" : "lazy") +
83
+ " decoding=\"async\">");
84
+ }
85
+
86
+ if (item.advertiserName) {
87
+ out.w("<span class=\"edition-ad__advertiser\">" +
88
+ marko_escapeXml(item.advertiserName) +
89
+ "</span>");
90
+ }
91
+
92
+ out.w("</div>");
93
+ }
94
+
95
+ marko_template._ = marko_renderer(render, {
96
+ d_: true,
97
+ e_: marko_componentType
98
+ });
99
+
100
+ marko_template.meta = {
101
+ id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/ad.marko"
102
+ };
@@ -0,0 +1,134 @@
1
+ import { anchorFor, teaserHtml, editionPath, articleAdLayout, cropUrl, cropSrcset } from "../lib";
2
+ import { articleEntity } from "../lib/entities";
3
+
4
+ $ const { manifest, item, canAccess, headingLevel } = input;
5
+ $ const entity = articleEntity(manifest, item);
6
+ $ const H = headingLevel === 1 ? "h1" : "h2";
7
+ $ const href = editionPath(manifest.issueId, { slug: item.slug });
8
+ $ const layout = canAccess ? articleAdLayout(manifest, item) : null;
9
+
10
+ <!--
11
+ One article. Shared by the hub (first article only), the standalone article page,
12
+ and the `?partial=1` fragment the scroll reader appends — so the three can never
13
+ drift apart.
14
+ -->
15
+ <article
16
+ class="edition-article"
17
+ id=anchorFor(item)
18
+ data-edition-article-slug=item.slug
19
+ data-edition-article-href=href
20
+ data-edition-print-page=item.printPage
21
+ data-edition-entity=JSON.stringify(entity)
22
+ data-edition-full-body=(canAccess ? "1" : null)
23
+ >
24
+ <header class="edition-article__header">
25
+ <p class="edition-article__page">
26
+ Page ${item.printPage}
27
+ <if(item.pageRefs && item.pageRefs.length > 1)>
28
+ <span class="edition-article__spread">· continues p.&nbsp;${item.pageRefs.slice(1).join(", ")}</span>
29
+ </if>
30
+ </p>
31
+
32
+ <if(item.headline)>
33
+ <${H} class="edition-article__headline">${item.headline}</${H}>
34
+ </if>
35
+ <if(item.deck)>
36
+ <p class="edition-article__deck">${item.deck}</p>
37
+ </if>
38
+
39
+ <!--
40
+ Byline: the site's injected author renderer (e.g. the monorail author node with
41
+ a linked headshot) when the print byline resolved to a CMS contact, plain text
42
+ otherwise. See components/author.marko.
43
+ -->
44
+ <if(item.authorNode)>
45
+ <div class="edition-article__author">
46
+ <marko-web-digital-edition-author author=item.authorNode />
47
+ </div>
48
+ </if>
49
+ <else-if(item.byline)>
50
+ <p class="edition-article__byline">By ${item.byline}</p>
51
+ </else-if>
52
+
53
+ <if(item.match && item.match.path)>
54
+ <p class="edition-article__also">
55
+ Also published online:
56
+ <a href=item.match.path>${item.match.contentName || "read the web version"}</a>
57
+ </p>
58
+ </if>
59
+ </header>
60
+
61
+ <!--
62
+ Primary art, detected by the ingest from the PDF's image blocks (clustered
63
+ collage tiles, trimmed clear of any display type that would otherwise be
64
+ baked into the crop). Shown in both access states — it is the invitation.
65
+ -->
66
+ <if(item.leadImage)>
67
+ <figure class="edition-article__lead">
68
+ <img
69
+ src=cropUrl(manifest, item.leadImage.page, item.leadImage.bbox, { w: 1200 })
70
+ srcset=cropSrcset(manifest, item.leadImage.page, item.leadImage.bbox)
71
+ sizes="(min-width: 48rem) 672px, 100vw"
72
+ alt=(item.headline || "Article image")
73
+ width=Math.round(item.leadImage.bbox[2] * manifest.pdf.baseWidth)
74
+ height=Math.round(item.leadImage.bbox[3] * manifest.pdf.baseHeight)
75
+ loading=(headingLevel === 1 ? "eager" : "lazy")
76
+ decoding="async"
77
+ />
78
+ </figure>
79
+ </if>
80
+
81
+ <!--
82
+ Full body: the print ads that sat inside this article's page run float into the
83
+ copy at matching depth (see edition/index.js inlineAdPlacements) — the ad div
84
+ must precede the paragraph it floats against INSIDE the body div, so it shares
85
+ the paragraphs' formatting context and the text wraps it.
86
+ -->
87
+ <if(canAccess)>
88
+ <div class="edition-article__body">
89
+ <for|seg| of=layout.segments>
90
+ <if(seg.ad)>
91
+ <marko-web-digital-edition-ad manifest=manifest item=seg.ad inline=true />
92
+ </if>
93
+ $!{seg.html}
94
+ </for>
95
+ </div>
96
+ </if>
97
+ <else>
98
+ <!--
99
+ Teaser plus one quiet, identical affordance per story. The fade says "there is
100
+ more"; this says how. The gate's script turns any click on the band or the
101
+ link into: open the sign-in bar (hub) or scroll to the inline panel (article
102
+ page) and focus the email field. Without JS the link is the article's own URL.
103
+ -->
104
+ <div class="edition-article__gated" data-edition-continue data-edition-slug=item.slug>
105
+ <div class="edition-article__body edition-article__body--teaser edition-gated-body">
106
+ $!{teaserHtml(item)}
107
+ </div>
108
+ <a class="edition-article__continue" href=href>
109
+ Continue reading<span class="edition-article__continue-arrow" aria-hidden="true">→</span>
110
+ </a>
111
+ </div>
112
+ </else>
113
+
114
+ <!-- Photo credits, pulled out of the body copy by the ingest cleanup. -->
115
+ <if(item.credits && item.credits.length)>
116
+ <p class="edition-article__credits">
117
+ <for|credit, i| of=item.credits>
118
+ <if(i)>&nbsp;· </if>${credit}
119
+ </for>
120
+ </p>
121
+ </if>
122
+
123
+ <!--
124
+ Claimed ads that cannot float (directory strips, overflow beyond the pacing
125
+ cap) trail the article at ~print scale. They render INSIDE <article> so the
126
+ `?partial=1` fragment stays single-child — the scroll reader appends fragment
127
+ children one by one, and sibling ads used to be dropped by that loop.
128
+ -->
129
+ <if(canAccess && layout.trailing.length)>
130
+ <for|ad| of=layout.trailing>
131
+ <marko-web-digital-edition-ad manifest=manifest item=ad />
132
+ </for>
133
+ </if>
134
+ </article>
@@ -0,0 +1,186 @@
1
+ // Compiled using marko@4.20.2 - DO NOT EDIT
2
+ "use strict";
3
+
4
+ var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
+ marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/article-body.marko",
6
+ marko_renderer = require("marko/dist/runtime/components/renderer"),
7
+ module_lib_module = require("../lib"),
8
+ lib_module = module_lib_module.default || module_lib_module,
9
+ anchorFor = module_lib_module.anchorFor,
10
+ teaserHtml = module_lib_module.teaserHtml,
11
+ editionPath = module_lib_module.editionPath,
12
+ articleAdLayout = module_lib_module.articleAdLayout,
13
+ cropUrl = module_lib_module.cropUrl,
14
+ cropSrcset = module_lib_module.cropSrcset,
15
+ module_entities_module = require("../lib/entities"),
16
+ entities_module = module_entities_module.default || module_entities_module,
17
+ articleEntity = module_entities_module.articleEntity,
18
+ helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
19
+ marko_escapeXml = helpers_escape_xml.x,
20
+ marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
21
+ marko_web_digital_edition_author_template = require("./author.marko"),
22
+ marko_loadTag = require("marko/dist/runtime/helpers/load-tag"),
23
+ marko_web_digital_edition_author_tag = marko_loadTag(marko_web_digital_edition_author_template),
24
+ marko_attr = require("marko/dist/runtime/html/helpers/attr"),
25
+ marko_forOf = require("marko/dist/runtime/helpers/for-of"),
26
+ marko_web_digital_edition_ad_template = require("./ad.marko"),
27
+ marko_web_digital_edition_ad_tag = marko_loadTag(marko_web_digital_edition_ad_template),
28
+ marko_str = require("marko/dist/runtime/helpers/to-string");
29
+
30
+ function render(input, out, __component, component, state) {
31
+ var data = input;
32
+
33
+ const { manifest, item, canAccess, headingLevel } = input;
34
+
35
+ const entity = articleEntity(manifest, item);
36
+
37
+ const H = headingLevel === 1 ? "h1" : "h2";
38
+
39
+ const href = editionPath(manifest.issueId, { slug: item.slug });
40
+
41
+ const layout = canAccess ? articleAdLayout(manifest, item) : null;
42
+
43
+ out.w("<article class=\"edition-article\"" +
44
+ marko_attr("id", anchorFor(item)) +
45
+ marko_attr("data-edition-article-slug", item.slug) +
46
+ marko_attr("data-edition-article-href", href) +
47
+ marko_attr("data-edition-print-page", item.printPage) +
48
+ marko_attr("data-edition-entity", JSON.stringify(entity)) +
49
+ marko_attr("data-edition-full-body", canAccess ? "1" : null) +
50
+ "><header class=\"edition-article__header\"><p class=\"edition-article__page\">Page " +
51
+ marko_escapeXml(item.printPage));
52
+
53
+ if (item.pageRefs && (item.pageRefs.length > 1)) {
54
+ out.w("<span class=\"edition-article__spread\">· continues p.&nbsp;" +
55
+ marko_escapeXml(item.pageRefs.slice(1).join(", ")) +
56
+ "</span>");
57
+ }
58
+
59
+ out.w("</p>");
60
+
61
+ if (item.headline) {
62
+ marko_dynamicTag(out, H, function() {
63
+ return {
64
+ class: "edition-article__headline"
65
+ };
66
+ }, function(out) {
67
+ out.w(marko_escapeXml(item.headline));
68
+ }, null, null, __component, "4");
69
+ }
70
+
71
+ if (item.deck) {
72
+ out.w("<p class=\"edition-article__deck\">" +
73
+ marko_escapeXml(item.deck) +
74
+ "</p>");
75
+ }
76
+
77
+ if (item.authorNode) {
78
+ out.w("<div class=\"edition-article__author\">");
79
+
80
+ marko_web_digital_edition_author_tag({
81
+ author: item.authorNode
82
+ }, out, __component, "7");
83
+
84
+ out.w("</div>");
85
+ } else if (item.byline) {
86
+ out.w("<p class=\"edition-article__byline\">By " +
87
+ marko_escapeXml(item.byline) +
88
+ "</p>");
89
+ }
90
+
91
+ if (item.match && item.match.path) {
92
+ out.w("<p class=\"edition-article__also\">Also published online: <a" +
93
+ marko_attr("href", item.match.path) +
94
+ ">" +
95
+ marko_escapeXml(item.match.contentName || "read the web version") +
96
+ "</a></p>");
97
+ }
98
+
99
+ out.w("</header>");
100
+
101
+ if (item.leadImage) {
102
+ out.w("<figure class=\"edition-article__lead\"><img" +
103
+ marko_attr("src", cropUrl(manifest, item.leadImage.page, item.leadImage.bbox, {
104
+ w: 1200
105
+ })) +
106
+ marko_attr("srcset", cropSrcset(manifest, item.leadImage.page, item.leadImage.bbox)) +
107
+ " sizes=\"(min-width: 48rem) 672px, 100vw\"" +
108
+ marko_attr("alt", item.headline || "Article image") +
109
+ marko_attr("width", Math.round(item.leadImage.bbox[2] * manifest.pdf.baseWidth)) +
110
+ marko_attr("height", Math.round(item.leadImage.bbox[3] * manifest.pdf.baseHeight)) +
111
+ marko_attr("loading", headingLevel === 1 ? "eager" : "lazy") +
112
+ " decoding=\"async\"></figure>");
113
+ }
114
+
115
+ if (canAccess) {
116
+ out.w("<div class=\"edition-article__body\">");
117
+
118
+ var $for$0 = 0;
119
+
120
+ marko_forOf(layout.segments, function(seg) {
121
+ var $keyScope$0 = "[" + (($for$0++) + "]");
122
+
123
+ if (seg.ad) {
124
+ marko_web_digital_edition_ad_tag({
125
+ manifest: manifest,
126
+ item: seg.ad,
127
+ inline: true
128
+ }, out, __component, "14" + $keyScope$0);
129
+ }
130
+
131
+ out.w(marko_str(seg.html));
132
+ });
133
+
134
+ out.w("</div>");
135
+ } else {
136
+ out.w("<div class=\"edition-article__gated\" data-edition-continue" +
137
+ marko_attr("data-edition-slug", item.slug) +
138
+ "><div class=\"edition-article__body edition-article__body--teaser edition-gated-body\">" +
139
+ marko_str(teaserHtml(item)) +
140
+ "</div><a class=\"edition-article__continue\"" +
141
+ marko_attr("href", href) +
142
+ ">Continue reading<span class=\"edition-article__continue-arrow\" aria-hidden=\"true\">→</span></a></div>");
143
+ }
144
+
145
+ if (item.credits && item.credits.length) {
146
+ out.w("<p class=\"edition-article__credits\">");
147
+
148
+ marko_forOf(item.credits, function(credit, i) {
149
+ if (i) {
150
+ out.w("&nbsp;· ");
151
+ }
152
+
153
+ out.w(marko_escapeXml(credit));
154
+ });
155
+
156
+ out.w("</p>");
157
+ }
158
+
159
+ if (canAccess && layout.trailing.length) {
160
+ var $for$1 = 0;
161
+
162
+ marko_forOf(layout.trailing, function(ad) {
163
+ var $keyScope$1 = "[" + (($for$1++) + "]");
164
+
165
+ marko_web_digital_edition_ad_tag({
166
+ manifest: manifest,
167
+ item: ad
168
+ }, out, __component, "20" + $keyScope$1);
169
+ });
170
+ }
171
+
172
+ out.w("</article>");
173
+ }
174
+
175
+ marko_template._ = marko_renderer(render, {
176
+ d_: true,
177
+ e_: marko_componentType
178
+ });
179
+
180
+ marko_template.meta = {
181
+ id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/article-body.marko",
182
+ tags: [
183
+ "./author.marko",
184
+ "./ad.marko"
185
+ ]
186
+ };
@@ -0,0 +1,30 @@
1
+ $ const { author } = input;
2
+ $ const { authorNode } = (out.global.digitalEdition || {});
3
+ $ const path = author && author.siteContext && author.siteContext.path;
4
+
5
+ <!--
6
+ Byline for an article whose print byline resolved to a CMS contact.
7
+
8
+ The consuming site injects its own author renderer through the routes factory
9
+ (`authorNode`), e.g. the monorail theme's `nodes/author.marko`, so this package has no
10
+ dependency on any theme. Without one, a plain (optionally linked) name renders.
11
+ -->
12
+ <if(author)>
13
+ <if(authorNode)>
14
+ <${authorNode} author=author />
15
+ </if>
16
+ <else>
17
+ <p class="edition-article__byline">
18
+ By
19
+ <if(path)>
20
+ <a href=path>${author.name}</a>
21
+ </if>
22
+ <else>
23
+ ${author.name}
24
+ </else>
25
+ <if(author.title)>
26
+ <span class="edition-article__byline-title">, ${author.title}</span>
27
+ </if>
28
+ </p>
29
+ </else>
30
+ </if>