From 7e2dc1586134c18ff3c83f97a245001f37266453 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 | 19 +++++++++- app/sanitize.server.ts | 8 +++++ package.json | 2 ++ pnpm-lock.yaml | 36 ++++++++++++++----- 4 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 app/sanitize.server.ts diff --git a/app/routes/shows.$seasonYear.$seasonName.($sex).$brandSlug/route.tsx b/app/routes/shows.$seasonYear.$seasonName.($sex).$brandSlug/route.tsx index 2cb42194..ead1325b 100644 --- a/app/routes/shows.$seasonYear.$seasonName.($sex).$brandSlug/route.tsx +++ b/app/routes/shows.$seasonYear.$seasonName.($sex).$brandSlug/route.tsx @@ -9,6 +9,7 @@ import { type SitemapFunction } from 'remix-sitemap' import { prisma } from 'db.server' import { log } from 'log.server' import { type Handle } from 'root' +import { sanitize } from 'sanitize.server' import { invert } from 'utils' import { cn } from 'utils/cn' import { getScores } from 'utils/scores.server' @@ -40,7 +41,9 @@ export const meta: V2_MetaFunction = ({ data }) => { { title: `${data.name} Collection | Nicholas Chiang` }, { 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 }, @@ -108,10 +111,24 @@ 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). + show.reviews.forEach((review) => { + review.content = sanitize(review.content) + }) + show.description = sanitize(show.description) + show.collections.forEach((collection) => { + collection.designers.forEach((designer) => { + designer.description = sanitize(designer.description) + }) + }) + + // 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 aaeff7d4..73df1a63 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "react-hotkeys-hook": "^4.4.1", "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", @@ -125,6 +126,7 @@ "@types/progress": "^2.0.5", "@types/react": "^18.2.16", "@types/react-dom": "^18.2.7", + "@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 7f7d2074..6189ec3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,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) @@ -237,6 +240,9 @@ devDependencies: '@types/react-dom': specifier: ^18.2.7 version: 18.2.7 + '@types/sanitize-html': + specifier: ^2.9.0 + version: 2.9.0 '@types/sharp': specifier: ^0.31.1 version: 0.31.1 @@ -4831,6 +4837,12 @@ packages: '@types/node': 18.17.1 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==} @@ -7036,7 +7048,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==} @@ -7190,18 +7201,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==} @@ -7209,7 +7217,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==} @@ -7329,7 +7336,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==} @@ -7757,7 +7763,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==} @@ -9363,7 +9368,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==} @@ -9843,7 +9847,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==} @@ -12047,6 +12050,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: @@ -13547,6 +13554,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: