├── .all-contributorsrc
├── .gitattributes
├── .github
└── workflows
│ ├── main.yml
│ └── size.yml
├── .gitignore
├── .vscode
└── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── docs
└── example.jpeg
├── other
├── MAINTAINING.md
├── USERS.md
└── manual-releases.md
├── package-lock.json
├── package.json
├── src
├── __tests__
│ └── index.test.ts
├── defaultRender.ts
├── encodeHtml.ts
├── index.ts
└── openGraphNinja.ts
└── tsconfig.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "transformer-open-graph",
3 | "projectOwner": "donavon",
4 | "imageSize": 100,
5 | "commit": false,
6 | "commitConvention": "none",
7 | "contributorsPerLine": 7,
8 | "repoHost": "https://github.com",
9 | "repoType": "github",
10 | "skipCi": false,
11 | "files": [
12 | "README.md"
13 | ],
14 | "contributors": [
15 | {
16 | "login": "donavon",
17 | "name": "Donavon West",
18 | "avatar_url": "https://avatars.githubusercontent.com/u/887639?v=4",
19 | "profile": "https://donavon.com",
20 | "contributions": [
21 | "code",
22 | "doc",
23 | "infra",
24 | "test"
25 | ]
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 | jobs:
4 | build:
5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
6 |
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | node: ['12.x', '14.x', '16.x']
11 | os: [ubuntu-latest, windows-latest, macOS-latest]
12 |
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v2
16 |
17 | - name: Use Node ${{ matrix.node }}
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: ${{ matrix.node }}
21 |
22 | - name: Install deps and build (with cache)
23 | uses: bahmutov/npm-install@v1
24 |
25 | - name: Lint
26 | run: yarn lint
27 |
28 | - name: Test
29 | run: yarn test --ci --coverage --maxWorkers=2
30 |
31 | - name: Build
32 | run: yarn build
33 |
--------------------------------------------------------------------------------
/.github/workflows/size.yml:
--------------------------------------------------------------------------------
1 | name: size
2 | on: [pull_request]
3 | jobs:
4 | size:
5 | runs-on: ubuntu-latest
6 | env:
7 | CI_JOB_NUMBER: 1
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: andresz1/size-limit-action@v1
11 | with:
12 | github_token: ${{ secrets.GITHUB_TOKEN }}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | .DS_Store
5 | .env
6 | .env.local
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | The changelog is automatically updated using
4 | [semantic-release](https://github.com/semantic-release/semantic-release). You
5 | can see it on the [releases page](../../releases).
6 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | coc@donavon.com. All complaints will be reviewed and investigated promptly
64 | and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series of
86 | actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or permanent
93 | ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within the
113 | community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by
122 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2022 Donavon West
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # transformer-open-graph
2 |
3 | A transformer for [`@remark-embedder/core`](https://github.com/remark-embedder/core) that extracts Open Graph metadata from a URL and create an HTML preview.
4 |
5 | - Works with [`@remark-embedder/core`](https://github.com/remark-embedder/core)
6 | - Supports Twitter Cards and Open Graph
7 | - Powered by [Open Graph Ninja](https://opengraph.ninja/)'s API
8 | - 100% test coverage
9 | - Written in TypeScript and fully typed
10 | - Awesome! 🎉
11 |
12 | Want to see an example? Check out [https://donavon.com/blog/markdown#social-links](https://donavon.com/blog/markdown#social-links)
13 |
14 | ## Usage
15 |
16 | ```ts
17 | import remarkEmbedder from '@remark-embedder/core';
18 | import transformerOpenGraph from 'transformer-open-graph';
19 | import remark from 'remark';
20 | import html from 'remark-html';
21 |
22 | const exampleMarkdown = `
23 | This is my website:
24 |
25 | https://donavon.com
26 | `;
27 |
28 | async function go() {
29 | const result = await remark()
30 | .use(remarkEmbedder, {
31 | transformers: [transformerOpenGraph],
32 | })
33 | .use(html)
34 | .process(exampleMarkdown);
35 |
36 | console.log(result.toString());
37 | // logs:
38 | //
This is my website:
39 | //
49 | }
50 |
51 | go();
52 | ```
53 |
54 | ## Config options
55 |
56 | You can pass the following config options
57 |
58 | | Options name | description |
59 | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
60 | | `render` | A custom render function that accepts one argument, `data` of type [`OpenGraphNinja`](./src/openGraphNinja.ts), and returns an HTML string. |
61 |
62 | ### Example custom render
63 |
64 | ```ts
65 | const exampleMarkdown = `
66 | This is my website:
67 |
68 | https://donavon.com
69 | `;
70 |
71 | const customRender: OpenGraphRender = (data) =>
72 | `Hello ${data.hostname}
`;
73 |
74 | const config: OpenGraphConfig = { render: customRender };
75 |
76 | const result = await remark()
77 | .use(remarkEmbedder, {
78 | transformers: [[transformerOpenGraph, config]],
79 | })
80 | .use(html)
81 | .process(exampleMarkdown);
82 | // This is my website:
83 | // Hello donavon.com
84 | ```
85 |
86 | ## Use with other transformers
87 |
88 | If you are using other @remark-embedder transformers (e.g. [`@remark-embedder/transformer-oembed`](https://github.com/remark-embedder/transformer-oembed)), place `transformer-open-graph` last in the `transformers` array.
89 |
90 | For example.
91 |
92 | ```ts
93 | import remarkEmbedder, { RemarkEmbedderOptions } from '@remark-embedder/core';
94 | import oembedTransformer from '@remark-embedder/transformer-oembed';
95 | import transformerOpenGraph from 'transformer-open-graph';
96 |
97 | const remarkEmbedderOptions: RemarkEmbedderOptions = {
98 | transformers: [oembedTransformer, transformerOpenGraph],
99 | };
100 |
101 | const result = await remark()
102 | .use(remarkEmbedder, remarkEmbedderOptions)
103 | .use(html)
104 | .process(exampleMarkdown);
105 | ```
106 |
107 | ## Example output
108 |
109 | You'll need to B.Y.O. CSS, but you can get beautiful web page preview output like this.
110 |
111 | 
112 |
113 | ## Getting Started
114 |
115 | Install the library with your package manager of choice, e.g.:
116 |
117 | ```
118 | $ npm i transformer-open-graph
119 | # or
120 | $ yarn add transformer-open-graph
121 | ```
122 |
123 | ## License
124 |
125 | © 2022 Donavon West. Released under [MIT license](./LICENSE).
126 |
--------------------------------------------------------------------------------
/docs/example.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donavon/transformer-open-graph/f753e42b71f16e5bc881533674b702d6f03ad164/docs/example.jpeg
--------------------------------------------------------------------------------
/other/MAINTAINING.md:
--------------------------------------------------------------------------------
1 | # Maintaining
2 |
3 |
4 |
5 |
6 | **Table of Contents**
7 |
8 | - [Code of Conduct](#code-of-conduct)
9 | - [Issues](#issues)
10 | - [Pull Requests](#pull-requests)
11 | - [Release](#release)
12 | - [Thanks!](#thanks)
13 |
14 |
15 |
16 | This is documentation for maintainers of this project.
17 |
18 | ## Code of Conduct
19 |
20 | Please review, understand, and be an example of it. Violations of the code of
21 | conduct are taken seriously, even (especially) for maintainers.
22 |
23 | ## Issues
24 |
25 | We want to support and build the community. We do that best by helping people
26 | learn to solve their own problems. We have an issue template and hopefully most
27 | folks follow it. If it's not clear what the issue is, invite them to create a
28 | minimal reproduction of what they're trying to accomplish or the bug they think
29 | they've found.
30 |
31 | Once it's determined that a code change is necessary, point people to
32 | [makeapullrequest.com](https://makeapullrequest.com) and invite them to make a
33 | pull request. If they're the one who needs the feature, they're the one who can
34 | build it. If they need some hand holding and you have time to lend a hand,
35 | please do so. It's an investment into another human being, and an investment
36 | into a potential maintainer.
37 |
38 | Remember that this is open source, so the code is not yours, it's ours. If
39 | someone needs a change in the codebase, you don't have to make it happen
40 | yourself. Commit as much time to the project as you want/need to. Nobody can ask
41 | any more of you than that.
42 |
43 | ## Pull Requests
44 |
45 | As a maintainer, you're fine to make your branches on the main repo or on your
46 | own fork. Either way is fine.
47 |
48 | When we receive a pull request, a GitHub Action is kicked off automatically (see
49 | the `.github/workflows/validate.yml` for what runs in the Action). We avoid
50 | merging anything that breaks the GitHub Action.
51 |
52 | Please review PRs and focus on the code rather than the individual. You never
53 | know when this is someone's first ever PR and we want their experience to be as
54 | positive as possible, so be uplifting and constructive.
55 |
56 | When you merge the pull request, 99% of the time you should use the
57 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/)
58 | feature. This keeps our git history clean, but more importantly, this allows us
59 | to make any necessary changes to the commit message so we release what we want
60 | to release. See the next section on Releases for more about that.
61 |
62 | ## Release
63 |
64 | Our releases are automatic. They happen whenever code lands into `main`. A
65 | GitHub Action gets kicked off and if it's successful, a tool called
66 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is
67 | used to automatically publish a new release to npm as well as a changelog to
68 | GitHub. It is only able to determine the version and whether a release is
69 | necessary by the git commit messages. With this in mind, **please brush up on
70 | [the commit message convention][commit] which drives our releases.**
71 |
72 | > One important note about this: Please make sure that commit messages do NOT
73 | > contain the words "BREAKING CHANGE" in them unless we want to push a major
74 | > version. I've been burned by this more than once where someone will include
75 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not
76 | > a huge deal honestly, but kind of annoying...
77 |
78 | ## Thanks!
79 |
80 | Thank you so much for helping to maintain this project!
81 |
82 |
83 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md
84 |
85 |
--------------------------------------------------------------------------------
/other/USERS.md:
--------------------------------------------------------------------------------
1 | # Users
2 |
3 | If you or your company uses this project, add your name to this list! Eventually
4 | we may have a website to showcase these (wanna build it!?)
5 |
6 | > No users have been added yet!
7 |
8 |
13 |
--------------------------------------------------------------------------------
/other/manual-releases.md:
--------------------------------------------------------------------------------
1 | # manual-releases
2 |
3 | This project has an automated release set up. So things are only released when
4 | there are useful changes in the code that justify a release. But sometimes
5 | things get messed up one way or another and we need to trigger the release
6 | ourselves. When this happens, simply bump the number below and commit that with
7 | the following commit message based on your needs:
8 |
9 | **Major**
10 |
11 | ```
12 | fix(release): manually release a major version
13 |
14 | There was an issue with a major release, so this manual-releases.md
15 | change is to release a new major version.
16 |
17 | Reference: #
18 |
19 | BREAKING CHANGE:
20 | ```
21 |
22 | **Minor**
23 |
24 | ```
25 | feat(release): manually release a minor version
26 |
27 | There was an issue with a minor release, so this manual-releases.md
28 | change is to release a new minor version.
29 |
30 | Reference: #
31 | ```
32 |
33 | **Patch**
34 |
35 | ```
36 | fix(release): manually release a patch version
37 |
38 | There was an issue with a patch release, so this manual-releases.md
39 | change is to release a new patch version.
40 |
41 | Reference: #
42 | ```
43 |
44 | The number of times we've had to do a manual release is: 0
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transformer-open-graph",
3 | "version": "0.3.0",
4 | "publishConfig": {
5 | "access": "public"
6 | },
7 | "description": "@remark-embedder transformer for URLs that support Open Graph",
8 | "main": "dist/index.js",
9 | "types": "dist/index.d.ts",
10 | "keywords": [
11 | "remark",
12 | "remark-plugin",
13 | "remark-embedder",
14 | "transformer",
15 | "open-graph",
16 | "open graph"
17 | ],
18 | "author": "Donavon West (https://donavon.com)",
19 | "license": "MIT",
20 | "engines": {
21 | "node": ">=12",
22 | "npm": ">=8"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/donavon/transformer-open-graph"
27 | },
28 | "bugs": {
29 | "url": "https://github.com/donavon/transformer-open-graph/issues"
30 | },
31 | "homepage": "https://github.com/donavon/transformer-open-graph#readme",
32 | "files": [
33 | "dist"
34 | ],
35 | "scripts": {
36 | "build": "kcd-scripts build",
37 | "lint": "kcd-scripts lint",
38 | "setup": "npm install && npm run validate -s",
39 | "test": "kcd-scripts test",
40 | "test:update": "npm test -- --updateSnapshot --coverage",
41 | "coverage": "npm test -- --coverage",
42 | "typecheck": "kcd-scripts typecheck",
43 | "validate": "kcd-scripts validate"
44 | },
45 | "dependencies": {
46 | "@babel/runtime": "^7.17.8",
47 | "make-fetch-happen": "^10.1.1"
48 | },
49 | "devDependencies": {
50 | "@remark-embedder/core": "^2.0.0",
51 | "@types/make-fetch-happen": "^9.0.2",
52 | "@types/jest": "^27.4.1",
53 | "kcd-scripts": "^12.1.0",
54 | "msw": "^0.39.2",
55 | "remark": "^13.0.0",
56 | "remark-html": "^13.0.2",
57 | "typescript": "^4.6.3"
58 | },
59 | "peerDependencies": {
60 | "@remark-embedder/core": "^2.0.0"
61 | },
62 | "eslintConfig": {
63 | "extends": "./node_modules/kcd-scripts/eslint.js",
64 | "rules": {
65 | "babel/camelcase": "off"
66 | }
67 | },
68 | "eslintIgnore": [
69 | "node_modules",
70 | "coverage",
71 | "dist"
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | import remarkEmbedder from '@remark-embedder/core';
2 | import { rest } from 'msw';
3 | import { setupServer } from 'msw/node';
4 | import remark from 'remark';
5 | import remarkHTML from 'remark-html';
6 | import transformer, { OpenGraphConfig, OpenGraphRender } from '..';
7 | import { OpenGraphNinja, openGraphNinjaUrl } from '../openGraphNinja';
8 |
9 | const testJson: OpenGraphNinja = {
10 | hostname: 'donavon.com',
11 | requestUrl: 'https://donavon.com',
12 | title: 'TITLE',
13 | description: 'DESCRIPTION',
14 | image: {
15 | url: 'https://donavon.com/img/donavon-avatar.jpeg',
16 | alt: 'ALT',
17 | },
18 | details: {
19 | author: 'Donavon West',
20 | ogTitle: "Donavon West's website",
21 | ogDescription:
22 | 'Donavon West is a full-stack software engineer living in the New York City area.',
23 | ogUrl: 'https://donavon.com/',
24 | ogLocale: 'en_US',
25 | ogType: 'website',
26 | ogSiteName: "Donavon West's website",
27 | twitterCard: 'summary',
28 | twitterTitle: "Donavon West's website",
29 | twitterDescription:
30 | 'Donavon West is a full-stack software engineer living in the New York City area.',
31 | twitterSite: '@donavon',
32 | twitterCreator: '@donavon',
33 | },
34 | };
35 |
36 | // this removes the quotes around strings...
37 | const unquoteSerializer = {
38 | serialize: (val: string) => val.trim(),
39 | test: (val: unknown) => typeof val === 'string',
40 | };
41 |
42 | expect.addSnapshotSerializer(unquoteSerializer);
43 |
44 | const server = setupServer(
45 | rest.get(openGraphNinjaUrl, (req, res, ctx) => {
46 | const url = req.url.searchParams.get('url');
47 |
48 | if (url === 'https://example.com/') {
49 | return res(ctx.json(testJson));
50 | }
51 | if (url === 'https://example.com/noimage') {
52 | // deep clone the object
53 | const testWithoutImage = JSON.parse(
54 | JSON.stringify(testJson)
55 | ) as OpenGraphNinja;
56 | delete testWithoutImage.image;
57 | return res(ctx.json(testWithoutImage));
58 | }
59 | if (url === 'https://example.com/noimage-alt') {
60 | // deep clone the object
61 | const testWithoutImage = JSON.parse(
62 | JSON.stringify(testJson)
63 | ) as OpenGraphNinja;
64 | delete testWithoutImage.image?.alt;
65 | return res(ctx.json(testWithoutImage));
66 | }
67 | if (url === 'https://example.com/noimage-alt-title') {
68 | // deep clone the object
69 | const testWithoutImage = JSON.parse(
70 | JSON.stringify(testJson)
71 | ) as OpenGraphNinja;
72 | delete testWithoutImage.image?.alt;
73 | delete testWithoutImage.title;
74 | return res(ctx.json(testWithoutImage));
75 | }
76 | if (url === 'https://example.com/nodetails') {
77 | // deep clone the object
78 | const testWithoutImage = JSON.parse(
79 | JSON.stringify(testJson)
80 | ) as OpenGraphNinja;
81 | delete testWithoutImage.details;
82 | return res(ctx.json(testWithoutImage));
83 | }
84 | if (url === 'https://example.com/nodescription') {
85 | // deep clone the object
86 | const testWithoutImage = JSON.parse(
87 | JSON.stringify(testJson)
88 | ) as OpenGraphNinja;
89 | delete testWithoutImage.description;
90 | return res(ctx.json(testWithoutImage));
91 | }
92 | if (url === 'https://example.com/boom') {
93 | throw new Error('boom');
94 | }
95 | if (url === 'https://example.com/404') {
96 | return res(ctx.status(404), ctx.text('Not found'));
97 | }
98 |
99 | return res(ctx.json({}));
100 | })
101 | );
102 |
103 | // enable API mocking in test runs using the same request handlers
104 | // as for the client-side mocking.
105 | beforeAll(() => server.listen());
106 | afterAll(() => server.close());
107 | afterEach(() => server.resetHandlers());
108 |
109 | test('renders nothing if the fetch throws', async () => {
110 | const result = await remark()
111 | .use(remarkEmbedder, {
112 | transformers: [transformer],
113 | })
114 | .use(remarkHTML, { sanitize: false })
115 | .process('https://example.com/boom');
116 |
117 | expect(result.toString()).toMatchInlineSnapshot(
118 | `https://example.com/boom
`
119 | );
120 | });
121 |
122 | test('renders nothing if fetch returns a non-2xx status', async () => {
123 | const result = await remark()
124 | .use(remarkEmbedder, {
125 | transformers: [transformer],
126 | })
127 | .use(remarkHTML, { sanitize: false })
128 | .process('https://example.com/404');
129 |
130 | expect(result.toString()).toMatchInlineSnapshot(
131 | `https://example.com/404
`
132 | );
133 | });
134 |
135 | describe('config options', () => {
136 | test('render allows for custom markup', async () => {
137 | const customRender: OpenGraphRender = (data) =>
138 | `Hello ${data.hostname}
`;
139 |
140 | const config: OpenGraphConfig = { render: customRender };
141 | const result = await remark()
142 | .use(remarkEmbedder, {
143 | transformers: [[transformer, config]],
144 | })
145 | .use(remarkHTML, { sanitize: false })
146 | .process('https://example.com');
147 |
148 | console.log(result.toString());
149 | expect(result.toString()).toMatchInlineSnapshot(
150 | `Hello donavon.com
`
151 | );
152 | });
153 | });
154 |
155 | test('renders a website with all Open Graph meta tags', async () => {
156 | const result = await remark()
157 | .use(remarkEmbedder, {
158 | transformers: [transformer],
159 | })
160 | .use(remarkHTML, { sanitize: false })
161 | .process('https://example.com');
162 |
163 | expect(result.toString()).toMatchInlineSnapshot(`
164 |
174 | `);
175 | });
176 |
177 | describe('the default renders using fallback properties if missing', () => {
178 | test('.image', async () => {
179 | const result = await remark()
180 | .use(remarkEmbedder, {
181 | transformers: [transformer],
182 | })
183 | .use(remarkHTML, { sanitize: false })
184 | .process('https://example.com/noimage');
185 |
186 | expect(result.toString()).toMatchInlineSnapshot(`
187 |
197 | `);
198 | });
199 |
200 | test('.image.alt', async () => {
201 | const result = await remark()
202 | .use(remarkEmbedder, {
203 | transformers: [transformer],
204 | })
205 | .use(remarkHTML, { sanitize: false })
206 | .process('https://example.com/noimage-alt');
207 |
208 | expect(result.toString()).toMatchInlineSnapshot(`
209 |
219 | `);
220 | });
221 |
222 | test('.image.alt and .title', async () => {
223 | const result = await remark()
224 | .use(remarkEmbedder, {
225 | transformers: [transformer],
226 | })
227 | .use(remarkHTML, { sanitize: false })
228 | .process('https://example.com/noimage-alt-title');
229 |
230 | expect(result.toString()).toMatchInlineSnapshot(`
231 |
241 | `);
242 | });
243 |
244 | test('.details', async () => {
245 | const result = await remark()
246 | .use(remarkEmbedder, {
247 | transformers: [transformer],
248 | })
249 | .use(remarkHTML, { sanitize: false })
250 | .process('https://example.com/nodetails');
251 |
252 | expect(result.toString()).toMatchInlineSnapshot(`
253 |
263 | `);
264 | });
265 |
266 | test('.description', async () => {
267 | const result = await remark()
268 | .use(remarkEmbedder, {
269 | transformers: [transformer],
270 | })
271 | .use(remarkHTML, { sanitize: false })
272 | .process('https://example.com/nodescription');
273 |
274 | expect(result.toString()).toMatchInlineSnapshot(`
275 |
285 | `);
286 | });
287 | });
288 |
--------------------------------------------------------------------------------
/src/defaultRender.ts:
--------------------------------------------------------------------------------
1 | import { encodeHtml } from './encodeHtml';
2 | import { OpenGraphNinja } from './openGraphNinja';
3 |
4 | const getImageHtml = (data?: OpenGraphNinja) =>
5 | data?.image
6 | ? `
`
9 | : '';
10 |
11 | export const defaultRender = (data: OpenGraphNinja): string =>
12 | `
13 |
29 | `;
30 |
--------------------------------------------------------------------------------
/src/encodeHtml.ts:
--------------------------------------------------------------------------------
1 | export const encodeHtml = (html: string) =>
2 | html
3 | .replace(/&/g, '&')
4 | .replace(//g, '>')
6 | .replace(/"/g, '"')
7 | .replace(/'/g, ''');
8 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import type { Transformer } from '@remark-embedder/core';
3 | import fetch from 'make-fetch-happen';
4 | import { OpenGraphNinja, openGraphNinjaUrl } from './openGraphNinja';
5 | import { defaultRender } from './defaultRender';
6 |
7 | fetch.defaults({
8 | cachePath: path.join(
9 | process.cwd(),
10 | 'node_modules/.cache/donavon/transformer-open-graph/fetch'
11 | ),
12 | });
13 |
14 | async function fetchOpenGraph(url: string): Promise {
15 | try {
16 | const res = await fetch(
17 | `${openGraphNinjaUrl}?url=${encodeURIComponent(url)}`
18 | );
19 | if (!res.ok) return null;
20 |
21 | const data = (await res.json()) as OpenGraphNinja;
22 | return data;
23 | } catch {
24 | return null;
25 | }
26 | }
27 | export type OpenGraphRender = (ogData: OpenGraphNinja) => string;
28 | export type OpenGraphConfig = {
29 | render?: OpenGraphRender;
30 | };
31 |
32 | const transformer: Transformer = {
33 | name: 'donavon/transformer-open-graph',
34 |
35 | shouldTransform: async (url) => {
36 | const result = await fetchOpenGraph(url);
37 | return !!result;
38 | },
39 |
40 | getHTML: async (urlString, config = {}) => {
41 | const data = await fetchOpenGraph(urlString);
42 |
43 | // istanbul ignore if (shouldTransform prevents this, but if someone calls this directly then this would save them)
44 | if (!data) return null;
45 |
46 | const { render = defaultRender } = config;
47 | return render(data);
48 | },
49 | };
50 |
51 | export default transformer;
52 |
--------------------------------------------------------------------------------
/src/openGraphNinja.ts:
--------------------------------------------------------------------------------
1 | export const openGraphNinjaUrl = 'https://opengraph.ninja/api/v1';
2 |
3 | export type OpenGraphNinja = {
4 | hostname: string;
5 | requestUrl: string;
6 | title?: string;
7 | description?: string;
8 | image?: {
9 | url: string;
10 | alt?: string;
11 | };
12 | details?: {
13 | author?: string;
14 | ogUrl?: string;
15 | ogTitle?: string;
16 | ogDescription?: string;
17 | twitterCard?: string;
18 | twitterCreator?: string;
19 | twitterSite?: string;
20 | twitterTitle?: string;
21 | twitterDescription?: string;
22 | ogType?: string;
23 | ogSiteName?: string;
24 | ogImage?: {
25 | url?: string;
26 | width?: string;
27 | height?: string;
28 | type?: string;
29 | };
30 | twitterImage?: {
31 | url?: string;
32 | width?: string;
33 | height?: string;
34 | alt?: string;
35 | };
36 | ogLocale?: string;
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["node_modules"],
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "moduleResolution": "node",
8 | "noEmit": true,
9 | "strict": true,
10 | "jsx": "react",
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "baseUrl": "../../src",
14 | "paths": {
15 | "*": ["*", "../tests/*"]
16 | },
17 | "preserveWatchOutput": true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------