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'],