├── .env.example ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── example.md ├── package.json ├── packages ├── webembeds-core │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── modules │ │ │ ├── Platform.ts │ │ │ └── WebembedHandler.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── common.ts │ │ │ ├── graphql.ts │ │ │ ├── html.utils.ts │ │ │ ├── providers │ │ │ ├── codepen.provider.ts │ │ │ ├── expo.provider.ts │ │ │ ├── facebook.provider.ts │ │ │ ├── giphy.provider.ts │ │ │ ├── gist.provider.ts │ │ │ ├── glitch.provider.ts │ │ │ ├── index.ts │ │ │ ├── instagram.provider.ts │ │ │ ├── loom.provider.ts │ │ │ ├── oembed.providers.js │ │ │ ├── opensea.provider.ts │ │ │ ├── snappify.provider.ts │ │ │ ├── tenor.provider.ts │ │ │ └── twitch.provider.ts │ │ │ └── requestHandler.ts │ ├── tsconfig.json │ └── webpack.config.js └── webembeds-website │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── components │ ├── Layout.tsx │ └── Loader.tsx │ ├── next-env.d.ts │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── embed.ts │ │ └── html.ts │ ├── app.css │ ├── index.tsx │ └── test.tsx │ ├── postcss.config.js │ ├── public │ ├── favicons │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png │ └── js │ │ └── buttons.js │ ├── styles │ └── main.css │ ├── tailwind.config.js │ ├── tsconfig.json │ └── types.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | FB_APP_TOKEN= 2 | 3 | HASHNODE_GRAPHQL_URL= 4 | HASHNODE_GRAPHQL_USER_ACCESS_TOKEN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env* 73 | !.env.example 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | .DS_Store 119 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.format.enable": true, 3 | "eslint.debug": true, 4 | "eslint.packageManager": "yarn", 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![webembeds logo](https://user-images.githubusercontent.com/12823517/167348331-92344edd-e253-4bf7-ba1a-b92daae8ab3b.png) 2 | 3 | 4 | # Webembeds 5 | 6 | **(⚠️ Not to be used in production yet)** 7 | 8 | Built and supported by [Hashnode](https://hashnode.com) 9 | 10 | - Checkout demo here https://webembeds.com/demo 11 | 12 | This project is in its very infant stage. There is high scope of improvement here and we have some plans for improvements and stability. We would appreciate any kind of contribution to this project. 13 | 14 | ## Development 15 | - Run `yarn` within the repo 16 | - Run `core:watch` to start the website in development mode 17 | - Run `website:dev` to start the website in development mode (Make sure to rerun when you change @webembeds/core files) 18 | 19 | ## Host it anywhere 20 | - Clone the repo 21 | - Run `yarn` within the repo 22 | - `yarn build-all && yarn website:start` 23 | - Visit http://localhost:3000 24 | 25 | ## Contributing 26 | - We will be working mainly on `development` branch and the `master` branch remains untouched. 27 | - Create feature branches checked out from `development` branch and raise PR against `development` once done. 28 | - Continuous deployment is setup for `development` branch to be deployed to https://staging.webembeds.com and the master branch to https://webembeds.com 29 | 30 | Clone the repo and run 31 | 32 | `yarn` and checkout package.json for all available scripts. 33 | 34 | ## Bugs, Future plans and Improvements 35 | 36 | Please visit [issue section](https://github.com/Hashnode/webembeds/issues) of this repo. 37 | 38 | ### Commit style 39 | We follow conventional commits specs (https://www.conventionalcommits.org/en/v1.0.0/). 40 | Once done you can run `git cz` or `yarn commit` to commit your changes. 41 | -------------------------------------------------------------------------------- /example.md: -------------------------------------------------------------------------------- 1 | ## Markdown embed examples 2 | 3 | %[https://opensea.io/assets/0x1301566b3cb584e550a02d09562041ddc4989b91/28] 4 | 5 | %[https://opensea.io/assets/0x495f947276749ce646f68ac8c248420045cb7b5e/103558253085473991925726424936776054683291300677177700259118885402889773121537] 6 | 7 | %[https://opensea.io/collection/cyberpunk-vol-1] 8 | 9 | %[https://codesandbox.io/s/y2lrywpk21] 10 | 11 | %[https://codepen.io/szs/pen/JhgKC] 12 | 13 | %[https://vimeo.com/336812660] 14 | 15 | %[https://www.loom.com/share/0281766fa2d04bb788eaf19e65135184] 16 | 17 | %[https://anchor.fm/startapodcast/episodes/Whats-your-podcast-about-e17krq/a-a2q3ft] 18 | 19 | %[https://soundcloud.com/hit-jatt/jatt-disde-arjan-dhillon] 20 | 21 | %[https://repl.it/@GirishPatil4/AdvancedRespectfulGigahertz] 22 | %[https://replit.com/@BearGrappler/playground] 23 | 24 | 25 | %[https://runkit.com/runkit/welcome] 26 | 27 | %[https://open.spotify.com/track/3G8o2zm7LaF6eeVuvLlrkJ?si=Sx1sCnhDT6GXqSLIwSLOeQ] 28 | 29 | %[https://gist.github.com/theevilhead/7ac2fbc3cda897ebd87dbe9aeac130d6] 30 | 31 | %[https://www.canva.com/design/DAET1m0_11c/jFBlYrKc8CQCb2boU9KC-A/view] 32 | 33 | %[https://www.youtube.com/watch?v=32I0Qso4sDg] 34 | 35 | %[https://glitch.com/edit/#!/remote-hands] 36 | 37 | %[https://snack.expo.io/@girishhashnode/unnamed-snack] 38 | 39 | %[https://www.twitch.tv/fresh] 40 | 41 | %[https://twitter.com/hashnode/status/1352525138659430400] 42 | 43 | %[https://hashnode.com] 44 | 45 | %[https://www.canva.com/design/DAEWSa9kfIs/view] 46 | 47 | %[https://www.canva.com/design/DAEWRhUKdvg/view] 48 | 49 | %[https://giphy.com/gifs/cbsnews-inauguration-2021-XEMbxm9vl9JIIMcE7M] 50 | 51 | %[https://www.instagram.com/p/CL8vNB_n_I3/] 52 | 53 | %[https://www.facebook.com/barackobama/posts/10158541668386749] 54 | 55 | %[https://fb.watch/4yOE3vHgMr] 56 | 57 | %[https://www.instagram.com/reel/CMgbGuOgo9 58 | 59 | %[https://snappify.com/view/bcc54061-6e8f-44c5-a4f4-1abcad520108] 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webembeds", 3 | "version": "0.0.1", 4 | "author": "Girish Patil ", 5 | "license": "MIT", 6 | "private": true, 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "scripts": { 11 | "website:dev": "yarn workspace @webembeds/website dev", 12 | "website:start": "yarn workspace @webembeds/website start", 13 | "website:build": "yarn workspace @webembeds/website build", 14 | "core:watch": "yarn workspace @webembeds/core watch", 15 | "core:build": "yarn workspace @webembeds/core build", 16 | "build-all": "yarn core:build && yarn website:build", 17 | "test": "yarn workspace @webembeds/core test", 18 | "commit": "git-cz" 19 | }, 20 | "dependencies": { 21 | "axios": "^0.21.1" 22 | }, 23 | "devDependencies": { 24 | "commitizen": "^4.2.3", 25 | "cz-conventional-changelog": "^3.3.0", 26 | "typescript": "^4.1.3" 27 | }, 28 | "engines": { 29 | "node": ">=12.x" 30 | }, 31 | "config": { 32 | "commitizen": { 33 | "path": "./node_modules/cz-conventional-changelog" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/webembeds-core/.eslintignore: -------------------------------------------------------------------------------- 1 | build/** 2 | -------------------------------------------------------------------------------- /packages/webembeds-core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "jest": true 5 | }, 6 | "extends": ["airbnb-base"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint", "jest"], 13 | "rules": { 14 | "quotes": ["error", "double"], 15 | "import/extensions": "off", 16 | "no-undef": "off" 17 | }, 18 | "settings": { 19 | "import/resolver": { 20 | "node": { 21 | "paths": ["src"], 22 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/webembeds-core/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules -------------------------------------------------------------------------------- /packages/webembeds-core/README.md: -------------------------------------------------------------------------------- 1 | # webembeds-core (⚠️ Not stable yet) 2 | Current version : 0.0.1 3 | 4 | Built and supported by [Hashnode](https://hashnode.com) 5 | 6 | - Checkout demo here https://webembeds.com 7 | 8 | This is the core package that deals with the whole embedding flow. 9 | The build file can be imported elsewhere and used directly 10 | 11 | **Example**: 12 | 13 | ```js 14 | const webembed = require("../build/bundle"); 15 | (async function () { 16 | try { 17 | const output = await webembed.default("https://www.youtube.com/watch?v=32I0Qso4sDg"); 18 | console.log("Embed output", output); 19 | } catch (error) { 20 | console.log(error); 21 | } 22 | }()); 23 | ``` 24 | 25 | ## TODO 26 | [-] Add minimal tests to make sure embeds are working fine. (WIP) 27 | 28 | ## Future plans 29 | [-] Ship `@webembeds/core` as a separate npm package. 30 | 31 | ## Contributing 32 | Please check this README.md on instructions to contributing. https://github.com/Hashnode/webembeds/blob/master/README.md 33 | -------------------------------------------------------------------------------- /packages/webembeds-core/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | export default { 7 | testEnvironment: 'node', 8 | transform: { 9 | "^.+\\.tsx?$": "ts-jest" 10 | }, 11 | moduleFileExtensions: [ 12 | "ts", 13 | "tsx", 14 | "js", 15 | "jsx", 16 | "json", 17 | "node", 18 | ], 19 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)x?$', 20 | }; 21 | -------------------------------------------------------------------------------- /packages/webembeds-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webembeds/core", 3 | "version": "0.0.1", 4 | "main": "build/bundle.js", 5 | "license": "MIT", 6 | "types": "build/index.d.ts", 7 | "scripts": { 8 | "watch": "rimraf ./build && NODE_ENV=development node_modules/.bin/webpack --watch", 9 | "build": "NODE_ENV=production node_modules/.bin/webpack", 10 | "test": "jest" 11 | }, 12 | "devDependencies": { 13 | "@types/async": "^3.2.5", 14 | "@types/axios": "^0.14.0", 15 | "@types/cheerio": "^0.22.23", 16 | "@types/jest": "^26.0.20", 17 | "@types/url-metadata": "^2.1.0", 18 | "@types/url-parse": "^1.4.3", 19 | "@typescript-eslint/eslint-plugin": "^4.12.0", 20 | "@typescript-eslint/parser": "^4.12.0", 21 | "eslint": "^7.17.0", 22 | "eslint-config-airbnb-base": "^14.2.1", 23 | "eslint-plugin-import": "^2.22.1", 24 | "eslint-plugin-jest": "^24.1.5", 25 | "jest": "^26.6.3", 26 | "ts-jest": "^26.5.1", 27 | "ts-loader": "^8.0.13", 28 | "ts-node": "^9.1.1", 29 | "webpack": "^5.11.1", 30 | "webpack-cli": "^4.3.1" 31 | }, 32 | "dependencies": { 33 | "@types/url-metadata": "^2.1.0", 34 | "async": "^3.2.0", 35 | "axios": "^0.21.1", 36 | "cheerio": "^1.0.0-rc.5", 37 | "fastimage": "^3.2.0", 38 | "node-fetch": "^2.6.1", 39 | "oembed": "^0.1.2", 40 | "request": "^2.88.2", 41 | "rimraf": "^5.0.0", 42 | "sanitize-html": "^2.6.1", 43 | "url-metadata": "^2.5.0", 44 | "url-parse": "^1.4.7" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/index.ts: -------------------------------------------------------------------------------- 1 | import WebembedHandler from "./modules/WebembedHandler"; 2 | import type { WebEmbedInitOptions } from "./types"; 3 | 4 | /** 5 | * @param {string} incomingURL 6 | * @param {object} options 7 | */ 8 | function init(incomingURL: string, options?: WebEmbedInitOptions) { 9 | try { 10 | // eslint-disable-next-line no-new 11 | new URL(incomingURL); 12 | } catch (error) { 13 | return { 14 | output: null, 15 | error: true, 16 | }; 17 | } 18 | 19 | const handler = new WebembedHandler(incomingURL, options || {}); 20 | 21 | return handler.generateResponse(); 22 | } 23 | 24 | export default init; 25 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/modules/Platform.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | import queryString from "querystring"; 3 | import { makeRequest } from "../utils/requestHandler"; 4 | import { wrapHTML } from "../utils/html.utils"; 5 | 6 | import type { 7 | OembedRequestQueryParamsType, 8 | OEmbedResponseType, 9 | PlatformType, 10 | WebEmbedInitOptions, 11 | RequestResponseType, 12 | } from "../types"; 13 | 14 | class Platform { 15 | provider: { 16 | custom? : boolean, 17 | customClass?: any, 18 | discover: boolean, 19 | noCustomWrap?: boolean, 20 | } | null; 21 | 22 | embedURL: string; 23 | 24 | targetURL: string | undefined; 25 | 26 | response: RequestResponseType = null; 27 | 28 | queryParams: OembedRequestQueryParamsType; 29 | 30 | cheerio: any; 31 | 32 | options: WebEmbedInitOptions; 33 | 34 | constructor({ 35 | provider, targetURL, embedURL, queryParams, options, 36 | }: PlatformType) { 37 | this.provider = provider; 38 | this.targetURL = targetURL; 39 | this.embedURL = embedURL; 40 | this.queryParams = queryParams; 41 | this.cheerio = cheerio; 42 | 43 | this.options = { 44 | host: options.host || null, 45 | queryParams: options.queryParams, 46 | webembedWrap: options.webembedWrap || false, 47 | }; 48 | } 49 | 50 | async run(): Promise { 51 | const qs = queryString.stringify({ 52 | ...this.queryParams, 53 | url: this.embedURL, 54 | }); 55 | 56 | const response = await makeRequest(`${this.targetURL}?${qs}`); 57 | this.response = response; 58 | 59 | if (response && response.data) { 60 | let { html } = response.data; 61 | 62 | if (this.provider && !this.provider.noCustomWrap) { 63 | html = wrapHTML(response.data, this.queryParams); 64 | } 65 | 66 | return { 67 | version: 0.1, 68 | type: "rich", 69 | title: "WebEmbed", 70 | html, 71 | }; 72 | } 73 | 74 | return null; 75 | } 76 | } 77 | 78 | export default Platform; 79 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/modules/WebembedHandler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import oembed from "oembed"; 3 | import tryEach from "async/tryEach"; 4 | import Platform from "./Platform"; 5 | import oEmbedProviders from "../utils/providers/oembed.providers"; 6 | import { getMetaData } from "../utils/requestHandler"; 7 | import { wrapFallbackHTML, wrapHTML } from "../utils/html.utils"; 8 | 9 | import type { 10 | OEmbedResponseType, 11 | ProviderDetails, 12 | } from "../types"; 13 | 14 | export default class WebembedHandler { 15 | // The main embed URL 16 | embedURL: string; 17 | 18 | finalResponse: {} = {}; 19 | 20 | queryParams: { 21 | forceFallback: boolean, 22 | } = { 23 | forceFallback: false, 24 | }; 25 | 26 | platform: any = {}; 27 | 28 | matchedPlatform: {} | null = null; 29 | 30 | providerDetails: ProviderDetails; 31 | 32 | options: any; 33 | 34 | constructor(incomingURL: string, options: any) { 35 | const { queryParams = {} } = options; 36 | // Replace x.com with twitter.com before doing anything as Provider is setup for twitter.com 37 | const twitterXRegex = new RegExp(/https?:\/\/([a-zA-Z0-9-]+\.)*x\.com/); 38 | this.embedURL = incomingURL.match(twitterXRegex) ? incomingURL.replace(twitterXRegex, "https://twitter.com") : incomingURL; 39 | this.options = options; 40 | this.queryParams = queryParams; 41 | this.providerDetails = this.detectProvider(); 42 | } 43 | 44 | /** 45 | * @desc Goes through providers list and tries to find respective provider for incoming embedURL 46 | * @returns {object} providerDetails.provider the respective provider object from list 47 | * @returns {targetURL} providerDetails.targetURL the final url where the request 48 | * must be made with embedURL, if no targetURL found, it will be the embedURL itself. 49 | */ 50 | detectProvider = () => { 51 | let destinationProvider: { endpoints: any, provider_name: string } | null = null; 52 | let targetURL = null; // The endpoint that the embedURL should be queried upon 53 | 54 | let found = false; 55 | oEmbedProviders.some((provider: { endpoints: any[], provider_name: string }) => { 56 | provider.endpoints.some((endpoint) => { 57 | if (!endpoint.schemes || endpoint.schemes.length === 0) { 58 | // If there are no schemes Ex. https://www.beautiful.ai/ 59 | // Consider the url to be the targetURL 60 | 61 | if (this.embedURL.match(endpoint.url.replace(/\*/g, ".*").replace(/\//g, "\/").replace(/\//g, "\\/"))) { 62 | targetURL = endpoint.url; 63 | destinationProvider = provider; 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | found = endpoint.schemes.some((scheme: string) => { 70 | // eslint-disable-next-line no-useless-escape 71 | if (this.embedURL.match(scheme.replace(/\*/g, ".*").replace(/\//g, "\/").replace(/\//g, "\\/"))) { 72 | targetURL = endpoint.url; 73 | destinationProvider = provider; 74 | return true; 75 | } 76 | return false; 77 | }); 78 | return found; 79 | }); 80 | return found; 81 | }); 82 | return { 83 | provider: destinationProvider, 84 | targetURL: targetURL || this.embedURL, 85 | }; 86 | } 87 | 88 | generateOEmbed = (callback: any) => { 89 | const { embedURL, queryParams } = this; 90 | const { provider } = this.providerDetails; 91 | 92 | if (!provider || (provider && provider.custom)) { 93 | callback(true); 94 | return; 95 | } 96 | 97 | const { noCustomWrap = false } = provider; 98 | 99 | oembed.fetch(embedURL, { format: "json", ...queryParams }, (error: any, result: OEmbedResponseType): any => { 100 | if (error) { 101 | callback(true); 102 | return; 103 | } 104 | const final = result; 105 | 106 | if (final && final.html && !noCustomWrap) { 107 | final.html = wrapHTML(final); 108 | } 109 | 110 | callback(null, final); 111 | }); 112 | } 113 | 114 | // eslint-disable-next-line no-async-promise-executor 115 | generateManually = async () => { 116 | const { provider, targetURL } = this.providerDetails; 117 | const { embedURL, queryParams } = this; 118 | 119 | if (!provider || !targetURL) { 120 | throw new Error(); 121 | } 122 | 123 | // This should fetch an oembed response 124 | if (provider && provider.custom && provider.customClass) { 125 | const CustomClass = provider.customClass; 126 | this.platform = new CustomClass({ 127 | provider, targetURL, embedURL, queryParams, options: this.options, 128 | }); 129 | } else { 130 | this.platform = new Platform({ 131 | provider, targetURL, embedURL, queryParams, options: this.options, 132 | }); 133 | } 134 | 135 | const finalResponse = await this.platform.run(); 136 | return finalResponse; 137 | } 138 | 139 | // Generate a common fallback here by scraping for the common metadata from the platform 140 | // Use this.platform to generate fallback as it already has a response object 141 | generateFallback = async () => { 142 | try { 143 | const data = await getMetaData(this.embedURL); 144 | const html = await wrapFallbackHTML(data); 145 | return { ...data, html }; 146 | } catch (error) { 147 | return null; 148 | } 149 | }; 150 | 151 | /** 152 | * First try with oembed() 153 | If error is thrown 154 | - Try with our providers list 155 | 1. detect platform 156 | 2. make a request 157 | 3. generate response if successful request is made 158 | If request breaks or some error is returned 159 | - Try with fallback response 160 | - Try generating fallback cover with the response details 161 | If this fails too, return a fatal error 162 | */ 163 | // eslint-disable-next-line max-len 164 | generateOutput = async (): Promise => new Promise((resolve, reject) => { 165 | if (this.queryParams.forceFallback) { 166 | tryEach([this.generateFallback], 167 | (error: any, results: any): void => { 168 | if (error) { 169 | return reject(error); 170 | } 171 | return resolve(results); 172 | }); 173 | } 174 | 175 | const { provider } = this.providerDetails; 176 | 177 | let actions: any = []; 178 | 179 | if (provider && provider.provider_name === "Twitter") { 180 | actions = [this.generateManually, this.generateFallback]; 181 | } else { 182 | actions = [this.generateOEmbed, this.generateManually, this.generateFallback]; 183 | } 184 | 185 | tryEach(actions, 186 | (error: any, results: any): void => { 187 | if (error) { 188 | reject(error); 189 | } 190 | resolve(results); 191 | }); 192 | }) 193 | 194 | generateResponse = async (): Promise<{ output?: OEmbedResponseType | null, error?: boolean }> => { 195 | const output = await this.generateOutput(); 196 | 197 | if (output) { 198 | return { 199 | output, 200 | error: false, 201 | }; 202 | } 203 | 204 | return { output: null, error: true }; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/types.ts: -------------------------------------------------------------------------------- 1 | type OembedRequestQueryParamsType = { [key: string]: string | number }; 2 | 3 | /* eslint-disable camelcase */ 4 | // eslint-disable-next-line no-unused-vars 5 | type OEmbedResponseType = { 6 | type: string, 7 | url?: string, 8 | version: number, 9 | title: string, 10 | author_name?: string, 11 | author_url?: string, 12 | provider_name?: string, 13 | provider_url?: string, 14 | cache_age?: number, 15 | thumbnail_url?: string, 16 | // The width of the optional thumbnail. 17 | // If this parameter is present, thumbnail_url and thumbnail_height must also be present. 18 | thumbnail_width?: string, 19 | // The height of the optional thumbnail. 20 | // If this parameter is present, thumbnail_url and thumbnail_width must also be present. 21 | thumbnail_height?: string, 22 | html?: string, 23 | width?: number, 24 | height?: number, 25 | }; 26 | 27 | type WebEmbedInitOptions = { 28 | host?: string | null, 29 | queryParams: {}, 30 | webembedWrap: boolean | undefined, 31 | }; 32 | 33 | type Provider = { 34 | custom? : boolean, 35 | customClass?: any, 36 | discover: boolean, 37 | noCustomWrap: boolean, 38 | provider_name: string, 39 | } 40 | 41 | type PlatformType = { 42 | provider: Provider | null, 43 | targetURL?: string, 44 | embedURL: string, 45 | options: WebEmbedInitOptions, 46 | queryParams: {}, 47 | }; 48 | 49 | type EmbedErrorType = { 50 | type: "request-error", 51 | html?: string | null, 52 | message: string, 53 | code?: number, 54 | }; 55 | 56 | type ProviderDetails = { 57 | provider: Provider | null, 58 | targetURL: string, 59 | }; 60 | 61 | type RequestResponseType = { 62 | data: OEmbedResponseType 63 | } | null; 64 | 65 | type APIResponse = { 66 | error?: boolean | true, 67 | data?: {} | null, 68 | message?: null 69 | }; 70 | 71 | type CustomAtrributes = { 72 | height?: number | string; 73 | width?: number | string; 74 | className?: string; 75 | }; 76 | 77 | export type { 78 | OEmbedResponseType, 79 | PlatformType, 80 | OembedRequestQueryParamsType, 81 | EmbedErrorType, 82 | ProviderDetails, 83 | WebEmbedInitOptions, 84 | RequestResponseType, 85 | APIResponse, 86 | CustomAtrributes, 87 | Provider, 88 | }; 89 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import type { APIResponse } from "../types"; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const apiResponse = ({ data, message, error }: { data: {}, message: "", error: true }): APIResponse => ({ 5 | data: data || null, 6 | message: message || null, 7 | error: error || true, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/graphql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | const { HASHNODE_GRAPHQL_URL, HASHNODE_GRAPHQL_USER_ACCESS_TOKEN } = process.env; 4 | 5 | if (!HASHNODE_GRAPHQL_URL) { 6 | throw new Error("HASHNODE_GRAPHQL_URL is not defined"); 7 | } 8 | 9 | if (!HASHNODE_GRAPHQL_USER_ACCESS_TOKEN) { 10 | throw new Error("HASHNODE_GRAPHQL_USER_ACCESS_TOKEN is not defined"); 11 | } 12 | 13 | // client tracking (https://dev.stellate.co/docs/graphql-metrics/clients) 14 | const STELLATE_CLIENT_NAME_HEADER = "x-graphql-client-name"; 15 | const STELLATE_CLIENT_VERSION_HEADER = "x-graphql-client-version"; 16 | 17 | const isServer = typeof window === "undefined"; 18 | 19 | /** 20 | * Executes a GraphQL query and returns the data and errors. 21 | */ 22 | export const fetchGraphQL = async (options: { 23 | query: string; 24 | variables?: Record 25 | }) => { 26 | const { query, variables = {} } = options || {}; 27 | 28 | try { 29 | const response = await fetch(HASHNODE_GRAPHQL_URL, { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | "hn-trace-app": "Embeds", 34 | [STELLATE_CLIENT_NAME_HEADER]: "webembeds", 35 | [STELLATE_CLIENT_VERSION_HEADER]: isServer ? "server" : "browser", 36 | Authorization: HASHNODE_GRAPHQL_USER_ACCESS_TOKEN, 37 | }, 38 | body: JSON.stringify({ 39 | query, 40 | ...(variables ? { variables } : {}), 41 | }), 42 | }); 43 | 44 | if (!response.ok) { 45 | throw new Error(`Error fetching GraphQL. Status code: ${response.status}.`); 46 | } 47 | 48 | const json = await response.json(); 49 | const { data, errors } = json; 50 | return { data, errors }; 51 | } catch (error) { 52 | console.error("Error fetching GraphQL", { error }); 53 | throw error; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/html.utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable no-tabs */ 3 | import urlMetadata from "url-metadata"; 4 | import cheerio from "cheerio"; 5 | import type { CustomAtrributes, OEmbedResponseType } from "../types"; 6 | import { fetchGraphQL } from "./graphql"; 7 | 8 | const isProd = process.env.NODE_ENV === "production"; 9 | 10 | // interface MetaTagType { 11 | // name: string, 12 | // property: string, 13 | // type: "meta" 14 | // } 15 | 16 | const nodeToObject = (allNodes: [any]) => { 17 | const allTags: any = []; 18 | let i = 0; 19 | 20 | do { 21 | let currentNode = allNodes[i]; 22 | const temp = {} as any; 23 | Object.keys(currentNode.attribs).forEach((key) => { 24 | temp[key] = currentNode.attribs[key]; 25 | }); 26 | temp.type = currentNode.type; 27 | temp.name = currentNode.name; 28 | temp.namespace = currentNode.namespace; 29 | 30 | if (temp) { 31 | allTags.push(temp); 32 | } 33 | 34 | i += 1; 35 | if (allNodes[i]) { 36 | currentNode = allNodes[i]; 37 | } else { 38 | i = -1; 39 | } 40 | } while (i >= 0); 41 | 42 | return allTags; 43 | }; 44 | 45 | // eslint-disable-next-line no-unused-vars 46 | export const extractMetaTags = ($: any) => { 47 | const metaTags = $.html($("meta")).toArray(); 48 | return nodeToObject(metaTags); 49 | }; 50 | 51 | // eslint-disable-next-line no-unused-vars 52 | export const extractLinkTags = ($: any) => { 53 | const linkTags = $($.html($("link"))).toArray(); 54 | return nodeToObject(linkTags); 55 | }; 56 | 57 | // export const extractOEmbedContent = (metaTags: []): { 58 | // oEmbed: {}, custom: {} 59 | // } => { 60 | // const filteredMetaTags = metaTags.filter((tag: MetaTagType) => { 61 | // if (tag && tag.property && tag.property.match(/[og:].*/)) { 62 | // return tag; 63 | // } 64 | // return false; 65 | // }); 66 | 67 | // const reformedTags: any = {}; 68 | // filteredMetaTags.forEach((tag: MetaTagType) => { 69 | // const { property, ...remaining } = tag; 70 | // reformedTags[property.replace("og:", "")] = remaining; 71 | // }); 72 | 73 | // console.log(reformedTags); 74 | 75 | // return { 76 | // oEmbed: {}, 77 | // custom: {}, 78 | // }; 79 | // }; 80 | 81 | export const wrapHTML = (oembedResponse: OEmbedResponseType, 82 | customAtrributes: CustomAtrributes = {}) => { 83 | const { html = "" } = oembedResponse; 84 | 85 | const $ = cheerio.load(html, { xmlMode: true }); 86 | const iframe = $("iframe"); 87 | 88 | const iframeExists = iframe.length > 0; 89 | 90 | const { width = "100%", height = "100%" } = customAtrributes; 91 | 92 | const fHeight = Number(oembedResponse.height) > 0 ? Number(oembedResponse.height) : 360; 93 | const fWidth = Number(oembedResponse.width) > 0 ? Number(oembedResponse.width) : 640; 94 | const paddingTop = fHeight / fWidth; 95 | 96 | if (iframeExists) { 97 | iframe.attr("width", String(width)); 98 | iframe.attr("height", String(height)); 99 | 100 | iframe.attr("style", "position: absolute; top: 0; left: 0; border: 0;"); 101 | iframe.attr("class", "webembed-iframe"); 102 | 103 | $("iframe").wrap( 104 | `
`, 105 | ); 106 | } 107 | 108 | return $.html(); 109 | }; 110 | 111 | async function uploadImageByUrl(url: string) { 112 | let properURL: URL; 113 | try { 114 | properURL = new URL(url); 115 | if (properURL.hostname.includes("hashnode.com") || properURL.hostname.includes("images.unsplash.com")) { 116 | return url; 117 | } 118 | } catch (error) { 119 | throw new Error(`Invalid URL: ${url}`); 120 | } 121 | 122 | const { data, errors } = await fetchGraphQL({ 123 | query: ` 124 | mutation UploadImageByURL($input: UploadImageInput!) { 125 | uploadImageByURL(input: $input) { 126 | imageURL 127 | } 128 | } 129 | `, 130 | variables: { 131 | input: { 132 | url: properURL.toString(), 133 | }, 134 | }, 135 | }); 136 | 137 | if (!data || !data.uploadImageByURL?.imageURL || !!errors) { 138 | console.error("Unexpected response uploading image", { data, errors }); 139 | throw new Error("Error uploading image"); 140 | } 141 | 142 | return data.uploadImageByURL.imageURL; 143 | } 144 | 145 | export const wrapFallbackHTML = async (data: urlMetadata.Result) => { 146 | let mainURL; 147 | let isGitHubLink = false; 148 | 149 | const desc = data["og:description"] || data.description; 150 | let coverImage: any = data["og:image"] || data.image; 151 | 152 | if (isProd) { 153 | // Download the image and upload to our CDN 154 | coverImage = (await uploadImageByUrl(coverImage)) || coverImage; 155 | } 156 | 157 | try { 158 | mainURL = new URL(data["og:url"] || data.url).hostname; 159 | } catch (error) { 160 | mainURL = "/"; 161 | } 162 | 163 | const description = `${desc.substring(0, 150)}${ 164 | desc.length > 150 ? "..." : "" 165 | }`; 166 | 167 | if (mainURL.includes("github.com")) { 168 | isGitHubLink = true; 169 | } 170 | 171 | return ` 172 | 175 | 176 | 208 | `; 209 | }; 210 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/codepen.provider.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../modules/Platform"; 2 | import type { OEmbedResponseType, PlatformType } from "../../types"; 3 | 4 | export default class Giphy extends Platform { 5 | // eslint-disable-next-line no-useless-constructor 6 | constructor(args: PlatformType) { 7 | super(args); 8 | } 9 | 10 | run = async (): Promise => { 11 | const host = "https://codepen.io/"; 12 | let path = this.embedURL.split(host)[1]; 13 | path = path.replace("pen", "embed"); 14 | 15 | return { 16 | version: 0.1, 17 | type: "rich", 18 | title: "Codepen", 19 | html: ``, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/expo.provider.ts: -------------------------------------------------------------------------------- 1 | import UrlParse from "url-parse"; 2 | import Platform from "../../modules/Platform"; 3 | import type { OEmbedResponseType, PlatformType } from "../../types"; 4 | 5 | export default class ExpoSnack extends Platform { 6 | // eslint-disable-next-line no-useless-constructor 7 | constructor(args: PlatformType) { 8 | super(args); 9 | } 10 | 11 | run = async (): Promise => { 12 | const { cheerio } = this; 13 | 14 | const url = UrlParse(this.embedURL, true); 15 | const snackId = url.pathname.replace(/^\/|\/$/g, ""); 16 | 17 | const { theme = "light" } = this.queryParams; 18 | 19 | const html = `
`; 20 | 21 | return { 22 | version: 0.1, 23 | type: "rich", 24 | title: "Expo", 25 | html, 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/facebook.provider.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from "../requestHandler"; 2 | import Platform from "../../modules/Platform"; 3 | import type { OEmbedResponseType, PlatformType } from "../../types"; 4 | 5 | const { FB_APP_TOKEN } = process.env; 6 | 7 | export default class Facebook extends Platform { 8 | hasError: boolean = false; 9 | 10 | // eslint-disable-next-line no-useless-constructor 11 | constructor(args: PlatformType) { 12 | super(args); 13 | if (!FB_APP_TOKEN) { 14 | this.hasError = true; 15 | } 16 | } 17 | 18 | run = async (): Promise => { 19 | if (this.hasError) { 20 | return null; 21 | } 22 | 23 | const response = await makeRequest(`${this.targetURL}?url=${encodeURIComponent(this.embedURL)}&access_token=${FB_APP_TOKEN}`); 24 | const data = response ? response.data : null; 25 | 26 | if (!data) { 27 | return null; 28 | } 29 | 30 | return data; 31 | } 32 | } 33 | 34 | export {}; 35 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/giphy.provider.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from "../requestHandler"; 2 | import Platform from "../../modules/Platform"; 3 | import type { OEmbedResponseType, PlatformType } from "../../types"; 4 | import { wrapHTML } from "../html.utils"; 5 | 6 | export default class Giphy extends Platform { 7 | // eslint-disable-next-line no-useless-constructor 8 | constructor(args: PlatformType) { 9 | super(args); 10 | } 11 | 12 | run = async (): Promise => { 13 | const response = await makeRequest(`${this.targetURL}?url=${encodeURIComponent(this.embedURL)}`); 14 | const data = response ? response.data : null; 15 | 16 | if (!data) { 17 | return null; 18 | } 19 | 20 | const { url } = data; 21 | 22 | if (!url) { 23 | return null; 24 | } 25 | 26 | const cleanURL = url.replace("/giphy.gif", ""); 27 | const extractedID = cleanURL.substr(cleanURL.lastIndexOf("/") + 1); 28 | 29 | const html = ``; 30 | 31 | const temp = { 32 | version: 0.1, 33 | type: "rich", 34 | title: "Giphy", 35 | html, 36 | }; 37 | 38 | const wrappedHTML = wrapHTML(temp); 39 | 40 | return { 41 | ...temp, 42 | html: wrappedHTML, 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/gist.provider.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../modules/Platform"; 2 | import type { OEmbedResponseType, PlatformType } from "../../types"; 3 | import { wrapHTML } from "../html.utils"; 4 | 5 | export default class GithubGist extends Platform { 6 | // eslint-disable-next-line no-useless-constructor 7 | constructor(args: PlatformType) { 8 | super(args); 9 | } 10 | 11 | run = async (): Promise => { 12 | const response = { 13 | version: 0.1, 14 | type: "rich", 15 | title: "Github Gist", 16 | html: "", 17 | }; 18 | 19 | // if (this.options.webembedWrap) { 20 | // response.html = ``; 21 | // response.html = wrapHTML(response); 22 | // return response; 23 | // } 24 | 25 | return { 26 | ...response, 27 | html: ``, 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/glitch.provider.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../modules/Platform"; 2 | import type { OEmbedResponseType, PlatformType } from "../../types"; 3 | 4 | export default class Glitch extends Platform { 5 | // eslint-disable-next-line no-useless-constructor 6 | constructor(args: PlatformType) { 7 | super(args); 8 | } 9 | 10 | // 16 | 17 | // https://glitch.com/embed/#!/embed/remote-hands?path=README.md&previewSize=100 18 | // https://glitch.com/edit/#!/remote-hands?path=README.md%3A1%3A0 19 | // https://glitch.com/embed/#!/embed/remote-hands?previewSize=100&previewFirst=true&sidebarCollapsed=true 20 | run = async (): Promise => { 21 | return { 22 | version: 0.1, 23 | type: "rich", 24 | title: "Glitch", 25 | html: ``, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/index.ts: -------------------------------------------------------------------------------- 1 | import * as GithubGist from "./gist.provider"; 2 | import * as ExpoSnack from "./expo.provider"; 3 | import * as Giphy from "./giphy.provider"; 4 | import * as Instagram from "./instagram.provider"; 5 | import * as Twitch from "./twitch.provider"; 6 | import * as Glitch from "./glitch.provider"; 7 | import * as Facebook from "./facebook.provider"; 8 | import * as Opensea from "./opensea.provider"; 9 | import * as Snappify from "./snappify.provider"; 10 | 11 | export default { 12 | GithubGist, 13 | ExpoSnack, 14 | Giphy, 15 | Instagram, 16 | Twitch, 17 | Glitch, 18 | Facebook, 19 | Opensea, 20 | Snappify, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/instagram.provider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { makeRequest } from "../requestHandler"; 3 | import Platform from "../../modules/Platform"; 4 | import type { OEmbedResponseType, PlatformType } from "../../types"; 5 | import { wrapHTML } from "../html.utils"; 6 | 7 | const { FB_APP_TOKEN } = process.env; 8 | 9 | export default class Instagram extends Platform { 10 | hasError: boolean = false; 11 | 12 | // eslint-disable-next-line no-useless-constructor 13 | constructor(args: PlatformType) { 14 | super(args); 15 | if (!FB_APP_TOKEN) { 16 | this.hasError = true; 17 | } 18 | } 19 | 20 | run = async (): Promise => { 21 | if (this.hasError) { 22 | return null; 23 | } 24 | 25 | const response = await makeRequest(`${this.targetURL}?url=${encodeURIComponent(this.embedURL)}&access_token=${FB_APP_TOKEN}`); 26 | const data = response ? response.data : null; 27 | 28 | if (!data) { 29 | return null; 30 | } 31 | 32 | return data; 33 | } 34 | } 35 | 36 | export {}; 37 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/loom.provider.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../modules/Platform"; 2 | import type { OEmbedResponseType, PlatformType } from "../../types"; 3 | 4 | export default class Loom extends Platform { 5 | // eslint-disable-next-line no-useless-constructor 6 | constructor(args: PlatformType) { 7 | super(args); 8 | } 9 | 10 | run = async (): Promise => { 11 | const loomId = this.embedURL.replace("https://www.loom.com/share/", ""); 12 | return { 13 | version: 0.1, 14 | type: "rich", 15 | title: "Loom", 16 | html: `
`, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/opensea.provider.ts: -------------------------------------------------------------------------------- 1 | import sanitizeHtml from "sanitize-html"; 2 | 3 | import Platform from "../../modules/Platform"; 4 | import type { OEmbedResponseType, PlatformType } from "../../types"; 5 | 6 | export default class Opensea extends Platform { 7 | // eslint-disable-next-line no-useless-constructor 8 | constructor(args: PlatformType) { 9 | super(args); 10 | } 11 | 12 | run = async (): Promise => { 13 | const url = new URL(this.embedURL); 14 | url.searchParams.set("embed", "true"); 15 | 16 | let html = ""; 17 | 18 | if (url.pathname.includes("/assets")) { 19 | const pathnameChunks = url.pathname.split("/"); 20 | 21 | const hasNetwork = pathnameChunks.length === 5; 22 | const network = hasNetwork ? pathnameChunks[2] : "mainnet"; 23 | const contractAddress = hasNetwork ? pathnameChunks[3] : pathnameChunks[2]; 24 | const tokenId = hasNetwork ? pathnameChunks[4] : pathnameChunks[3]; 25 | 26 | html = ` 31 | 32 | `; 33 | } else { 34 | html = `
`; 35 | } 36 | 37 | return { 38 | version: 0.1, 39 | type: "rich", 40 | title: "Opensea", 41 | html, 42 | }; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/snappify.provider.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../modules/Platform"; 2 | import type { OEmbedResponseType, PlatformType } from "../../types"; 3 | import { makeRequest } from "../requestHandler"; 4 | 5 | export default class Snappify extends Platform { 6 | // eslint-disable-next-line no-useless-constructor 7 | constructor(args: PlatformType) { 8 | super(args); 9 | } 10 | 11 | run = async (): Promise => { 12 | const response = await makeRequest(`${this.targetURL}?url=${encodeURIComponent(this.embedURL)}`); 13 | const data = response ? response.data : null; 14 | 15 | if (!data || !data.width || !data.height) { 16 | return null; 17 | } 18 | 19 | const host = "https://snappify.com/"; 20 | let path = this.embedURL.split(host)[1]; 21 | path = path.replace("view", "embed"); 22 | 23 | const aspectRatioPercentage = (1 / (data.width / data.height)) * 100; 24 | 25 | const wrapperDivStyle = `position:relative;overflow:hidden;margin-left:auto;margin-right:auto;border-radius:10px;width:100%;max-width:${data.width}px`; 26 | const aspectRatioDivStyle = `width:100%;padding-bottom:${aspectRatioPercentage}%`; 27 | const iframeStyle = "position:absolute;left:0;top:0;width:100%"; 28 | 29 | return { 30 | version: 0.1, 31 | type: "rich", 32 | title: "snappify", 33 | html: `
`, 34 | }; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/tenor.provider.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../modules/Platform"; 2 | import type { 3 | OEmbedResponseType, 4 | PlatformType, 5 | } from "../../types"; 6 | import { wrapHTML } from "../html.utils"; 7 | 8 | export default class Tenor extends Platform { 9 | // eslint-disable-next-line no-useless-constructor 10 | constructor(args: PlatformType) { 11 | super(args); 12 | } 13 | 14 | run = async (): Promise => { 15 | const splits = this.embedURL.split("/"); 16 | const lastPart = splits[splits.length - 1]; 17 | const extractedID = lastPart.substring( 18 | lastPart.lastIndexOf("-") + 1, 19 | ); 20 | 21 | const html = ``; 22 | 23 | const temp = { 24 | version: 0.1, 25 | type: "rich", 26 | title: "Tenor", 27 | html, 28 | }; 29 | 30 | const wrappedHTML = wrapHTML(temp); 31 | 32 | return { 33 | ...temp, 34 | html: wrappedHTML, 35 | }; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/providers/twitch.provider.ts: -------------------------------------------------------------------------------- 1 | import Platform from "../../modules/Platform"; 2 | import type { OEmbedResponseType } from "../../types"; 3 | import { wrapHTML } from "../html.utils"; 4 | 5 | export default class Twitch extends Platform { 6 | // eslint-disable-next-line no-useless-constructor 7 | 8 | run = async (): Promise => { 9 | const { host } = this.options; 10 | let parentURL = host || "localhost"; 11 | 12 | try { 13 | const url = new URL(`https://${parentURL}`); 14 | parentURL = `${url.hostname}`; 15 | } catch (error) { 16 | parentURL = "localhost"; 17 | } 18 | 19 | try { 20 | const url = new URL(this.embedURL); 21 | let href = `https://player.twitch.tv/?autoplay=false&parent=${parentURL}`; 22 | // Supports 23 | // Channel Ex, https://www.twitch.tv/lck 24 | // Video Ex: https://www.twitch.tv/videos/668650517 25 | // Clips Ex: https://www.twitch.tv/loltyler1/clip/CooperativeSpikyScorpionOptimizePrime 26 | 27 | if (url.pathname.indexOf("/videos") === 0) { 28 | href += (`&video=${url.pathname.substring(url.pathname.indexOf("/videos") + 7).replace(/\//g, "")}`); 29 | } else if (url.pathname.includes("/clip")) { 30 | const clipId = url.pathname.substring(url.pathname.indexOf("/clip") + 5).replace(/\//g, ""); 31 | url.hostname = "clips.twitch.tv"; 32 | url.pathname = "embed"; 33 | url.searchParams.set("clip", clipId); 34 | url.searchParams.set("parent", parentURL); 35 | url.searchParams.set("autoplay", "false"); 36 | href = url.toString(); 37 | } else { 38 | const channelName = new URL(this.embedURL).pathname.replace(/\//g, ""); 39 | href += `&channel=${channelName}`; 40 | } 41 | 42 | const response: OEmbedResponseType = { 43 | version: 0.1, 44 | type: "rich", 45 | title: "Twitch", 46 | html: ``, 48 | }; 49 | 50 | const wrappedHTML = wrapHTML(response, {}); 51 | 52 | return { 53 | version: 0.1, 54 | type: "rich", 55 | title: "Twitch", 56 | html: wrappedHTML, 57 | }; 58 | } catch (error) { 59 | return null; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/webembeds-core/src/utils/requestHandler.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { info as FastImage } from "fastimage"; 3 | import urlMetaData, { Result } from "url-metadata"; 4 | import type { RequestResponseType } from "../types"; 5 | 6 | export const makeRequest = async (url: string): Promise => { 7 | try { 8 | const response = await axios.get(url, { 9 | params: { 10 | format: "json", 11 | }, 12 | headers: { 13 | // eslint-disable-next-line max-len 14 | // "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36", 15 | // Connection: "keep-alive", 16 | // Accept: "*/*", 17 | // "accept-encoding": "gzip, deflate, br", 18 | }, 19 | }); 20 | return response; 21 | } catch (error) { 22 | // console.log(error); 23 | return null; 24 | } 25 | }; 26 | 27 | // eslint-disable-next-line max-len 28 | export const getMetaData = (url: string): Promise => new Promise((resolve, reject) => { 29 | urlMetaData(url).then( 30 | (metadata: urlMetaData.Result) => { 31 | // success handler 32 | // if (!metadata["og:image:width"] || !metadata["og:image:height"]) { 33 | // FastImage(metadata["og:image"], (error: any, imageData: any): any => { 34 | // if (error) { 35 | // console.log(error); 36 | // return resolve(metadata); 37 | // } 38 | // const newMetaData = metadata; 39 | // if (imageData) { 40 | // newMetaData["og:image:width"] = imageData.width; 41 | // newMetaData["og:image:height"] = imageData.height; 42 | // } 43 | // return resolve(newMetaData); 44 | // }); 45 | // } 46 | resolve(metadata); 47 | }, 48 | (error) => { // failure handler 49 | console.log(error); 50 | reject(error); 51 | }, 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/webembeds-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noImplicitAny": false, 15 | "outDir": "./build", 16 | "sourceMap": true, 17 | "declaration": true, 18 | }, 19 | "include": ["./src/**/*", "__tests__/**/*"], 20 | } 21 | -------------------------------------------------------------------------------- /packages/webembeds-core/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: process.env.NODE_ENV, 5 | entry: "./src/index.ts", 6 | devtool: "inline-source-map", 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.tsx?$/, 11 | use: "ts-loader", 12 | exclude: /node_modules/, 13 | }, 14 | ], 15 | }, 16 | resolve: { 17 | extensions: [".ts", ".js"], 18 | }, 19 | target: "node", 20 | output: { 21 | filename: "bundle.js", 22 | path: path.resolve(__dirname, "build"), 23 | libraryTarget: "umd", // very important line 24 | umdNamedDefine: true, // very important line 25 | globalObject: "this", 26 | library: "webembed", 27 | }, 28 | externals: ["axios", "cheerio"], 29 | }; 30 | -------------------------------------------------------------------------------- /packages/webembeds-website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "ecmaVersion": 12 15 | }, 16 | "plugins": [ 17 | "react", 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | "quotes": ["error", "double"], 22 | "react/prop-types": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/webembeds-website/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules -------------------------------------------------------------------------------- /packages/webembeds-website/README.md: -------------------------------------------------------------------------------- 1 | # webembeds-website 2 | Built and supported by [Hashnode](https://hashnode.com) 3 | 4 | - Checkout demo here https://webembeds.com/demo 5 | 6 | This package deals with the website. Right now website only hosts a demo of webembeds. 7 | 8 | Built with Next.js 9 | 10 | ## Contributing 11 | Please check this README.md on instructions to contributing. https://github.com/Hashnode/webembeds/blob/master/README.md 12 | -------------------------------------------------------------------------------- /packages/webembeds-website/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface Props {} 4 | 5 | function Layout(props: Props) { 6 | const {} = props 7 | 8 | return ( 9 |
10 | 11 |
12 | ) 13 | } 14 | 15 | export default Layout 16 | -------------------------------------------------------------------------------- /packages/webembeds-website/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props {} 4 | 5 | function Loader(props: Props) { 6 | const {} = props 7 | 8 | return
9 |
10 |
11 | } 12 | 13 | export default Loader 14 | -------------------------------------------------------------------------------- /packages/webembeds-website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /packages/webembeds-website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webembeds/website", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next -p 3001", 8 | "build": "next build", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@webembeds/core": "0.0.1", 13 | "autoprefixer": "^10.2.4", 14 | "axios": "^0.21.1", 15 | "express": "^4.17.1", 16 | "next": "^10.0.4", 17 | "postcss": "^8.2.4", 18 | "react": "^17.0.1", 19 | "react-dom": "^17.0.1", 20 | "tailwindcss": "^2.0.2", 21 | "url-parse": "^1.4.7" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^14.14.20", 25 | "@types/react": "^17.0.0", 26 | "@typescript-eslint/eslint-plugin": "^4.12.0", 27 | "@typescript-eslint/parser": "^4.12.0", 28 | "eslint": "^7.17.0", 29 | "eslint-config-airbnb": "^18.2.1", 30 | "eslint-plugin-import": "^2.22.1", 31 | "eslint-plugin-jsx-a11y": "^6.4.1", 32 | "eslint-plugin-react": "^7.22.0", 33 | "eslint-plugin-react-hooks": "^4.2.0", 34 | "typescript": "^4.1.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/webembeds-website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { AppProps } from "next/app"; 4 | import Head from "next/head"; 5 | 6 | import "../styles/main.css"; 7 | 8 | // eslint-disable-next-line react/jsx-props-no-spreading 9 | const WebembedsApp = ({ 10 | Component, 11 | pageProps, 12 | }: AppProps) => { 13 | return ( 14 | <> 15 | 16 | Webembeds 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default WebembedsApp; 24 | -------------------------------------------------------------------------------- /packages/webembeds-website/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { Html, Head, Main, NextScript } from "next/document"; 3 | 4 | class CustomDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default CustomDocument; 34 | -------------------------------------------------------------------------------- /packages/webembeds-website/pages/api/embed.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import webembed from "@webembeds/core"; 3 | 4 | import type { EmbedRequest, CustomResponse } from "../../types"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | const { url = "", customHost = "", ...restOfTheQueryParams }: EmbedRequest = req.query; 8 | 9 | const embedURL = decodeURIComponent(url); 10 | 11 | res.setHeader("Access-Control-Allow-Origin", "*"); 12 | res.setHeader("Access-Control-Allow-Headers", "*"); 13 | 14 | res.setHeader("Cache-Control", "public, s-maxage=31540000"); // 1 year 15 | 16 | // Twitch needs a parent url where the embed is being used. 17 | 18 | const embedResponse = await webembed(embedURL, { 19 | host: decodeURIComponent(customHost), 20 | webembedWrap: true, 21 | queryParams: { 22 | ...restOfTheQueryParams, 23 | }, 24 | }); 25 | 26 | res.json({ data: embedResponse }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/webembeds-website/pages/api/html.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import webembed from "@webembeds/core"; 3 | 4 | import type { EmbedRequest } from "../../types"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | const { url = "" }: EmbedRequest = req.query; 8 | const embedURL = decodeURIComponent(url); 9 | const embedResponse = await webembed(embedURL); 10 | 11 | res.setHeader("Content-Type", "text/html; charset='utf-8'"); 12 | res.setHeader("Access-Control-Allow-Origin", "*"); 13 | 14 | if (embedResponse.output) { 15 | if (embedResponse.output.html) { 16 | return res.send(`${embedResponse.output.html}`); 17 | } 18 | } 19 | 20 | res.send("Not available"); 21 | } 22 | -------------------------------------------------------------------------------- /packages/webembeds-website/pages/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/webembeds/6be2d81e10f01e6bb1b87bc29be4421570583506/packages/webembeds-website/pages/app.css -------------------------------------------------------------------------------- /packages/webembeds-website/pages/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React, { useState, useEffect, useRef } from "react"; 3 | import Loader from "../components/Loader"; 4 | 5 | const links: any = { 6 | spotify: "https://open.spotify.com/track/3G8o2zm7LaF6eeVuvLlrkJ?si=Sx1sCnhDT6GXqSLIwSLOeQ", 7 | gist: "https://gist.github.com/theevilhead/908d9f761a82809a33fcede797d06334", 8 | // canva1: "https://www.canva.com/design/DAEWSa9kfIs/view", 9 | // canva2: "https://www.canva.com/design/DAEWRhUKdvg/view", 10 | twitter: "https://twitter.com/hashnode/status/1352525138659430400", 11 | expo: "https://snack.expo.io/@girishhashnode/unnamed-snack", 12 | runkit: "https://runkit.com/runkit/welcome", 13 | canva: "https://www.canva.com/design/DAET1m0_11c/jFBlYrKc8CQCb2boU9KC-A/view", 14 | codepen: "https://codepen.io/bsehovac/pen/EMyWVv", 15 | youtube: "https://www.youtube.com/watch?v=32I0Qso4sDg", 16 | glitch: "https://glitch.com/edit/#!/remote-hands", 17 | twitch: "https://www.twitch.tv/fresh", 18 | giphy: "https://giphy.com/gifs/cbsnews-inauguration-2021-XEMbxm9vl9JIIMcE7M", 19 | metascraper: "https://metascraper.js.org/", 20 | repl: "https://repl.it/@GirishPatil4/AdvancedRespectfulGigahertz", 21 | soundcloud: "https://soundcloud.com/hit-jatt/jatt-disde-arjan-dhillon", 22 | anchor: "https://anchor.fm/startapodcast/episodes/Whats-your-podcast-about-e17krq/a-a2q3ft", 23 | loom: "https://www.loom.com/share/0281766fa2d04bb788eaf19e65135184", 24 | vimeo: "https://vimeo.com/485593347", 25 | snappify: "https://snappify.com/view/bcc54061-6e8f-44c5-a4f4-1abcad520108", 26 | fallback: "https://github.com", 27 | }; 28 | 29 | function Demo() { 30 | const urlRef = useRef(null); 31 | const [result, setResult] = useState<{ output?: { html?: string }; error: boolean } | null>(); 32 | const [isLoading, setLoading] = useState(false); 33 | let parentNode: HTMLElement | null = null; 34 | 35 | useEffect(() => { 36 | parentNode = document.getElementById("embed-platform"); 37 | 38 | if (!window) { 39 | return; 40 | } 41 | 42 | (window as any).adjustIframeSize = function(id: string, newHeight: string) { 43 | const ele = document.getElementById(id); 44 | if (!ele) return; 45 | ele.style.height = parseInt(newHeight) + "px"; 46 | } 47 | 48 | // Can be ported to others too 49 | window.addEventListener("message", function(e) { 50 | if (e.origin !== "https://runkit.com") return; 51 | 52 | try { 53 | var data = JSON.parse(e.data); 54 | } catch (e) { 55 | return false; 56 | } 57 | 58 | if (data.context !== "iframe.resize") { 59 | return false; 60 | } 61 | 62 | const iframe: HTMLIFrameElement | null = document.querySelector("iframe[src=\"" + data.src + "\"]"); 63 | 64 | if (!iframe) { 65 | return false; 66 | } 67 | 68 | iframe.setAttribute("width", "100%"); 69 | 70 | if (data.height) { 71 | iframe.setAttribute("height", data.height); 72 | } 73 | }); 74 | 75 | handleURL(links.vimeo); 76 | }, []); 77 | 78 | const handleURL = async (incomingURL?: string) => { 79 | if (null === urlRef) { 80 | return; 81 | } 82 | 83 | if (null === urlRef.current) { 84 | return; 85 | } 86 | 87 | parentNode = document.getElementById("embed-platform"); 88 | if (!parentNode) { 89 | return; 90 | } 91 | 92 | const url = incomingURL || (urlRef !== null ? urlRef.current.value : null); 93 | 94 | if (!url) { 95 | return; 96 | } 97 | 98 | setLoading(true); 99 | setResult(null); 100 | parentNode.innerHTML = ""; 101 | 102 | const requestURL = `/api/embed?url=${encodeURIComponent(url)}&customHost=${encodeURIComponent(window.location.hostname)}`; 103 | const response = await fetch(requestURL, { 104 | method: "GET", 105 | headers: { 106 | Accept: "application/json", 107 | "Content-Type": "application/json", 108 | }, 109 | }); 110 | const json = await response.json(); 111 | 112 | setLoading(false); 113 | 114 | if(parentNode && !url.includes("gist.github.com")) { 115 | parentNode.innerHTML = json.data.output.html; 116 | Array.from(parentNode.querySelectorAll("script")).forEach( async oldScript => { 117 | const newScript = document.createElement("script"); 118 | Array.from(oldScript.attributes).forEach( attr => newScript.setAttribute(attr.name, attr.value) ); 119 | 120 | if (oldScript.innerHTML) { 121 | newScript.appendChild(document.createTextNode(oldScript.innerHTML)); 122 | } 123 | 124 | if (oldScript && oldScript.parentNode) { 125 | oldScript.parentNode.replaceChild(newScript, oldScript); 126 | } 127 | }); 128 | } else { 129 | 130 | const gistFrame = document.createElement("iframe"); 131 | gistFrame.setAttribute("width", "100%"); 132 | gistFrame.setAttribute("frameBorder", "0"); 133 | gistFrame.setAttribute("scrolling", "no"); 134 | gistFrame.id = `gist-${new Date().getTime()}`; 135 | 136 | parentNode.innerHTML = ""; 137 | parentNode.appendChild(gistFrame); 138 | 139 | // Create the iframe's document 140 | const gistFrameHTML = `${json.data.output.html}`; 141 | 142 | // Set iframe's document with a trigger for this document to adjust the height 143 | let gistFrameDoc = gistFrame.contentWindow?.document; 144 | 145 | if (gistFrame.contentDocument) { 146 | gistFrameDoc = gistFrame.contentDocument; 147 | } else if (gistFrame.contentWindow) { 148 | gistFrameDoc = gistFrame.contentWindow.document; 149 | } 150 | 151 | if (!gistFrameDoc) { 152 | return; 153 | } 154 | gistFrameDoc.open(); 155 | gistFrameDoc.writeln(gistFrameHTML); 156 | gistFrameDoc.close(); 157 | } 158 | 159 | // setResult(json ? json.data : null); 160 | setLoading(false); 161 | }; 162 | 163 | return ( 164 |
165 |
166 | Webembeds 167 |
168 | {/* Follow @hashnode 169 |   */} 170 | Star us on Github 171 |
172 |
173 | 174 |
175 | {/*
176 | {result ?
{JSON.stringify(result)}
: "No result"} 177 |
*/} 178 | {isLoading ? : null} 179 | {/* {result && !result.error && !isLoading ? ( */} 180 |
185 | {/* ) : null} */} 186 | {result && result.error ? "Something went wrong" : ""} 187 |
188 | 189 |
190 | 191 |
192 |
193 | 199 | 208 |
209 | 210 |

Or select from below

211 | 212 |
213 | {Object.keys(links).map((key, index) => { 214 | return ( 215 | 222 | ); 223 | })} 224 |
225 |
226 |
227 | Made with ❤️   by{" "} 228 | 229 | Hashnode 230 | 231 |
232 |
233 | ); 234 | } 235 | 236 | export default Demo; 237 | -------------------------------------------------------------------------------- /packages/webembeds-website/pages/test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React, { useRef, useState } from "react"; 3 | import axios from "axios"; 4 | 5 | function Index() { 6 | const urlRef = useRef(null); 7 | const [isEmbedVisible, setEmbedVisible] = useState(false); 8 | const [embedData, setEmbedData] = useState({} as any); 9 | const [currentURL, setURL] = useState(""); 10 | 11 | const onSubmit = () => { 12 | const url = urlRef?.current?.value || ""; 13 | 14 | setURL(url); 15 | 16 | // axios.get(`/api/embed/?url=${encodeURIComponent(url)}`) 17 | // .then((res) => { 18 | // console.log(res.data.data.output); 19 | // setEmbedVisible(true); 20 | // if (res.data.data.output && res.data.data.output.html) { 21 | // setEmbedData(res.data.data.output.html); 22 | // } else { 23 | // setEmbedData(res.data.data.output); 24 | // } 25 | // }) 26 | // .catch(console.log); 27 | }; 28 | 29 | return ( 30 | <> 31 |
e.preventDefault()}> 32 | 33 | 34 | {/* { 35 | isEmbedVisible 36 | &&
37 | } */} 38 | 39 | { 40 | currentURL ? ( 41 |
48 |