From 02714c747e2c1a89e2890fcfdcfd20890e552324 Mon Sep 17 00:00:00 2001 From: Nicholas Chiang Date: Sat, 5 Aug 2023 18:01:48 -0600 Subject: [PATCH] feat(show): use show description as meta description This patch updates the show page description to use the show description if it exists, and revert back to the Vogue inspired description if it does not. This patch also adds some XSS support that may be somewhat premature, but was easy enough to add that it felt it wouldn't hurt. Closes: NC-666 --- .../route.tsx | 21 ++++++++++- app/sanitize.server.ts | 8 +++++ package.json | 2 ++ pnpm-lock.yaml | 36 ++++++++++++++----- remix.config.js | 19 +++++++++- 5 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 app/sanitize.server.ts diff --git a/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx b/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx index 31979934..fe5d19eb 100644 --- a/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx +++ b/app/routes/_header.shows.($location).$year.$season.$sex.$level.$brand/route.tsx @@ -26,6 +26,7 @@ import { prisma } from 'db.server' import { type Filter, FILTER_PARAM, filterToSearchParam } from 'filters' import { log } from 'log.server' import { type Handle } from 'root' +import { sanitize } from 'sanitize.server' import { getUserId } from 'session.server' import { About } from './about' @@ -39,7 +40,9 @@ export const meta: V2_MetaFunction = ({ data }) => { { title: `${data.brand.name} ${getShowSeason(data)} Collection | ${NAME}` }, { name: 'description', - content: `${data.name} collection, runway looks, beauty, models, and reviews.`, + content: data.description + ? sanitize(data.description, { allowedTags: [] }) + : `${data.name} collection, runway looks, beauty, models, and reviews.`, }, { name: 'keywords', content: keywords }, { name: 'news_keywords', content: keywords }, @@ -142,10 +145,26 @@ export async function loader({ request, params }: LoaderArgs) { }) if (show == null) throw miss log.debug('got show %o', show) + + // Sanitize HTML (perhaps this should be in a separate helper function). + /* eslint-disable no-param-reassign */ + show.articles.forEach((article) => { + article.content = sanitize(article.content) + }) + show.description = sanitize(show.description) + show.collections.forEach((collection) => { + collection.designers.forEach((designer) => { + designer.description = sanitize(designer.description) + }) + }) + /* eslint-enable no-param-reassign */ + + // Derive the show's scores and get the current user's review of it. const [scores, review] = await Promise.all([ getScores(show.id), getReview(show.id, request), ]) + return { ...show, scores, review } } diff --git a/app/sanitize.server.ts b/app/sanitize.server.ts new file mode 100644 index 00000000..01380600 --- /dev/null +++ b/app/sanitize.server.ts @@ -0,0 +1,8 @@ +import sanitizeHtml from 'sanitize-html' + +export function sanitize(html: string, options?: sanitizeHtml.IOptions): string +export function sanitize(html: null, options?: sanitizeHtml.IOptions): null +export function sanitize(html: string | null, options?: sanitizeHtml.IOptions): string | null +export function sanitize(html: string | null, options?: sanitizeHtml.IOptions) { + return html ? sanitizeHtml(html, options) : html +} diff --git a/package.json b/package.json index cf63ae78..92b5b9a3 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "react-simple-maps": "^3.0.0", "remix-sitemap": "^2.2.0", "rfdc": "^1.3.0", + "sanitize-html": "^2.11.0", "schema-dts": "^1.1.2", "sharp": "^0.32.4", "tailwind-merge": "^1.14.0", @@ -131,6 +132,7 @@ "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@types/react-simple-maps": "^3.0.0", + "@types/sanitize-html": "^2.9.0", "@types/sharp": "^0.31.1", "@types/user-agents": "^1.0.2", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 236987b0..6cf27087 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ dependencies: rfdc: specifier: ^1.3.0 version: 1.3.0 + sanitize-html: + specifier: ^2.11.0 + version: 2.11.0 schema-dts: specifier: ^1.1.2 version: 1.1.2(typescript@4.9.5) @@ -255,6 +258,9 @@ devDependencies: '@types/react-simple-maps': specifier: ^3.0.0 version: 3.0.0 + '@types/sanitize-html': + specifier: ^2.9.0 + version: 2.9.0 '@types/sharp': specifier: ^0.31.1 version: 0.31.1 @@ -4915,6 +4921,12 @@ packages: '@types/node': 18.17.4 dev: true + /@types/sanitize-html@2.9.0: + resolution: {integrity: sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==} + dependencies: + htmlparser2: 8.0.2 + dev: true + /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} @@ -7116,7 +7128,6 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: true /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -7259,18 +7270,15 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.4.0 - dev: true /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} dependencies: domelementtype: 2.3.0 - dev: true /domutils@3.0.1: resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} @@ -7278,7 +7286,6 @@ packages: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - dev: true /dot-json@1.3.0: resolution: {integrity: sha512-Pu11Prog/Yjf2lBICow82/DSV46n3a2XT1Rqt/CeuhkO1fuacF7xydYhI0SwQx2Ue0jCyLtQzgKPFEO6ewv+bQ==} @@ -7398,7 +7405,6 @@ packages: /entities@4.4.0: resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} engines: {node: '>=0.12'} - dev: true /env-ci@9.1.0: resolution: {integrity: sha512-ZCEas2sDVFR3gpumwwzSU4OJZwWJ46yqJH3TqH3vSxEBzeAlC0uCJLGAnZC0vX1TIXzHzjcwpKmUn2xw5mC/qA==} @@ -7896,7 +7902,6 @@ packages: /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - dev: true /escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} @@ -9457,7 +9462,6 @@ packages: domhandler: 5.0.3 domutils: 3.0.1 entities: 4.4.0 - dev: true /http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -9924,7 +9928,6 @@ packages: /is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - dev: true /is-reference@3.0.1: resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} @@ -12087,6 +12090,10 @@ packages: engines: {node: '>=6'} dev: true + /parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + dev: false + /parse5-htmlparser2-tree-adapter@7.0.0: resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} dependencies: @@ -13585,6 +13592,17 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /sanitize-html@2.11.0: + resolution: {integrity: sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==} + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.27 + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: diff --git a/remix.config.js b/remix.config.js index 2ba88d81..69fe794f 100644 --- a/remix.config.js +++ b/remix.config.js @@ -13,7 +13,24 @@ module.exports = { cacheDirectory: './node_modules/.cache/remix', ignoredRouteFiles: ['**/.*', '**/*.css', '**/*.test.{js,jsx,ts,tsx}'], serverModuleFormat: 'cjs', - serverDependenciesToBundle: ['nanoid/non-secure', /d3-.*/], + serverDependenciesToBundle: [ + // TODO figure out why I need to do this; why can't Vercel just include + // these in the `node_modules` and resolve them normally? I'm guessing this + // is because `sanitize-html` uses `require()` and not `import` (and thus it + // is not recognized by Vercel's Edge Runtime). + // @see {@link https://vercel.com/docs/functions/edge-functions/edge-runtime#unsupported-apis} + 'sanitize-html', + 'htmlparser2', + 'escape-string-regexp', + 'is-plain-object', + 'deepmerge', + 'parse-srcset', + 'postcss', + // TODO why did I have to mark `nanoid` as a server dependency? + 'nanoid/non-secure', + // TODO why can't the D3 packages be resolved normally? + /d3-.*/, + ], images: { sizes: [200, 300, 400, 500, 600, 700, 800, 900, 1000], domains: ['aritzia.scene7.com'],