├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── config └── config.exs ├── lib ├── open_graph.ex └── open_graph │ └── error.ex ├── mix.exs ├── mix.lock ├── ogp.livemd └── test ├── open_graph_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: OTP ${{matrix.pair.otp}} / Elixir ${{matrix.pair.elixir}} 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - pair: 18 | elixir: "1.13" 19 | otp: "24.3.4.10" 20 | - pair: 21 | elixir: "1.17" 22 | otp: "27.0.1" 23 | lint: lint 24 | 25 | env: 26 | MIX_ENV: test 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: erlef/setup-beam@main 31 | with: 32 | otp-version: ${{ matrix.pair.otp }} 33 | elixir-version: ${{ matrix.pair.elixir }} 34 | version-type: strict 35 | 36 | - run: mix deps.get --check-locked 37 | - run: mix format --check-formatted 38 | - run: mix test --slowest 5 39 | - run: mix coveralls.github 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ogp-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yejun Su 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ogp 2 | 3 | The [Open Graph protocol](https://ogp.me/) library in Elixir. 4 | 5 | [](https://github.com/goofansu/ogp/actions/workflows/ci.yml) 6 | [](https://coveralls.io/github/goofansu/ogp?branch=main) 7 | [](https://hex.pm/packages/ogp) 8 | 9 | ## Installation 10 | 11 | ```elixir 12 | def deps do 13 | [ 14 | {:ogp, "~> 1.1.0"} 15 | ] 16 | end 17 | ``` 18 | 19 | ## Usage 20 | 21 | It is recommended to run [ogp.livemd](https://github.com/goofansu/ogp/blob/main/ogp.livemd) in [Livebook](https://github.com/elixir-nx/livebook) for more details. 22 | 23 | ### Parse HTML 24 | 25 | ```elixir 26 | iex> html = """ 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | """ 42 | iex> OpenGraph.parse(html) 43 | %OpenGraph{ 44 | audio: "https://example.com/bond/theme.mp3", 45 | description: "Sean Connery found fame and fortune as the\n suave, sophisticated British agent, James Bond.", 46 | determiner: "the", 47 | image: "https://ia.media-imdb.com/images/rock.jpg", 48 | locale: "en_GB", 49 | site_name: "IMDb", 50 | title: "The Rock", 51 | type: "video.movie", 52 | url: "https://www.imdb.com/title/tt0117500/", 53 | video: "https://example.com/bond/trailer.swf" 54 | } 55 | ``` 56 | 57 | ### Fetch URL 58 | 59 | ```elixir 60 | iex> OpenGraph.fetch!("https://github.com") 61 | %OpenGraph{ 62 | audio: nil, 63 | description: "GitHub is where over 65 million developers shape the future of software, together. Contribute to the open source community, manage your Git repositories, review code like a pro, track bugs and feat...", 64 | determiner: nil, 65 | image: "https://github.githubassets.com/images/modules/site/social-cards/github-social.png", 66 | locale: nil, 67 | site_name: "GitHub", 68 | title: "GitHub: Where the world builds software", 69 | type: "object", 70 | url: "https://github.com/", 71 | video: nil 72 | } 73 | ``` 74 | 75 | Redirects are followed automatically by default. 76 | 77 | ```elixir 78 | iex> OpenGraph.fetch!("https://producthunt.com") 79 | 80 | [debug] redirecting to https://www.producthunt.com/ 81 | %OpenGraph{ 82 | title: " Product Hunt – The best new products in tech. ", 83 | type: "article", 84 | image: "https://ph-static.imgix.net/ph-logo-1.png", 85 | url: "https://www.producthunt.com/", 86 | audio: nil, 87 | description: "Product Hunt is a curation of the best new products, every day. Discover the latest mobile apps, websites, and technology products that everyone's talking about.", 88 | determiner: nil, 89 | locale: "en_US", 90 | site_name: "Product Hunt", 91 | video: nil 92 | } 93 | ``` 94 | 95 | You can control redirects by configuring `req_options`. 96 | 97 | - Disable redirects: 98 | 99 | ```elixir 100 | config :ogp, 101 | req_options: [ 102 | redirect: false 103 | ] 104 | ``` 105 | 106 | - Set a different `max_redirects` (default is `10`): 107 | 108 | ```elixir 109 | config :ogp, 110 | req_options: [ 111 | max_redirects: 3 112 | ] 113 | ``` 114 | 115 | See https://hexdocs.pm/req/Req.html#new/1-options for the full `req` options. 116 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if config_env() == :test do 4 | config :ogp, 5 | req_options: [ 6 | plug: {Req.Test, MyStub}, 7 | retry: false 8 | ] 9 | end 10 | -------------------------------------------------------------------------------- /lib/open_graph.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenGraph do 2 | defstruct [ 3 | # Basic Metadata 4 | :title, 5 | :type, 6 | :image, 7 | :url, 8 | # Optional Metadata 9 | :audio, 10 | :description, 11 | :determiner, 12 | :locale, 13 | :site_name, 14 | :video 15 | ] 16 | 17 | @type url() :: URI.t() | String.t() 18 | @type value() :: String.t() | nil 19 | 20 | @type t() :: %__MODULE__{ 21 | title: value(), 22 | type: value(), 23 | image: value(), 24 | url: value(), 25 | audio: value(), 26 | description: value(), 27 | determiner: value(), 28 | locale: value(), 29 | site_name: value(), 30 | video: value() 31 | } 32 | 33 | @doc """ 34 | Fetch URL and parse Open Graph protocol. 35 | 36 | Returns `{:ok, %OpenGraph{}}` for succussful request, otherwise, returns `{:error, %OpenGraph.Error{}}`. 37 | """ 38 | 39 | @spec fetch(url(), req_options :: keyword()) :: 40 | {:ok, OpenGraph.t()} | {:error, OpenGraph.Error.t()} 41 | def fetch(url, req_options \\ []) do 42 | options = Keyword.merge(req_options, Application.get_env(:ogp, :req_options, [])) 43 | 44 | url 45 | |> Req.get(options) 46 | |> handle_response() 47 | end 48 | 49 | defp handle_response({:ok, %Req.Response{status: status, body: body}}) 50 | when status in 200..299 and is_binary(body) do 51 | {:ok, parse(body)} 52 | end 53 | 54 | defp handle_response({:ok, %Req.Response{status: status, body: body}}) 55 | when status in 200..299 do 56 | {:error, %OpenGraph.Error{reason: {:unexpected_format, body}}} 57 | end 58 | 59 | defp handle_response({:ok, %Req.Response{status: status}}) when status in 300..399 do 60 | {:error, %OpenGraph.Error{reason: {:missing_redirect_location, status}}} 61 | end 62 | 63 | defp handle_response({:ok, %Req.Response{status: status}}) do 64 | {:error, %OpenGraph.Error{reason: {:unexpected_status_code, status}}} 65 | end 66 | 67 | defp handle_response({:error, error}) do 68 | {:error, %OpenGraph.Error{reason: {:request_error, Exception.message(error)}}} 69 | end 70 | 71 | @doc """ 72 | Similar to `fetch/2` but raises an `OpenGraph.Error` if request failed. 73 | 74 | Returns `%OpenGraph{}`. 75 | """ 76 | @spec fetch!(url(), req_options :: keyword()) :: OpenGraph.t() 77 | def fetch!(url, req_options \\ []) do 78 | case fetch(url, req_options) do 79 | {:ok, result} -> 80 | result 81 | 82 | {:error, error} -> 83 | raise error 84 | end 85 | end 86 | 87 | @doc """ 88 | Parse Open Graph protocol. 89 | 90 | Returns `%OpenGraph{}`. 91 | 92 | ## Examples 93 | 94 | iex> OpenGraph.parse("") 95 | %OpenGraph{title: "GitHub"} 96 | """ 97 | @spec parse(String.t()) :: OpenGraph.t() 98 | def parse(html) do 99 | {:ok, document} = Floki.parse_document(html) 100 | og_elements = Floki.find(document, "meta[property^='og:'][content]") 101 | properties = Floki.attribute(og_elements, "property") 102 | contents = Floki.attribute(og_elements, "content") 103 | 104 | fields = 105 | [properties, contents] 106 | |> List.zip() 107 | |> Enum.reduce(%{}, &put_field/2) 108 | 109 | struct(__MODULE__, fields) 110 | end 111 | 112 | defp put_field({"og:" <> property, content}, acc) do 113 | Map.put_new(acc, String.to_existing_atom(property), content) 114 | rescue 115 | ArgumentError -> 116 | acc 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/open_graph/error.ex: -------------------------------------------------------------------------------- 1 | defmodule OpenGraph.Error do 2 | defexception [:reason] 3 | 4 | @type status_code() :: integer() 5 | 6 | @type reason() :: 7 | {:missing_redirect_location, status_code()} 8 | | {:unexpected_status_code, status_code()} 9 | | {:request_error, String.t()} 10 | 11 | @type t() :: %__MODULE__{reason: reason()} 12 | 13 | @impl true 14 | def message(%__MODULE__{reason: reason}) do 15 | format_reason(reason) 16 | end 17 | 18 | defp format_reason({:missing_redirect_location, status_code}) do 19 | "redirect response is received but location not found in HTTP headers. HTTP status code: #{status_code}" 20 | end 21 | 22 | defp format_reason({:unexpected_status_code, status_code}) do 23 | "unexpected response is received. HTTP status code: #{status_code}" 24 | end 25 | 26 | defp format_reason({:unexpected_format, body}) do 27 | "unexpected response format is received. body: #{inspect(body)}" 28 | end 29 | 30 | defp format_reason({:request_error, reason}) do 31 | "request error. reason: #{reason}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule OpenGraph.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ogp, 7 | version: "1.1.1", 8 | elixir: "~> 1.13", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | test_coverage: [tool: ExCoveralls], 12 | 13 | # Hex 14 | description: "The Open Graph protocol library in Elixir.", 15 | package: package(), 16 | docs: docs() 17 | ] 18 | end 19 | 20 | defp package do 21 | [ 22 | name: "ogp", 23 | licenses: ["MIT"], 24 | links: %{"GitHub" => "https://github.com/goofansu/ogp"}, 25 | source_url: "https://github.com/goofansu/ogp", 26 | homepage_url: "https://github.com/goofansu/ogp" 27 | ] 28 | end 29 | 30 | defp docs do 31 | [ 32 | main: "readme", 33 | extras: ["README.md"] 34 | ] 35 | end 36 | 37 | # Run "mix help compile.app" to learn about applications. 38 | def application do 39 | [ 40 | extra_applications: [:logger] 41 | ] 42 | end 43 | 44 | # Run "mix help deps" to learn about dependencies. 45 | defp deps do 46 | [ 47 | {:req, "~> 0.5"}, 48 | {:floki, "~> 0.35"}, 49 | {:plug, "~> 1.16", only: :test}, 50 | {:excoveralls, "~> 0.18.2", only: :test}, 51 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 52 | ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 4 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 5 | "excoveralls": {:hex, :excoveralls, "0.18.2", "86efd87a0676a3198ff50b8c77620ea2f445e7d414afa9ec6c4ba84c9f8bdcc2", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "230262c418f0de64077626a498bd4fdf1126d5c2559bb0e6b43deac3005225a4"}, 6 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 7 | "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, 8 | "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, 9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 13 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 14 | "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, 15 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 17 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 18 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 19 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 20 | "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, 21 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 22 | } 23 | -------------------------------------------------------------------------------- /ogp.livemd: -------------------------------------------------------------------------------- 1 | # ogp 2 | 3 | ## Install 4 | 5 | ```elixir 6 | Mix.install([{:ogp, "~> 1.0.0"}]) 7 | ``` 8 | 9 | ## Usage 10 | 11 | ### Parse HTML 12 | 13 | #### [Basic metadata](https://ogp.me/#metadata) 14 | 15 | ```elixir 16 | # Basic Metadata 17 | 18 | basic_metadata = """ 19 | 20 | 21 | 22 | 23 | 24 | """ 25 | 26 | OpenGraph.parse(basic_metadata) 27 | ``` 28 | 29 | #### [Optional metadata](https://ogp.me/#optional) 30 | 31 | ```elixir 32 | # Optional Metadata 33 | 34 | optional_metadata = """ 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | """ 45 | 46 | OpenGraph.parse(optional_metadata) 47 | ``` 48 | 49 | ### Fetch URL 50 | 51 | #### Input URL 52 | 53 | 54 | 55 | ```elixir 56 | url = IO.gets("URL") |> String.trim("\n") 57 | OpenGraph.fetch!(url) 58 | ``` 59 | -------------------------------------------------------------------------------- /test/open_graph_test.exs: -------------------------------------------------------------------------------- 1 | defmodule OpenGraphTest do 2 | use ExUnit.Case, async: true 3 | doctest OpenGraph 4 | 5 | import OpenGraph 6 | alias OpenGraph.Error 7 | 8 | @html """ 9 | 10 |
11 |