@mindful-web/marko-web-theme-monorail 1.83.5 → 1.85.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.
- package/components/ads/fixed-ad-bottom.marko.js +2 -2
- package/components/ads/reveal-ad-handler.marko.js +2 -2
- package/components/blocks/author-bios.marko.js +2 -2
- package/components/blocks/callout-cards.marko.js +2 -2
- package/components/blocks/company-inquiry-form.marko.js +2 -2
- package/components/blocks/company-search.marko.js +2 -2
- package/components/blocks/content-card-deck.marko.js +2 -2
- package/components/blocks/content-meter.marko.js +2 -2
- package/components/blocks/full-width-native-ad.marko.js +2 -2
- package/components/blocks/geo-summary.marko.js +2 -2
- package/components/blocks/header-announcement.marko.js +2 -2
- package/components/blocks/hero-card.marko.js +2 -2
- package/components/blocks/latest-content-list.marko.js +2 -2
- package/components/blocks/latest-podcast.marko.js +2 -2
- package/components/blocks/latest-products-feed.marko.js +2 -2
- package/components/blocks/more-products.marko.js +2 -2
- package/components/blocks/most-popular-list.marko.js +2 -2
- package/components/blocks/most-popular.marko.js +2 -2
- package/components/blocks/native-x-card-deck.marko.js +2 -2
- package/components/blocks/native-x-promo-card.marko.js +2 -2
- package/components/blocks/newsletter-signup-banner-external.marko.js +2 -2
- package/components/blocks/newsletter-signup-banner-large.marko.js +2 -2
- package/components/blocks/newsletter-signup-banner.marko.js +2 -2
- package/components/blocks/opinion.marko.js +2 -2
- package/components/blocks/primary-image.marko.js +2 -2
- package/components/blocks/products.marko.js +2 -2
- package/components/blocks/read-next.marko.js +2 -2
- package/components/blocks/related-stories.marko.js +2 -2
- package/components/blocks/resource-library.marko.js +2 -2
- package/components/blocks/section-body-feed.marko.js +2 -2
- package/components/blocks/section-card-deck-featured.marko.js +2 -2
- package/components/blocks/section-card-deck.marko.js +2 -2
- package/components/blocks/section-card-list.marko.js +2 -2
- package/components/blocks/section-feed.marko.js +2 -2
- package/components/blocks/section-forbes.marko.js +2 -2
- package/components/blocks/section-list-deck.marko.js +2 -2
- package/components/blocks/section-list.marko.js +2 -2
- package/components/blocks/section-search.marko.js +2 -2
- package/components/blocks/section-title-feed.marko.js +2 -2
- package/components/blocks/top-stories-menu.marko.js +2 -2
- package/components/blocks/top-stories.marko.js +2 -2
- package/components/blocks/white-papers.marko.js +2 -2
- package/components/breadcrumbs/content-page.marko.js +2 -2
- package/components/breadcrumbs/index.marko.js +2 -2
- package/components/breadcrumbs/website-section.marko.js +2 -2
- package/components/breadcrumbs/with-home.marko.js +2 -2
- package/components/client-side-block-loader.marko.js +2 -2
- package/components/client-side-blocks/most-popular-list.marko.js +2 -2
- package/components/client-side-blocks/most-popular.marko.js +2 -2
- package/components/content/body-with-injection.marko.js +2 -2
- package/components/content/contact-details/index.marko.js +2 -2
- package/components/content/contact-details/section.marko.js +2 -2
- package/components/content/download/form-dot-com.marko.js +2 -2
- package/components/content/download/identity-x.marko.js +2 -2
- package/components/content/download/index.marko.js +2 -2
- package/components/content/download/link.marko.js +2 -2
- package/components/content/download/wufoo.marko.js +2 -2
- package/components/content/link-url/index.marko.js +2 -2
- package/components/content/sections-listed-in.marko.js +2 -2
- package/components/content-attribution/attribution.marko.js +2 -2
- package/components/content-attribution/components/authors.marko.js +2 -2
- package/components/content-attribution/components/byline.marko.js +2 -2
- package/components/content-attribution/components/company.marko.js +2 -2
- package/components/content-attribution/components/contributors.marko.js +2 -2
- package/components/content-attribution/components/photographers.marko.js +2 -2
- package/components/content-attribution/components/source.marko.js +2 -2
- package/components/content-attribution/components/speakers.marko.js +2 -2
- package/components/content-attribution/components/sponsors.marko.js +2 -2
- package/components/content-attribution/index.marko.js +2 -2
- package/components/directory-facets/facet-container/facet-container.marko.js +2 -2
- package/components/directory-facets/facet-list/facet-list.marko.js +2 -2
- package/components/directory-facets/facet.marko.js +2 -2
- package/components/directory-facets/index.marko.js +2 -2
- package/components/facebook-connect.marko.js +2 -2
- package/components/flows/card-deck.marko.js +2 -2
- package/components/flows/content/card-deck-col.marko.js +2 -2
- package/components/flows/content/directory-section.marko.js +2 -2
- package/components/flows/content/list.marko.js +2 -2
- package/components/flows/section-feed.marko.js +2 -2
- package/components/gam/components/define-display-ad.marko.js +2 -2
- package/components/gam/define-display-ad.marko.js +2 -2
- package/components/gam/inject-ads.marko.js +2 -2
- package/components/gam/wallpaper-ad.marko.js +2 -2
- package/components/identity-x/comment-stream.marko.js +2 -2
- package/components/identity-x/content-page-gate.marko.js +2 -2
- package/components/identity-x/marko.json +16 -0
- package/components/identity-x/newsletter-footer.marko.js +2 -2
- package/components/identity-x/newsletter-inline.marko.js +2 -2
- package/components/identity-x/newsletter-modal.marko.js +2 -2
- package/components/identity-x/newsletter-pushdown.marko.js +2 -2
- package/components/identity-x/progressive-profile-gate.marko +36 -0
- package/components/identity-x/progressive-profile-gate.marko.js +50 -0
- package/components/layouts/ad-preview.marko.js +2 -2
- package/components/menu-toggle-button.marko.js +2 -2
- package/components/native-x/render.marko.js +2 -2
- package/components/nodes/author-bio.marko.js +2 -2
- package/components/nodes/author-card.marko.js +2 -2
- package/components/nodes/author-published.marko.js +2 -2
- package/components/nodes/author.marko.js +2 -2
- package/components/nodes/components/attribution.marko.js +2 -2
- package/components/nodes/components/authors.marko.js +2 -2
- package/components/nodes/components/company-name.marko.js +2 -2
- package/components/nodes/components/dates.marko.js +2 -2
- package/components/nodes/components/section-sponsor.marko.js +2 -2
- package/components/nodes/content-published.marko.js +2 -2
- package/components/nodes/content.marko.js +2 -2
- package/components/nodes/directory-section-feed-content.marko.js +2 -2
- package/components/nodes/featured-products-card.marko.js +2 -2
- package/components/nodes/product-feed-content.marko.js +2 -2
- package/components/nodes/section-feed-content.marko.js +2 -2
- package/components/nodes/standard-promo.marko.js +2 -2
- package/components/nodes/youtube-card.marko.js +2 -2
- package/components/page/attribution.marko.js +2 -2
- package/components/page/contact-details.marko.js +2 -2
- package/components/page/content.marko.js +2 -2
- package/components/page/contents.marko.js +2 -2
- package/components/page/dates.marko.js +2 -2
- package/components/page/default.marko.js +2 -2
- package/components/page/website-section.marko.js +2 -2
- package/components/pagination-controls.marko.js +2 -2
- package/components/query-total-count.marko.js +2 -2
- package/components/reveal-ad-handler.marko.js +2 -2
- package/components/site-footer/container.marko.js +2 -2
- package/components/site-footer/copyright.marko.js +2 -2
- package/components/site-footer/index.marko.js +2 -2
- package/components/site-footer/social-icons.marko.js +2 -2
- package/components/site-header.marko.js +2 -2
- package/components/site-menu/components/search.marko.js +2 -2
- package/components/site-menu/components/social-icons.marko.js +2 -2
- package/components/site-menu/header.marko.js +2 -2
- package/components/site-menu/index.marko.js +2 -2
- package/components/site-menu/section.marko.js +2 -2
- package/components/site-navbar/brand.marko.js +2 -2
- package/components/site-navbar/index.marko.js +2 -2
- package/components/site-navbar/item.marko.js +2 -2
- package/components/site-navbar/items.marko.js +2 -2
- package/components/site-navbar/logo.marko.js +2 -2
- package/components/site-navbar/search.marko.js +2 -2
- package/components/site-newsletter-menu.marko.js +2 -2
- package/components/social/icon-link.marko.js +2 -2
- package/components/social/icons.marko.js +2 -2
- package/components/social-follow.marko.js +2 -2
- package/components/sponsored-label-logo.marko.js +2 -2
- package/components/sponsored-section-logo.marko.js +2 -2
- package/components/sponsored-section-name-logo.marko.js +2 -2
- package/components/ssr-html-inject.marko.js +2 -2
- package/graphql/fragments/content-meter.js +8 -0
- package/middleware/content-metering.js +11 -9
- package/middleware/progressive-profiling.js +238 -0
- package/package.json +10 -10
- package/scss/components/_content-page-gate.scss +29 -0
- package/templates/ad-preview.marko.js +2 -2
- package/templates/content/components/presented-by.marko.js +2 -2
- package/templates/content/native-x-story.marko.js +2 -2
- package/templates/content/print.marko.js +2 -2
- package/templates/email-preview.marko.js +2 -2
- package/templates/search.marko.js +2 -2
- package/templates/taxonomy.marko.js +2 -2
- package/utils/load-gating-content.js +27 -0
- package/utils/progressive-profile-helpers.js +39 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const debug = require('debug')('progressive-profile');
|
|
2
|
+
const Joi = require('@parameter1/joi');
|
|
3
|
+
const { validate } = require('@parameter1/joi/utils');
|
|
4
|
+
const { asyncRoute, isFunction } = require('@mindful-web/utils');
|
|
5
|
+
const { get, getAsArray, getAsObject } = require('@mindful-web/object-path');
|
|
6
|
+
const getMissingProgressiveFields = require('@mindful-web/marko-web-identity-x/utils/get-missing-progressive-fields');
|
|
7
|
+
const { buildName } = require('@mindful-web/marko-web-identity-x/utils/content-form-cookie');
|
|
8
|
+
const {
|
|
9
|
+
getAttempts,
|
|
10
|
+
incrementAttempts,
|
|
11
|
+
isDismissed,
|
|
12
|
+
isUnverifiedSuppressed,
|
|
13
|
+
} = require('@mindful-web/marko-web-identity-x/utils/progressive-profile-cookies');
|
|
14
|
+
const loadGatingContent = require('../utils/load-gating-content');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render-side keys only, deliberately. The write route's protections — reCAPTCHA score floor,
|
|
18
|
+
* suppression durations — live on the IdentityX config (`progressiveProfile.*`), so a site that
|
|
19
|
+
* misconfigures its theme cannot relax what `POST /__idx/progressive-profile` enforces. There is
|
|
20
|
+
* no `viewLimit`/`timeframe` (the default cadence is a hard gate on every article), no bot
|
|
21
|
+
* predicate for this middleware to consult, and no form token for the gate component to embed —
|
|
22
|
+
* both were removed with the hand-rolled bot safeguards in favor of submit-time reCAPTCHA.
|
|
23
|
+
*/
|
|
24
|
+
const configSchema = Joi.object({
|
|
25
|
+
enabled: Joi.boolean().default(false)
|
|
26
|
+
.description('Should progressive profiling be enabled?'),
|
|
27
|
+
excludeLabels: Joi.array().items(Joi.string())
|
|
28
|
+
.description('Content labels that should be excluded from the profile gate.'),
|
|
29
|
+
excludeContentTypes: Joi.array().items(Joi.string())
|
|
30
|
+
.description('Content types that should be excluded from the profile gate.'),
|
|
31
|
+
excludePrimarySectionIds: Joi.array().items(Joi.number())
|
|
32
|
+
.description('Sections whose primary content should be excluded from the profile gate.'),
|
|
33
|
+
excludePrimarySectionAlias: Joi.array().items(Joi.string())
|
|
34
|
+
.description('Sections whose primary content should be excluded from the profile gate.'),
|
|
35
|
+
previewParagraphs: Joi.number().min(1).default(3)
|
|
36
|
+
.description('How many paragraphs of the body remain readable above the gate.'),
|
|
37
|
+
displayOverlay: Joi.boolean().default(true)
|
|
38
|
+
.description('If the fade-out overlay should be displayed above the gate.'),
|
|
39
|
+
promoCode: Joi.string().default('progressive_profile')
|
|
40
|
+
.description('The Omeda promo code to use with progressive profiling events.'),
|
|
41
|
+
dismissible: Joi.boolean().default(false)
|
|
42
|
+
.description('If the gate should render a skip affordance.'),
|
|
43
|
+
suppressAfterDismissDays: Joi.number().min(0).default(0)
|
|
44
|
+
.description('Days to suppress the gate after a dismissal. Render-side echo of the IdentityX `progressiveProfile.suppressAfterDismissDays` option, which is what the dismiss route reads.'),
|
|
45
|
+
maxAttemptsPerUser: Joi.number().min(1).default(3)
|
|
46
|
+
.description('Gate renders without a successful submit before the content is released.'),
|
|
47
|
+
bypassQueryParam: Joi.string().default('bypassProgressiveProfile')
|
|
48
|
+
.description('Query parameter that bypasses the gate when set to 1/true.'),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const { error } = console;
|
|
52
|
+
|
|
53
|
+
const defaultRegFn = ({ content }) => get(content, 'userRegistration.isCurrentlyRequired', false);
|
|
54
|
+
const defaultBypassGatingFn = ({ content }) => get(content, 'userRegistration.bypassGating', false);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Content-level checks, mirroring the meter's `shouldMeter`: any gate with an owner of its own
|
|
58
|
+
* wins, and the configured exclusions apply.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately absent: the meter's Omeda bypass (`utils/has-omeda-id.js`). An `oly_enc_id`
|
|
61
|
+
* visitor with missing fields is not a false positive to wave through — they are exactly who this
|
|
62
|
+
* feature exists for, the identified-but-not-authenticated newsletter clicker. Do NOT "fix" this
|
|
63
|
+
* by copying the meter's block. Same for its `utm_medium=email` bypass, and for the same reason.
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* Returns the name of the first content-level check that blocks the gate, or null when nothing
|
|
67
|
+
* does. The name is only ever debug output — but "which check said no" is the entire content of a
|
|
68
|
+
* "the gate is not showing" support question, so it is worth naming.
|
|
69
|
+
*/
|
|
70
|
+
const contentBlockReason = ({ req, content, config }) => {
|
|
71
|
+
if (!content) return 'no-content';
|
|
72
|
+
|
|
73
|
+
// Content gated by registration of some sort: the reg gate wins.
|
|
74
|
+
const localFn = req.app.locals.contentGatingHandler;
|
|
75
|
+
const contentGatingHandler = (localFn && isFunction(localFn)) ? localFn : defaultRegFn;
|
|
76
|
+
if (contentGatingHandler({ content })) return 'reg-gated';
|
|
77
|
+
|
|
78
|
+
const localBypassFn = req.app.locals.contentBypassGatingHandler;
|
|
79
|
+
const bypassGatingFn = (localBypassFn && isFunction(localBypassFn))
|
|
80
|
+
? localBypassFn
|
|
81
|
+
: defaultBypassGatingFn;
|
|
82
|
+
if (bypassGatingFn({ content })) return 'bypass-gating';
|
|
83
|
+
|
|
84
|
+
// An idx survey form about to display owns this page. `res.locals.contentAccessState` is not
|
|
85
|
+
// consultable here — `withContent`'s formatResponse sets it *after* this middleware runs — so
|
|
86
|
+
// this re-derives the same "would the form display" test from the content's gating fields and
|
|
87
|
+
// the access cookie the survey submit sets. Claiming a survey page would burn an attempt on a
|
|
88
|
+
// gate the layout never renders and stand the meter down for the wrong owner.
|
|
89
|
+
const { surveyType, surveyId } = getAsObject(content, 'gating');
|
|
90
|
+
if (surveyType === 'idx' && surveyId) {
|
|
91
|
+
const accessCookie = get(req, `cookies.${buildName({ surveyId, contentId: content.id })}`);
|
|
92
|
+
if (!accessCookie) return 'idx-survey';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (getAsArray(config, 'excludeContentTypes').includes(content.type)) return 'excluded-type';
|
|
96
|
+
if (getAsArray(config, 'excludePrimarySectionIds').includes(get(content, 'primarySection.id'))) return 'excluded-section-id';
|
|
97
|
+
// Read via the schema's key. The meter reads `excludePrimarySectionAliass` (double-s), which no
|
|
98
|
+
// schema or site config sets, so its alias exclusion has never matched — a shared-code bug that
|
|
99
|
+
// is noted for the follow-up PR, not copied here.
|
|
100
|
+
if (getAsArray(config, 'excludePrimarySectionAlias').includes(get(content, 'primarySection.alias'))) return 'excluded-section-alias';
|
|
101
|
+
if (getAsArray(config, 'excludeLabels').some((l) => getAsArray(content, 'labels').includes(l))) return 'excluded-label';
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Detects an identified or authenticated visitor with unanswered progressive-profile fields and
|
|
107
|
+
* claims the request for the gate by setting `res.locals.progressiveProfileState`.
|
|
108
|
+
*
|
|
109
|
+
* Chain on content routes *before* `contentMetering` (the meter stands down when this claims the
|
|
110
|
+
* request) and after `newsletterState`. Do NOT install globally in `start-server.js`:
|
|
111
|
+
* `/print/content/:id` registers separately and must render the full body.
|
|
112
|
+
*
|
|
113
|
+
* Anonymous visitors bail before any i/o, so bots, SEO crawlers and the logged-out majority are
|
|
114
|
+
* byte-identical to today. The added cost is one idx lookup, only for identified visitors, only
|
|
115
|
+
* when the identity resolves — every idx call is bounded at 15s and instrumented as
|
|
116
|
+
* `MindfulApiCall`.
|
|
117
|
+
*/
|
|
118
|
+
module.exports = (params = {}) => {
|
|
119
|
+
// Validated at construction, not per request: a malformed site config should fail the boot,
|
|
120
|
+
// the same posture as the IdentityX config's field-row validation.
|
|
121
|
+
const config = validate(configSchema, params);
|
|
122
|
+
|
|
123
|
+
return asyncRoute(async (req, res, next) => {
|
|
124
|
+
const { identityX } = req;
|
|
125
|
+
if (!config.enabled) return next();
|
|
126
|
+
if (!identityX) {
|
|
127
|
+
error('IdentityX middleware must be loaded before progressive profiling middleware!');
|
|
128
|
+
return next();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fieldRows = identityX.config.getProgressiveProfileFieldRows();
|
|
132
|
+
if (!fieldRows.length) return next();
|
|
133
|
+
|
|
134
|
+
// The cheap bails first — all cookie reads. These are *request-time* values by construction
|
|
135
|
+
// (the helpers read `req.cookies` only), so a cookie set during this request can never
|
|
136
|
+
// suppress the gate that set it; cf. `middleware/newsletter-state.js`.
|
|
137
|
+
const bypassQuery = ['1', 'true'].includes(get(req, `query.${config.bypassQueryParam}`));
|
|
138
|
+
// "User skipped" and "we discarded their answers" are different populations — keep them
|
|
139
|
+
// distinguishable in the debug output. The unverified bail is not optional: a submit that
|
|
140
|
+
// fails reCAPTCHA is passively accepted (content unlocks, answers discarded), so the member's
|
|
141
|
+
// record is unchanged and without `__idx_pp_sup` the diff would re-gate them on the very next
|
|
142
|
+
// article, forever.
|
|
143
|
+
const dismissed = isDismissed(req);
|
|
144
|
+
const unverifiedSuppressed = isUnverifiedSuppressed(req);
|
|
145
|
+
const attempts = getAttempts(req);
|
|
146
|
+
const attemptsExhausted = attempts >= config.maxAttemptsPerUser;
|
|
147
|
+
|
|
148
|
+
debug({
|
|
149
|
+
config, bypassQuery, dismissed, unverifiedSuppressed, attempts, attemptsExhausted,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (bypassQuery || dismissed || unverifiedSuppressed) return next();
|
|
153
|
+
// The escape valve: a gate rendered `maxAttemptsPerUser` times without a successful submit
|
|
154
|
+
// releases the content. A gate that can trap a real person is worse than no gate.
|
|
155
|
+
if (attemptsExhausted) return next();
|
|
156
|
+
|
|
157
|
+
// Resolve the visitor. `loadActiveContext` is memoized per request; `getIdentity(res)` reads
|
|
158
|
+
// the request cookie and falls back to the outgoing response cookies, so an identity minted
|
|
159
|
+
// earlier in this same chain by `set-identity-cookie.js` (the first-time newsletter clicker)
|
|
160
|
+
// is visible here. A lookup failure must never break an article page — fail open.
|
|
161
|
+
let appUser;
|
|
162
|
+
let audience;
|
|
163
|
+
try {
|
|
164
|
+
const { user: authUser } = await identityX.loadActiveContext();
|
|
165
|
+
if (authUser) {
|
|
166
|
+
appUser = authUser;
|
|
167
|
+
audience = 'authenticated';
|
|
168
|
+
} else {
|
|
169
|
+
const identityId = identityX.getIdentity(res);
|
|
170
|
+
if (!identityId) return next(); // anonymous — the common path out.
|
|
171
|
+
appUser = await identityX.findUserByIdMemoized(identityId);
|
|
172
|
+
audience = 'identified';
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
error('Progressive profiling failed to resolve the visitor; releasing the content.', e);
|
|
176
|
+
return next();
|
|
177
|
+
}
|
|
178
|
+
// A member with no email is almost certainly a broken import, not a reader worth gating.
|
|
179
|
+
if (!appUser || !appUser.email) return next();
|
|
180
|
+
|
|
181
|
+
const {
|
|
182
|
+
hasMissing, missingRows, missingCustomFieldIds, missingCount,
|
|
183
|
+
} = getMissingProgressiveFields({ appUser, fieldRows });
|
|
184
|
+
|
|
185
|
+
debug({ audience, hasMissing, missingCustomFieldIds });
|
|
186
|
+
|
|
187
|
+
if (!hasMissing) return next();
|
|
188
|
+
|
|
189
|
+
// The reg gate owns an authenticated visitor who still owes *registration* input —
|
|
190
|
+
// required server fields, `mustReVerifyProfile`, required custom fields, consent policies.
|
|
191
|
+
// Claiming them here produced a two-gate sequence in testing: submit the progressive form,
|
|
192
|
+
// reload, land on the full profile gate anyway. Standing down lets the meter's
|
|
193
|
+
// `isLoggedIn && requiresUserInput` path render the profile form FIRST; once registration
|
|
194
|
+
// is complete this diff picks up whatever is still missing. The layout cannot enforce this
|
|
195
|
+
// precedence itself: when this middleware claims, the meter stands down and the
|
|
196
|
+
// `<marko-web-identity-x-access>` wrapper is disabled, so `context.requiresUserInput` is
|
|
197
|
+
// never populated there. Identified (non-authenticated) visitors are not checked — they
|
|
198
|
+
// have no session, and `requiresUserInput` reports on the active session only.
|
|
199
|
+
if (audience === 'authenticated') {
|
|
200
|
+
try {
|
|
201
|
+
const access = await identityX.checkContentAccess({
|
|
202
|
+
isEnabled: true,
|
|
203
|
+
requiredAccessLevelIds: [],
|
|
204
|
+
});
|
|
205
|
+
debug({ requiresUserInput: access.requiresUserInput });
|
|
206
|
+
if (access.requiresUserInput) return next();
|
|
207
|
+
} catch (e) {
|
|
208
|
+
error('Progressive profiling failed to check content access; releasing the content.', e);
|
|
209
|
+
return next();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const content = await loadGatingContent(req);
|
|
214
|
+
const blockReason = contentBlockReason({ req, content, config });
|
|
215
|
+
debug({ contentId: content && content.id, blockReason });
|
|
216
|
+
if (blockReason) return next();
|
|
217
|
+
|
|
218
|
+
// Claim the request. The render counts against the escape valve whether or not the visitor
|
|
219
|
+
// engages; the write route clears the counter on any successful submit.
|
|
220
|
+
incrementAttempts(req, res);
|
|
221
|
+
|
|
222
|
+
res.locals.progressiveProfileState = {
|
|
223
|
+
previewParagraphs: config.previewParagraphs,
|
|
224
|
+
displayOverlay: config.displayOverlay,
|
|
225
|
+
promoCode: config.promoCode,
|
|
226
|
+
dismissible: config.dismissible,
|
|
227
|
+
suppressAfterDismissDays: config.suppressAfterDismissDays,
|
|
228
|
+
userId: appUser.id,
|
|
229
|
+
audience,
|
|
230
|
+
missingRows,
|
|
231
|
+
missingCustomFieldIds,
|
|
232
|
+
missingCount,
|
|
233
|
+
attempts: attempts + 1,
|
|
234
|
+
displayGate: true,
|
|
235
|
+
};
|
|
236
|
+
return next();
|
|
237
|
+
});
|
|
238
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindful-web/marko-web-theme-monorail",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.85.0",
|
|
4
4
|
"author": "Brian Miller <brian@parameter1.com>",
|
|
5
5
|
"repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-theme-monorail",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,21 +13,21 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@mindful-web/dayjs": "^1.49.2",
|
|
16
|
-
"@mindful-web/html": "^1.
|
|
17
|
-
"@mindful-web/image": "^1.
|
|
16
|
+
"@mindful-web/html": "^1.84.0",
|
|
17
|
+
"@mindful-web/image": "^1.84.0",
|
|
18
18
|
"@mindful-web/inflector": "^1.0.0",
|
|
19
19
|
"@mindful-web/marko-web-deferred-script-loader": "^1.83.1",
|
|
20
20
|
"@mindful-web/marko-web-gam": "^1.83.1",
|
|
21
21
|
"@mindful-web/marko-web-gtm": "^1.83.1",
|
|
22
22
|
"@mindful-web/marko-web-html-sitemap": "^1.83.1",
|
|
23
23
|
"@mindful-web/marko-web-icons": "^1.59.2",
|
|
24
|
-
"@mindful-web/marko-web-identity-x": "^1.
|
|
25
|
-
"@mindful-web/marko-web-inquiry": "^1.
|
|
24
|
+
"@mindful-web/marko-web-identity-x": "^1.85.0",
|
|
25
|
+
"@mindful-web/marko-web-inquiry": "^1.85.0",
|
|
26
26
|
"@mindful-web/marko-web-native-x": "^1.83.1",
|
|
27
|
-
"@mindful-web/marko-web-omeda": "^1.
|
|
28
|
-
"@mindful-web/marko-web-omeda-identity-x": "^1.
|
|
29
|
-
"@mindful-web/marko-web-p1-events": "^1.
|
|
30
|
-
"@mindful-web/marko-web-recaptcha": "^1.
|
|
27
|
+
"@mindful-web/marko-web-omeda": "^1.85.0",
|
|
28
|
+
"@mindful-web/marko-web-omeda-identity-x": "^1.85.0",
|
|
29
|
+
"@mindful-web/marko-web-p1-events": "^1.85.0",
|
|
30
|
+
"@mindful-web/marko-web-recaptcha": "^1.85.0",
|
|
31
31
|
"@mindful-web/marko-web-reveal-ad": "^1.83.1",
|
|
32
32
|
"@mindful-web/marko-web-search": "^1.83.1",
|
|
33
33
|
"@mindful-web/marko-web-social-sharing": "^1.83.2",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"publishConfig": {
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "f605eeeddb1f44c632feb239d37a98560bb82416"
|
|
58
58
|
}
|
|
@@ -26,6 +26,35 @@
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
// The gate form's lead-in copy. Styled to match `newsletter-signup-banner__description--large`.
|
|
31
|
+
&__description {
|
|
32
|
+
@include skin-typography($style: "teaser-text-1");
|
|
33
|
+
max-width: 520px;
|
|
34
|
+
margin-bottom: 14px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The submit/dismiss button row, spaced off the form fields above it.
|
|
38
|
+
&__actions {
|
|
39
|
+
margin-top: 20px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The progressive-profile gate (`browser/progressive-profile.vue` renders this modifier on its
|
|
43
|
+
// root). Same box as the other gates; its body is a form rather than login copy, plus a
|
|
44
|
+
// post-submit success message. The fill matches `.newsletter-signup-banner-large`, so the gate
|
|
45
|
+
// stands out from the article body the same way the inline signup block does.
|
|
46
|
+
&--progressive-profile {
|
|
47
|
+
background-color: $gray-100;
|
|
48
|
+
.success-message {
|
|
49
|
+
margin-bottom: 15px;
|
|
50
|
+
font-family: $theme-font-family-serif;
|
|
51
|
+
font-size: 16px;
|
|
52
|
+
color: #222;
|
|
53
|
+
}
|
|
54
|
+
form {
|
|
55
|
+
margin-top: 15px;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
29
58
|
}
|
|
30
59
|
|
|
31
60
|
.content-page-preview-overlay {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/ad-preview.marko",
|
|
6
6
|
marko_component = require("./ad-preview.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -139,7 +139,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
139
139
|
}, marko_component);
|
|
140
140
|
|
|
141
141
|
marko_template.meta = {
|
|
142
|
-
id: "/@mindful-web/marko-web-theme-monorail$1.
|
|
142
|
+
id: "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/ad-preview.marko",
|
|
143
143
|
component: "./ad-preview.marko",
|
|
144
144
|
tags: [
|
|
145
145
|
"../components/flows/section-feed.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/content/components/presented-by.marko",
|
|
6
6
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
7
7
|
module_image_module = require("@mindful-web/image"),
|
|
8
8
|
image_module = module_image_module.default || module_image_module,
|
|
@@ -67,7 +67,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
marko_template.meta = {
|
|
70
|
-
id: "/@mindful-web/marko-web-theme-monorail$1.
|
|
70
|
+
id: "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/content/components/presented-by.marko",
|
|
71
71
|
tags: [
|
|
72
72
|
"@mindful-web/marko-web/components/element/index.marko",
|
|
73
73
|
"@mindful-web/marko-web/components/element/img.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/content/native-x-story.marko",
|
|
6
6
|
marko_component = require("./native-x-story.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_convert = require("@mindful-web/marko-web-native-x/utils/convert-story-to-content"),
|
|
@@ -223,7 +223,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
223
223
|
}, marko_component);
|
|
224
224
|
|
|
225
225
|
marko_template.meta = {
|
|
226
|
-
id: "/@mindful-web/marko-web-theme-monorail$1.
|
|
226
|
+
id: "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/content/native-x-story.marko",
|
|
227
227
|
component: "./native-x-story.marko",
|
|
228
228
|
tags: [
|
|
229
229
|
"@mindful-web/marko-web-native-x/components/gtm-init.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/content/print.marko",
|
|
6
6
|
marko_component = require("./print.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_objectPath_module = require("@mindful-web/object-path"),
|
|
@@ -402,7 +402,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
402
402
|
}, marko_component);
|
|
403
403
|
|
|
404
404
|
marko_template.meta = {
|
|
405
|
-
id: "/@mindful-web/marko-web-theme-monorail$1.
|
|
405
|
+
id: "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/content/print.marko",
|
|
406
406
|
component: "./print.marko",
|
|
407
407
|
tags: [
|
|
408
408
|
"@mindful-web/marko-web/components/page/metadata/content.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/email-preview.marko",
|
|
6
6
|
marko_component = require("./email-preview.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
marko_web_gtm_push_template = require("@mindful-web/marko-web-gtm/components/push.marko"),
|
|
@@ -86,7 +86,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
86
86
|
}, marko_component);
|
|
87
87
|
|
|
88
88
|
marko_template.meta = {
|
|
89
|
-
id: "/@mindful-web/marko-web-theme-monorail$1.
|
|
89
|
+
id: "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/email-preview.marko",
|
|
90
90
|
component: "./email-preview.marko",
|
|
91
91
|
tags: [
|
|
92
92
|
"@mindful-web/marko-web-gtm/components/push.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/search.marko",
|
|
6
6
|
marko_component = require("./search.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_queryFragment = require("../graphql/fragments/section-feed-block"),
|
|
@@ -302,7 +302,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
302
302
|
}, marko_component);
|
|
303
303
|
|
|
304
304
|
marko_template.meta = {
|
|
305
|
-
id: "/@mindful-web/marko-web-theme-monorail$1.
|
|
305
|
+
id: "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/search.marko",
|
|
306
306
|
component: "./search.marko",
|
|
307
307
|
tags: [
|
|
308
308
|
"@mindful-web/marko-web-p1-events/components/track-search.marko",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
var marko_template = module.exports = require("marko/dist/html").t(__filename),
|
|
5
|
-
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.
|
|
5
|
+
marko_componentType = "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/taxonomy.marko",
|
|
6
6
|
marko_component = require("./taxonomy.marko"),
|
|
7
7
|
marko_renderer = require("marko/dist/runtime/components/renderer"),
|
|
8
8
|
module_hierarchyAliases = require("@mindful-web/marko-web/utils/hierarchy-aliases"),
|
|
@@ -178,7 +178,7 @@ marko_template._ = marko_renderer(render, {
|
|
|
178
178
|
}, marko_component);
|
|
179
179
|
|
|
180
180
|
marko_template.meta = {
|
|
181
|
-
id: "/@mindful-web/marko-web-theme-monorail$1.
|
|
181
|
+
id: "/@mindful-web/marko-web-theme-monorail$1.85.0/templates/taxonomy.marko",
|
|
182
182
|
component: "./taxonomy.marko",
|
|
183
183
|
tags: [
|
|
184
184
|
"../components/gam/define-display-ad.marko",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { content: loader } = require('@mindful-web/web-common/page-loaders');
|
|
2
|
+
const buildContentInput = require('@mindful-web/marko-web/utils/build-content-input');
|
|
3
|
+
const queryFragment = require('../graphql/fragments/content-meter');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads the gating-relevant fields of the current content request, memoized on the request.
|
|
7
|
+
*
|
|
8
|
+
* The progressive-profiling and content-metering middlewares chain on the same content route and
|
|
9
|
+
* each needs the same handful of fields to decide whether to claim the request. Without this they
|
|
10
|
+
* run two identical GraphQL round trips for a document `withContent` will load again anyway; with
|
|
11
|
+
* it they share one.
|
|
12
|
+
*
|
|
13
|
+
* The *promise* is cached rather than its result, so concurrent callers join the in-flight query,
|
|
14
|
+
* and a rejection is shared too: a 404 from the loader should fail the request once, not be
|
|
15
|
+
* retried per middleware.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('express').Request} req
|
|
18
|
+
* @returns {Promise<object>} The content document.
|
|
19
|
+
*/
|
|
20
|
+
module.exports = (req) => {
|
|
21
|
+
if (!req.$gatingContent) {
|
|
22
|
+
const { apollo, params } = req;
|
|
23
|
+
const additionalInput = buildContentInput({ req });
|
|
24
|
+
req.$gatingContent = loader(apollo, { id: params.id, additionalInput, queryFragment });
|
|
25
|
+
}
|
|
26
|
+
return req.$gatingContent;
|
|
27
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const debug = require('debug')('progressive-profile');
|
|
2
|
+
const { getAsArray } = require('@mindful-web/object-path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Analogues of `content-meter-helpers.js`, so the layout reads identically for both gates.
|
|
6
|
+
*
|
|
7
|
+
* Both answer off `res.locals.progressiveProfileState`, which the middleware only sets when it
|
|
8
|
+
* has claimed the request — so unlike the meter there is no logged-in/requires-input matrix here:
|
|
9
|
+
* one gate, hard by default, either owns the page or the state is absent.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const shouldGateForProfile = (state) => {
|
|
13
|
+
if (!state || !state.displayGate) return false;
|
|
14
|
+
// The form renders nothing without a resolved user and at least one missing row (the same
|
|
15
|
+
// guard `progressive-profile-form.marko` applies) — so without them, do not truncate the body
|
|
16
|
+
// under a gate that will not appear.
|
|
17
|
+
if (!state.userId) return false;
|
|
18
|
+
return Boolean(getAsArray(state, 'missingRows').length);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Separate name, same decision: the gate and the truncation must never disagree, or the full
|
|
23
|
+
* body renders under the form (no gate) or the preview ends in nothing (no body). Kept as two
|
|
24
|
+
* exports so the layout mirrors the meter's `shouldTruncate`/gate split.
|
|
25
|
+
*/
|
|
26
|
+
const shouldTruncateForProfile = (state) => shouldGateForProfile(state);
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
shouldGateForProfile: (...args) => {
|
|
30
|
+
const out = shouldGateForProfile(...args);
|
|
31
|
+
debug('shouldGateForProfile', out);
|
|
32
|
+
return out;
|
|
33
|
+
},
|
|
34
|
+
shouldTruncateForProfile: (...args) => {
|
|
35
|
+
const out = shouldTruncateForProfile(...args);
|
|
36
|
+
debug('shouldTruncateForProfile', out);
|
|
37
|
+
return out;
|
|
38
|
+
},
|
|
39
|
+
};
|