Skip to content

Commit

Permalink
Support host-specific templates
Browse files Browse the repository at this point in the history
  • Loading branch information
xjensen committed Oct 27, 2023
1 parent bc61dfa commit 0748a90
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 72 deletions.
20 changes: 16 additions & 4 deletions src/http/get-benefits/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const { assembleLinks } = require("@architect/shared/links");
const { applyRules } = require("@architect/shared/rules");
const { getDefinitions } = require("@architect/shared/s3");
const { generateHtml } = require("@architect/shared/templates");
const { matchHostDef } = require("@architect/shared/hosts");
const { getThrottles } = require("@architect/shared/throttles");

// Definitions will be loaded from benefits-recs-defs.json in S3.
// We keep it outside the handler to cache it between Lambda runs.
Expand All @@ -17,14 +19,24 @@ exports.handler = arc.http.async(async (req) => {
definitions = await getDefinitions();
}

const {
targets: targetDefs = [],
hosts: hostDefs = [],
throttles: throttleDefs = [],
} = definitions;

// Grab data from headers and URL query parameters.
const host = decodeURIComponent(req.query.host || "");
const language = req.query.language || "en";
const acceptHeader = req.headers?.accept;

// Process target links.
const allLinks = assembleLinks(definitions, language, host);
const links = await applyRules(definitions, allLinks, host);
// Process metadata.
const hostDef = matchHostDef(host, hostDefs);
const throttles = await getThrottles(throttleDefs);

// Create target links.
const allLinks = assembleLinks(targetDefs, language, hostDef);
const links = await applyRules(throttles, allLinks, hostDef);

// If we don't have any links, exit now with no content.
if (links.length === 0) {
Expand All @@ -46,7 +58,7 @@ exports.handler = arc.http.async(async (req) => {
if (acceptHeader === "text/html") {
return {
cors: true,
html: generateHtml(data),
html: generateHtml(data, hostDef),
};
}

Expand Down
26 changes: 26 additions & 0 deletions src/shared/hosts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const url = require("./url");

/**
* Find the host definition for a given host url.
* We want to match up the origin of the request (host) with our own metadata (hostDefs).
* @param {string} host
* The host page, as a URL (string), from which the widget sent this request.
* @param {import('./s3.js').Host[]} hostDefs
* A list of host objects from the Airtable-derived definitions.
* @returns {import('./s3.js').Host|undefined}
* A matching host definition, or undefined if none are found.
*/
exports.matchHostDef = (host, hostDefs) => {
const pHostUrl = url.parse(host);

if (pHostUrl && hostDefs) {
return hostDefs.find((hostDef) =>
hostDef.urls.some((hostDefUrl) => {
const pHostDefUrl = url.parse(hostDefUrl);
return pHostDefUrl.hostname === pHostUrl.hostname;
})
);
} else {
return undefined;
}
};
38 changes: 5 additions & 33 deletions src/shared/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,6 @@ const AnalyticEngines = {
PIWIK: "piwik",
};

/**
* Find the host definition for a given host url.
* We want to match up the origin of the request (host) with our own metadata (hostDefs).
* @param {string} host
* The host page, as a URL (string), from which the widget sent this request.
* @param {import('./s3.js').Host[]} hostDefs
* A list of host objects from the Airtable-derived definitions.
* @returns {import('./s3.js').Host|undefined}
* A matching host definition, or undefined if none are found.
*/
const findHostDef = (host, hostDefs) => {
const pHostUrl = url.parse(host);

if (pHostUrl) {
return hostDefs.find((hostDef) =>
hostDef.urls.some((hostDefUrl) => {
const pHostDefUrl = url.parse(hostDefUrl);
return pHostDefUrl.hostname === pHostUrl.hostname;
})
);
} else {
return undefined;
}
};

/**
* Add query string parameters for the target site's analytics engine.
* @param {string} linkUrl
Expand Down Expand Up @@ -76,20 +51,17 @@ const addAnalytics = (linkUrl, analytics, hostDef) => {
*/

/**
* Construct link objects for the given language and host.
* @param {import('./s3.js').Definitions} definitions
* A parsed object representing the Airtable-derived `benefits-recs-defs.json` file.
* From the list of target definitions, construct link objects for the given language and host.
* @param {import('./s3.js').Target[]} targets
* Target definitions from the Airtable-derived `benefits-recs-defs.json` file.
* @param {string} language
* The language for this request, as an ISO 639-1 code.
* @param {string} host
* @param {import('./s3.js').Host} hostDef
* The host page, as a URL (string), from which the widget sent this request.
* @returns {TargetLink[]}
* A processed list of target links in the requested language.
*/
exports.assembleLinks = (definitions, language, host) => {
const { targets, hosts: hostDefs } = definitions;
const hostDef = findHostDef(host, hostDefs);

exports.assembleLinks = (targets, language, hostDef) => {
// Unless it's Chinese, strip the language code down to two characters.
// We need to preserve the Chinese code to display Traditional vs. Simplified.
const langKey = language.startsWith("zh") ? language : language.slice(0, 2);
Expand Down
23 changes: 10 additions & 13 deletions src/shared/rules.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const url = require("./url");
const { getThrottles } = require("./throttles");

/**
* rules.js
Expand All @@ -17,8 +16,8 @@ const { getThrottles } = require("./throttles");
* A processed list of all target links in the user's requested language.
* @param {object} params
* A collection of parameters from the request to help inform the rule.
* @param {string} [params.host]
* The host page, as a URL (string), from which the widget sent this request.
* @param {import('./s3.js').Host} [params.hostDef]
* The host partner from which the widget sent this request.
* @param {import('./s3.js').Throttle[]} [params.throttles]
* A processed list of target link throttles.
* @returns {import('./links.js').TargetLink[]}
Expand All @@ -42,8 +41,8 @@ const removeThrottledLinks = (links, { throttles }) =>
* Remove links that point back to the same host site as the widget.
* @type {Rule}
*/
const removeLinkBacks = (links, { host }) =>
links.filter((link) => !url.matchHosts(host, link.url));
const removeLinkBacks = (links, { hostDef }) =>
links.filter((link) => !url.findMatch(hostDef?.urls || [], link.url));

/**
* Randomize the order of the links.
Expand Down Expand Up @@ -74,20 +73,18 @@ const rules = [

/**
* Apply the rules to the given list of links.
* @param {import('./s3.js').Definitions} definitions
* A parsed object representing the Airtable-derived `benefits-recs-defs.json` file.
* @param {import('./s3.js').Throttle[]} throttles
* Active throttles against target links.
* @param {import('./links.js').TargetLink[]} allLinks
* A processed list of all target links in the user's requested language.
* @param {string} host
* The host page, as a URL (string), from which the widget sent this request.
* @param {import('./s3.js').Host} hostDef
* The identified host partner from which the widget sent this request.
* @returns {Promise<import('./links.js').TargetLink[]>}
* A targetted list of target links.
*/
const applyRules = async (definitions, allLinks, host) => {
const throttles = await getThrottles(definitions);

const applyRules = async (throttles, allLinks, hostDef) => {
const params = {
host,
hostDef,
throttles,
};

Expand Down
58 changes: 43 additions & 15 deletions src/shared/templates.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const css = /* css */ `
const defaultCss = /* css */ `
:host {
--benefits-recs-background-color: #E1F1EE;
--benefits-recs-highlight-color: #006C58;
Expand Down Expand Up @@ -132,7 +132,7 @@ const openIcon = /* html */ `
</svg>
`;

const linkHtml = (link) => /* html */ `
const defaultLinkHtml = (link) => /* html */ `
<li>
<a href="${link.url}" target="_blank" rel="noopener noreferrer">
<span class="link-start">
Expand All @@ -147,17 +147,45 @@ const linkHtml = (link) => /* html */ `
</li>
`;

exports.generateHtml = (data) => /* html */ `
<style>${css}</style>
<section
aria-label="benefits recommendations"
data-experimentName="${data.experimentName}"
data-experimentVariation="${data.experimentVariation}"
>
<h2>${data.header}</h2>
<p class="tagline">${data.tagline}</p>
<ul class="link-list">
${data.links.map((link) => linkHtml(link)).join("\n")}
</ul>
</section>
const defaultHtml = (data) => {
const linkList = data.links.map((link) => defaultLinkHtml(link)).join("\n");

return /* html */ `
<section
aria-label="benefits recommendations"
data-experimentName="${data.experimentName}"
data-experimentVariation="${data.experimentVariation}"
>
<h2>${data.header}</h2>
<p class="tagline">${data.tagline}</p>
<ul class="link-list">
${linkList}
</ul>
</section>
`;
};

const defaultTemplate = (data) => /* html */ `
<style>
${defaultCss}
</style>
${defaultHtml(data)}
`;

const eddUiTemplate = (data) => /* html */ `
<style>
${defaultCss}
section {
margin: 3rem 0;
}
</style>
${defaultHtml(data)}
`;

exports.generateHtml = (data, hostDef) => {
if (hostDef.id === "edd_ui_recert") {
return eddUiTemplate(data);
}

return defaultTemplate(data);
};
10 changes: 4 additions & 6 deletions src/shared/throttles.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ const arc = require("@architect/functions");
* We check DynamoDB to see if each throttle is still within daily clickthrough limits.
* We also look at other parameters, like start and end dates.
* The goal is to see which throttles need to be activated for this request.
* @param {import('./s3.js').Definitions} definitions
* A parsed object representing the Airtable-derived `benefits-recs-defs.json` file.
* @param {import('./s3.js').Throttle[]} throttleDefs
* Throttle definitions from the Airtable-derived `benefits-recs-defs.json` file.
* @returns {Promise<import('./s3.js').Throttle[]>}
* A hydrated list of target link throttles.
*/
exports.getThrottles = async (definitions) => {
const { throttles: throttleDefs } = definitions;

exports.getThrottles = async (throttleDefs) => {
try {
const throttles = [...throttleDefs];

Expand Down Expand Up @@ -47,7 +45,7 @@ exports.getThrottles = async (definitions) => {
}

// If the throttle is still open, we need to check DynamoDB.
if ((throttle.exceeded = false)) {
if (limit && throttle.exceeded === false) {
const promise = dynamo.throttleclicks
.get({ name, day })
.then((response) => {
Expand Down
2 changes: 1 addition & 1 deletion src/shared/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const matchPaths = (url1, url2) => {

/**
* Checks if a URL is included in a list of other URLs.
* @param {Array} urls An array of URL strings.
* @param {string[]} urls An array of URL strings.
* @param {string} queryUrl A string representation of the URL to check.
* @returns {boolean}
*/
Expand Down

0 comments on commit 0748a90

Please sign in to comment.