├── . eslintignore ├── .eslintrc.js ├── .firebaserc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierignore ├── README.md ├── firebase.json ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── public ├── airplane.svg └── twitter.svg ├── src ├── constatns │ ├── cloudStorageKeys.ts │ ├── gcsPath.ts │ ├── hostingURL.ts │ └── sampleCode.ts ├── helper │ ├── createGcsURL.ts │ ├── createHostingURL.ts │ ├── env.ts │ └── util.ts ├── infrastructure │ ├── Firebase.test.ts │ └── Firebase.ts ├── pages │ ├── [pid].tsx │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx ├── repository │ └── postPng.ts ├── types │ └── util │ │ ├── env.ts │ │ └── firebase.ts └── vendor │ └── css │ ├── monaco.css │ ├── normal.css │ └── reset.css ├── storage.rules ├── tsconfig.json ├── types └── global.d.ts └── yarn.lock /. eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | package.json 3 | package-lock.json -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2020: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended", 12 | "prettier/react", 13 | "prettier/@typescript-eslint", 14 | ], 15 | parser: "@typescript-eslint/parser", 16 | parserOptions: { 17 | ecmaFeatures: { 18 | jsx: true, 19 | }, 20 | ecmaVersion: 11, 21 | sourceType: "module", 22 | }, 23 | plugins: ["react", "@typescript-eslint"], 24 | rules: { 25 | "@typescript-eslint/explicit-module-boundary-types": "off", 26 | "@typescript-eslint/ban-ts-comment": "off", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ogpng-fed0a" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "*" 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 12.x 19 | - name: npm install 20 | run: | 21 | npm install 22 | - name: type check 23 | run: | 24 | npm run typecheck 25 | - name: lint & format 26 | run: | 27 | npm run lint 28 | - name: test 29 | run: | 30 | npm run test 31 | - name: gen test cov 32 | run: npm run test-cov 33 | - name: Cov Deploy 34 | uses: peaceiris/actions-gh-pages@v3 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | publish_dir: ./coverage 38 | - name: build 39 | run: | 40 | npm run build 41 | -------------------------------------------------------------------------------- /.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 | # TypeScript v1 declaration files 45 | typings/ 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.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .vercel -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | package.json 3 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ogpng 2 | 3 | Engineer 向け OGP シェアサービス。 4 | 5 | ソースコードの仕組みや実装の解説は、 [ソースコードから OGP を生成しシェアする Ogp as a Service を作った(そして飽きたのでコードを公開する)](http://localhost:8000/share-ogp) をご覧ください。 6 | 7 | ## firebase 周り 8 | 9 | ぼくの APIKEY を埋め込んでいますが、無料プランなので使いすぎると止まります。 10 | 自分の鍵でお試しください。 11 | 12 | ## Dev 13 | 14 | ```sh 15 | yarn install 16 | 17 | yarn develop 18 | ``` 19 | 20 | ## 機能 21 | 22 | - Editor で 画面を作れる 23 | - 画面を画像として保存して OGP 化 24 | - 認証 25 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "storage": { 3 | "rules": "storage.rules" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | testEnvironment: "node", 4 | clearMocks: true, 5 | coverageDirectory: "coverage", 6 | preset: "ts-jest", 7 | }; 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withCSS = require("@zeit/next-css"); 2 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 3 | 4 | module.exports = withCSS({ 5 | webpack: (config) => { 6 | config.module.rules.push({ 7 | test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/, 8 | use: { 9 | loader: "url-loader", 10 | options: { 11 | limit: 100000, 12 | }, 13 | }, 14 | }); 15 | 16 | config.plugins.push( 17 | new MonacoWebpackPlugin({ 18 | // Add languages as needed... 19 | languages: ["javascript", "typescript", "html"], 20 | filename: "static/[name].worker.js", 21 | }) 22 | ); 23 | return config; 24 | }, 25 | env: { NEXT_PUBLIC_DEPLOY_ENV: process.env.DEPLOY_ENV }, 26 | }); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ogpng", 3 | "version": "1.0.0", 4 | "description": "Engineer 向け OGP シェアサービス", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test-cov": "npm run test -- --coverage", 9 | "develop": "next", 10 | "build": "next build", 11 | "start": "next start", 12 | "typecheck": "tsc -p . --noEmit", 13 | "format": "prettier --write '**/*.{ts,json}'", 14 | "lint": "eslint 'src/**/*.{js,json,jsx,ts,tsx}'" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/sadnessOjisan/ogpng.git" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/sadnessOjisan/ogpng/issues" 24 | }, 25 | "homepage": "https://github.com/sadnessOjisan/ogpng#readme", 26 | "dependencies": { 27 | "@zeit/next-css": "^1.0.1", 28 | "dom-to-image": "^2.6.0", 29 | "file-saver": "^2.0.2", 30 | "firebase": "^7.15.1", 31 | "monaco-editor": "^0.20.0", 32 | "next": "^9.4.4", 33 | "next-fonts": "^1.1.0", 34 | "next-transpile-modules": "^3.3.0", 35 | "react": "^16.13.1", 36 | "react-dom": "^16.13.1", 37 | "react-monaco-editor": "^0.36.0", 38 | "reactel-to-html": "^1.1.0", 39 | "use-media": "^1.4.0" 40 | }, 41 | "devDependencies": { 42 | "@types/dom-to-image": "^2.6.1", 43 | "@types/jest": "^26.0.0", 44 | "@types/node": "^14.0.13", 45 | "@types/react": "^16.9.36", 46 | "@typescript-eslint/eslint-plugin": "^3.2.0", 47 | "@typescript-eslint/parser": "^3.2.0", 48 | "dotenv-webpack": "^1.8.0", 49 | "eslint": "^7.2.0", 50 | "eslint-config-prettier": "^6.11.0", 51 | "eslint-plugin-prettier": "^3.1.3", 52 | "eslint-plugin-react": "^7.20.0", 53 | "jest": "^26.0.1", 54 | "monaco-editor-webpack-plugin": "^1.9.0", 55 | "prettier": "^2.0.5", 56 | "ts-jest": "^26.1.0", 57 | "typescript": "^3.9.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/airplane.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/constatns/cloudStorageKeys.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | OGP: "OGP", 3 | }; 4 | -------------------------------------------------------------------------------- /src/constatns/gcsPath.ts: -------------------------------------------------------------------------------- 1 | import { EnvType } from "../types/util/env"; 2 | 3 | const gscPath: { [key in EnvType]: string } = { 4 | development: "https://storage.googleapis.com/ogpng-dev-ca280.appspot.com", 5 | production: "https://storage.googleapis.com/ogpng-fed0a.appspot.com", 6 | test: "https://storage.googleapis.com/ogpng-dev-ca280.appspot.com", 7 | }; 8 | 9 | export default gscPath; 10 | -------------------------------------------------------------------------------- /src/constatns/hostingURL.ts: -------------------------------------------------------------------------------- 1 | import { EnvType } from "../types/util/env"; 2 | 3 | const hostingURL: { [key in EnvType]: string } = { 4 | development: "https://dev.ogpng.vercel.app", 5 | production: "https://ogpng.vercel.app", 6 | test: "https://test.ogpng.vercel.app", 7 | }; 8 | 9 | export default hostingURL; 10 | -------------------------------------------------------------------------------- /src/constatns/sampleCode.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | html: { 3 | mobile: `
17 |

HTMLならなんでも書き込めます。

18 |

TwitterのOGPは438 x 220 です。

19 |

JS & JSX でも使えます。

20 |
`, 21 | pc: `
35 |

HTMLならなんでも書き込めます。

36 |

TwitterのOGPは438 x 220 です。

37 |

JS & JSX の対応をいま頑張ってます。

38 |
`, 39 | }, 40 | jsx: { 41 | mobile: `
55 |

HTMLならなんでも書き込めます。

56 |

TwitterのOGPは438 x 220 です。

57 |

JS & JSX の対応をいま頑張ってます。

58 |
`, 59 | pc: `
73 |

HTMLならなんでも書き込めます。

74 |

TwitterのOGPは438 x 220 です。

75 |

JS & JSX の対応しました。

76 |
`, 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /src/helper/createGcsURL.ts: -------------------------------------------------------------------------------- 1 | import gcsPath from "../constatns/gcsPath"; 2 | import { EnvType } from "../types/util/env"; 3 | 4 | export default (env: EnvType) => { 5 | if (env === "test") { 6 | throw new Error("いまはtest環境は使っていない"); 7 | } 8 | return gcsPath[env]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/helper/createHostingURL.ts: -------------------------------------------------------------------------------- 1 | import { EnvType } from "../types/util/env"; 2 | import hostingURL from "../constatns/hostingURL"; 3 | 4 | export default (env: EnvType) => { 5 | if (env === "test") { 6 | throw new Error("いまはtest環境は使っていない"); 7 | } 8 | return hostingURL[env]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/helper/env.ts: -------------------------------------------------------------------------------- 1 | import { EnvType } from "../types/util/env"; 2 | 3 | export default (): EnvType => { 4 | // vercelの環境変数VERCEL_ENVで 'development', 'production', 'test' を指定する 5 | const env = process.env.NEXT_PUBLIC_DEPLOY_ENV; 6 | //@ts-ignore 7 | return env || "production"; 8 | }; 9 | -------------------------------------------------------------------------------- /src/helper/util.ts: -------------------------------------------------------------------------------- 1 | export const generateRandomId = () => { 2 | return Math.random().toString(36).slice(-8); 3 | }; 4 | -------------------------------------------------------------------------------- /src/infrastructure/Firebase.test.ts: -------------------------------------------------------------------------------- 1 | import { genFirebaseConfig } from "./Firebase"; 2 | 3 | describe("Firebase", () => { 4 | describe("genFirebaseConfig", () => { 5 | test("exception", () => { 6 | // @ts-expect-error 7 | expect(() => genFirebaseConfig("")).toThrowError(); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/infrastructure/Firebase.ts: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import "firebase/analytics"; 3 | import "firebase/storage"; 4 | import createEnv from "../helper/env"; 5 | import { EnvType } from "../types/util/env"; 6 | import { FirebaseConfigType } from "../types/util/firebase"; 7 | 8 | export const genFirebaseConfig = (env: EnvType): FirebaseConfigType => { 9 | switch (env) { 10 | case "development": 11 | return { 12 | apiKey: "AIzaSyAjIt9xC4sxdqerZhe6OQcgqqy7IpERgXc", 13 | authDomain: "ogpng-dev-ca280.firebaseapp.com", 14 | databaseURL: "https://ogpng-dev-ca280.firebaseio.com", 15 | projectId: "ogpng-dev-ca280", 16 | storageBucket: "ogpng-dev-ca280.appspot.com", 17 | messagingSenderId: "12952861645", 18 | appId: "1:12952861645:web:5cd3a751ac755e3cf0a284", 19 | measurementId: "G-RRBQC4QNBH", 20 | }; 21 | case "production": 22 | return { 23 | apiKey: "AIzaSyCTZTIzkXdFLP_oFKy__oKjhUUMLDeU-gk", 24 | authDomain: "ogpng-fed0a.firebaseapp.com", 25 | databaseURL: "https://ogpng-fed0a.firebaseio.com", 26 | projectId: "ogpng-fed0a", 27 | storageBucket: "ogpng-fed0a.appspot.com", 28 | messagingSenderId: "1072339628875", 29 | appId: "1:1072339628875:web:5aa1b319bdf18c8576aece", 30 | measurementId: "G-BSNFTVKR5C", 31 | }; 32 | default: 33 | throw new Error("unexpected env"); 34 | } 35 | }; 36 | 37 | export default class Firebase { 38 | private static _instance: Firebase; 39 | private _app: firebase.app.App; 40 | private _storage: firebase.storage.Storage; 41 | 42 | private constructor() { 43 | // https://github.com/zeit/next.js/issues/1999#issuecomment-302244429 44 | if (!firebase.apps.length) { 45 | const env = genFirebaseConfig(createEnv()); 46 | firebase.initializeApp(env); 47 | if (process.browser) { 48 | firebase.analytics(); 49 | } 50 | } 51 | if (process.browser) { 52 | firebase.analytics(); 53 | } 54 | } 55 | 56 | init() { 57 | this.app; 58 | this.storage; 59 | } 60 | 61 | public static get instance(): Firebase { 62 | if (!this._instance) { 63 | this._instance = new Firebase(); 64 | } 65 | return this._instance; 66 | } 67 | 68 | public get app() { 69 | if (this._app) { 70 | return this._app; 71 | } else { 72 | this._app = firebase.app(); 73 | return this._app; 74 | } 75 | } 76 | 77 | public get storage() { 78 | if (this._storage) { 79 | return this._storage; 80 | } else { 81 | this._storage = firebase.storage(); 82 | return this._storage; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/pages/[pid].tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Head from "next/head"; 3 | import Link from "next/link"; 4 | import createHostingURL from "../helper/createHostingURL"; 5 | import cloudStorageKeys from "../constatns/cloudStorageKeys"; 6 | import env from "../helper/env"; 7 | import createGcsURL from "../helper/createGcsURL"; 8 | import { GetServerSideProps, NextPage } from "next"; 9 | 10 | export default function Result(props: NextPage & { pid: string }) { 11 | const [url, setURL] = React.useState(""); 12 | React.useEffect(() => { 13 | setURL(window.location.href); 14 | }, []); 15 | const appEnv = env(); 16 | return ( 17 |
18 | 19 | {"created OGP"} 20 | 26 | 30 | 31 | 35 | 36 | 37 | 41 | 47 | 48 |

生成された画像

49 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | 125 |
126 | ); 127 | } 128 | 129 | export const getServerSideProps: GetServerSideProps = async (context) => { 130 | const { pid } = context.query; 131 | return { props: { pid } }; 132 | }; 133 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @file _app.tsxはクライアントサイドで動くJSのエントリポイント. Firebaseの初期化処理など全ページにおける共通処理を書ける. 3 | */ 4 | 5 | import React, { useEffect } from "react"; 6 | import Firebase from "../infrastructure/Firebase"; 7 | import "../vendor/css/reset.css"; 8 | 9 | export default function App({ Component, pageProps }: any) { 10 | useEffect(() => { 11 | Firebase.instance.init(); 12 | }, []); 13 | return ( 14 | <> 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Document, { Head, Main, NextScript } from "next/document"; 3 | 4 | export default class MyDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 14 | 18 | 19 | 23 | 24 | 28 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useRouter } from "next/router"; 3 | import dynamic from "next/dynamic"; 4 | import domtoimage from "dom-to-image"; 5 | import { saveOgp } from "../repository/postPng"; 6 | import { generateRandomId } from "../helper/util"; 7 | import convert from "reactel-to-html"; 8 | import "../vendor/css/monaco.css"; 9 | import "../vendor/css/normal.css"; 10 | import sampleCode from "../constatns/sampleCode"; 11 | 12 | const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false }); 13 | 14 | type ModeType = "HTML" | "JSX"; 15 | 16 | const mediaQueries = { 17 | mobile: "(max-width: 767px)", 18 | prefersReducedMotion: "(prefers-reduced-motion: reduce)", 19 | }; 20 | 21 | function useMedia(query) { 22 | const [matches, setMatches] = React.useState( 23 | typeof window !== "undefined" ? window.matchMedia(query).matches : false 24 | ); 25 | 26 | React.useEffect(() => { 27 | const media = window.matchMedia(query); 28 | if (media.matches !== matches) { 29 | setMatches(media.matches); 30 | } 31 | 32 | const listener = () => setMatches(media.matches); 33 | media.addListener(listener); 34 | 35 | return () => media.removeListener(listener); 36 | }, [query]); 37 | 38 | return matches; 39 | } 40 | 41 | export default function Editor() { 42 | const mobileView = useMedia(mediaQueries.mobile); 43 | const [mode, setMode] = React.useState("HTML"); 44 | const [isMount, setMount] = React.useState(false); 45 | const router = useRouter(); 46 | const [text, edit] = React.useState(""); 47 | const [code, setHTML] = React.useState( 48 | mobileView ? sampleCode.html.mobile : sampleCode.html.pc 49 | ); 50 | 51 | React.useEffect(() => { 52 | setMount(true); 53 | edit(mobileView ? sampleCode.html.mobile : sampleCode.html.pc); 54 | }, []); 55 | 56 | React.useEffect(() => { 57 | if (isMount) { 58 | if (mode === "HTML") { 59 | edit(mobileView ? sampleCode.html.mobile : sampleCode.html.pc); 60 | setHTML(mobileView ? sampleCode.html.mobile : sampleCode.html.pc); 61 | } else if (mode === "JSX") { 62 | edit(mobileView ? sampleCode.jsx.mobile : sampleCode.jsx.pc); 63 | setHTML( 64 | mobileView 65 | ? convert(sampleCode.jsx.mobile) 66 | : convert(sampleCode.jsx.pc) 67 | ); 68 | } 69 | } else { 70 | setMount(true); 71 | } 72 | }, [mode]); 73 | 74 | const ref = React.useRef(null); 75 | 76 | const handleClick = () => { 77 | const scale = 2; 78 | const imageId = generateRandomId(); 79 | domtoimage 80 | .toPng(ref.current, { 81 | // NOTE: 画質対応 82 | // https://github.com/tsayen/dom-to-image/issues/69 83 | height: ref.current.offsetHeight * scale, 84 | width: ref.current.offsetWidth * scale, 85 | style: { 86 | transform: "scale(" + scale + ")", 87 | transformOrigin: "top left", 88 | width: ref.current.offsetWidth + "px", 89 | height: ref.current.offsetHeight + "px", 90 | }, 91 | }) 92 | .then((dataURL) => { 93 | const img = new Image(); 94 | img.src = dataURL; 95 | img.onload = () => { 96 | const canvas = document.createElement("canvas"); 97 | canvas.width = ref.current.offsetWidth * 2; 98 | canvas.height = ref.current.offsetHeight * 2; 99 | const ctx = canvas.getContext("2d"); 100 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 101 | // canvasをblobに変換し、FileSaverでダウンロードを行う 102 | canvas.toBlob(async (blob) => { 103 | await saveOgp(imageId, blob); 104 | router.push(`/${imageId}`); 105 | }); 106 | }; 107 | }) 108 | .catch(function (error) { 109 | console.error("oops, something went wrong!", error); 110 | }); 111 | }; 112 | 113 | return ( 114 |
115 |

116 | OGPNG 117 | beta 118 |

119 |

コードから画像作成してシェアできるサービス

120 |

121 | (注)現状の設計上、imgタグに外部URLを指定することができません。もし外部URLを使いたい場合は 122 | 127 | こちら 128 | 129 | で外部URLをdataURIに変換し、それをimgタグのsrcに指定してください。 130 |

131 |
132 |
133 |
134 | setMode("HTML")} 141 | /> 142 | 146 |
147 |
148 | setMode("JSX")} 155 | /> 156 | 160 |
161 |
162 |
163 | { 169 | edit(str); 170 | if (mode === "HTML") { 171 | setHTML(str); 172 | } else if (mode === "JSX") { 173 | try { 174 | setHTML(convert(str)); 175 | } catch { 176 | setHTML(str); 177 | } 178 | } 179 | }} 180 | editorDidMount={() => { 181 | // @ts-ignore 182 | window.MonacoEnvironment.getWorkerUrl = (moduleId, label) => { 183 | if (label === "json") return "/_next/static/json.worker.js"; 184 | if (label === "css") return "/_next/static/css.worker.js"; 185 | if (label === "html") return "/_next/static/html.worker.js"; 186 | if (label === "typescript" || label === "javascript") 187 | return "/_next/static/ts.worker.js"; 188 | return "/_next/static/editor.worker.js"; 189 | }; 190 | }} 191 | /> 192 |
193 |
194 |
200 |
201 |
202 | 206 |

207 | (注)beta版です。予告なくデータを消すことがあるかもしれません。他人を誹謗中傷する内容や公的良俗にそぐわない内容の投稿は禁止します。疑問などがございましたら 208 | 213 | @sadnessOjisan 214 | 215 | まで 216 |

217 | 410 |
411 | ); 412 | } 413 | -------------------------------------------------------------------------------- /src/repository/postPng.ts: -------------------------------------------------------------------------------- 1 | import Firebase from "../infrastructure/Firebase"; 2 | import CLOUD_STORAGE_KEYS from "../constatns/cloudStorageKeys"; 3 | 4 | export const saveOgp = (imageId: string, image: any): Promise<{ e: any }> => { 5 | const storage = Firebase.instance.storage; 6 | const storageRef = storage.ref(); 7 | // TODO: tokenを自分でセットすると、gcs使わなくて良さそう. https://twitter.com/axross_/status/1272603518755409920?s=20 8 | const ogpRef = storageRef.child(`${CLOUD_STORAGE_KEYS.OGP}/${imageId}`); 9 | return ogpRef 10 | .put(image) 11 | .then((snapshot) => { 12 | console.log("snapshot", snapshot); 13 | }) 14 | .catch((e) => { 15 | console.log("ERROR", e); 16 | return { e }; 17 | }); 18 | }; 19 | 20 | export const getOgpUrl = (imageId: string) => { 21 | const storage = Firebase.instance.storage; 22 | const pathReferenceRef = storage.ref(`${CLOUD_STORAGE_KEYS.OGP}/${imageId}`); 23 | return pathReferenceRef 24 | .getDownloadURL() 25 | .then((url: string) => url) 26 | .catch((e) => console.log("ERROR", e)); 27 | }; 28 | -------------------------------------------------------------------------------- /src/types/util/env.ts: -------------------------------------------------------------------------------- 1 | export type EnvType = "development" | "production" | "test"; 2 | -------------------------------------------------------------------------------- /src/types/util/firebase.ts: -------------------------------------------------------------------------------- 1 | export type FirebaseConfigType = { 2 | apiKey: string; 3 | authDomain: string; 4 | databaseURL: string; 5 | projectId: string; 6 | storageBucket: string; 7 | messagingSenderId: string; 8 | appId: string; 9 | measurementId: string; 10 | }; 11 | -------------------------------------------------------------------------------- /src/vendor/css/monaco.css: -------------------------------------------------------------------------------- 1 | .react-monaco-editor-container { 2 | width: 90% !important; 3 | height: 90% !important; 4 | margin-top: 5% !important; 5 | margin: auto; 6 | box-shadow: inset 2px 2px 5px #babecc, inset -5px -5px 10px #fff; 7 | } 8 | 9 | .view-lines { 10 | box-shadow: inset 2px 2px 5px #babecc, inset -5px -5px 10px #fff; 11 | } 12 | 13 | .margin { 14 | box-shadow: inset 2px 2px 5px #babecc, inset -5px -5px 10px #fff; 15 | background-color: #ebecf0 !important; 16 | } 17 | 18 | .monaco-editor-background { 19 | background-color: #ebecf0 !important; 20 | } 21 | 22 | .monaco-editor { 23 | background-color: #ebecf0 !important; 24 | } 25 | 26 | .margin { 27 | height: 100% !important; 28 | } 29 | 30 | .monaco-scrollable-element { 31 | height: 100% !important; 32 | box-shadow: inset 2px 2px 5px #babecc, inset -5px -5px 10px #fff; 33 | } 34 | 35 | .decorationsOverviewRuler { 36 | display: none; 37 | } 38 | 39 | .tree { 40 | background-color: #ebecf0 !important; 41 | border: none !important; 42 | box-shadow: inset 2px 2px 5px #babecc, inset -5px -5px 10px #fff !important; 43 | } 44 | 45 | .details { 46 | /* border: solid 1px red !important; 47 | box-shadow: 0px 0px 4px 1px red !important; */ 48 | border: none !important; 49 | box-shadow: inset 2px 2px 5px #babecc, inset -5px -5px 10px #fff !important; 50 | } 51 | 52 | .view-lines { 53 | height: 726px !important; 54 | } 55 | 56 | .margin-view-overlays { 57 | height: 726px !important; 58 | } 59 | -------------------------------------------------------------------------------- /src/vendor/css/normal.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #ebecf0; 3 | } 4 | -------------------------------------------------------------------------------- /src/vendor/css/reset.css: -------------------------------------------------------------------------------- 1 | /*! destyle.css v1.0.13 | MIT License | https://github.com/nicolas-cusan/destyle.css */ 2 | 3 | /* Reset box-model 4 | ========================================================================== */ 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | ::before, 11 | ::after { 12 | box-sizing: inherit; 13 | } 14 | 15 | /* Document 16 | ========================================================================== */ 17 | 18 | /** 19 | * 1. Correct the line height in all browsers. 20 | * 2. Prevent adjustments of font size after orientation changes in iOS. 21 | * 3. Remove gray overlay on links for iOS. 22 | */ 23 | 24 | html { 25 | line-height: 1.15; /* 1 */ 26 | -webkit-text-size-adjust: 100%; /* 2 */ 27 | -webkit-tap-highlight-color: transparent; /* 3*/ 28 | } 29 | 30 | /* Sections 31 | ========================================================================== */ 32 | 33 | /** 34 | * Remove the margin in all browsers. 35 | */ 36 | 37 | body { 38 | margin: 0; 39 | } 40 | 41 | /** 42 | * Render the `main` element consistently in IE. 43 | */ 44 | 45 | main { 46 | display: block; 47 | } 48 | 49 | /* Vertical rhythm 50 | ========================================================================== */ 51 | 52 | p, 53 | table, 54 | blockquote, 55 | address, 56 | pre, 57 | iframe, 58 | form, 59 | figure, 60 | dl { 61 | margin: 0; 62 | } 63 | 64 | /* Headings 65 | ========================================================================== */ 66 | 67 | h1, 68 | h2, 69 | h3, 70 | h4, 71 | h5, 72 | h6 { 73 | font-size: inherit; 74 | line-height: inherit; 75 | font-weight: inherit; 76 | margin: 0; 77 | } 78 | 79 | /* Lists (enumeration) 80 | ========================================================================== */ 81 | 82 | ul, 83 | ol { 84 | margin: 0; 85 | padding: 0; 86 | list-style: none; 87 | } 88 | 89 | /* Lists (definition) 90 | ========================================================================== */ 91 | 92 | dt { 93 | font-weight: bold; 94 | } 95 | 96 | dd { 97 | margin-left: 0; 98 | } 99 | 100 | /* Grouping content 101 | ========================================================================== */ 102 | 103 | /** 104 | * 1. Add the correct box sizing in Firefox. 105 | * 2. Show the overflow in Edge and IE. 106 | */ 107 | 108 | hr { 109 | box-sizing: content-box; /* 1 */ 110 | height: 0; /* 1 */ 111 | overflow: visible; /* 2 */ 112 | border: 0; 113 | border-top: 1px solid; 114 | margin: 0; 115 | clear: both; 116 | color: inherit; 117 | } 118 | 119 | /** 120 | * 1. Correct the inheritance and scaling of font size in all browsers. 121 | * 2. Correct the odd `em` font sizing in all browsers. 122 | */ 123 | 124 | pre { 125 | font-family: monospace, monospace; /* 1 */ 126 | font-size: inherit; /* 2 */ 127 | } 128 | 129 | address { 130 | font-style: inherit; 131 | } 132 | 133 | /* Text-level semantics 134 | ========================================================================== */ 135 | 136 | /** 137 | * Remove the gray background on active links in IE 10. 138 | */ 139 | 140 | a { 141 | background-color: transparent; 142 | text-decoration: none; 143 | color: inherit; 144 | } 145 | 146 | /** 147 | * 1. Remove the bottom border in Chrome 57- 148 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 149 | */ 150 | 151 | abbr[title] { 152 | border-bottom: none; /* 1 */ 153 | text-decoration: underline; /* 2 */ 154 | text-decoration: underline dotted; /* 2 */ 155 | } 156 | 157 | /** 158 | * Add the correct font weight in Chrome, Edge, and Safari. 159 | */ 160 | 161 | b, 162 | strong { 163 | font-weight: bolder; 164 | } 165 | 166 | /** 167 | * 1. Correct the inheritance and scaling of font size in all browsers. 168 | * 2. Correct the odd `em` font sizing in all browsers. 169 | */ 170 | 171 | code, 172 | kbd, 173 | samp { 174 | font-family: monospace, monospace; /* 1 */ 175 | font-size: inherit; /* 2 */ 176 | } 177 | 178 | /** 179 | * Add the correct font size in all browsers. 180 | */ 181 | 182 | small { 183 | font-size: 80%; 184 | } 185 | 186 | /** 187 | * Prevent `sub` and `sup` elements from affecting the line height in 188 | * all browsers. 189 | */ 190 | 191 | sub, 192 | sup { 193 | font-size: 75%; 194 | line-height: 0; 195 | position: relative; 196 | vertical-align: baseline; 197 | } 198 | 199 | sub { 200 | bottom: -0.25em; 201 | } 202 | 203 | sup { 204 | top: -0.5em; 205 | } 206 | 207 | /* Embedded content 208 | ========================================================================== */ 209 | 210 | /** 211 | * Remove the border on images inside links in IE 10. 212 | */ 213 | 214 | img { 215 | border-style: none; 216 | vertical-align: bottom; 217 | } 218 | 219 | embed, 220 | object, 221 | iframe { 222 | border: 0; 223 | vertical-align: bottom; 224 | } 225 | 226 | /* Forms 227 | ========================================================================== */ 228 | 229 | /** 230 | * Reset form fields to make them styleable 231 | * 1. Reset radio and checkbox to preserve their look in iOS. 232 | */ 233 | 234 | button, 235 | input, 236 | optgroup, 237 | select, 238 | textarea { 239 | -webkit-appearance: none; 240 | appearance: none; 241 | vertical-align: middle; 242 | color: inherit; 243 | font: inherit; 244 | border: 0; 245 | background: transparent; 246 | padding: 0; 247 | margin: 0; 248 | outline: 0; 249 | border-radius: 0; 250 | text-align: inherit; 251 | } 252 | 253 | [type="checkbox"] { 254 | /* 1 */ 255 | -webkit-appearance: checkbox; 256 | appearance: checkbox; 257 | } 258 | 259 | [type="radio"] { 260 | /* 1 */ 261 | -webkit-appearance: radio; 262 | appearance: radio; 263 | } 264 | 265 | /** 266 | * Show the overflow in IE. 267 | * 1. Show the overflow in Edge. 268 | */ 269 | 270 | button, 271 | input { 272 | /* 1 */ 273 | overflow: visible; 274 | } 275 | 276 | /** 277 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 278 | * 1. Remove the inheritance of text transform in Firefox. 279 | */ 280 | 281 | button, 282 | select { 283 | /* 1 */ 284 | text-transform: none; 285 | } 286 | 287 | /** 288 | * Correct the inability to style clickable types in iOS and Safari. 289 | */ 290 | 291 | button, 292 | [type="button"], 293 | [type="reset"], 294 | [type="submit"] { 295 | cursor: pointer; 296 | -webkit-appearance: none; 297 | appearance: none; 298 | } 299 | 300 | button[disabled], 301 | [type="button"][disabled], 302 | [type="reset"][disabled], 303 | [type="submit"][disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove the inner border and padding in Firefox. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | [type="button"]::-moz-focus-inner, 313 | [type="reset"]::-moz-focus-inner, 314 | [type="submit"]::-moz-focus-inner { 315 | border-style: none; 316 | padding: 0; 317 | } 318 | 319 | /** 320 | * Restore the focus styles unset by the previous rule. 321 | */ 322 | 323 | button:-moz-focusring, 324 | [type="button"]:-moz-focusring, 325 | [type="reset"]:-moz-focusring, 326 | [type="submit"]:-moz-focusring { 327 | outline: 1px dotted ButtonText; 328 | } 329 | 330 | /** 331 | * Remove padding 332 | */ 333 | 334 | option { 335 | padding: 0; 336 | } 337 | 338 | /** 339 | * Reset to invisible 340 | */ 341 | 342 | fieldset { 343 | margin: 0; 344 | padding: 0; 345 | border: 0; 346 | min-width: 0; 347 | } 348 | 349 | /** 350 | * 1. Correct the text wrapping in Edge and IE. 351 | * 2. Correct the color inheritance from `fieldset` elements in IE. 352 | * 3. Remove the padding so developers are not caught out when they zero out 353 | * `fieldset` elements in all browsers. 354 | */ 355 | 356 | legend { 357 | color: inherit; /* 2 */ 358 | display: table; /* 1 */ 359 | max-width: 100%; /* 1 */ 360 | padding: 0; /* 3 */ 361 | white-space: normal; /* 1 */ 362 | } 363 | 364 | /** 365 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 366 | */ 367 | 368 | progress { 369 | vertical-align: baseline; 370 | } 371 | 372 | /** 373 | * Remove the default vertical scrollbar in IE 10+. 374 | */ 375 | 376 | textarea { 377 | overflow: auto; 378 | } 379 | 380 | /** 381 | * 1. Remove the padding in IE 10. 382 | */ 383 | 384 | [type="checkbox"], 385 | [type="radio"] { 386 | padding: 0; /* 1 */ 387 | } 388 | 389 | /** 390 | * Correct the cursor style of increment and decrement buttons in Chrome. 391 | */ 392 | 393 | [type="number"]::-webkit-inner-spin-button, 394 | [type="number"]::-webkit-outer-spin-button { 395 | height: auto; 396 | } 397 | 398 | /** 399 | * 1. Correct the outline style in Safari. 400 | */ 401 | 402 | [type="search"] { 403 | outline-offset: -2px; /* 1 */ 404 | } 405 | 406 | /** 407 | * Remove the inner padding in Chrome and Safari on macOS. 408 | */ 409 | 410 | [type="search"]::-webkit-search-decoration { 411 | -webkit-appearance: none; 412 | } 413 | 414 | /** 415 | * 1. Correct the inability to style clickable types in iOS and Safari. 416 | * 2. Change font properties to `inherit` in Safari. 417 | */ 418 | 419 | ::-webkit-file-upload-button { 420 | -webkit-appearance: button; /* 1 */ 421 | font: inherit; /* 2 */ 422 | } 423 | 424 | /** 425 | * Clickable labels 426 | */ 427 | 428 | label[for] { 429 | cursor: pointer; 430 | } 431 | 432 | /* Interactive 433 | ========================================================================== */ 434 | 435 | /* 436 | * Add the correct display in Edge, IE 10+, and Firefox. 437 | */ 438 | 439 | details { 440 | display: block; 441 | } 442 | 443 | /* 444 | * Add the correct display in all browsers. 445 | */ 446 | 447 | summary { 448 | display: list-item; 449 | } 450 | 451 | /* Table 452 | ========================================================================== */ 453 | 454 | table { 455 | border-collapse: collapse; 456 | border-spacing: 0; 457 | } 458 | 459 | caption { 460 | text-align: left; 461 | } 462 | 463 | td, 464 | th { 465 | vertical-align: top; 466 | padding: 0; 467 | } 468 | 469 | th { 470 | text-align: left; 471 | font-weight: bold; 472 | } 473 | 474 | /* Misc 475 | ========================================================================== */ 476 | 477 | /** 478 | * Add the correct display in IE 10+. 479 | */ 480 | 481 | template { 482 | display: none; 483 | } 484 | 485 | /** 486 | * Add the correct display in IE 10. 487 | */ 488 | 489 | [hidden] { 490 | display: none; 491 | } 492 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /{allPaths=**} { 5 | allow read, create; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "types": ["node", "jest"] 17 | }, 18 | 19 | "exclude": ["node_modules"], 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] 21 | } 22 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadnessOjisan/ogpng/302e718ae6e69d93ad6942339412f04c6c13ad21/types/global.d.ts --------------------------------------------------------------------------------