{"id":13552720,"url":"https://github.com/dwyl/hits","last_synced_at":"2025-04-03T03:32:47.108Z","repository":{"id":36806006,"uuid":"41112850","full_name":"dwyl/hits","owner":"dwyl","description":":chart_with_upwards_trend: General purpose hits (page views) counter ","archived":false,"fork":false,"pushed_at":"2025-03-31T19:49:38.000Z","size":1076,"stargazers_count":444,"open_issues_count":31,"forks_count":62,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-03-31T20:35:01.069Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://hits.dwyl.com","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dwyl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-08-20T18:39:29.000Z","updated_at":"2025-03-31T19:49:35.000Z","dependencies_parsed_at":"2023-11-08T06:45:07.005Z","dependency_job_id":"41c8bc04-b634-4441-91f8-18e8f2259fa9","html_url":"https://github.com/dwyl/hits","commit_stats":{"total_commits":498,"total_committers":19,"mean_commits":"26.210526315789473","dds":0.7329317269076305,"last_synced_commit":"d9fbe2df2665565ca8470db16bac0bd2916b3021"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fhits","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fhits/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fhits/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fhits/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dwyl","download_url":"https://codeload.github.com/dwyl/hits/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246933421,"owners_count":20857049,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T12:02:08.458Z","updated_at":"2025-04-03T03:32:42.071Z","avatar_url":"https://github.com/dwyl.png","language":"Elixir","readme":"# Hits\n\n![hits-dwyl-teal-banner](https://user-images.githubusercontent.com/194400/30136430-d1b2c2b8-9356-11e7-9ed5-3d84f6e44066.png)\n\n\u003cdiv align=\"center\"\u003e\n\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/hits/ci.yml?label=build\u0026style=flat-square\u0026branch=main)\n[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/hits/master.svg?style=flat-square)](https://codecov.io/github/dwyl/hits?branch=master)\n[![HitCount](https://hits.dwyl.com/dwyl/hits.svg)](https://github.com/dwyl/hits)\n[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/hits/issues/74)\n\n\u003c!-- Docs badge not working ... if you have time to help investigate, please do.\n[![Inline docs](https://inch-ci.org/github/dwyl/hits.svg?style=flat-square)](https://inch-ci.org/github/dwyl/hits)\n--\u003e\n\n\u003c/div\u003e\n\n\n## Why?\n\n@dwyl we have a _few_ projects on GitHub ... \u003cbr /\u003e\n\nWe want to _instantly see_ the _popularity_\nof _each_ of our repos\nto know what people are finding _useful_ and help us\ndecide where we need to be investing our time.\n\nWhile GitHub has a _basic_\n\"[traffic](https://github.com/blog/1672-introducing-github-traffic-analytics)\"\n[tab](https://github.com/dwyl/start-here/graphs/traffic)\nwhich displays page view stats, GitHub only records the data\nfor the [_past 14 days_](https://github.com/dwyl/hits/issues/49)\nand then it gets reset.\nThe data is not relayed to the \"owner\" in \"***real time***\"\nand you would need to use the API and \"poll\" for data ...\n_Manually_ checking who has viewed a\nproject is _exceptionally_ tedious when you have\nmore than a _handful_ of projects.\n\n\u003c!--\n### Scratch Your Own Itch?\n\nSome people claim that scratching your own itch is a \"terrible idea\" ...\nMost of those people have never built anything.\n\nWe think Jessica Livingston (YCombinator Co-founder)\nknows more about people successfully building ideas than _anyone_ else.\n\n\u003e \"_**Solve** a **problem** that you **yourself have**\nthen you will at least know\nthat it's something that **at least one person really wants**.\nAnd when you are **part** of the **target market** you will have **insights**\nabout it that you wouldn't otherwise._\"\n~ **Jessica Livingston** https://youtu.be/a2B4cVFIVpg?t=2m56s\n\nHits solves a problem for _us_ @dwyl\nand we make the service available to others.\nWe are very much building a \"scratcher\" for our own itch,\nand making it available to anyone else who wants it.\n\nRead:\nhttps://hbr.org/2014/05/when-scratch-your-own-itch-is-dangerous-advice-for-entrepreneurs\n--\u003e\n\n\u003cbr /\u003e\n\n### Why Phoenix (Elixir + PostgreSQL/Ecto)?\n\nWe wrote our MVP in `Node.js`, see:\nhttps://github.com/dwyl/hits-nodejs \u003cbr /\u003e\nThat worked quite well to test the idea while writing minimal code.\n\nWe decided to re-write in `Elixir`/`Phoenix` because we want\nthe reliability and fault tolerance of `Erlang`,\nbuilt-in application monitoring\n([`supervisor`](https://erlang.org/doc/man/supervisor.html))\nand metrics ([`telemetry`](https://github.com/beam-telemetry/telemetry))\nand the built-in support for _highly_ scalable WebSockets\nthat will allow us to build an _awesome_ real-time UX!\n\nFor more on \"Why Elixir?\" see:\nhttps://github.com/dwyl/learn-elixir/issues/102\n\n\n\u003cbr /\u003e\n\n## What?\n\nA _simple \u0026 easy_ way to see how many people\nhave _viewed_ your GitHub Repository.\n\nThere are already *many* \"badges\" that people use in their repos.\nSee: [github.com/dwyl/**repo-badges**](https://github.com/dwyl/repo-badges) \u003cbr /\u003e\nBut we haven't seen one that gives a \"***hit counter***\"\nof the number of times a GitHub page has been viewed ... \u003cbr /\u003e\nSo, in today's mini project we're going to _create_ a _basic **Web Counter**_.\nhttps://en.wikipedia.org/wiki/Web_counter\nThe counter is incremented only when the user agent or the ip addres is different.\nWhen testing the counter you can open a new browser to see the badge changed.\n\n#### A Fully Working Production Phoenix App _And_ Step-by-Step Tutorial?\n\nYes, that's right!\nNot only is this a fully functioning web app\nthat is serving _millions_ of requests per day\nin production _right_ now,\nit's also a step-by-step example/tutorial\nshowing you _exactly_\nhow it's implemented.\n\n\u003cbr /\u003e\n\n## How?\n\n\u003e If you simply want to display a \"hit count badge\"\nin your project's GitHub page, visit:\nhttps://hits.dwyl.com\nto get the Markdown!\n\n\n\n### _Run_ the App on `localhost`\n\nTo _run_ the app on your localhost follow these easy steps:\n\n#### 0. Ensure your `localhost` has Node.js \u0026 Phoenix installed\n\nsee: [before you start](https://github.com/dwyl/phoenix-chat-example#0-pre-requisites-before-you-start)\n\n\n#### 1. Clone/Download the Code\n\n```\ngit clone https://github.com/dwyl/hits.git \u0026\u0026 cd hits\n```\n\n#### 2. Install the Dependencies\n\nInstall elixir/node dependencies\nand setup Webpack static asset compilation (_with hot reloading_):\n\n```\nmix deps.get\ncd assets \u0026\u0026 npm install\nnode node_modules/webpack/bin/webpack.js --mode development \u0026\u0026 cd ..\n```\n\n#### 3. Create the database\n\n```\nmix ecto.create \u0026\u0026 mix ecto.migrate\n```\n\n### 4. Run the App\n\n```\nmix phx.server\n```\n\nThat's it! \u003cbr /\u003e\n\n\nVisit: http://localhost:4000/ (_in your web browser_)\n\n![hits-homepage-phoenix](https://user-images.githubusercontent.com/194400/57912373-0b2b9d00-7882-11e9-8dfd-df1021e9d076.png)\n\n\nOr visit _any_ endpoint that includes `.svg` in the url,\ne.g: http://localhost:4000/yourname/project.svg\n\n![hits-example-badge](https://user-images.githubusercontent.com/194400/57980413-57faa980-7a23-11e9-91cd-cc9e106be1ee.png)\n\nRefresh the page a few times and watch the count go up!\n\n![hit-count-42](https://user-images.githubusercontent.com/194400/57980416-62b53e80-7a23-11e9-948a-7c423ecb18c1.png)\n\n\u003e note: the \"Zoom\" in chrome to 500% for _effect_.\n\n\nNow, take your time to peruse the code in `/test` and `/lib`,\nand _ask_ any questions by opening GitHub Issues:\nhttps://github.com/dwyl/hits/issues\n\n\n### Run the Tests\n\nTo run the tests on your localhost,\nexecute the following command in your terminal:\n\n```elixir\nmix test\n```\n\nTo run the tests with coverage,\nrun the following command\nin your terminal:\n\n```elixir\nMIX_ENV=test mix cover\n```\n\nIf you want to view the coverage in a web browser:\n\n```elixir\nmix coveralls.html \u0026\u0026 open cover/excoveralls.html\n```\n\n\u003cbr /\u003e \u003cbr /\u003e\n\n\n# _Implementation_\n\nThis is a step-by-step guide\nto _building_ the Hits App\nfrom scratch\nin Phoenix.\n\n\n### Assumptions / Prerequisites\n\n+ [x] `Elixir` \u0026 `Phoenix` installed.\nsee: [**_before_ you start**](https://github.com/dwyl/phoenix-chat-example#0-pre-requisites-before-you-start)\n+ [x] Basic knowledge/understanding of `Elixir` syntax:\nhttps://github.com/dwyl/learn-elixir#how\n+ [x] Basic understanding of `Phoenix`:\nhttps://github.com/dwyl/learn-phoenix-framework\n+ [x] Basic PostgreSQL knowledge:\n[github.com/dwyl/**learn-postgresql**](https://github.com/dwyl/learn-postgresql)\n+ [x] Test Driven Development (TDD):\n[github.com/dwyl/**learn-tdd**](https://github.com/dwyl/learn-tdd)\n\n## Create New Phoenix App\n\n\n```sh\nmix phx.new hits\n```\nWhen prompted to install the dependencies:\n```sh\nFetch and install dependencies? [Yn]\n```\nType `Y` and the `Enter` key to install.\n\nYou should see something like this in your terminal:\n```sh\n* running mix deps.get\n* running cd assets \u0026\u0026 npm install \u0026\u0026 node node_modules/webpack/bin/webpack.js --mode development\n* running mix deps.compile\n\nWe are almost there! The following steps are missing:\n\n    $ cd hits\n\nThen configure your database in config/dev.exs and run:\n\n    $ mix ecto.create\n\nStart your Phoenix app with:\n\n    $ mix phx.server\n\nYou can also run your app inside IEx (Interactive Elixir) as:\n\n    $ iex -S mix phx.server\n```\n\nFollow the instructions (run the following commands)\nto create the PostgreSQL database for the app:\n\n```sh\ncd hits\nmix ecto.create\n```\n\nYou should see the following in your terminal:\n\n```sh\nCompiling 13 files (.ex)\nGenerated hits app\nThe database for Hits.Repo has already been created\n```\n\nRun the default tests to confirm everything is working:\n\n```sh\nmix test\n```\nYou should see the following output\n```sh\nGenerated hits app\n...\n\nFinished in 0.03 seconds\n3 tests, 0 failures\n\nRandomized with seed 98214\n```\n\n\nStart the Phoenix server:\n```sh\nmix phx.server\n```\n\nThat spits out a bunch of data about Webpack compilation:\n```sh\n[info] Running HitsWeb.Endpoint with cowboy 2.6.3 at 0.0.0.0:4000 (http)\n[info] Access HitsWeb.Endpoint at http://localhost:4000\n\nWebpack is watching the files…\n\nHash: 1fc94cc9b786e491ad40\nVersion: webpack 4.4.0\nTime: 609ms\nBuilt at: 05/05/2019 08:58:46\n                Asset       Size       Chunks             Chunk Names\n       ../css/app.css   10.6 KiB  ./js/app.js  [emitted]  ./js/app.js\n               app.js   7.26 KiB  ./js/app.js  [emitted]  ./js/app.js\n       ../favicon.ico   1.23 KiB               [emitted]\n        ../robots.txt  202 bytes               [emitted]\n../images/phoenix.png   13.6 KiB               [emitted]\n   [0] multi ./js/app.js 28 bytes {./js/app.js} [built]\n[../deps/phoenix_html/priv/static/phoenix_html.js] 2.21 KiB {./js/app.js} [built]\n[./css/app.css] 39 bytes {./js/app.js} [built]\n[./js/app.js] 493 bytes {./js/app.js} [built]\n    + 2 hidden modules\nChild mini-css-extract-plugin node_modules/css-loader/dist/cjs.js!css/app.css:\n    [./node_modules/css-loader/dist/cjs.js!./css/app.css] 284 bytes {mini-css-extract-plugin} [built]\n    [./node_modules/css-loader/dist/cjs.js!./css/phoenix.css] 10.9 KiB {mini-css-extract-plugin} [built]\n        + 1 hidden module\n```\n\nVisit the app in your web browser to confirm it's all working:\nhttp://localhost:4000\n![phoenix-app-default-homepage](https://user-images.githubusercontent.com/194400/57190794-71293380-6f16-11e9-8df3-1fb87139e6a3.png)\n\nThe default Phoenix App home page\nshould be familiar to you\nif you followed our Chat example/tutorial\n[github.com/dwyl/**phoenix-chat-example**](https://github.com/dwyl/phoenix-chat-example)\n\n\n## Create the _Static_ Home Page\n\nIn order to help people understand what Hits is\nand how they can add a counter badge to their project,\nwe have a simple (_static_) home page.\nIn the interest of doing a \"feature parity\" migration\nfrom the Node.js MVP to the Phoenix version,\nwe are just copying over the\n[`index.html`](https://github.com/dwyl/hits/blob/0a44edd692b5b765c20c85ed4057a50bbd872507/lib/index.html)\nat this stage; we can/will enhance it later.\n\nPhoenix has the concept of a Layout template\nwhich allows us to put all layout related\ncode in a single file and\nthen each subsequent page of content\ndoes not have to worry about static (CSS/JS) assets\nand metadata.\nOpen the file\n`/lib/hits_web/templates/layout/app.html.eex`\nin your text editor. It should look like this:\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"/\u003e\n    \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/\u003e\n    \u003ctitle\u003eHits · Phoenix Framework\u003c/title\u003e\n    \u003clink rel=\"stylesheet\" href=\"\u003c%= Routes.static_path(@conn, \"/css/app.css\") %\u003e\"/\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cheader\u003e\n      \u003csection class=\"container\"\u003e\n        \u003cnav role=\"navigation\"\u003e\n          \u003cul\u003e\n            \u003cli\u003e\u003ca href=\"https://hexdocs.pm/phoenix/overview.html\"\u003eGet Started\u003c/a\u003e\u003c/li\u003e\n          \u003c/ul\u003e\n        \u003c/nav\u003e\n        \u003ca href=\"https://phoenixframework.org/\" class=\"phx-logo\"\u003e\n          \u003cimg src=\"\u003c%= Routes.static_path(@conn, \"/images/phoenix.png\") %\u003e\" alt=\"Phoenix Framework Logo\"/\u003e\n        \u003c/a\u003e\n      \u003c/section\u003e\n    \u003c/header\u003e\n    \u003cmain role=\"main\" class=\"container\"\u003e\n      \u003cp class=\"alert alert-info\" role=\"alert\"\u003e\u003c%= get_flash(@conn, :info) %\u003e\u003c/p\u003e\n      \u003cp class=\"alert alert-danger\" role=\"alert\"\u003e\u003c%= get_flash(@conn, :error) %\u003e\u003c/p\u003e\n      \u003c%= render @view_module, @view_template, assigns %\u003e\n    \u003c/main\u003e\n    \u003cscript type=\"text/javascript\" src=\"\u003c%= Routes.static_path(@conn, \"/js/app.js\") %\u003e\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\nLet's remove the cruft and keep only the essential layout html:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"/\u003e\n    \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/\u003e\n    \u003ctitle\u003eHits\u003c/title\u003e\n    \u003c!-- \u003clink rel=\"stylesheet\" href=\"\u003c%= Routes.static_path(@conn, \"/css/app.css\") %\u003e\"/\u003e --\u003e\n    \u003clink rel=\"stylesheet\" href=\"https://unpkg.com/tachyons@4.8.0/css/tachyons.min.css\"/\u003e\n  \u003c/head\u003e\n  \u003cbody class=\"\"\u003e\n    \u003cmain role=\"main\"\"\u003e\n\n      \u003c%= render @view_module, @view_template, assigns %\u003e\n    \u003c/main\u003e\n    \u003cscript type=\"text/javascript\" src=\"\u003c%= Routes.static_path(@conn, \"/js/app.js\") %\u003e\"\u003e\u003c/script\u003e\n    \u003cstyle\u003e /* custom classes for specific @dwyl color scheme */\n      .teal {\n        color: #4DB6AC;\n      }\n      .bg-teal {\n        background: #4DB6AC;\n      }\n      body { /* dwyl font */\n        font-family: \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n      }\n    \u003c/style\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\nWe removed the link to `app.css`\nand a couple of elements\nas we don't need them;\nwe can always add them back later,\nthat's the beauty of version control,\nnothing is ever \"lost\".\n\nIf you refresh the page you should see the following:\n![phoenix-homepage-no-style](https://user-images.githubusercontent.com/194400/57190961-4cce5680-6f18-11e9-8542-582c9120196f.png)\n\nDon't panic, this is _expected_!\nWe just removed `app.css` in the layout template\nand Phoenix does not have/use any Tachyons classes\nso no styling is present.\nWe'll fix it in the next step.\n\nOpen the homepage template file in your editor:\n`lib/hits_web/templates/page/index.html.eex`\n\nYou should see something like this:\n```html\n\u003csection class=\"phx-hero\"\u003e\n  \u003ch1\u003e\u003c%= gettext \"Welcome to %{name}!\", name: \"Phoenix\" %\u003e\u003c/h1\u003e\n  \u003cp\u003eA productive web framework that\u003cbr/\u003edoes not compromise speed or maintainability.\u003c/p\u003e\n\u003c/section\u003e\n\n\u003csection class=\"row\"\u003e\n  \u003carticle class=\"column\"\u003e\n    \u003ch2\u003eResources\u003c/h2\u003e\n    \u003cul\u003e\n      \u003cli\u003e\n        \u003ca href=\"https://hexdocs.pm/phoenix/overview.html\"\u003eGuides \u0026amp; Docs\u003c/a\u003e\n      \u003c/li\u003e\n      \u003cli\u003e\n        \u003ca href=\"https://github.com/phoenixframework/phoenix\"\u003eSource\u003c/a\u003e\n      \u003c/li\u003e\n      \u003cli\u003e\n        \u003ca href=\"https://github.com/phoenixframework/phoenix/blob/v1.4/CHANGELOG.md\"\u003ev1.4 Changelog\u003c/a\u003e\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/article\u003e\n  \u003carticle class=\"column\"\u003e\n    \u003ch2\u003eHelp\u003c/h2\u003e\n    \u003cul\u003e\n      \u003cli\u003e\n        \u003ca href=\"https://elixirforum.com/c/phoenix-forum\"\u003eForum\u003c/a\u003e\n      \u003c/li\u003e\n      \u003cli\u003e\n        \u003ca href=\"https://webchat.freenode.net/?channels=elixir-lang\"\u003e#elixir-lang on Freenode IRC\u003c/a\u003e\n      \u003c/li\u003e\n      \u003cli\u003e\n        \u003ca href=\"https://twitter.com/elixirphoenix\"\u003eTwitter @elixirphoenix\u003c/a\u003e\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/article\u003e\n\u003c/section\u003e\n```\n\nNotice how the page template only has the HTML code\nrelevant to rendering _this_ page. \u003cbr /\u003e\nLet's replace the code in the file\nwith the markup relevant to the Hits homepage:\n\n```html\n\u003ch2 class=\"bg-teal white h-25 tc ttu f1 lh-title lh-solid mt0 pa2 pb3 mb0 pb0\"\u003e\n  Hits!\n  \u003ca href=\"https://hits.dwyl.com/\" \u003e\n    \u003cimg src=\"https://hits.dwyl.com/dwyl/homepage.svg\" alt=\"Hit Count\" class=\"pa0 ba bw1 b--white\"\u003e\n  \u003c/a\u003e\n\u003c/h2\u003e\n\u003ch4 class=\"mt0 tc fw5 f5 teal pa2 mb0\"\u003e\n  The \u003cem\u003eeasy\u003c/em\u003e way to know how many people are\n  \u003cstrong\u003e\u003cem\u003eviewing\u003c/em\u003e\u003c/strong\u003e your GitHub projects!\n\u003c/h4\u003e\n\n\u003ch2 class=\"mt0 fw5 tc f2 bg-teal white pa2\"\u003e\u003cem\u003eHow?\u003c/em\u003e\u003c/h2\u003e\n\u003cdiv id=\"how\" class=\"dn pa3\"\u003e\n\n  \u003ctable class=\"collapse pv2 ph3 w-100 pa4\"\u003e\n    \u003ctr class=\"bb-0\"\u003e\n      \u003ctd class=\"pv2 ph3 w-30\"\u003e\n        Input your \u003cstrong class=\"fw5\"\u003eGitHub Username\u003c/strong\u003e\n        (\u003cem\u003e \u003cstrong class=\"u\"\u003eor\u003c/strong\u003e org name\u003c/em\u003e):\n      \u003c/td\u003e\n      \u003ctd class=\"pv2 ph3 w-30\"\u003e\n        \u003cinput class=\"input-reset f4 pa2 ba mr5 w-80\" type=\"text\"\n        id=\"username\" name=\"username\" placeholder=\"username\" autofocus maxlength=\"50\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr class=\"\"\u003e\n      \u003ctd class=\"pv2 ph3 w-40\"\u003e\n        Input the \u003cstrong class=\"fw5\"\u003eGitHub Project/Repository\u003c/strong\u003e\n        name:\n      \u003c/td\u003e\n      \u003ctd class=\"pv2 ph3 w-40\"\u003e\n        \u003cinput class=\"input-reset f4 pa2 ba mr5 w-80\" type=\"text\"\n        id=\"repo\" name=\"repo\" placeholder=\"repo/project\" maxlength=\"100\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr class=\"\"\u003e\n      \u003ctd class=\"pv2 ph3 w-40\"\u003e\n        Choose a style for your badge:\n      \u003c/td\u003e\n      \u003ctd class=\"pv2 ph3 w-40\"\u003e\n    \u003cselect id=\"styles\"\u003e\n      \u003coption value=\"flat-square\" selected\u003eFlat Square\u003c/option\u003e\n      \u003coption value=\"flat\"\u003eFlat\u003c/option\u003e\n    \u003c/select\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/table\u003e\n\u003c/div\u003e\n\n\u003ch3 class=\"mt3 fw5 tc db f3 bg-teal white pa2\"\u003eYour Badge \u003cem\u003eMarkdown:\u003c/em\u003e\u003c/h3\u003e\n\u003cpre id=\"badge\" class=\"fw4 ba bw1 pa3 ma2\" style=\"white-space: pre-wrap; word-break: keep-all;\"\u003e\n  [![HitCount](https://hits.dwyl.com/{username}/{repo}.svg?style={style})](https://hits.dwyl.com/{username}/{repo})\n\u003c/pre\u003e\n\n\u003cp class=\"pl2\" id=\"nojs\"\u003e\n  Using the above markdown as a template, \u003cbr /\u003e\n  \u003cem\u003eReplace\u003c/em\u003e the \u003cstrong class=\"code\"\u003e{username}\u003c/strong\u003e with \u003cem\u003eyour\u003c/em\u003e GitHub username \u003cbr /\u003e\n  \u003cem\u003eReplace\u003c/em\u003e the \u003cstrong class=\"code\"\u003e{repo}\u003c/strong\u003e with the repo name.\n\u003c/p\u003e\n\n\u003cp class=\"pl2 ml2\"\u003e\n\u003cem\u003eCopy\u003c/em\u003e the markdown snippet and \u003cem\u003ePaste\u003c/em\u003e it into your\n\u003cstrong class=\"code\"\u003eREADME.md\u003c/strong\u003e file \u003cbr /\u003e\n  to start tracking the view count on your GitHub project!\n\u003c/p\u003e\n\n\u003ch2 class=\"mt0 fw5 tc f4 bg-teal white pa2 mt5\"\u003e\u003cem\u003eRecently\u003c/em\u003e Viewed Projects (\u003cem\u003etracked by Hits\u003c/em\u003e)\u003c/h2\u003e\n\u003cdiv class=\"h5 pl2\" id='hits'\u003e\n  \u003cdiv style=\"display:none\"\u003eDummy Child Node for insertBefore to work\u003c/div\u003e\n\u003c/div\u003e\n```\n\n\u003e _**Note**: we are using Tachyons (Functional) CSS\nfor styling the page,\nif you haven't yet learned about Tachyons,\nwe recommend reading_:\n[github.com/dwyl/**learn-tachyons**](https://github.com/dwyl/learn-tachyons)\n\nThis is a fairly simple homepage.\nThe only _interesting_ part are the Tachyons styles\nwhich are fairly straightforward.\n\nFinally we need to update\n`assets/js/app.js`\nto add the code to render a badge\nwhen people input their `username` and `repo` name.\n\nOpen the `assets/js/app.js` which should look like this:\n\n```js\n// We need to import the CSS so that webpack will load it.\n// The MiniCssExtractPlugin is used to separate it out into\n// its own CSS file.\nimport css from \"../css/app.css\"\n\n// webpack automatically bundles all modules in your\n// entry points. Those entry points can be configured\n// in \"webpack.config.js\".\n//\n// Import dependencies\n//\nimport \"phoenix_html\"\n\n// Import local files\n//\n// Local files can be imported directly using relative paths, for example:\n// import socket from \"./socket\"\n```\n\nAdd the following lines to the end:\n```js\n// Markdown Template\nvar mt = document.getElementById('badge').innerHTML;\n\nfunction generate_markdown () {\n  var user = document.getElementById(\"username\").value || '{username}';\n  var repo = document.getElementById(\"repo\").value || '{project}';\n  var style = document.getElementById(\"styles\").value || '{style}';\n  // console.log('user: ', user, 'repo: ', repo);\n  user = user.replace(/[.*+?^$\u003c\u003e()|[\\]\\\\]/g, ''); // trim and escape\n  repo = repo.replace(/[.*+?^$\u003c\u003e()|[\\]\\\\]/g, '');\n  return mt.replace(/{username}/g, user).replace(/{repo}/g, repo).replace(/{style}/g, style);\n}\n\nfunction display_badge_markdown () {\n  var md = generate_markdown()\n  var pre = document.getElementById(\"badge\").innerHTML = md;\n}\n\nsetTimeout(function () {\n  var how = document.getElementById(\"how\");\n  // show form if JS available (progressive enhancement)\n  if(how) {\n    document.getElementById(\"how\").classList.remove('dn');\n    document.getElementById(\"nojs\").classList.add('dn');\n    display_badge_markdown(); // render initial markdown template\n    var get = document.getElementsByTagName('input');\n   for (var i = 0; i \u003c get.length; i++) {\n       get[i].addEventListener('keyup', display_badge_markdown, false);\n       get[i].addEventListener('keyup', display_badge_markdown, false);\n   }\n\n    // changing markdown preview whenever an option is selected\n    document.getElementById(\"styles\").onchange = function(e) {\n      display_badge_markdown()\n    }\n  }\n}, 500);\n```\n\nRun the Phoenix server to see the static page:\n```\nmix phx.server\n```\nNow visit the route in your web browser:\nhttp://localhost:4000\n\n![hits-static-homepage](https://user-images.githubusercontent.com/194400/57684208-3ff2e680-762d-11e9-89c4-0b0d04694f5a.png)\n\nNow that the static homepage is working,\nwe can move on to the _interesting_ part of the Hits Application!\n\n\u003e As always, if you have questions or got stuck at any point,\nplease open an issue and we will help!\nhttps://github.com/dwyl/hits/issues\n\n### _Fix_ The Failing Test\n\nBefore moving on to building the app,\nlet's make sure that the default tests are passing ...\n\n```\nmix test\n```\n![failing-test](https://user-images.githubusercontent.com/194400/57686427-7f233680-7631-11e9-83ef-931016d7b68b.png)\n\nThe reason for this failing test is pretty clear,\nthe page no longer contains the words \"Welcome to Phoenix!\".\n\nOpen the file `test/hits_web/controllers/page_controller_test.exs`\nand update the assertion text.\n\nFrom:\n\n```elixir\ntest \"GET /\", %{conn: conn} do\n  conn = get(conn, \"/\")\n  assert html_response(conn, 200) =~ \"Welcome to Phoenix!\"\nend\n```\n\nTo:\n\n```elixir\ntest \"GET /\", %{conn: conn} do\n  conn = get(conn, \"/\")\n  assert html_response(conn, 200) =~ \"Hits!\"\nend\n```\n\nRe-run the test:\n\n```sh\nmix test\n```\n\n![hits-static-page-test-passing](https://user-images.githubusercontent.com/194400/57686862-46d02800-7632-11e9-8be0-76e46c4d1cd9.png)\n\nThe test should now pass\nand we can crack on with creating the schemas!\n\n\n## Create The Database for Storing Data\n\nAs is typical of most Phoenix applications,\nwe will be using a PostgreSQL database for storing data.\n\nIn your terminal, run the create script:\n\n```sh\nmix ecto.create\n```\nIn your terminal you should see:\n\n```sh\nCompiling 2 files (.ex)\nThe database for Hits.Repo has been created\n```\nThis tells you the PostgreSQL database **`hits_dev`** was successfully created.\n\n### Note on Database Normalization\n\nIn designing the Hits App database,\nwe decided to normalize\nthe database tables for efficient storage\nbecause we wanted to make the storage of an individual hit\nas minimal as possible.\nThis means we have 4 schemas/tables to ensure there is no duplicate data\nand each bit of data is only stored _once_.\nWe could have stored all the data in a _single_ table\nand on the surface this is appealing\nbecause it would only require one insert\nquery and no \"joins\" when selecting/counting hits.\nBut the initial benefit of a single table\nwould be considerably outweighed\nby the wasted space of duplicate data.\n\nThis is not the time or place\nto dive into the merits\nof database normalization and denormalisation.\nWe will have a chance to explore it later\nwhen we need to optimise query performance.\nFor now we are focussing on building the App\nwith a database normalized to the third normal form (3NF)\nbecause it achieves a good balance of\neliminating data duplication thus maximising storage efficiency\nwhile still having adequate query performance.\n\nYou won't need to understand any of these concepts\nto follow along with building the Hits app.\nBut if you are curious about any of these words, read the following pages:\n+ https://en.wikipedia.org/wiki/Database_normalization\n+ https://en.wikipedia.org/wiki/Denormalization\n+ https://en.wikipedia.org/wiki/Third_normal_form\n\n### Create the 4 Schemas\n\n+ users - for simplicity sake we are assuming that\nall repositories belong to a \"user\" and not an organisation.\n+ repositories - the projects on GitHub\n+ useragents - the web browsers viewing the project pages\n+ hits - the record of each \"hit\" (page view).\n\n```sh\nmix phx.gen.schema User users name:string\nmix phx.gen.schema Repository repositories name:string user_id:references:users\nmix phx.gen.schema Useragent useragents name:string ip:string\nmix phx.gen.schema Hit hits repo_id:references:repositories useragent_id:references:useragents\n```\nIn your terminal,\nyou will see a suggestion in the terminal output similar to this:\n\n\nBefore we can run the database migration, we must create the database.\n\nNow we can run the scripts to create the database tables:\n```\nmix ecto.migrate\n```\n\nIn your terminal, you should see:\n\n```sh\nCompiling 17 files (.ex)\nGenerated hits app\n[info] == Running 20190515211749 Hits.Repo.Migrations.CreateUsers.change/0 forward\n[info] create table users\n[info] == Migrated 20190515211749 in 0.0s\n[info] == Running 20190515211755 Hits.Repo.Migrations.CreateRepositories.change/0 forward\n[info] create table repositories\n[info] create index repositories_user_id_index\n[info] == Migrated 20190515211755 in 0.0s\n[info] == Running 20190515211804 Hits.Repo.Migrations.CreateUseragents.change/0 forward\n[info] create table useragents\n[info] == Migrated 20190515211804 in 0.0s\n[info] == Running 20190515211819 Hits.Repo.Migrations.CreateHits.change/0 forward\n[info] create table hits\n[info] create index hits_repo_id_index\n[info] create index hits_useragent_id_index\n[info] == Migrated 20190515211819 in 0.0s\n```\n\u003e _**Note**: the dates of your migration files will differ from these.\nThe 14 digit number corresponds to the date and time\nin the format **`YYYYMMDDHHMMSS`**.\nThis is helpful for knowing when the database schemas/fields\nwere created or updated._\n\nTo make sure users, useragents and repositories are unique,\nthree more migrations are created to add `unique_index`:\n\nFor users we want the name to be unique\n```elixir\n  def change do\n    create unique_index(:users, [:name])\n  end\n```\n\nFor useragents, we want the name and the ip address unique\n\n```elixir\n  def change do\n    create unique_index(:useragents, [:name, :ip])\n  end\n```\n\nFinally for repositories we want the name and the relation to the user to be\nunique\n\n```elixir\n  def change do\n    create unique_index(:repositories, [:name, :user_id])\n  end\n```\n\nThese unique indexes insure that no duplicates are created at the database level.\n\n\nWe can now use the `upsert` Ecto/Postgres feature to only create new items\nor updating the existing items.\n\n\nFor example with useragent:\n```elixir\n    Repo.insert!(changeset,\n      on_conflict: [set: [ip: changeset.changes.ip, name: changeset.changes.name]],\n      conflict_target: [:ip, :name]\n    )\n```\n\n- `conflict_target`: Define which fields to check for existing entry\n- `on_conflict`: Define what to do when there is a conflict. In our case\nwe update the ip and name values.\n\n\n#### View the Entity Relationship (ER) Diagram\n\nNow that the Postgres database tables have been created,\nyou can fire up your database client\n(_e.g: DBeaver in this case_)\nand view the Entity Relationship (ER) Diagram:\n\n![hits-er-diagram](https://user-images.githubusercontent.com/194400/57219989-b1a9af80-6ff1-11e9-8968-e3b76428093d.png)\n\nThis us shows us the four tables we created above\nand how they are related (_with foreign keys_).\nIt also shows us that there is `schema_migrations` table,\nwhich is _unrelated_ to the tables we created for our app,\nbut contains the log of the schema migrations that have been run\nand when they were applied to the database:\n\n![hits-schema-migrations](https://user-images.githubusercontent.com/194400/57811257-b55fd380-7761-11e9-9ad3-cf06757a410b.png)\n\nThe keen observer will note that the migration table data:\n```sh\nversion       |inserted_at        |\n--------------|-------------------|\n20190515211749|2019-05-15 21:18:38|\n20190515211755|2019-05-15 21:18:38|\n20190515211804|2019-05-15 21:18:38|\n20190515211819|2019-05-15 21:18:38|\n```\nThe version column corresponds to the date timestamps\nin the migration file names:\n\npriv/repo/migrations/**20190515211749**_create_users.exs \u003cbr /\u003e\npriv/repo/migrations/**20190515211755**_create_repositories.exs \u003cbr /\u003e\npriv/repo/migrations/**20190515211804**_create_useragents.exs \u003cbr /\u003e\npriv/repo/migrations/**20190515211819**_create_hits.exs \u003cbr /\u003e\n\n\n### _Run_ the Tests\n\nOnce you have created the schemas and run the resulting migrations,\nit's time to run the tests!\n\n```sh\nmix test\n```\n\nEverything should still pass because `phx.gen.schema`\ndoes not create any new tests\nand our previous tests are unaffected.\n\n\u003cbr /\u003e\n\n## SVG Badge Template\n\nWe created the SVG badge template for our MVP\n[`template.svg`](https://github.com/dwyl/hits-nodejs/blob/master/lib/template.svg)\nand it still serves our needs\nso there's no need to change it.\n\nCreate a new file `lib/hits_web/templates/hit/badge_flat_square.svg`\nand paste the following SVG code in it:\n\n```svg\n\u003c?xml version=\"1.0\"?\u003e \u003c!-- SVG container is 80 x 20 pixel rectangle --\u003e\n\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"80\" height=\"20\"\u003e\n\t\u003crect width=\"30\" height=\"20\" fill=\"#555\"/\u003e \u003c!-- grey rectangle 30px width --\u003e\n\t\u003crect x=\"30\" width=\"50\" height=\"20\" fill=\"#4c1\"/\u003e \u003c!-- green rect 30px --\u003e\n\t\u003cg fill=\"#fff\" text-anchor=\"middle\" font-size=\"11\"\n    font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\"\u003e   \u003c!-- group \u0026 font --\u003e\n\t    \u003ctext x=\"15\" y=\"14\"\u003ehits\u003c/text\u003e                      \u003c!-- \"hits\" label --\u003e\n\t    \u003ctext x=\"54\" y=\"14\"\u003e{count}\u003c/text\u003e  \u003c!-- count is replaced with number --\u003e\n\t\u003c/g\u003e\n\u003c/svg\u003e \u003c!-- that's it! pretty simple, right? :-) Any questions? Ask! --\u003e\n```\nThe comments are there for beginner-friendliness,\nthey are stripped out before sending the badge to the client\nto conserve bandwidth.\n\n# Alternative Badge Formats 🌈\n\nSeveral people have requested\nan alternative badge format.\nRather than spend a lot of time\ncustomizing the badges ourselves,\nwe are going to use \n[shields.io/endpoint](https://shields.io/endpoint)\nthat allows full badge customization.\n## Adding `JSON` Content Negotiation\n\nFirst thing we need to do \nis add the ability to return `JSON`\ninstead of `SVG`.\nIn `HTTP` this is referred to as \nContent Negotiation:\n[wikipedia.org/wiki/Content_negotiation](https://en.wikipedia.org/wiki/Content_negotiation)\n\n### Installing `params` and `content`\n\nWe are using \n[`params`](https://github.com/vic/params)\nto validate the query parameters\nand \n[`content`](https://github.com/dwyl/content)\nto add content negotiation on our endpoints.\n\nLet's install these\nby adding them to the `deps`\nsection `mix.exs`:\n\n```elixir\n  defp deps do\n    [\n      # For content negotiation\n      {:content, \"~\u003e 1.3.0\"},\n\n      # Query param schema validation\n      {:params, \"~\u003e 2.0\"},\n    ]\n  end\n```\n\n### Defining Validation Schema\n\nThe schema **must be compatible with `shield.io`**.\nWe make use of a `schema validator` \nso we know that the parameters\npassed by the users are valid.\n\nThe possible values of each field\nwere determined according to\n[shields.io/endpoint](https://shields.io/endpoint)\n\nThe valid parameters are:\n\n```elixir\ndefparams schema_validator %{\n  user!: :string,\n  repository!: :string,\n  style: [\n    field: Ecto.Enum, \n    values: [\n      plastic: \"plastic\", \n      flat: \"flat\", \n      flatSquare: \"flat-square\", \n      forTheBadge: \"for-the-badge\", \n      social: \"social\"\n    ], \n    default: :flat\n  ],\n  color: [field: :string, default: \"lightgrey\"],\n  show: [field: :string, default: nil],\n}\n```\n\nBy default, each badge is `lightgrey`\nand has a `flat` style.\n\nThis `defparams` defintion is in the\n`/lib/hits_web/controllers/hit_controller.ex` \nfile.\n\n### Content negotiation\n\nLuckily, the `content` package \nmakes it relatively easy to differentiate\n`HTTP` and `JSON` requests.\n\nThe way we implement different behaviours\nfor `JSON` requests is made through\nthe following template:\n\n```elixir\n  if Content.get_accept_header(conn) =~ \"json\" do\n    # return json \n  else\n    # render page\n  end\n```\n\nYou will notice this behaviour in \n[`lib/hits_web/controllers/hit_controller.ex`](https://github.com/dwyl/hits/blob/37d3a91022f4aad25558f4c6f3e2bd01c933d63a/lib/hits_web/controllers/hit_controller.ex#L50-L54)\n\nAfter correct setup,\nthe returned JSON object\ndepends on the parameters the user defines.\n\n```elixir\n  def render_json(conn, count, params) do\n    json_response = %{\n      \"schemaVersion\" =\u003e \"1\",\n      \"label\" =\u003e \"hits\",\n      \"style\" =\u003e params.style,\n      \"message\" =\u003e count,\n      \"color\" =\u003e params.color\n    }\n    json(conn, json_response)\n  end\n```\n\nThis function effectively makes it so\nthe endpoint *returns* a `JSON` object\nfollowing Shields.io schema convention\nwhich can later be used in \n[shields.io/endpoint](https://shields.io/endpoint)\n\n### Expected `JSON` response\n\nIf you run `mix phx.server`\nand open a separate terminal session, \npaste the following `cURL` command and run:\n\n```sh\ncurl -H \"Accept: application/json\" http://localhost:4000/user/repo\\?color=blue\n```\n\nThe output will be the following.\n\n```sh\n{\"color\":\"blue\",\"label\":\"hits\",\"message\":6,\"schemaVersion\":\"1\",\"style\":\"flat\"}%\n```\n\nYou can easily check the `JSON` in a web browser too.\nSimply open Firefox and visit the URL:\nhttp://localhost:4000/user/repo.json?color=blue\n\n![json-in-browser](https://user-images.githubusercontent.com/194400/208438106-e2fc8528-d9d5-4906-9fa1-6f588d9d5b3e.png)\n\nAnd if you replace the `.json` in the URL with `.svg`\nyou will see the badge as expected:\nhttp://localhost:4000/user/repo.svg\n\n![svg-in-browser](https://user-images.githubusercontent.com/194400/208438454-0645cf7b-62f8-4b3d-9153-c6e66747456b.png)\n\nThe **same endpoint** is used\nfor both `HTTP` requests\nand also outputs a `JSON` object.\n\nNow for the fun part!!\n\n## Using Shields to Create _Any_ Style of Button!\n\n```md\nhttps://img.shields.io/endpoint?url=https://hits.dwyl.com/dwyl/hits.json?style=flat-square\u0026show=unique?color=orange\n```\n\nFully customizable:\n\n![fully-custom](https://user-images.githubusercontent.com/194400/208462345-fdfa1dc4-561e-437a-9727-5e92f5853cca.png)\n\n![Custom badge](https://img.shields.io/endpoint?color=red\u0026label=amaze\u0026logo=ducati\u0026logoColor=pink\u0026url=https%3A%2F%2Fhits.dwyl.com%2Fnelson%2Fhello.json%3Fstyle%3Dflat-square%26show%3Dunique%26color%3Dpink)\n![Custom badge](https://img.shields.io/endpoint?color=blue\u0026label=amaze\u0026logo=ducati\u0026logoColor=blue\u0026style=for-the-badge\u0026url=https%3A%2F%2Fhits.dwyl.com%2Fnelson%2Fhello.json%3Fstyle%3Dflat-square%26show%3Dunique%26color%3Dpink)\n![Custom badge](https://img.shields.io/endpoint?color=%23ff00bf\u0026label=amaze\u0026logo=elixir\u0026logoColor=%23ff00bf\u0026style=for-the-badge\u0026url=https%3A%2F%2Fhits.dwyl.com%2Fdwyl%2Fhits.json%3Fstyle%3Dflat-square%26show%3Dunique%26color%3Dpink)\n\nPlenty of logos to chose from at:\nhttps://simpleicons.org\n\n\n# tl;dr\n\n![draw-the-dog](https://user-images.githubusercontent.com/194400/58163803-88895000-7c7c-11e9-82f1-8afe63b40f99.png)\n\n\u003e But seriously, if you want a step-by-step tutorial,\nleave a comment on: https://github.com/dwyl/hits/issues/74\n\n\u003c!--\n\n\n### Create a _Failing_ Test\n\nLet's start by addressing the _first_ failing test:\n\n![first-failing-test](https://user-images.githubusercontent.com/194400/57181253-35945800-6e89-11e9-8fa5-f829e3d03090.png)\n\n\nRun the following command to execute the _single_ test starting on line 36\n```sh\nmix test test/hits_web/controllers/hit_controller_test.exs:36\n```\nOpen `test/hits_web/controllers/hit_controller_test.exs` in your editor.\n\n--\u003e\n\n## Add Channel\n\nIf you are new to Phoenix Channels, please recap:\nhttps://github.com/dwyl/phoenix-chat-example\n\nIn your terminal, run the following command:\n```sh\nmix phx.gen.channel Hit\n```\nYou should see the following output:\n\n```\n* creating lib/hits_web/channels/hit_channel.ex\n* creating test/hits_web/channels/hit_channel_test.exs\n\nAdd the channel to your `lib/hits_web/channels/user_socket.ex` handler, for example:\n\n    channel \"hit:lobby\", HitsWeb.HitChannel\n```\n\n\u003e If you want to see the code required\nto render the hits on the homepage in realtime,\nplease see: https://github.com/dwyl/hits/pull/80/files\n\n\n\n\n## Research \u0026 Background Reading\n\nIf you found this repository useful, please ⭐️ it so we (and others) know you liked it!\n\nWe found the following links/articles/posts _useful_\nwhen learning how to build this mini-project:\n\n+ Plug Docs: https://hexdocs.pm/plug/readme.html (_the official Plug docs_)\n+ Plug Conn (_connection struct specific_) Docs:\nhttps://hexdocs.pm/plug/Plug.Conn.html\n(_the are feature-complete but no practical/usage examples!_)\n+ Understanding Plug (Phoenix Blog): https://hexdocs.pm/phoenix/plug.html\n+ Elixir School Plug:\nhttps://elixirschool.com/en/lessons/specifics/plug/\n+ Getting started with Plug in Elixir:\nhttps://www.brianstorti.com/getting-started-with-plug-elixir\n(_has a good/simple example of \"Plug.Builder\"_)\n+ Elixir Plug unveiled:\nhttps://medium.com/@kansi/elixir-plug-unveiled-bf354e364641\n+ Building a web framework from scratch in Elixir:\nhttps://codewords.recurse.com/issues/five/building-a-web-framework-from-scratch-in-elixir\n+ Testing Plugs: https://robots.thoughtbot.com/testing-elixir-plugs\n+ How to broadcast a message from a Phoenix Controller to a Channel?\nhttps://stackoverflow.com/questions/33960207/how-to-broadcast-a-message-from-a-phoenix-controller-to-a-channel\n","funding_links":[],"categories":["Elixir","🧩 Badges 👇","Badges"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwyl%2Fhits","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdwyl%2Fhits","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwyl%2Fhits/lists"}