{"id":14991368,"url":"https://github.com/joshuaclayton/page_ez","last_synced_at":"2025-10-28T11:10:11.701Z","repository":{"id":174000353,"uuid":"651609186","full_name":"joshuaclayton/page_ez","owner":"joshuaclayton","description":"PageEz is a tool to define page objects with Capybara.","archived":false,"fork":false,"pushed_at":"2025-01-06T16:15:31.000Z","size":173,"stargazers_count":18,"open_issues_count":1,"forks_count":2,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-21T23:48:16.826Z","etag":null,"topics":["capybara","page-object","rspec","testing"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshuaclayton.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-06-09T16:14:36.000Z","updated_at":"2025-01-08T09:46:32.000Z","dependencies_parsed_at":null,"dependency_job_id":"f61fd0ca-3951-4ba2-8019-9a0561643692","html_url":"https://github.com/joshuaclayton/page_ez","commit_stats":null,"previous_names":["joshuaclayton/page_ez"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuaclayton%2Fpage_ez","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuaclayton%2Fpage_ez/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuaclayton%2Fpage_ez/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuaclayton%2Fpage_ez/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshuaclayton","download_url":"https://codeload.github.com/joshuaclayton/page_ez/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248512537,"owners_count":21116619,"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":["capybara","page-object","rspec","testing"],"created_at":"2024-09-24T14:27:33.267Z","updated_at":"2025-10-28T11:10:11.624Z","avatar_url":"https://github.com/joshuaclayton.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PageEz\n\n[![Coverage Status](https://coveralls.io/repos/github/joshuaclayton/page_ez/badge.svg?branch=main)](https://coveralls.io/github/joshuaclayton/page_ez?branch=main)\n\nPageEz is a tool to define page objects with [Capybara].\n\n[Capybara]: https://github.com/teamcapybara/capybara\n\n## Installation\n\nAdd the gem to your `Gemfile`:\n\n```rb\ngem \"page_ez\"\n```\n\n## Usage\n\nDefine a page object:\n\n```rb\nclass TodosIndex \u003c PageEz::Page\n  has_one :active_list, \"section.active ul\" do\n    has_many :items do\n      has_one :name, \"span[data-role=todo-name]\"\n      has_one :checkbox, \"input[type=checkbox]\"\n\n      def mark_complete\n        checkbox.click\n      end\n    end\n  end\n\n  def active_todo_names\n    items.map { _1.name.text }\n  end\n\n  has_one :completed_list, \"section.complete ul\" do\n    has_many :items do\n      has_one :name, \"span[data-role=todo-name]\"\n      has_one :checkbox, \"input[type=checkbox]\"\n\n      def mark_incomplete\n        checkbox.click\n      end\n    end\n  end\nend\n```\n\nUse your page object:\n\n```rb\nit \"manages todos state when completing\" do\n  user = create(:user)\n  create(:todo, name: \"Buy milk\", user:)\n  create(:todo, name: \"Buy eggs\", user:)\n\n  sign_in_as user\n\n  todos_index = TodosIndex.new\n\n  expect(todos_index.active_todo_names).to eq([\"Buy milk\", \"Buy eggs\"])\n  todos_index.active_list.item_matching(text: \"Buy milk\").mark_complete\n  expect(todos_index.active_todo_names).to eq([\"Buy eggs\"])\n  todos_index.active_list.item_matching(text: \"Buy eggs\").mark_complete\n\n  expect(todos_index.active_todo_names).to be_empty\n\n  todos_index.completed_list.item_matching(text: \"Buy milk\").mark_incomplete\n  expect(todos_index.active_todo_names).to eq([\"Buy milk\"])\n  todos_index.completed_list.item_matching(text: \"Buy eggs\").mark_incomplete\n  expect(todos_index.active_todo_names).to eq([\"Buy milk\", \"Buy eggs\"])\nend\n```\n\n### `has_one`\n\nYou can define accessors to individual elements (matched with Capybara's `find`):\n\n```rb\nclass BlogPost \u003c PageEz::Page\n  has_one :post_title, \"header h2\"\n  has_one :body, \"section[data-role=post-body]\"\n  has_one :published_date, \"time[data-role=published-date]\"\nend\n\n# generates the following methods:\n\nblog_post = BlogPost.new\n\nblog_post.post_title             # =\u003e find(\"header h2\")\nblog_post.has_post_title?        # =\u003e has_css?(\"header h2\")\nblog_post.has_no_post_title?     # =\u003e has_no_css?(\"header h2\")\n\nblog_post.body                   # =\u003e find(\"section[data-role=post-body]\")\nblog_post.has_body?              # =\u003e has_css?(\"section[data-role=post-body]\")\nblog_post.has_no_body?           # =\u003e has_no_css?(\"section[data-role=post-body]\")\n\nblog_post.published_date         # =\u003e find(\"time[data-role=published-date]\")\nblog_post.has_published_date?    # =\u003e has_css?(\"time[data-role=published-date]\")\nblog_post.has_no_published_date? # =\u003e has_no_css?(\"time[data-role=published-date]\")\n\nblog_post.post_title(text: \"Writing Ruby is Fun!\")           # =\u003e find(\"header h2\", text: \"Writing Ruby is Fun!\")\nblog_post.has_post_title?(text: \"Writing Ruby is Fun!\")      # =\u003e has_css?(\"header h2\", text: \"Writing Ruby is Fun!\")\nblog_post.has_no_post_title?(text: \"Writing Ruby is Boring\") # =\u003e has_no_css?(\"header h2\", text: \"Writing Ruby is Boring\")\n```\n\nThe methods defined by PageEz can be passed additional options from Capybara. Refer to documentation for the following methods:\n\n* [`Capybara::Node::Finders#find`]\n* [`Capybara::Node::Matchers#has_css?`]\n\n### `has_many`\n\nYou can define accessors to multiple elements (matched with Capybara's `all`):\n\n```rb\nclass TodosIndex \u003c PageEz::Page\n  has_many :todos, \"ul li span[data-role=todo-name]\"\nend\n\n# generates the following methods:\n\ntodos_index = TodosIndex.new\n\ntodos_index.todos                                   # =\u003e all(\"ul li span[data-role=todo-name]\")\ntodos_index.has_todos?                              # =\u003e has_css?(\"ul li span[data-role=todo-name]\")\ntodos_index.has_no_todos?                           # =\u003e has_no_css?(\"ul li span[data-role=todo-name]\")\n\ntodos_index.todo_matching(text: \"Buy milk\")         # =\u003e find(\"ul li span[data-role=todo-name]\", text: \"Buy milk\")\ntodos_index.has_todo_matching?(text: \"Buy milk\")    # =\u003e has_css?(\"ul li span[data-role=todo-name]\", text: \"Buy milk\")\ntodos_index.has_no_todo_matching?(text: \"Buy milk\") # =\u003e has_no_css?(\"ul li span[data-role=todo-name]\", text: \"Buy milk\")\n\ntodos_index.todos.has_count_of?(number)             # =\u003e has_css?(\"ul li span[data-role=todo-name]\", count: number)\ntodos_index.has_todos_count?(number)                # =\u003e has_css?(\"ul li span[data-role=todo-name]\", count: number)\n\ntodos_index.todos.has_any_elements?                 # =\u003e has_css?(\"ul li span[data-role=todo-name]\")\ntodos_index.todos.has_no_elements?                  # =\u003e has_no_css?(\"ul li span[data-role=todo-name]\")\n```\n\nThe methods defined by PageEz can be passed additional options from Capybara. Refer to documentation for the following methods:\n\n* [`Capybara::Node::Finders#all`]\n* [`Capybara::Node::Matchers#has_css?`]\n\n### `has_many_ordered`\n\nThis mirrors the `has_many` macro but adds additional methods for accessing\nelements at a specific index.\n\n```rb\nclass TodosIndex \u003c PageEz::Page\n  has_many_ordered :todos, \"ul[data-role=todo-list] li\"\nend\n\n# generates the base has_many methods (see above)\n\n# in addition, it generates the ability to access at an index. The index passed\n# to Ruby will be translated to the appropriate `:nth-of-child` (which is a\n# 1-based index rather than 0-based)\n\ntodos_index.todo_at(0)                   # =\u003e find(\"ul[data-role=todo-list] li:nth-of-type(1)\")\ntodos_index.has_todo_at?(0)              # =\u003e has_css?(\"ul[data-role=todo-list] li:nth-of-type(1)\")\ntodos_index.has_no_todo_at?(0)           # =\u003e has_no_css?(\"ul[data-role=todo-list] li:nth-of-type(1)\")\n\ntodos_index.todo_at(0, text: \"Buy milk\") # =\u003e find(\"ul[data-role=todo-list] li:nth-of-type(1)\", text: \"Buy milk\")\n```\n\nThe methods defined by PageEz can be passed additional options from Capybara. Refer to documentation for the following methods:\n\n* [`Capybara::Node::Finders#find`]\n* [`Capybara::Node::Matchers#has_css?`]\n\n### `contains`\n\nThis provides a shorthand for delegating methods from one page object to\nanother, flattening the hierarchy of page objects and making it easier to\ninteract with application-level componentry.\n\n```rb\nclass SidebarModal \u003c PageEz::Page\n  base_selector \"div[data-role=sidebar]\"\n\n  has_one :sidebar_heading, \"h2\"\n  has_one :sidebar_contents, \"section[data-role=contents]\"\nend\n\nclass PeopleIndex \u003c PageEz::Page\n  contains SidebarModal\n\n  has_many :people_rows, \"ul[data-role=people-list] li\" do\n    has_one :name, \"span[data-role=person-name]\"\n    has_one :edit_link, \"a\", text: \"Edit\"\n  end\n\n  def change_person_name(from:, to:)\n    people_row_matching(text: from).edit_link.click\n\n    within sidebar_contents do\n      fill_in \"Name\", with: to\n      click_on \"Save Person\"\n    end\n  end\nend\n```\n\nBy default, this delegates all methods to an instance of the page object. If\nyou prefer to delegate a subset of the methods, you can do so with the `only`\noption:\n\n```rb\nclass SidebarModal \u003c PageEz::Page\n  base_selector \"div[data-role=sidebar]\"\n\n  has_one :sidebar_heading, \"h2\"\n  has_one :sidebar_contents, \"section[data-role=contents]\"\nend\n\nclass PeopleIndex \u003c PageEz::Page\n  contains SidebarModal, only: %i[sidebar_contents]\nend\n```\n\nThe equivalent functionality could be achieved with:\n\n```rb\nclass SidebarModal \u003c PageEz::Page\n  base_selector \"div[data-role=sidebar]\"\n\n  has_one :sidebar_heading, \"h2\"\n  has_one :sidebar_contents, \"section[data-role=contents]\"\nend\n\nclass PeopleIndex \u003c PageEz::Page\n  has_one :sidebar_modal, SidebarModal\n  delegate :sidebar_contents, to: :sidebar_modal\nend\n```\n\n### Using Methods as Dynamic Selectors\n\nIn the examples above, the CSS selectors are static.\n\nHowever, there are a few different ways to define `has_one`, `has_many`, and\n`has_many_ordered` elements as dynamic.\n\n```rb\nclass TodosIndex \u003c PageEz::Page\n  has_one :todo_by_id\n\n  def todo_by_id(id:)\n    \"[data-model=todo][data-model-id=#{id}]\"\n  end\nend\n\n# generates the same methods as has_one (see above) but with a required `id:` keyword argument\n\ntodos_index = TodosIndex.new\ntodos_index.todo_by_id(id: 5)         # =\u003e find(\"[data-model=todo][data-model-id=5]\")\ntodos_index.has_todo_by_id?(id: 5)    # =\u003e has_css?(\"[data-model=todo][data-model-id=5]\")\ntodos_index.has_no_todo_by_id?(id: 5) # =\u003e has_no_css?(\"[data-model=todo][data-model-id=5]\")\n```\n\nThe first mechanism declares the `has_one :todo_by_id` at the top of the file,\nand the definition for the selector later on. This allows for grouping multiple\n`has_one`s together for readability.\n\nThe second approach syntactically mirrors Ruby's `private_class_method`:\n\n```rb\nclass TodosIndex \u003c PageEz::Page\n  has_one def todo_by_id(id:)\n    \"[data-model=todo][data-model-id=#{id}]\"\n  end\n\n  # or\n\n  def todo_by_id(id:)\n    \"[data-model=todo][data-model-id=#{id}]\"\n  end\n  has_one :todo_by_id\nend\n```\n\nIn either case, the method needs to return a CSS string. PageEz will generate\nthe corresponding predicate methods as expected, as well (in the example above,\n`#has_todo_by_id?(id:)` and `#has_no_todo_by_id?(id:)`\n\nFor the additional methods generated with the `has_many_ordered` macro (e.g.\nfor `has_many_ordered :items`, the methods `#item_at` and `#has_item_at?`), the\nfirst argument is the index of the element, and all other args will be passed\nthrough.\n\n```rb\nclass TodosList \u003c PageEz::Page\n  has_many_ordered :items do\n    has_one :name, \"[data-role='title']\"\n    has_one :checkbox, \"input[type='checkbox']\"\n  end\n\n  def items(state:)\n    \"li[data-state='#{state}']\"\n  end\nend\n```\n\nThis would enable usage as follows:\n\n```rb\ntodos = TodosList.new\n\nexpect(todos.items(state: \"complete\")).to have_count_of(1)\nexpect(todos.items(state: \"incomplete\")).to have_count_of(2)\n\nexpect(todos).to have_item_at(0, state: \"complete\")\nexpect(todos).not_to have_item_at(1, state: \"complete\")\nexpect(todos).to have_item_at(0, state: \"incomplete\")\nexpect(todos).to have_item_at(1, state: \"incomplete\")\nexpect(todos).not_to have_item_at(2, state: \"incomplete\")\n```\n\nOne key aspect of PageEz is that page hierarchy can be codified and scoped for interaction.\n\n```rb\nclass TodosList \u003c PageEz::Page\n  has_many_ordered :items, \"li\" do\n    has_one :name, \"span[data-role=name]\"\n    has_one :complete_button, \"input[type=checkbox][data-action=toggle-complete]\"\n  end\nend\n\n# generates the following method chains\n\ntodos_list = TodosList.new\n\ntodos_list.items.first.name                             # =\u003e all(\"li\").first.find(\"span[data-role=name]\")\ntodos_list.items.first.has_name?                        # =\u003e all(\"li\").first.has_css?(\"span[data-role=name]\")\ntodos_list.items.first.has_no_name?(text: \"Buy yogurt\") # =\u003e all(\"li\").first.has_no_css?(\"span[data-role=name]\", text: \"Buy yogurt\")\ntodos_list.items.first.complete_button.click            # =\u003e all(\"li\").first.find(\"input[type=checkbox][data-action=toggle-complete]\").click\n\n# and, because we're using has_many_ordered:\n\ntodos_list.item_at(0).name                              # =\u003e find(\"li:nth-of-type(1)\").find(\"span[data-role=name]\")\ntodos_list.item_at(0).has_name?                         # =\u003e find(\"li:nth-of-type(1)\").has_css?(\"span[data-role=name]\")\ntodos_list.item_at(0).has_no_name?(text: \"Buy yogurt\")  # =\u003e find(\"li:nth-of-type(1)\").has_no_css?(\"span[data-role=name]\", text: \"Buy yogurt\")\ntodos_list.item_at(0).complete_button.click             # =\u003e find(\"li:nth-of-type(1)\").find(\"input[type=checkbox][data-action=toggle-complete]\").click\n```\n\n[`Capybara::Node::Finders#all`]: https://rubydoc.info/github/teamcapybara/capybara/Capybara/Node/Finders#all-instance_method\n[`Capybara::Node::Finders#find`]: https://rubydoc.info/github/teamcapybara/capybara/Capybara/Node/Finders#find-instance_method\n[`Capybara::Node::Matchers#has_css?`]: https://rubydoc.info/github/teamcapybara/capybara/Capybara/Node/Matchers#has_css%3F-instance_method\n\n## Base Selectors\n\nCertain components may exist across multiple pages but have a base selector\nfrom which all interactions should be scoped.\n\nThis can be configured on a per-object basis:\n\n```rb\nclass ApplicationHeader \u003c PageEz::Page\n  base_selector \"header[data-role=primary]\"\n\n  has_one :application_title, \"h1\"\nend\n```\n\n## Page Object Composition\n\nBecause page objects can encompass as much or as little of the DOM as desired,\nit's possible to compose multiple page objects.\n\n### Composition via DSL\n\n```rb\nclass Card \u003c PageEz::Page\n  has_one :header, \"h3\"\nend\n\nclass PrimaryNav \u003c PageEz::Page\n  has_one :home_link, \"a[data-role='home-link']\"\nend\n\nclass Dashboard \u003c PageEz::Page\n  has_many_ordered :metrics, \"ul.metrics li\" do\n    has_one :card, Card\n  end\n\n  has_one :primary_nav, PrimaryNav, base_selector: \"nav.primary\"\nend\n```\n\n### Manual Composition\n\n```rb\nclass Card \u003c PageEz::Page\n  has_one :header, \"h3\"\nend\n\nclass PrimaryNav \u003c PageEz::Page\n  has_one :home_link, \"a[data-role='home-link']\"\nend\n\nclass Dashboard \u003c PageEz::Page\n  has_many_ordered :metrics, \"ul.metrics li\" do\n    def card\n      # passing `self` is required to scope the query for the specific card\n      # within the metric when nested inside `has_one`, `has_many`, and\n      # `has_many_ordered`\n      Card.new(self)\n    end\n  end\n\n  def primary_nav\n    # pass the element `Capybara::Node::Element` to scope page interaction when\n    # composing at the top-level PageEz::Page class\n    PrimaryNav.new(find(\"nav.primary\"))\n  end\nend\n```\n\nWith the following markup:\n\n```html\n\u003cnav class=\"primary\"\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca data-role=\"home-link\" href=\"/\"\u003eHome\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/nav\u003e\n\n\u003cul class=\"metrics\"\u003e\n  \u003cli\u003e\u003ch3\u003eMetric 0\u003c/h3\u003e\u003c/li\u003e\n  \u003cli\u003e\u003ch3\u003eMetric 1\u003c/h3\u003e\u003c/li\u003e\n  \u003cli\u003e\u003ch3\u003eMetric 2\u003c/h3\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cul class=\"stats\"\u003e\n  \u003cli\u003e\u003ch3\u003eStat 1\u003c/h3\u003e\u003c/li\u003e\n  \u003cli\u003e\u003ch3\u003eStat 2\u003c/h3\u003e\u003c/li\u003e\n  \u003cli\u003e\u003ch3\u003eStat 3\u003c/h3\u003e\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nOne could then interact with the card as such:\n\n```rb\n# within a spec file\n\nvisit \"/\"\n\ndashboard = Dashboard.new\n\nexpect(dashboard.primary_nav).to have_home_link\nexpect(dashboard.metric_at(0).card.header).to have_text(\"Metric 0\")\n```\n\nReview page object composition within the [composition specs].\n\n[composition specs]: ./spec/features/composition_spec.rb\n\n## Configuration\n\n### Logger\n\nConfigure PageEz's logger to capture debugging information about\nwhich page objects and methods are defined.\n\n\n```rb\nPageEz.configure do |config|\n  config.logger = Logger.new($stdout)\nend\n```\n\n### Pluralization Warnings\n\nUse of the different macros imply singular or plural values, e.g.\n\n* `has_one :todos_list, \"ul\"`\n* `has_many :cards, \"li[data-role=card]\"`\n\nBy default, PageEz allows for any pluralization usage regardless of macro. You\ncan configure PageEz to either warn (via its logger) or raise an exception if\npluralization doesn't look to align. Behind the scenes, PageEz uses\nActiveSupport's pluralization mechanisms.\n\n```rb\nPageEz.configure do |config|\n  config.on_pluralization_mismatch = :warn # or :raise, nil is the default\nend\n```\n\n### Collisions with Capybara's RSpec Matchers\n\nCapybara ships with a set of RSpec matchers, including:\n\n* `have_title`\n* `have_link`\n* `have_button`\n* `have_field`\n* `have_select`\n* `have_table`\n* `have_text`\n\nBy default, if any elements are declared in PageEz that would overlap\nwith these matchers (e.g. `has_one :title, \"h3\"`), PageEz will raise an\nexception in order to prevent confusing errors when asserting via predicate\nmatchers (since PageEz will define corresponding `has_title?` and\n`has_no_title?` methods).\n\nYou can configure the behavior to warn (or do nothing):\n\n```rb\nPageEz.configure do |config|\n  config.on_matcher_collision = :warn # or nil, :raise is the default\nend\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run\n`rake spec` to run the tests. You can also run `bin/console` for an interactive\nprompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To\nrelease a new version, update the version number in `version.rb`, and then run\n`bundle exec rake release`, which will create a git tag for the version, push\ngit commits and the created tag, and push the `.gem` file to\n[rubygems.org](https://rubygems.org).\n\n## Feature Tests\n\nThis uses a test harness for Rack app generation called `AppGenerator`, which\nhandles mounting HTML responses to endpoints accessible via `GET`. In tests,\ncall `build_page` with the markup you'd like and it will mount that response to\nthe root of the application.\n\n```ruby\npage = build_page(\u003c\u003c-HTML)\n  \u003cform\u003e\n    \u003cinput name=\"name\" type=\"text\" /\u003e\n    \u003cinput name=\"email\" type=\"text\" /\u003e\n  \u003c/form\u003e\nHTML\n```\n\nTo drive interactions with a headless browser, add the RSpec metadata `:js` to\neither individual `it`s or `describe`s.\n\n## Roadmap\n\n* [x] Verify page object interactions work within `within`\n* [ ] Define `form` syntax\n* [ ] Define `define` syntax (FactoryBot style)\n* [x] Nested/reference-able page objects (from `define` syntax, by symbol, or by class name)\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/joshuaclayton/page_ez. This project is intended to be a\nsafe, welcoming space for collaboration, and contributors are expected to\nadhere to the [code of\nconduct](https://github.com/joshuaclayton/page_ez/blob/main/CODE_OF_CONDUCT.md).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT\nLicense](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the PageEz project's codebases, issue trackers, chat\nrooms and mailing lists is expected to follow the [code of\nconduct](https://github.com/joshuaclayton/page_ez/blob/main/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshuaclayton%2Fpage_ez","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshuaclayton%2Fpage_ez","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshuaclayton%2Fpage_ez/lists"}