├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── dd-service-catalog.yml │ ├── pr-verification.yml │ ├── publish-develop.yml │ ├── publish-prod.yml │ └── publish-sandbox.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── __mocks__ ├── fusion:consumer.js ├── fusion:content-config.js ├── fusion:custom-types.js ├── fusion:environment.js ├── fusion:is-required.js ├── fusion:json.js ├── fusion:prop-types.js ├── fusion:properties.js └── fusion:taggables.js ├── babel.config.js ├── blocks ├── ans-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── ans │ │ │ ├── __snapshots__ │ │ │ └── json.test.js.snap │ │ │ ├── json.js │ │ │ └── json.test.js │ └── package.json ├── feeds-source-collections-block │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── sources │ │ └── collections.js ├── feeds-source-content-api-block │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── sources │ │ ├── feeds-content-api.js │ │ └── feeds-content-api.test.js ├── feeds-source-content-api-by-day-block │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── sources │ │ └── feeds-content-api-by-day.js ├── feeds-source-content-api-by-day2-block │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── sources │ │ └── feeds-content-api-by-day2.js ├── feeds-source-content-api-by-day3-block │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── sources │ │ └── feeds-content-api-by-day3.js ├── feeds-source-single-content-block │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── sources │ │ └── single-content.js ├── feeds-source-video-api-block │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── sources │ │ ├── feeds-video-api.js │ │ └── feeds-video-api.test.js ├── json-output-block │ ├── CHANGELOG.md │ ├── README.md │ ├── output-types │ │ └── json.js │ └── package.json ├── mrss-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── mrss │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── rss-alexa-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── rss │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── rss-fbia-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── rss │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── rss-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── rss │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── rss-flipboard-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── rss-flipboard │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── rss-google-news-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── google-news-rss │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── rss-msn-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── msn-rss │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── sitemap-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── sitemap │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── sitemap-index-by-day-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── sitemap-index │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── sitemap-index-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── sitemap-index │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── sitemap-news-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── news-sitemap │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── sitemap-section-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── sitemap-section │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── sitemap-section-index-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── sitemap-section-front-index │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ └── package.json ├── sitemap-video-feature-block │ ├── CHANGELOG.md │ ├── README.md │ ├── features │ │ └── video-sitemap │ │ │ ├── __snapshots__ │ │ │ └── xml.test.js.snap │ │ │ ├── xml.js │ │ │ └── xml.test.js │ ├── package.json │ └── searchHelper.js ├── text-output-block │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── output-types │ │ ├── .npmignore │ │ ├── __snapshots__ │ │ │ └── text.test.jsx.snap │ │ ├── text.js │ │ └── text.test.jsx │ └── package.json └── textfile-block │ ├── CHANGELOG.md │ ├── features │ └── textfile │ │ ├── __snapshots__ │ │ └── text.test.js.snap │ │ ├── text.js │ │ └── text.test.js │ ├── jest.config.js │ └── package.json ├── documentation └── README.md ├── jest.config.js ├── lerna.json ├── package-lock.json ├── package.json └── utils ├── content-elements ├── CHANGELOG.md ├── README.md ├── __mocks__ │ └── @wpmedia │ │ ├── feeds-find-video-stream.js │ │ └── feeds-resizer.js ├── package.json ├── rollup.config.js └── src │ ├── contentElements.js │ ├── contentElements.test.js │ └── index.js ├── content-source-utils ├── README.md ├── package.json ├── rollup.config.js └── src │ ├── formatSections.js │ ├── formatSections.test.js │ ├── generateDistributor.js │ ├── generateDistributor.test.js │ ├── generateParamList.js │ ├── generateParamList.test.js │ ├── index.js │ ├── transform.js │ └── transform.test.js ├── find-video-stream ├── CHANGELOG.md ├── README.md ├── package.json ├── rollup.config.js └── src │ ├── findVideo.js │ ├── findVideo.test.js │ └── index.js ├── promo-items ├── CHANGELOG.md ├── README.md ├── package.json ├── rollup.config.js └── src │ ├── index.js │ ├── promoItems.js │ └── promoItems.test.js ├── prop-types ├── CHANGELOG.md ├── README.md ├── package.json ├── rollup.config.js └── src │ ├── __snapshots__ │ └── props.test.js.snap │ ├── generatePropsForFeed.js │ ├── index.js │ ├── propInfo.js │ └── props.test.js ├── resizer ├── CHANGELOG.md ├── README.md ├── package.json ├── rollup.config.js └── src │ ├── calculateWidthAndHeight.js │ ├── handle-fetch-error │ ├── index.js │ └── index.test.js │ ├── image-ans-to-image-src │ ├── index.js │ └── index.test.js │ ├── index.js │ ├── index.test.js │ ├── sign-images-in-ans-object │ ├── index.js │ └── index.test.js │ └── signing-service │ ├── index.js │ └── index.test.js └── xml-output ├── CHANGELOG.md ├── README.md ├── package.json ├── rollup.config.js └── src ├── index.js └── xmlOutput.test.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@0.3.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "WPMedia/feed-components" } 6 | ], 7 | "commit": false, 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "prod" 11 | } 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['standard', 'prettier', 'plugin:react/recommended'], 3 | plugins: ['standard', 'prettier', 'react'], 4 | rules: { 5 | 'react/prop-types': 'warn', 6 | 'object-shorthand': 'off', 7 | }, 8 | settings: { 9 | react: { 10 | version: 'detect', 11 | }, 12 | }, 13 | overrides: [ 14 | { 15 | files: ['*.test.js', '__mocks__/**'], 16 | }, 17 | ], 18 | env: { 19 | jest: true, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | npm-registry-registry-npmjs-org: 4 | type: npm-registry 5 | url: https://registry.npmjs.org 6 | token: "${{secrets.NPM_REGISTRY_REGISTRY_NPMJS_ORG_TOKEN}}" 7 | npm-registry-npm-pkg-github-com: 8 | type: npm-registry 9 | url: https://npm.pkg.github.com 10 | token: "${{secrets.NPM_REGISTRY_NPM_PKG_GITHUB_COM_TOKEN}}" 11 | 12 | updates: 13 | - package-ecosystem: npm 14 | directory: "/" 15 | schedule: 16 | interval: weekly 17 | open-pull-requests-limit: 10 18 | ignore: 19 | - dependency-name: "@babel/core" 20 | versions: 21 | - 7.12.16 22 | - 7.13.1 23 | - 7.13.15 24 | - dependency-name: "@babel/preset-env" 25 | versions: 26 | - 7.12.16 27 | - 7.13.15 28 | - 7.13.9 29 | - dependency-name: eslint-plugin-jest 30 | versions: 31 | - 24.1.8 32 | - dependency-name: husky 33 | versions: 34 | - 5.0.9 35 | - dependency-name: "@preconstruct/cli" 36 | versions: 37 | - 2.0.2 38 | - dependency-name: "@changesets/cli" 39 | versions: 40 | - 2.14.0 41 | registries: 42 | - npm-registry-registry-npmjs-org 43 | - npm-registry-npm-pkg-github-com 44 | -------------------------------------------------------------------------------- /.github/workflows/dd-service-catalog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: DataDog Service Catalog 3 | on: 4 | push: 5 | branches: 6 | - prod 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | permissions: 12 | contents: read 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: DD-service-catalog 16 | uses: arcxp/datadog-service-catalog-metadata-provider@v2 17 | with: 18 | schema-version: v2.1 19 | github-token: ${{ secrets.WP_DOT_GITHUB }} 20 | datadog-key: ${{ secrets.DATADOG_API_KEY }} 21 | datadog-app-key: ${{ secrets.DATADOG_APPLICATION_KEY }} 22 | service-name: arc-feed-components 23 | team: experience-themes-xp 24 | application: Arc Feed Components 25 | description: | 26 | This is the Arc Feed Components 27 | lifecycle: production 28 | tier: p0 29 | email: arc-themes-devs@washpost.com 30 | slack-support-channel: 'https://washpost.enterprise.slack.com/archives/C0169HVBN2C' 31 | repo: https://github.com/WPMedia/feed-components 32 | tags: | 33 | - 'application:Arc Feed Components' 34 | - infrastructure:packages 35 | - language:nodejs 36 | - division:arc 37 | - data-sensitivity:high 38 | - component:themes 39 | links: | 40 | - name: Datadog Dashboard 41 | url: https://washpost.datadoghq.com/dashboard/b47-w8a-u9c/theme-settings-dashboard?view=spans 42 | type: dashboard 43 | - name: Themes Jira Project 44 | url: https://arcpublishing.atlassian.net/jira/software/c/projects/THEMES/boards/838 45 | provider: jira 46 | type: dashboard 47 | - name: Themes Runbooks 48 | url: https://arcpublishing.atlassian.net/wiki/spaces/TI/pages/3303637274/Runbooks 49 | type: runbook 50 | integrations: | 51 | opsgenie: 52 | service-url: https://washpost.app.opsgenie.com/teams/dashboard/6c290e84-4b44-4178-8bec-5fb72fac8239/main 53 | region: US -------------------------------------------------------------------------------- /.github/workflows/pr-verification.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Pull request verification 5 | 6 | on: 7 | push: 8 | branches: [develop, sandbox, prod] 9 | pull_request: 10 | branches: [develop, sandbox, prod] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js 18.x 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 18.x 22 | registry-url: https://npm.pkg.github.com 23 | scope: '@wpmedia' 24 | - run: npm install && npm run ci 25 | env: 26 | CI: true 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-develop.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js 18.x 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18.x 20 | registry-url: https://npm.pkg.github.com 21 | scope: '@wpmedia' 22 | 23 | - name: Install Dependencies 24 | run: npm install 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | CI: true 28 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Build Utils 31 | run: npm run build 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | # Create new branch in order to persist changesets through to prod branch 37 | - name: Create snapshot develop branch 38 | run: git checkout -b develop-tag 39 | 40 | - name: Initialize mandatory git config 41 | run: | 42 | git config user.name "GitHub Actions" 43 | git config user.email noreply@github.com 44 | 45 | - name: Create snaphost release for develop 46 | run: npm run release:snapshotDev 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Publish @canary release to npm 51 | # Calls changeset publish --tag [canary|beta|stable] 52 | run: npm run release:develop 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/publish-prod.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - prod 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js 18.x 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18.x 20 | registry-url: https://npm.pkg.github.com 21 | scope: '@wpmedia' 22 | 23 | - name: Install Dependencies 24 | run: npm install 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | CI: true 28 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Build Utils 31 | run: npm run build 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Create Release Pull Request or Publish to npm 37 | uses: changesets/action@v1 38 | with: 39 | # this runs release: which calls changeset publish --tag [beta|stable] 40 | publish: npm run release:prod 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/publish-sandbox.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - sandbox 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Node.js 18.x 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18.x 20 | registry-url: https://npm.pkg.github.com 21 | scope: '@wpmedia' 22 | 23 | - name: Install Dependencies 24 | run: npm install 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | CI: true 28 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Build Utils 31 | run: npm run build 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | # Create new branch in order to persist changesets through to prod branch 37 | - name: Create snapshot sandbox branch 38 | run: git checkout -b sandbox-tag 39 | 40 | - name: Initialize mandatory git config 41 | run: | 42 | git config user.name "GitHub Actions" 43 | git config user.email noreply@github.com 44 | 45 | - name: Create snaphost release for sandbox 46 | run: npm run release:snapshotSandbox 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Publish @beta release to npm 51 | # Calls changeset publish --tag [beta|stable] 52 | run: npm run release:sandbox 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | **/dist 3 | .DS_Store 4 | 5 | **/.DS_Store 6 | **/.env 7 | **/.npmrc 8 | **/node_modules 9 | .idea 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "arrowParens": "always", 5 | "trailingComma": "all", 6 | "jsxSingleQuote": false 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Feed-Components 2 | 3 | This is the monorepo for the blocks that make up Outbound Feeds (OBF). 4 | 5 | This repo contains the OBF Blocks, utilities and dependencies. It is used by the Arc I/O team to develop OBF. Clients do not need this repo to use OBF or run OBF locally. We have made this repo available so you can see the code and to copy blocks if you would like to create custom feeds. 6 | 7 | The steps to download and build your local OBF repo are [here](https://redirector.arcpublishing.com/alc/arc-products/arcio/user-docs/setup-a-new-outbound-feeds-repo/) 8 | 9 | You can find more information about custom development in [ALC](https://redirector.arcpublishing.com/alc/arc-products/arcio/dev/) 10 | 11 | ## Blocks 12 | 13 | All of the published blocks are located in the blocks directory. Each block mirrors the structure of the component in the fusion repo. For example, a feature block will have a features directory and a source block will have a sources directory. More details can be found [here](https://redirector.arcpublishing.com/alc/arc-products/arcio/user-docs/feature-blocks-architecture/) 14 | 15 | All content source blocks start with feeds-source. If the block has the word output in the name it is an output type. All other blocks are feature blocks. The xml output type can be found in the utils directory. It is not included in the blocks.json, instead it's loaded via the repos package.json. More details can be found [here](https://redirector.arcpublishing.com/alc/arc-products/arcio/user-docs/outbound-feeds-development-content-source/) 16 | 17 | ## Utils 18 | 19 | There are a set of common utilities used in the blocks. These are located in the utils directory. Each is it's own NPM package that the blocks add as dependencies. If you copy a block to your repo, be sure to add any dependencies required by the block to your OBF repo's package.json. More details can be found [here](https://redirector.arcpublishing.com/alc/arc-products/arcio/user-docs/outbound-feeds-development-utilities/) 20 | 21 | ## Custom Development 22 | 23 | If you would like to create a custom block, you can start with one of the existing OBF blocks or create something new. More details can be found [here](https://redirector.arcpublishing.com/alc/arc-products/arcio/user-docs/ejecting-a-block/) 24 | 25 | ## Local Development 26 | preconstruct is used to handle the dependency of packages depending on one another. It's set as postinstall and will likely run. If not follow the below: 27 | After `npm i` 28 | Run `npm run postinstall` 29 | ``` 30 | > postinstall 31 | > preconstruct dev 32 | 33 | 🎁 info project is valid! 34 | 🎁 success created links! 35 | ``` 36 | 37 | That should resolve any missing module/import errors from dependencies in the utils called in blocks. -------------------------------------------------------------------------------- /__mocks__/fusion:consumer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global mock for a fusion:consumer when running 3 | * unit tests of anything using a consumer HOC. 4 | * 5 | * In order to use this mock you must do 6 | * `import Consumer from 'fusion:consumer';` 7 | * at the top of your unit test file, this will 8 | * trigger jest to mock the Consumer import below 9 | * */ 10 | jest.mock('fusion:consumer', (component) => { 11 | return function (component) { 12 | class element extends component { 13 | constructor(props) { 14 | super(props) 15 | this.props = props 16 | } 17 | 18 | addEventListener() {} 19 | 20 | dispatchEvent() {} 21 | 22 | getContent() { 23 | return { 24 | cached: new Promise((resolve) => { 25 | return resolve() 26 | }), 27 | fetched: new Promise((resolve) => { 28 | return resolve() 29 | }), 30 | } 31 | } 32 | 33 | removeEventListener() {} 34 | 35 | setContent() {} 36 | } 37 | 38 | return element 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /__mocks__/fusion:content-config.js: -------------------------------------------------------------------------------- 1 | const isRequired = require('./fusion:is-required') 2 | const { taggablePrimitive } = require('./fusion:taggables') 3 | 4 | module.exports = (options, ...moreSchemas) => { 5 | const instance = (props, propName, componentName) => { 6 | const prop = props[propName] 7 | if (prop) { 8 | if (!(props.sourceName || prop.source || prop.contentService)) { 9 | return new Error( 10 | `${propName} is missing property 'contentService' on ${componentName}`, 11 | ) 12 | } 13 | if (!(prop.key || prop.contentConfigValues)) { 14 | return new Error( 15 | `${propName} is missing property 'contentConfigValues' on ${componentName}`, 16 | ) 17 | } 18 | } 19 | } 20 | 21 | instance.isRequired = isRequired(instance) 22 | 23 | const args = !(options instanceof Object) 24 | ? { schemas: [options].concat(...moreSchemas) } 25 | : Array.isArray(options) 26 | ? { schemas: options.concat(...moreSchemas) } 27 | : options 28 | 29 | return taggablePrimitive(instance, 'contentConfig', args) 30 | } 31 | -------------------------------------------------------------------------------- /__mocks__/fusion:custom-types.js: -------------------------------------------------------------------------------- 1 | const PropTypes = require('../node_modules/prop-types') 2 | const { taggable } = require('./fusion:taggables') 3 | 4 | module.exports = { 5 | boolean: taggable(PropTypes.bool, 'boolean'), 6 | contentConfig: require('./fusion:content-config'), 7 | date: taggable(PropTypes.string, 'date'), 8 | dateTime: taggable(PropTypes.string, 'dateTime'), 9 | disabled: taggable(PropTypes.string, 'disabled'), 10 | email: taggable(PropTypes.string, 'email'), 11 | json: require('./fusion:json'), // Taggable(PropTypes.string, 'json'), 12 | kvp: taggable(PropTypes.object, 'kvp'), 13 | label: taggable(PropTypes.string, 'label'), 14 | list: taggable(PropTypes.arrayOf(PropTypes.string), 'list'), 15 | richtext: taggable(PropTypes.string, 'richtext'), 16 | select: taggable(PropTypes.oneOf, 'select'), 17 | text: taggable(PropTypes.string, 'text'), 18 | url: taggable(PropTypes.string, 'url'), 19 | } 20 | -------------------------------------------------------------------------------- /__mocks__/fusion:environment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resizerKey: '12345', 3 | } 4 | -------------------------------------------------------------------------------- /__mocks__/fusion:is-required.js: -------------------------------------------------------------------------------- 1 | module.exports = (instance) => { 2 | return (props, propName, componentName) => { 3 | const prop = props[propName] 4 | if (!prop) { 5 | return new Error(`${propName} is required on ${componentName}`) 6 | } 7 | return instance(props, propName, componentName) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__mocks__/fusion:json.js: -------------------------------------------------------------------------------- 1 | const isRequired = require('./fusion:is-required') 2 | const { taggablePrimitive } = require('./fusion:taggables') 3 | 4 | const json = (props, propName, componentName) => { 5 | const prop = props[propName] 6 | if (prop) { 7 | try { 8 | JSON.parse(prop) 9 | } catch (e) { 10 | return new Error(`${propName} is not valid JSON on ${componentName}`) 11 | } 12 | } 13 | } 14 | 15 | json.isRequired = isRequired(json) 16 | 17 | module.exports = taggablePrimitive(json, 'json') 18 | -------------------------------------------------------------------------------- /__mocks__/fusion:prop-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global mock for a fusion:prop-types. This is required 3 | * when running unit tests for anything that uses the 4 | * .tag() in the prop-types. Instead of importing normal react prop-types 5 | * you will need to instead do: 6 | * 7 | * import PropTypes from 'fusion:prop-types'; 8 | * 9 | * and then in your test file the prop-types will be auto-mocked with 10 | * with the .tag() functionality so you don't get unit test errors 11 | * */ 12 | const PropTypes = require('../node_modules/prop-types') 13 | 14 | const { taggable } = require('./fusion:taggables') 15 | 16 | const isPropTypeSelfRef = (key) => { 17 | return ['PropTypes', 'checkPropTypes'].includes(key) 18 | } 19 | const isPropTypeMethod = (key) => { 20 | return ['isRequired', 'tag'].includes(key) 21 | } 22 | 23 | const ignorePropTypeSelfRefs = (key) => { 24 | return !isPropTypeSelfRef(key) 25 | } 26 | const ignorePropTypeMethods = (key) => { 27 | return !isPropTypeMethod(key) 28 | } 29 | 30 | const FusionPropTypes = Object.assign( 31 | ...Object.keys(PropTypes) 32 | .filter(ignorePropTypeSelfRefs) 33 | .map((key) => { 34 | return { [key]: taggable(PropTypes[key], key) } 35 | }), 36 | require('./fusion:custom-types'), 37 | ) 38 | 39 | // The basic JSON.stringify function ignores functions 40 | // but functions can have properties, just like any other object 41 | // this implementation exposes the properties of functions (while still ignoring their source) 42 | function _stringify(value) { 43 | const exists = (key) => { 44 | return value[key] !== undefined 45 | } 46 | 47 | return Array.isArray(value) 48 | ? `[${value.map(_stringify).join(',')}]` 49 | : value instanceof Object 50 | ? `{${Object.keys(value) 51 | .filter(ignorePropTypeMethods) 52 | .filter(exists) 53 | .map((key) => { 54 | return `"${key}":${_stringify(value[key])}` 55 | }) 56 | .join(',')}}` 57 | : JSON.stringify(value) 58 | } 59 | FusionPropTypes.stringify = function stringify(value, replacer, space) { 60 | const str = _stringify(value) 61 | return str ? JSON.stringify(JSON.parse(str), replacer, space) : str 62 | } 63 | 64 | module.exports = FusionPropTypes 65 | -------------------------------------------------------------------------------- /__mocks__/fusion:properties.js: -------------------------------------------------------------------------------- 1 | export default jest.fn().mockReturnValue({ 2 | resizerURL: 'hi', 3 | feedDomainURL: 'http://demo-prod.origin.arcpublishing.com', 4 | feedTitle: 'google news', 5 | feedLanguage: 'en', 6 | }) 7 | -------------------------------------------------------------------------------- /__mocks__/fusion:taggables.js: -------------------------------------------------------------------------------- 1 | const taggablePrimitive = (propType, typeName, complexArgs, isRequired) => { 2 | const propTypeCopy = (...args) => { 3 | return propType(...args) 4 | } 5 | 6 | const isRequiredName = `${typeName}.isRequired` 7 | 8 | propTypeCopy.type = typeName 9 | propTypeCopy.args = complexArgs 10 | 11 | propTypeCopy.tag = (tags) => { 12 | const instance = (...args) => { 13 | return propTypeCopy(...args) 14 | } 15 | instance.type = typeName 16 | instance.args = complexArgs 17 | instance.tags = tags 18 | if (!isRequired && propType.isRequired) { 19 | instance.isRequired = (...args) => { 20 | return propType.isRequired(...args) 21 | } 22 | instance.isRequired.type = isRequiredName 23 | instance.isRequired.args = complexArgs 24 | instance.isRequired.tags = tags 25 | } 26 | return instance 27 | } 28 | 29 | // In production, propType is just a placeholder function, so make sure we aren't recursing infinitely 30 | if (!isRequired && propType.isRequired) { 31 | propTypeCopy.isRequired = taggablePrimitive( 32 | propType.isRequired, 33 | isRequiredName, 34 | complexArgs, 35 | true, 36 | ) 37 | } 38 | 39 | return propTypeCopy 40 | } 41 | 42 | const taggableComplex = (propType, typeName) => { 43 | return (complexArgs) => { 44 | // We need to make a new function even for complex types, because in production mode, all types share an empty shim 45 | const f = (...args) => { 46 | return propType(complexArgs)(...args) 47 | } 48 | return taggablePrimitive(f, typeName, complexArgs) 49 | } 50 | } 51 | 52 | const taggable = (propType, typeName) => { 53 | return propType && propType.isRequired // (['shim', 'bound checkType'].includes(propType.name)) 54 | ? taggablePrimitive(propType, typeName) 55 | : taggableComplex(propType, typeName) 56 | } 57 | 58 | module.exports = { 59 | taggable, 60 | taggableComplex, 61 | taggablePrimitive, 62 | } 63 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { esmodules: false, node: 'current' }, 7 | }, 8 | ], 9 | '@babel/preset-react', 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /blocks/ans-feature-block/README.md: -------------------------------------------------------------------------------- 1 | # ANS 2 | 3 | Return ANS JSON. returns a list of content, not a results set. 4 | image url's are resized 5 | -------------------------------------------------------------------------------- /blocks/ans-feature-block/features/ans/__snapshots__/json.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ANS undefined test 1`] = ` 4 | [ 5 | {}, 6 | ] 7 | `; 8 | 9 | exports[`returns ANS for story 1`] = ` 10 | [ 11 | { 12 | "_id": "ABCD1234", 13 | "content_elements": [ 14 | { 15 | "content": "body goes here", 16 | "type": "text", 17 | }, 18 | { 19 | "content": "second paragraph goes here", 20 | "type": "text", 21 | }, 22 | ], 23 | "last_updated_date": "2020-04-07T15:02:08.918Z", 24 | "promo_items": { 25 | "basic": { 26 | "title": "Hand Washing", 27 | "url": "hi/abcdefghijklmnopqrstuvwxyz=/arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png", 28 | }, 29 | }, 30 | "type": "story", 31 | "website_url": "/food/2020/04/07/tips-for-safe-hand-washing", 32 | }, 33 | ] 34 | `; 35 | 36 | exports[`returns ANS from navigation set 1`] = ` 37 | [ 38 | [ 39 | { 40 | "_id": "/channels", 41 | "_website": "demo", 42 | "children": [ 43 | { 44 | "_id": "/channels/ms-teams", 45 | "_website": "demo", 46 | "children": [], 47 | "inactive": false, 48 | "name": "MS Teams", 49 | "node_type": "section", 50 | }, 51 | { 52 | "_id": "/channels/slack", 53 | "_website": "demo", 54 | "children": [], 55 | "inactive": false, 56 | "name": "Slack", 57 | "node_type": "section", 58 | }, 59 | { 60 | "_id": "/channels/twitter", 61 | "_website": "demo", 62 | "children": [], 63 | "inactive": false, 64 | "name": "Twitter", 65 | "node_type": "section", 66 | }, 67 | ], 68 | "inactive": false, 69 | "name": "Channels", 70 | "node_type": "section", 71 | }, 72 | ], 73 | ] 74 | `; 75 | 76 | exports[`returns ANS from results set 1`] = ` 77 | [ 78 | [ 79 | { 80 | "last_updated_date": "2020-04-07T15:02:08.918Z", 81 | "promo_items": { 82 | "basic": { 83 | "title": "Hand Washing", 84 | "url": "hi/abcdefghijklmnopqrstuvwxyz=/arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png", 85 | }, 86 | }, 87 | "type": "story", 88 | "website_url": "/food/2020/04/07/tips-for-safe-hand-washing", 89 | }, 90 | { 91 | "content_elements": [ 92 | { 93 | "type": "image", 94 | "url": "hi/abcdefghijklmnopqrstuvwxyz=/arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png", 95 | }, 96 | ], 97 | "last_updated_date": "2021-04-07T17:02:08.918Z", 98 | "type": "story", 99 | "website_url": "/food/2021/04/07/will-we-ever-stop-hand-washing", 100 | }, 101 | { 102 | "last_updated_date": "2021-04-03T13:02:08.918Z", 103 | "promo_image": { 104 | "title": "No kneed recipes", 105 | "url": "hi/abcdefghijklmnopqrstuvwxyz=/arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png", 106 | }, 107 | "promo_items": { 108 | "basic": { 109 | "type": "video", 110 | }, 111 | }, 112 | "type": "video", 113 | "website_url": "/food/2021/04/03/best-sourdough-recipes", 114 | }, 115 | ], 116 | ] 117 | `; 118 | -------------------------------------------------------------------------------- /blocks/ans-feature-block/features/ans/json.js: -------------------------------------------------------------------------------- 1 | import Consumer from 'fusion:consumer' 2 | import getProperties from 'fusion:properties' 3 | import PropTypes from 'fusion:prop-types' 4 | import { resizerKey, ENVIRONMENT } from 'fusion:environment' 5 | import { buildResizerURL } from '@wpmedia/feeds-resizer' 6 | 7 | export function ANSFeed({ globalContent = {}, customFields, arcSite }) { 8 | let { resizerURL = '' } = getProperties(arcSite) 9 | const { feedDomainURL = '', resizerURLs = {} } = getProperties(arcSite) 10 | const { width = 0, height = 0 } = customFields.resizerKVP || {} 11 | resizerURL = resizerURLs?.[ENVIRONMENT] || resizerURL 12 | 13 | const resizeImage = (img) => { 14 | if (img && img.url) { 15 | if ( 16 | img.additional_properties && 17 | img.additional_properties.fullSizeResizeUrl 18 | ) { 19 | img.url = `${feedDomainURL}${img.additional_properties.fullSizeResizeUrl}` 20 | } else { 21 | img.url = buildResizerURL( 22 | img.url, 23 | resizerKey, 24 | resizerURL, 25 | width, 26 | height, 27 | img, 28 | ) 29 | } 30 | return img 31 | } 32 | } 33 | 34 | let contentType 35 | let contentMap = [] 36 | if ( 37 | globalContent?.type === 'results' || 38 | globalContent?.type === 'collection' 39 | ) { 40 | contentMap = globalContent.content_elements 41 | } else if (globalContent?.children) { 42 | contentMap = globalContent.children 43 | } else { 44 | contentMap = [globalContent] 45 | contentType = 'item' 46 | } 47 | 48 | const resizedContent = contentMap.map((i) => { 49 | i.promo_items && 50 | Object.keys(i.promo_items).forEach((e) => { 51 | const promo = i.promo_items[e] 52 | const resizedPromo = resizeImage(promo) 53 | if (resizedPromo) i.promo_items[e] = resizedPromo 54 | }) 55 | i.content_elements && 56 | i.content_elements.map((e) => { 57 | switch (e.type) { 58 | case 'image': 59 | e = resizeImage(e) 60 | break 61 | case 'gallery': 62 | e.content_elements.forEach((i) => resizeImage(i)) 63 | break 64 | } 65 | return e 66 | }) 67 | i.related_items && 68 | Object.keys(i.related_content).forEach((k) => { 69 | i.related_content[k].map((e) => { 70 | switch (e.type) { 71 | case 'image': 72 | e = resizeImage(e) 73 | break 74 | case 'gallery': 75 | e.content_elements.forEach((i) => resizeImage(i)) 76 | break 77 | } 78 | return e 79 | }) 80 | }) 81 | i.promo_image && 82 | i.promo_image.url && 83 | (i.promo_image = resizeImage(i.promo_image)) 84 | return i 85 | }) 86 | if (contentType === 'item') return resizedContent || {} 87 | return [resizedContent || []] 88 | } 89 | 90 | ANSFeed.label = 'ANS' 91 | ANSFeed.icon = 'arc-json' 92 | ANSFeed.propTypes = { 93 | customFields: PropTypes.shape({ 94 | resizerKVP: PropTypes.kvp.tag({ 95 | label: 'Image height and or width', 96 | description: 'Height and width to resize all images to', 97 | defaultValue: { width: 0, height: 0 }, 98 | }), 99 | }), 100 | } 101 | 102 | export default Consumer(ANSFeed) 103 | -------------------------------------------------------------------------------- /blocks/ans-feature-block/features/ans/json.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import Consumer from 'fusion:consumer' 3 | import { ANSFeed } from './json' 4 | 5 | it('ANS undefined test', () => { 6 | const ans = ANSFeed({ 7 | arcSite: 'the-globe', 8 | globalContent: undefined, 9 | customFields: {}, 10 | }) 11 | expect(ans).toMatchSnapshot() 12 | }) 13 | 14 | it('returns ANS from results set', () => { 15 | const ans = ANSFeed({ 16 | arcSite: 'the-globe', 17 | globalContent: { 18 | type: 'results', 19 | content_elements: [ 20 | { 21 | type: 'story', 22 | last_updated_date: '2020-04-07T15:02:08.918Z', 23 | website_url: '/food/2020/04/07/tips-for-safe-hand-washing', 24 | promo_items: { 25 | basic: { 26 | title: 'Hand Washing', 27 | url: 'https://arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png', 28 | }, 29 | }, 30 | }, 31 | { 32 | type: 'story', 33 | last_updated_date: '2021-04-07T17:02:08.918Z', 34 | website_url: '/food/2021/04/07/will-we-ever-stop-hand-washing', 35 | content_elements: [ 36 | { 37 | type: 'image', 38 | url: 'https://arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png', 39 | }, 40 | ], 41 | }, 42 | { 43 | type: 'video', 44 | last_updated_date: '2021-04-03T13:02:08.918Z', 45 | website_url: '/food/2021/04/03/best-sourdough-recipes', 46 | promo_items: { basic: { type: 'video' } }, 47 | promo_image: { 48 | title: 'No kneed recipes', 49 | url: 'https://arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png', 50 | }, 51 | }, 52 | ], 53 | }, 54 | customFields: {}, 55 | }) 56 | expect(ans).toMatchSnapshot() 57 | }) 58 | 59 | it('returns ANS from navigation set', () => { 60 | const ans = ANSFeed({ 61 | arcSite: 'the-globe', 62 | globalContent: { 63 | _id: '/', 64 | _website: 'demo', 65 | name: 'Demo', 66 | inactive: false, 67 | node_type: 'section', 68 | parent: null, 69 | ancestors: null, 70 | order: null, 71 | children: [ 72 | { 73 | _id: '/channels', 74 | name: 'Channels', 75 | _website: 'demo', 76 | inactive: false, 77 | node_type: 'section', 78 | children: [ 79 | { 80 | _id: '/channels/ms-teams', 81 | name: 'MS Teams', 82 | _website: 'demo', 83 | inactive: false, 84 | node_type: 'section', 85 | children: [], 86 | }, 87 | { 88 | _id: '/channels/slack', 89 | name: 'Slack', 90 | _website: 'demo', 91 | inactive: false, 92 | node_type: 'section', 93 | children: [], 94 | }, 95 | { 96 | _id: '/channels/twitter', 97 | name: 'Twitter', 98 | _website: 'demo', 99 | inactive: false, 100 | node_type: 'section', 101 | children: [], 102 | }, 103 | ], 104 | }, 105 | ], 106 | }, 107 | customFields: {}, 108 | }) 109 | expect(ans).toMatchSnapshot() 110 | }) 111 | 112 | it('returns ANS for story', () => { 113 | const ans = ANSFeed({ 114 | arcSite: 'the-globe', 115 | globalContent: { 116 | _id: 'ABCD1234', 117 | type: 'story', 118 | last_updated_date: '2020-04-07T15:02:08.918Z', 119 | website_url: '/food/2020/04/07/tips-for-safe-hand-washing', 120 | content_elements: [ 121 | { type: 'text', content: 'body goes here' }, 122 | { type: 'text', content: 'second paragraph goes here' }, 123 | ], 124 | promo_items: { 125 | basic: { 126 | title: 'Hand Washing', 127 | url: 'https://arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png', 128 | }, 129 | }, 130 | }, 131 | customFields: {}, 132 | }) 133 | expect(ans).toMatchSnapshot() 134 | }) 135 | -------------------------------------------------------------------------------- /blocks/ans-feature-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/ans-feature-block", 3 | "version": "2.0.2", 4 | "description": "Fusion components for building ANS feeds", 5 | "main": "index.js", 6 | "files": [ 7 | "features" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/ans-feature-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Michelle Mark", 20 | "contributors": [ 21 | "Tim Kosmider " 22 | ], 23 | "homepage": "https://github.com/WPMedia/feed-components#readme", 24 | "scripts": { 25 | "build": "" 26 | }, 27 | "dependencies": { 28 | "@wpmedia/feeds-resizer": "2.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blocks/feeds-source-collections-block/README.md: -------------------------------------------------------------------------------- 1 | # Collections content source 2 | 3 | Searches for a collection by \_id or alias. Articles in a collection do not contain any content_elements. 4 | 5 | ## Globals 6 | 7 | - CONTENT_BASE 8 | 9 | ## Parameters 10 | 11 | - \_id: Collection \_id to search for 12 | - content_alias: Collection alias name to search for 13 | - from: number of rows to skip 14 | - size: number of rows to return 15 | 16 | ### Usage 17 | 18 | The `/content/v4/collections` content-api endpoint is used 19 | -------------------------------------------------------------------------------- /blocks/feeds-source-collections-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/feeds-source-collections-block", 3 | "version": "2.0.1", 4 | "description": "Content source to search collections by _id or alias", 5 | "main": "index.js", 6 | "files": [ 7 | "sources" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/feeds-source-collections-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "arc-io-dev@washpost.com", 20 | "contributors": [ 21 | "Cameron Woodmansee ", 22 | "Iyob Beyene ", 23 | "Tim Kosmider " 24 | ], 25 | "homepage": "https://github.com/WPMedia/feed-components#readme", 26 | "scripts": { 27 | "build": "" 28 | }, 29 | "dependencies": { 30 | "@wpmedia/feeds-content-source-utils": "1.0.8", 31 | "@wpmedia/feeds-resizer": "2.0.1", 32 | "axios": "^1.6.7" 33 | }, 34 | "devDependencies": { 35 | "prop-types": "^15.7.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /blocks/feeds-source-collections-block/sources/collections.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { 4 | ARC_ACCESS_TOKEN, 5 | CONTENT_BASE, 6 | RESIZER_TOKEN_VERSION, 7 | resizerKey, 8 | } from 'fusion:environment' 9 | 10 | import { signImagesInANSObject, resizerFetch } from '@wpmedia/feeds-resizer' 11 | import { defaultANSFields } from '@wpmedia/feeds-content-source-utils' 12 | 13 | const sortStories = (idsResp, collectionResp, ids, site) => { 14 | idsResp.content_elements.forEach((item) => { 15 | const storyIndex = ids.indexOf(item._id) 16 | // transform websites to sections 17 | if (item?.websites?.[site]?.website_section && !item?.taxonomy?.sections) { 18 | if (!item.taxonomy) item.taxonomy = {} 19 | item.taxonomy.sections = [item.websites[site].website_section] 20 | } 21 | if (item?.websites?.[site]?.website_url) 22 | item.website_url = item.websites[site].website_url 23 | item.website = site 24 | collectionResp.content_elements.splice(storyIndex, 1, item) 25 | }) 26 | return collectionResp 27 | } 28 | 29 | const fetch = async (key, { cachedCall }) => { 30 | const { 31 | 'arc-site': site, 32 | _id, 33 | content_alias: contentAlias, 34 | from = 0, 35 | size = 20, 36 | includeFields, 37 | excludeFields, 38 | } = key 39 | 40 | const collectionsQuery = new URLSearchParams({ 41 | website: site, 42 | from, 43 | size, 44 | published: true, 45 | ...(_id && { _id: _id.replace(/\//g, '') }), 46 | ...(contentAlias && { content_alias: contentAlias.replace(/\/$/, '') }), 47 | }) 48 | 49 | const options = { 50 | headers: { 51 | 'content-type': 'application/json', 52 | Authorization: `Bearer ${ARC_ACCESS_TOKEN}`, 53 | }, 54 | method: 'GET', 55 | } 56 | 57 | const collectionResp = await axios({ 58 | url: `${CONTENT_BASE}/content/v4/collections?${collectionsQuery.toString()}`, 59 | ...options, 60 | }) 61 | .then((result) => { 62 | if (resizerKey) { 63 | return result 64 | } 65 | return signImagesInANSObject( 66 | cachedCall, 67 | resizerFetch, 68 | RESIZER_TOKEN_VERSION, 69 | )(result) 70 | }) 71 | .then(({ data }) => data) 72 | .catch((error) => console.log('== error ==', error)) 73 | 74 | const ids = await collectionResp.content_elements.map((item) => { 75 | return item._id 76 | }) 77 | 78 | const ansFields = [ 79 | ...defaultANSFields, 80 | 'content_elements', 81 | `websites.${site}`, 82 | ] 83 | 84 | if (excludeFields) { 85 | excludeFields.split(',').forEach((i) => { 86 | if (i && ansFields.indexOf(i) !== -1) { 87 | ansFields.splice(ansFields.indexOf(i), 1) 88 | } 89 | }) 90 | } 91 | 92 | if (includeFields) { 93 | includeFields 94 | .split(',') 95 | .forEach((i) => i && !ansFields.includes(i) && ansFields.push(i)) 96 | } 97 | // If excluding content_elements, don't call the IDS endpoint 98 | if (!ansFields.includes('content_elements') || ids.length === 0) 99 | return collectionResp 100 | 101 | const idsQuery = new URLSearchParams({ 102 | ids: ids.join(','), 103 | website: site, 104 | included_fields: ansFields.join(','), 105 | }) 106 | const idsResp = await axios({ 107 | url: `${CONTENT_BASE}/content/v4/ids?${idsQuery.toString()}`, 108 | ...options, 109 | }) 110 | .then(({ data }) => data) 111 | .catch((error) => console.log('== error ==', error)) 112 | return await sortStories(idsResp, collectionResp, ids, site) 113 | } 114 | 115 | export default { 116 | fetch, 117 | params: [ 118 | { 119 | name: '_id', 120 | displayName: 'Collection ID', 121 | type: 'text', 122 | }, 123 | { 124 | name: 'content_alias', 125 | displayName: 'Collection Alias (Only populate ID or Alias)', 126 | type: 'text', 127 | }, 128 | { 129 | name: 'from', 130 | displayName: 'From - Integer offset to start from', 131 | type: 'number', 132 | }, 133 | { 134 | name: 'size', 135 | displayName: 'Number of records to return, Integer 1 - 20', 136 | type: 'number', 137 | }, 138 | { 139 | name: 'includeFields', 140 | displayName: 'ANS Fields to include, use commas between fields', 141 | type: 'text', 142 | }, 143 | { 144 | name: 'excludeFields', 145 | displayName: 'ANS Fields to Exclude, use commas between fields', 146 | type: 'text', 147 | }, 148 | ], 149 | ttl: 300, 150 | } 151 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-block/README.md: -------------------------------------------------------------------------------- 1 | # Content-API content source 2 | 3 | Creates an ElasticSearch DSL syntax to query Content-API 4 | 5 | ## Globals 6 | 7 | - CONTENT_BASE 8 | - feedDefaultQuery 9 | 10 | ## Parameters 11 | 12 | - Section: Comma separated list of sections, maps to `taxonomy.sections._id` 13 | - Author: Maps to `credits.by._id` 14 | - Keywords: Maps to taxonomy.seo_keywords. It can be a comma separated list of values 15 | - Tags-Text: Maps to `taxonomy.tags.text` and `variations.variants.content.taxonomy.tags.text` applying an `OR` logic. It can be a comma separated list of values. 16 | - Tags-Slug: Maps to `taxonomy.tags.slug` and `variations.variants.content.taxonomy.tags.slug` applying an `OR` logic. It can be a comma separated list of values. 17 | - Include-Terms: If you don’t want to use the default query you can enter a query here. It must be an array formatted like `[{"term":{"type": "story"}},{"range":{"last_updated_date:{"gte":"now-2d","lte":"now"}}}]` 18 | - Exclude-Terms: If you need to exclude terms in your query (NOT) enter them here as an array formatted the same as the Include-Terms 19 | - Exclude-Sections: Comma separated list of sections to exclude, maps to `taxonomy.sections._id` 20 | - Feed-Size: Integer 1 to 100. Defaults to 100 21 | - Feed-Offset: Integer. Defaults to 0 22 | - Sort: Comma separated list of fields to sort on. Defaults to `publish_date:desc` 23 | - Source-Exclude: ANS fields to remove from `_sourceIncludes` default values 24 | - Source-Include: ANS fields to add to `_sourceIncludes` default values 25 | - Sitemap-at-root: (string) if set replaces all '-' with '/' in Section field 26 | - Include-Distributor-Name: pass to C-API only one distributor field can be set 27 | - Exclude-Distributor-Name: pass to C-API only one distributor field can be set 28 | - Include-Distributor-Category: pass to C-API only one distributor field can be set 29 | - Exclude-Distributor-Category: pass to C-API only one distributor field can be set 30 | 31 | ### Usage 32 | 33 | Default query: `[{"term":{"type": "story"}},{"range":{"last_updated_date":{"gte":"now-2d","lte":"now"}}}]` 34 | You can create a feedDefaultQuery in blocks.json to override the default query. 35 | If no value in passed in Include-Terms the default query will be used. 36 | 37 | The `/content/v4/search/published` content-api is used 38 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/feeds-source-content-api-block", 3 | "version": "2.0.1", 4 | "description": "Fusion components for building sitemaps", 5 | "main": "index.js", 6 | "files": [ 7 | "sources" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/feeds-source-content-api-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Cameron Woodmansee", 20 | "contributors": [ 21 | "Iyob Beyene ", 22 | "Tim Kosmider " 23 | ], 24 | "homepage": "https://github.com/WPMedia/feed-components#readme", 25 | "scripts": { 26 | "build": "" 27 | }, 28 | "dependencies": { 29 | "@wpmedia/feeds-content-source-utils": "1.0.8", 30 | "@wpmedia/feeds-resizer": "2.0.1", 31 | "axios": "^1.6.7" 32 | }, 33 | "devDependencies": { 34 | "prop-types": "^15.7.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-by-day-block/README.md: -------------------------------------------------------------------------------- 1 | # Content-API By Day content source 2 | 3 | Creates an ElasticSearch DSL syntax to query Content-API limited to a single day. 4 | This is for sitemap/YYYY-MM-DD formats. 5 | 6 | ## Globals 7 | 8 | - CONTENT_BASE 9 | 10 | ## Parameters 11 | 12 | - dateField: ANS date field to use in range statement 13 | - dateRange: date to use in YYYY-MM-DD format or 'latest' for today. latest is used instead of generating today's date because of the chance that the process making the request (fusion) is in a different tz then the ES cluster. 14 | - Include-Terms: If you don’t want to use the default query you can enter a query here. It must be an array formatted like `[{"term":{"type": "story"}},{"range":{"last_updated_date:{"gte":"now-2d","lte":"now"}}}]` 15 | - Exclude-Terms: If you need to exclude terms in your query (NOT) enter them here as an array formatted the same as the Include-Terms 16 | - Feed-Size: Integer 1 to 100. Defaults to 100 17 | - Feed-Offset: Integer. Defaults to 0 18 | - Source-Exclude: ANS fields to exclude from the response. Defaults to `related_content` 19 | - Sort: Comma separated list of fields to sort on. Defaults to `publish_date:desc` 20 | - Include-Distributor-Name: distributor name, only the first populated distributor field will be used 21 | - Exclude-Distributor-Name: distributor name, only the first populated distributor field will be used 22 | - Include-Distributor-Category: distributor category, only the first populated distributor field will be used 23 | - Exclude-Distributor-Category: distributor category, only the first populated distributor field will be used 24 | 25 | ### Usage 26 | 27 | Default query: [{"term":{"type": "story"}}] 28 | range is passed from dateField and dateRange. 29 | latest: 30 | {range: {gte: YYYY-MM-DD, lte: now}} YYYY-MM-DD is calculated from now - 1 31 | YYYY-MM-DD: 32 | {range: {gte: YYYY-MM-DD, lte: YYYY-MM-DD}} 33 | 34 | If no value in passed in Include-Terms the default query will be used. 35 | 36 | The `/content/v4/search/published` content-api is used 37 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-by-day-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/feeds-source-content-api-by-day-block", 3 | "version": "2.0.1", 4 | "description": "content source to search by a single day, for sitemap/YYYY-MM-DD", 5 | "main": "index.js", 6 | "files": [ 7 | "sources" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/feeds-source-content-api-by-day-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Tim Kosmider ", 20 | "homepage": "https://github.com/WPMedia/feed-components#readme", 21 | "scripts": { 22 | "build": "" 23 | }, 24 | "dependencies": { 25 | "@wpmedia/feeds-content-source-utils": "1.0.8", 26 | "@wpmedia/feeds-resizer": "2.0.1", 27 | "axios": "^1.6.7", 28 | "moment": "^2.29.4" 29 | }, 30 | "devDependencies": { 31 | "prop-types": "^15.7.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-by-day2-block/README.md: -------------------------------------------------------------------------------- 1 | # Content-API By Day2 content source 2 | 3 | Creates an ElasticSearch DSL syntax to query Content-API limited to a single day. This is 4 | for sitemap/YYYY-MM-DD formats with 1 hour TTL (3600). 5 | 6 | ## Globals 7 | 8 | - CONTENT_BASE 9 | 10 | ## Parameters 11 | 12 | - dateField: ANS date field to use in range statement 13 | - dateRange: date to use in YYYY-MM-DD format or 'latest' for today. latest is used instead of generating today's date because of the chance that the process making the request (fusion) is in a different tz then the ES cluster. 14 | - Include-Terms: If you don’t want to use the default query you can enter a query here. It must be an array formatted like `[{"term":{"type": "story"}},{"range":{"last_updated_date:{"gte":"now-2d","lte":"now"}}}]` 15 | - Exclude-Terms: If you need to exclude terms in your query (NOT) enter them here as an array formatted the same as the Include-Terms 16 | - Feed-Size: Integer 1 to 100. Defaults to 100 17 | - Feed-Offset: Integer. Defaults to 0 18 | - Source-Exclude: ANS fields to exclude from the response. Defaults to `related_content` 19 | - Sort: Comma separated list of fields to sort on. Defaults to `publish_date:desc` 20 | - Include-Distributor-Name: distributor name, only the first populated distributor field will be used 21 | - Exclude-Distributor-Name: distributor name, only the first populated distributor field will be used 22 | - Include-Distributor-Category: distributor category, only the first populated distributor field will be used 23 | - Exclude-Distributor-Category: distributor category, only the first populated distributor field will be used 24 | 25 | ### Usage 26 | 27 | Default query: [{"term":{"type": "story"}}] 28 | range is passed from dateField and dateRange. 29 | latest: 30 | {range: {gte: YYYY-MM-DD, lte: now}} YYYY-MM-DD is calculated from now - 1 31 | YYYY-MM-DD: 32 | {range: {gte: YYYY-MM-DD, lte: YYYY-MM-DD}} 33 | 34 | If no value in passed in Include-Terms the default query will be used. 35 | 36 | The `/content/v4/search/published` content-api is used 37 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-by-day2-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/feeds-source-content-api-by-day2-block", 3 | "version": "2.0.1", 4 | "description": "content source to search by a single day, 1 hour TTL", 5 | "main": "index.js", 6 | "files": [ 7 | "sources" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/feeds-source-content-api-by-day2-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Tim Kosmider ", 20 | "homepage": "https://github.com/WPMedia/feed-components#readme", 21 | "scripts": { 22 | "build": "" 23 | }, 24 | "dependencies": { 25 | "@wpmedia/feeds-content-source-utils": "1.0.8", 26 | "@wpmedia/feeds-resizer": "2.0.1", 27 | "axios": "^1.6.7", 28 | "moment": "^2.29.4" 29 | }, 30 | "devDependencies": { 31 | "prop-types": "^15.7.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-by-day3-block/README.md: -------------------------------------------------------------------------------- 1 | # Content-API By Day3 content source 2 | 3 | Creates an ElasticSearch DSL syntax to query Content-API limited to a single day. This is 4 | for sitemap/YYYY-MM-DD formats with a 24 hour TTL. 5 | 6 | ## Globals 7 | 8 | - CONTENT_BASE 9 | 10 | ## Parameters 11 | 12 | - dateField: ANS date field to use in range statement 13 | - dateRange: date to use in YYYY-MM-DD format or 'latest' for today. latest is used instead of generating today's date because of the chance that the process making the request (fusion) is in a different tz then the ES cluster. 14 | - Include-Terms: If you don’t want to use the default query you can enter a query here. It must be an array formatted like `[{"term":{"type": "story"}},{"range":{"last_updated_date:{"gte":"now-2d","lte":"now"}}}]` 15 | - Exclude-Terms: If you need to exclude terms in your query (NOT) enter them here as an array formatted the same as the Include-Terms 16 | - Feed-Size: Integer 1 to 100. Defaults to 100 17 | - Feed-Offset: Integer. Defaults to 0 18 | - Source-Exclude: ANS fields to exclude from the response. Defaults to `related_content` 19 | - Sort: Comma separated list of fields to sort on. Defaults to `publish_date:desc` 20 | - Include-Distributor-Name: distributor name, only the first populated distributor field will be used 21 | - Exclude-Distributor-Name: distributor name, only the first populated distributor field will be used 22 | - Include-Distributor-Category: distributor category, only the first populated distributor field will be used 23 | - Exclude-Distributor-Category: distributor category, only the first populated distributor field will be used 24 | 25 | ### Usage 26 | 27 | Default query: [{"term":{"type": "story"}}] 28 | range is passed from dateField and dateRange. 29 | latest: 30 | {range: {gte: YYYY-MM-DD, lte: now}} YYYY-MM-DD is calculated from now - 1 31 | YYYY-MM-DD: 32 | {range: {gte: YYYY-MM-DD, lte: YYYY-MM-DD}} 33 | 34 | If no value in passed in Include-Terms the default query will be used. 35 | 36 | The `/content/v4/search/published` content-api is used 37 | -------------------------------------------------------------------------------- /blocks/feeds-source-content-api-by-day3-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/feeds-source-content-api-by-day3-block", 3 | "version": "2.0.1", 4 | "description": "content source to search by a single day, for sitemap/YYYY-MM-DD with 24 hour (86400) ttl.", 5 | "main": "index.js", 6 | "files": [ 7 | "sources" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/feeds-source-content-api-by-day3-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Tim Kosmider ", 20 | "homepage": "https://github.com/WPMedia/feed-components#readme", 21 | "scripts": { 22 | "build": "" 23 | }, 24 | "dependencies": { 25 | "@wpmedia/feeds-content-source-utils": "1.0.8", 26 | "@wpmedia/feeds-resizer": "2.0.1", 27 | "axios": "^1.6.7", 28 | "moment": "^2.29.4" 29 | }, 30 | "devDependencies": { 31 | "prop-types": "^15.7.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blocks/feeds-source-single-content-block/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @wpmedia/feeds-source-single-content-block 2 | 3 | ## 2.0.1 4 | 5 | ### Patch Changes 6 | 7 | - [#711](https://github.com/WPMedia/feed-components/pull/711) [`9c233c4`](https://github.com/WPMedia/feed-components/commit/9c233c4c7cc360008918141b5c51b434736bba04) Thanks [@nschubach](https://github.com/nschubach)! - Pin dependencies 8 | 9 | - Updated dependencies [[`9c233c4`](https://github.com/WPMedia/feed-components/commit/9c233c4c7cc360008918141b5c51b434736bba04), [`6138434`](https://github.com/WPMedia/feed-components/commit/613843452b6e5b5b25c6b095c5794e6d8fb704d5)]: 10 | - @wpmedia/feeds-resizer@2.0.1 11 | 12 | ## 2.0.0 13 | 14 | ### Major Changes 15 | 16 | - [#665](https://github.com/WPMedia/feed-components/pull/665) [`c39ce40`](https://github.com/WPMedia/feed-components/commit/c39ce40bc95a95755bff01b4616a170e69572995) Thanks [@nschubach](https://github.com/nschubach)! - Adds resizer v2 support. 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies [[`c39ce40`](https://github.com/WPMedia/feed-components/commit/c39ce40bc95a95755bff01b4616a170e69572995)]: 21 | - @wpmedia/feeds-resizer@2.0.0 22 | 23 | ## 1.15.0 24 | 25 | ### Minor Changes 26 | 27 | - [#658](https://github.com/WPMedia/feed-components/pull/658) [`a5039bd`](https://github.com/WPMedia/feed-components/commit/a5039bd9ec2f74f876a3fdf0718e0fbd6c5c05b8) Thanks [@vgalatro](https://github.com/vgalatro)! - OBF 1.15 release 28 | 29 | ### Patch Changes 30 | 31 | - [#660](https://github.com/WPMedia/feed-components/pull/660) [`ca229d0`](https://github.com/WPMedia/feed-components/commit/ca229d0826e865a1ce682812918a2c46980367df) Thanks [@vgalatro](https://github.com/vgalatro)! - OBF 1.15.1 to sandbox 32 | 33 | ## 1.14.0 34 | 35 | ### Minor Changes 36 | 37 | - [#629](https://github.com/WPMedia/feed-components/pull/629) [`6ea29e5`](https://github.com/WPMedia/feed-components/commit/6ea29e5324f5489407badfe280d15fe5b9fc50a2) Thanks [@tbrick855](https://github.com/tbrick855)! - OBF-1.14 38 | 39 | ## 1.13.0 40 | 41 | ### Minor Changes 42 | 43 | - [#622](https://github.com/WPMedia/feed-components/pull/622) [`3bd7b3a`](https://github.com/WPMedia/feed-components/commit/3bd7b3a0a8ae15b96fab6574062c96b5ca0af6f7) Thanks [@tbrick855](https://github.com/tbrick855)! - OBF-1.13 44 | 45 | ## 1.12.0 46 | 47 | ### Minor Changes 48 | 49 | - [#601](https://github.com/WPMedia/feed-components/pull/601) [`317a2c1`](https://github.com/WPMedia/feed-components/commit/317a2c125a07699e3ff616d651c712ca8005dc48) Thanks [@tbrick855](https://github.com/tbrick855)! - OBF-1.12 50 | 51 | ## 1.11.0 52 | 53 | ### Minor Changes 54 | 55 | - [#593](https://github.com/WPMedia/feed-components/pull/593) [`ffb80f9`](https://github.com/WPMedia/feed-components/commit/ffb80f9cbf48ca3835f7fa90af79699796f67d07) Thanks [@emilynielson](https://github.com/emilynielson)! - OBF 1.11 56 | 57 | ### Patch Changes 58 | 59 | - [#579](https://github.com/WPMedia/feed-components/pull/579) [`8294db1`](https://github.com/WPMedia/feed-components/commit/8294db117e13ad7ce146153cc28b2c16416a46d1) Thanks [@tbrick855](https://github.com/tbrick855)! - Add single-content source and modified collections for mobile SDK 60 | -------------------------------------------------------------------------------- /blocks/feeds-source-single-content-block/README.md: -------------------------------------------------------------------------------- 1 | # Single Content content source 2 | 3 | Get a single piece of content by `_id` or `website_url` 4 | 5 | -------------------------------------------------------------------------------- /blocks/feeds-source-single-content-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/feeds-source-single-content-block", 3 | "version": "2.0.1", 4 | "description": "content source to get by _id or website_url", 5 | "main": "index.js", 6 | "files": [ 7 | "sources" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/feeds-source-single-content-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Cameron Woodmansee", 20 | "contributors": [ 21 | "Iyob Beyene ", 22 | "Tim Kosmider " 23 | ], 24 | "homepage": "https://github.com/WPMedia/feed-components#readme", 25 | "scripts": { 26 | "build": "" 27 | }, 28 | "dependencies": { 29 | "@wpmedia/feeds-resizer": "2.0.1", 30 | "axios": "^1.6.7" 31 | }, 32 | "devDependencies": { 33 | "prop-types": "^15.7.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /blocks/feeds-source-single-content-block/sources/single-content.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { 4 | ARC_ACCESS_TOKEN, 5 | CONTENT_BASE, 6 | RESIZER_TOKEN_VERSION, 7 | resizerKey, 8 | } from 'fusion:environment' 9 | 10 | import { signImagesInANSObject, resizerFetch } from '@wpmedia/feeds-resizer' 11 | 12 | const params = { 13 | _id: 'text', 14 | website_url: 'text', 15 | } 16 | 17 | /* eslint-disable dot-notation */ 18 | const fetch = (key, { cachedCall }) => { 19 | const urlSearch = new URLSearchParams({ 20 | ...(key['_id'] ? { _id: key['_id'] } : { website_url: key['website_url'] }), 21 | ...(key['arc-site'] ? { website: key['arc-site'] } : {}), 22 | }) 23 | /* eslint-enable dot-notation */ 24 | const ret = axios({ 25 | url: `${CONTENT_BASE}/content/v4/?${urlSearch.toString()}`, 26 | headers: { 27 | 'content-type': 'application/json', 28 | Authorization: `Bearer ${ARC_ACCESS_TOKEN}`, 29 | }, 30 | method: 'GET', 31 | }) 32 | .then((result) => { 33 | if (resizerKey) { 34 | return result 35 | } 36 | return signImagesInANSObject( 37 | cachedCall, 38 | resizerFetch, 39 | RESIZER_TOKEN_VERSION, 40 | )(result) 41 | }) 42 | .then(({ data }) => data) 43 | .catch((error) => console.log('== error ==', error)) 44 | 45 | return ret 46 | } 47 | 48 | export default { 49 | fetch, 50 | params, 51 | schemaName: 'ans-item', 52 | } 53 | -------------------------------------------------------------------------------- /blocks/feeds-source-video-api-block/README.md: -------------------------------------------------------------------------------- 1 | #feeds-video-api 2 | 3 | ## parameters 4 | 5 | - Uuids: a comma separated list of uuids to lookup 6 | - Playlist: a playlist name 7 | - Count: Number of videos to return, only used with playlist, defaults to 10 8 | 9 | ## endpoints 10 | 11 | The Video API endpoints used are: 12 | 13 | ### /api/v1/ansvideos/findByUuids 14 | 15 | Returns videos with the given UUIDs parameters or an empty list. 16 | A sample query: video-api.demo.arcpublishing.com/api/v1/ansvideos/findByUuids?uuids=db9862d6-be50-11e7-9294-705f80164f6e&uuids=4594b2c0-6cc1-11e7-abbc-a53480672286 17 | 18 | This returns an array, not an obect. The feed block might need to be modified to handle it. Only mrss currently supports an array. 19 | 20 | ### /api/v1/ans/playlists/findByPlaylist 21 | 22 | Returns videos from a playlist up to count (10) 23 | A sample query: video-api.demo.arcpublishing.com/api/v1/ans/playlists/findByPlaylist?name=animals 24 | 25 | This returns an object with the videos in playlistItems. The feed block might meed to be modified to handle it. Only mrss currently supports playlistItems. 26 | -------------------------------------------------------------------------------- /blocks/feeds-source-video-api-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/feeds-source-video-api-block", 3 | "version": "2.0.1", 4 | "description": "Fusion components for building sitemaps", 5 | "main": "index.js", 6 | "files": [ 7 | "sources" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/feeds-source-video-api-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "arc-io-dev@washpost.com", 20 | "homepage": "https://github.com/WPMedia/feed-components#readme", 21 | "scripts": { 22 | "build": "" 23 | }, 24 | "dependencies": { 25 | "@wpmedia/feeds-resizer": "2.0.1", 26 | "axios": "^1.6.7" 27 | }, 28 | "devDependencies": { 29 | "prop-types": "^15.7.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blocks/feeds-source-video-api-block/sources/feeds-video-api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Leave CONTENT_BASE here. Without it fusion will not add a bearer token 3 | import axios from 'axios' 4 | 5 | import { 6 | ARC_ACCESS_TOKEN, 7 | CONTENT_BASE, 8 | VIDEO_BASE, 9 | RESIZER_TOKEN_VERSION, 10 | resizerKey, 11 | } from 'fusion:environment' 12 | 13 | import { signImagesInANSObject, resizerFetch } from '@wpmedia/feeds-resizer' 14 | 15 | const params = { 16 | Uuids: 'text', 17 | Playlist: 'text', 18 | Count: 'text', 19 | } 20 | 21 | const fetch = (key, { cachedCall }) => { 22 | let requestUri, uriParams 23 | if (key) { 24 | if (key.Uuids) { 25 | requestUri = `${VIDEO_BASE}/api/v1/ansvideos/findByUuids` 26 | uriParams = 'uuids=' + key.Uuids.split(',').join('&uuids=') 27 | } else { 28 | requestUri = `${VIDEO_BASE}/api/v1/ans/playlists/findByPlaylist` 29 | uriParams = `name=${key.Playlist}&count=${key.Count || 10}` 30 | } 31 | } 32 | const url = `${requestUri}?${uriParams}` 33 | 34 | const ret = axios({ 35 | url, 36 | headers: { 37 | 'content-type': 'application/json', 38 | Authorization: `Bearer ${ARC_ACCESS_TOKEN}`, 39 | }, 40 | method: 'GET', 41 | }) 42 | .then((result) => { 43 | if (resizerKey) { 44 | return result 45 | } 46 | return signImagesInANSObject( 47 | cachedCall, 48 | resizerFetch, 49 | RESIZER_TOKEN_VERSION, 50 | )(result) 51 | }) 52 | .then(({ data }) => data) 53 | .catch((error) => console.log('== error ==', error)) 54 | 55 | return ret 56 | } 57 | 58 | export default { 59 | fetch, 60 | schemaName: 'feeds', 61 | params, 62 | } 63 | -------------------------------------------------------------------------------- /blocks/feeds-source-video-api-block/sources/feeds-video-api.test.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | // eslint-disable-next-line no-unused-vars 3 | import Consumer from 'fusion:consumer' 4 | const source = require('./feeds-video-api') 5 | 6 | // Mock Axios 7 | jest.mock('axios') 8 | 9 | beforeEach(() => { 10 | // Reset Axios mocks before each test 11 | axios.mockClear() 12 | }) 13 | 14 | it('validate schemaName', () => { 15 | expect(source.default.schemaName).toBe('feeds') 16 | }) 17 | 18 | it('returns query with default values', async () => { 19 | const mockData = { data: 'response' } 20 | axios.mockResolvedValue(mockData) 21 | 22 | await source.default.fetch( 23 | { 24 | Uuids: 25 | 'db9862d6-be50-11e7-9294-705f80164f6e,4594b2c0-6cc1-11e7-abbc-a53480672286', 26 | }, 27 | { cachedCall: {} }, 28 | ) 29 | 30 | expect(axios).toHaveBeenCalledTimes(1) 31 | expect(axios).toHaveBeenCalledWith({ 32 | url: expect.stringContaining(`/api/v1/ansvideos/findByUuids`), // Check base URL 33 | method: 'GET', 34 | headers: expect.objectContaining({ 35 | 'content-type': 'application/json', 36 | Authorization: expect.stringContaining('Bearer '), 37 | }), 38 | }) 39 | 40 | const callUrl = axios.mock.calls[0][0].url 41 | expect(callUrl).toBe( 42 | 'undefined/api/v1/ansvideos/findByUuids?uuids=db9862d6-be50-11e7-9294-705f80164f6e&uuids=4594b2c0-6cc1-11e7-abbc-a53480672286', 43 | ) 44 | }) 45 | 46 | it('returns query with default values', async () => { 47 | const mockData = { data: 'response' } 48 | axios.mockResolvedValue(mockData) 49 | 50 | await source.default.fetch( 51 | { 52 | Playlist: 'playlist5', 53 | Count: '10', 54 | }, 55 | { cachedCall: {} }, 56 | ) 57 | 58 | const callUrl = axios.mock.calls[0][0].url 59 | expect(callUrl).toBe( 60 | 'undefined/api/v1/ans/playlists/findByPlaylist?name=playlist5&count=10', 61 | ) 62 | }) 63 | -------------------------------------------------------------------------------- /blocks/json-output-block/README.md: -------------------------------------------------------------------------------- 1 | # JSON Output Type 2 | 3 | Used to generate json output 4 | -------------------------------------------------------------------------------- /blocks/json-output-block/output-types/json.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line react/prop-types 2 | const Json = ({ children }) => { 3 | return Array.isArray(children) ? children[0] : null 4 | } 5 | 6 | Json.contentType = 'application/json' 7 | export default Json 8 | -------------------------------------------------------------------------------- /blocks/json-output-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/json-output-block", 3 | "version": "1.15.0", 4 | "description": "JSON Output Type", 5 | "main": "index.js", 6 | "files": [ 7 | "output-types" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/json-output-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Michelle Mark", 20 | "contributors": [ 21 | "Tim Kosmider " 22 | ], 23 | "homepage": "https://github.com/WPMedia/feed-components#readme", 24 | "scripts": { 25 | "build": "" 26 | }, 27 | "dependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /blocks/mrss-feature-block/README.md: -------------------------------------------------------------------------------- 1 | # MRSS 2 | 3 | Intended for videos. Uses media:content tag with additions of 4 | 5 | 6 | 7 | 8 | ## Globals 9 | 10 | feedTitle 11 | feedLanguage 12 | feedDomainURL 13 | feedResizer 14 | 15 | ## Custom Fields 16 | 17 | channelTitle: defaults to global website name 18 | channelDescription: defaults to global website name + "News Feed" 19 | channelLanguage: defaults to feedLanguage, use Exclude to remove field 20 | channelCopyright: defaults to Copyright YYYY global website name 21 | channelTTL: number of mins, defaults to 1 22 | channelUpdatePeriod: update period hours, days, weeks, months, years. Defaults to hours 23 | channelUpdateFrequency: Number, defaults to 1 24 | channelCategory: optional 25 | channelLogo: Should be a url to their logo, optional 26 | 27 | itemTitle: jmespath for title mapping headlines.basic 28 | itemDescription: jmespath for description mapping description.basic 29 | pubDate: date field defaults to display_date 30 | itemCategory: jmespath for category (eg. taxonomy.primary_section.name), defaults to no category 31 | includeContent: number of paragraphs to include 0-10, all 32 | 33 | promoItemsJmespath: Hard coded to \* since all content is assumed to be videos 34 | resizerKVP: key value pair of width and or height to use with the resizer 35 | imageTitle: defaults to title 36 | imageCaption: defaults to caption 37 | ImageCredits: defaults to credits.by[].name 38 | 39 | selectVideo: This criteria is used to filter videos encoded in the streams array, defaults to 40 | `{ bitrate: 5400, stream_type: 'mp4' }` 41 | 42 | ### Usage 43 | -------------------------------------------------------------------------------- /blocks/mrss-feature-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/mrss-feature-block", 3 | "version": "2.0.2", 4 | "description": "Fusion components for building rss", 5 | "main": "index.js", 6 | "files": [ 7 | "features" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/rss-feature-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Saru Kalva ", 20 | "contributors": [ 21 | "Tim Kosmider " 22 | ], 23 | "homepage": "https://github.com/WPMedia/feed-components#readme", 24 | "scripts": { 25 | "build": "" 26 | }, 27 | "dependencies": { 28 | "@wpmedia/feeds-promo-items": "2.0.1", 29 | "@wpmedia/feeds-prop-types": "2.0.0", 30 | "@wpmedia/feeds-resizer": "2.0.1", 31 | "jmespath": "^0.15.0", 32 | "moment": "^2.29.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /blocks/rss-alexa-feature-block/README.md: -------------------------------------------------------------------------------- 1 | # RSS Alexa 2 | 3 | Either text content that Alexa reads to the customer or audio content that Alexa plays to the customer. 4 | 5 | ## Globals 6 | 7 | feedTitle 8 | feedLanguage 9 | feedDomainURL 10 | feedResizer 11 | 12 | ## Custom Fields 13 | 14 | enclosure: defaults to mp3 type. 15 | 16 | channelTitle: defaults to global website name 17 | channelDescription: defaults to global website name + "News Feed" 18 | channelLanguage: defaults to feedLanguage, use Exclude to remove field 19 | channelCopyright: defaults to Copyright YYYY global website name 20 | channelTTL: number of mins, defaults to 1 21 | channelCategory: optional 22 | channelLogo: Should be a url to their logo, optional 23 | 24 | itemTitle: jmespath for title mapping headlines.basic 25 | itemCategory: jmespath for category mapping headlines.basic 26 | includeContent: number of paragraphs to include 0-10, all 27 | 28 | pubDate: date field defaults to display_date 29 | 30 | ### Usage 31 | -------------------------------------------------------------------------------- /blocks/rss-alexa-feature-block/features/rss/__snapshots__/xml.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`returns Alexa template with custom values 1`] = ` 4 | { 5 | "rss": { 6 | "@version": "2.0", 7 | "channel": { 8 | "category": "news", 9 | "copyright": "2020 The Washington Post LLC", 10 | "description": "All the news that's fit to print", 11 | "image": { 12 | "link": "http://demo-prod.origin.arcpublishing.com", 13 | "title": "The Daily Prophet", 14 | "url": "hi/abcdefghijklmnopqrstuvwxyz=/arc-anglerfish-arc2-prod-demo.s3.amazonaws.com/public/JTWX7EUOLJE4FCHYGN2COQAERY.png", 15 | }, 16 | "item": [ 17 | { 18 | "category": "coronvirus", 19 | "description": "try singing the happy birthday songbe sure to wash your thumbs", 20 | "enclosure": { 21 | "@type": "audio/mp3", 22 | "@url": "https://clark.com/wp-content/uploads/2021/01/Ask-Clark_GameStop_2021.mp3?_=4", 23 | }, 24 | "guid": undefined, 25 | "link": "http://demo-prod.origin.arcpublishing.com/food/2020/04/07/tips-for-safe-hand-washing", 26 | "pubDate": "2020-04-07T15:02:08.918Z", 27 | "title": "Tips for Safe Hand washing", 28 | }, 29 | { 30 | "description": "", 31 | "guid": "ABCD", 32 | "link": "http://demo-prod.origin.arcpublishing.com", 33 | "pubDate": "2020-04-07T15:02:08.918Z", 34 | "title": "", 35 | }, 36 | ], 37 | "language": "es", 38 | "lastBuildDate": StringMatching /\\\\w\\+, \\\\d\\+ \\\\w\\+ \\\\d\\{4\\} \\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\} \\\\\\+0000/, 39 | "link": "http://demo-prod.origin.arcpublishing.com", 40 | "title": "The Daily Prophet", 41 | "ttl": "60", 42 | }, 43 | }, 44 | } 45 | `; 46 | 47 | exports[`returns Alexa template with default values 1`] = ` 48 | { 49 | "rss": { 50 | "@version": "2.0", 51 | "channel": { 52 | "item": [ 53 | { 54 | "description": "", 55 | "enclosure": { 56 | "@type": "audio/mp3", 57 | "@url": "https://clark.com/wp-content/uploads/2021/01/Ask-Clark_GameStop_2021.mp3?_=4", 58 | }, 59 | "guid": undefined, 60 | "link": "http://demo-prod.origin.arcpublishing.com/food/2020/04/07/tips-for-safe-hand-washing", 61 | "pubDate": "2020-04-07T15:02:08.918Z", 62 | "title": "Tips for Safe Hand washing", 63 | }, 64 | { 65 | "description": "", 66 | "guid": "ABCD", 67 | "link": "http://demo-prod.origin.arcpublishing.com", 68 | "pubDate": "2020-04-07T15:02:08.918Z", 69 | "title": "", 70 | }, 71 | ], 72 | "language": "en", 73 | "lastBuildDate": StringMatching /\\\\w\\+, \\\\d\\+ \\\\w\\+ \\\\d\\{4\\} \\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\} \\\\\\+0000/, 74 | "link": "http://demo-prod.origin.arcpublishing.com", 75 | "ttl": "1", 76 | }, 77 | }, 78 | } 79 | `; 80 | 81 | exports[`returns Alexa template with empty values 1`] = ` 82 | { 83 | "rss": { 84 | "@version": "2.0", 85 | "channel": { 86 | "item": [ 87 | { 88 | "description": "try singing the happy birthday songbe sure to wash your thumbs", 89 | "guid": undefined, 90 | "link": "http://demo-prod.origin.arcpublishing.com/food/2020/04/07/tips-for-safe-hand-washing", 91 | "pubDate": "2020-04-07T15:02:08.918Z", 92 | }, 93 | { 94 | "description": "", 95 | "guid": "ABCD", 96 | "link": "http://demo-prod.origin.arcpublishing.com", 97 | "pubDate": "2020-04-07T15:02:08.918Z", 98 | }, 99 | ], 100 | "lastBuildDate": StringMatching /\\\\w\\+, \\\\d\\+ \\\\w\\+ \\\\d\\{4\\} \\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\} \\\\\\+0000/, 101 | "link": "http://demo-prod.origin.arcpublishing.com", 102 | }, 103 | }, 104 | } 105 | `; 106 | -------------------------------------------------------------------------------- /blocks/rss-alexa-feature-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wpmedia/rss-alexa-feature-block", 3 | "version": "2.0.2", 4 | "description": "Fusion components for building rss alexa feed", 5 | "main": "index.js", 6 | "files": [ 7 | "features" 8 | ], 9 | "license": "CC-BY-NC-ND-4.0", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/WPMedia/feed-components.git", 13 | "directory": "blocks/rss-alexa-feature-block" 14 | }, 15 | "publishConfig": { 16 | "registry": "https://npm.pkg.github.com/", 17 | "access": "public" 18 | }, 19 | "author": "Malavika Koppula ", 20 | "contributors": [ 21 | "Tim Kosminder ", 22 | "Cameron Woodmansee " 23 | ], 24 | "homepage": "https://github.com/WPMedia/feed-components#readme", 25 | "scripts": { 26 | "build": "" 27 | }, 28 | "dependencies": { 29 | "@wpmedia/feeds-content-elements": "2.0.1", 30 | "@wpmedia/feeds-prop-types": "2.0.0", 31 | "@wpmedia/feeds-resizer": "2.0.1", 32 | "cheerio": "1.0.0-rc.10", 33 | "jmespath": "^0.15.0", 34 | "moment": "^2.29.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blocks/rss-fbia-feature-block/README.md: -------------------------------------------------------------------------------- 1 | # Facebook Instant Articles 2 | 3 | ## Globals 4 | 5 | feedTitle 6 | feedLanguage 7 | feedDomainURL 8 | feedResizer 9 | 10 | ## Custom Fields 11 | 12 | channelTitle: defaults to global website name 13 | channelDescription: defaults to global website name + "News Feed" 14 | channelLanguage: defaults to feedLanguage, use Exclude to remove field 15 | channelCopyright: defaults to Copyright YYYY global website name 16 | channelTTL: number of mins, defaults to 1 17 | channelUpdatePeriod: update period hours, days, weeks, months, years. Defaults to hours 18 | channelUpdateFrequency: Number, defaults to 1 19 | channelCategory: optional 20 | channelLogo: Should be a url to their logo, optional 21 | 22 | itemTitle: jmespath for title mapping headlines.basic 23 | itemDescription: jmespath for description mapping description.basic 24 | pubDate: date field defaults to display_date 25 | itemCategory: jmespath for category mapping headlines.basic 26 | includeContent: number of paragraphs to include 0-10, all 27 | 28 | includePromo: bool to include promo image 29 | promoItemsJmespath: jmespath to promo_items (promo_items.basic || promo_items.lead_art) 30 | resizerKVP: key value pair of width and or height to use with the resizer 31 | imageTitle: defaults to title 32 | imageCaption: defaults to caption 33 | ImageCredits: defaults to credits.by[].name 34 | 35 | articleStyle: This parameter is optional and your default style is applied to this article if you do not specify an article style in your markup 36 | likesAndComments: Enable or disable, defaults to disable 37 | adPlacement: Enables automatic placement of ads within this article. This parameter is optional and defaults to false if you do not specify 38 | adDensity: How frequently you would like ads to appear in your article: default (<250 word gap), medium (350 word gap), low (>450 word gap) 39 | placementSection: Enter Javascript that goes between
in beginning of the body\'s header for recirculation ads that come from Facebook advertisers; leave blank if not used., 40 | adScripts: Enter third party scripts wrapped in a
tag. It will be added to the end of the article body. Multiple scripts can be included, usually each in its own iframe. If you need to reference data from the ANS content, use place holders in the format of <> like <> 41 | `
` 42 | iframeHxW: Height and/or width to use in oembed iframes 43 | raw_html_processing: should raw_html be excluded, included or wrapped in