Repository: hhursev/recipe-scrapers

Issue:
Clarify introductory steps in the README file (opening a Python session, expected interpreter output, ...)
This is definitely a "me" problem but I just don't know how to run this script. I'm trying to use it to scrape all the different recipe URLs from HelloFresh, and probably a couple other websites as well.

I've spent a few hours trying to figure it out now. To be frank, it isn't just this one, I just don't know how to run them.

Discussion:
jayaddison: @hhursev @jknndy @mlduff @strangetom any opinions on these changes?

Relocating the netiquette section is the aspect I'm least certain about.  The other parts might seem a bit verbose but I think they can be helpful.
jknndy: All these additions seem like improvements to me!

> Relocating the netiquette section is the aspect I'm least certain about. The other parts might seem a bit verbose but I think they can be helpful.

I think moving the netiquette section up the structure is a good call but I do wonder if it should maybe be just one section down, appearing right above the list of supported sites.

Patch:
diff --git a/README.rst b/README.rst
index 5414e01d..e69c12c6 100644
--- a/README.rst
+++ b/README.rst
@@ -27,43 +27,45 @@
 ------
 
 
-A simple web scraping tool for recipe sites.
+A simple scraping tool for recipe webpages.
+
+
+Netiquette
+----------
+
+If you're using this library to collect large numbers of recipes from the web, please use the software responsibly and try to avoid creating high volumes of network traffic.
+
+Python's standard library provides a ``robots.txt`` `parser <https://docs.python.org/3/library/urllib.robotparser.html>`_ that may be helpful to automatically follow common instructions specified by websites for web crawlers.
+
+Another parser option -- particularly if you find that many web requests from ``urllib.robotparser`` are blocked -- is the `robotexclusionrulesparser <https://pypi.org/project/robotexclusionrulesparser/>`_ library.
+
+
+Getting Started
+---------------
+
+Start by using `Python's built-in package installer <https://docs.python.org/3/installing/index.html>`_, ``pip``, to install the library:
 
 .. code:: shell
 
-    pip install recipe-scrapers
+    python -m pip install recipe-scrapers
 
-then:
+This should produce output about the installation process, with the final line reading: ``Successfully installed recipe-scrapers-<version-number>``.
 
-.. code:: python
+To learn what the library can do, you can open a `Python interpreter session <https://docs.python.org/3/tutorial/interpreter.html>`_, and then begin typing -- and/or modifying -- the statements below (on the lines containing the ``>>>`` prompt):
 
-    from recipe_scrapers import scrape_me
+.. code:: pycon
 
-    scraper = scrape_me('https://www.allrecipes.com/recipe/158968/spinach-and-feta-turkey-burgers/')
+    Python 4.0.4 (main, Oct 26 1985, 09:00:32) [GCC 22.3.4] on linux
+    Type "help", "copyright", "credits" or "license" for more information.
+    >>> from recipe_scrapers import scrape_me
+    >>> scraper = scrape_me('https://www.allrecipes.com/recipe/158968/spinach-and-feta-turkey-burgers/')
+    >>> help(scraper)
 
-    # Q: What if the recipe site I want to extract information from is not listed below?
-    # A: You can give it a try with the wild_mode option! If there is Schema/Recipe available it will work just fine.
-    scraper = scrape_me('https://www.feastingathome.com/tomato-risotto/', wild_mode=True)
+In the example above, we asked the library to scrape a web address (a.k.a. URL).
 
-    scraper.host()
-    scraper.title()
-    scraper.total_time()
-    scraper.image()
-    scraper.ingredients()
-    scraper.ingredient_groups()
-    scraper.instructions()
-    scraper.instructions_list()
-    scraper.yields()
-    scraper.to_json()
-    scraper.links()
-    scraper.nutrients()  # not always available
-    scraper.canonical_url()  # not always available
-    scraper.equipment()  # not always available
-    scraper.cooking_method()  # not always available
-    scraper.keywords()  # not always available
-    scraper.dietary_restrictions() # not always available
+Behind the scenes, the library made a web request to download the HTML from the URL, `parsed <https://en.wikipedia.org/wiki/Parsing>`_ the server's response with the assistance of other Python libraries, and then returned a `class instance <https://docs.python.org/3/tutorial/classes.html>`_ that we can use to access information about the recipe.
 
-You also have an option to scrape html-like content
+In situations where the HTML for a recipe webpage is already available to us, or will be retrieved using some other mechanism, we can ask the library to skip the download step -- but for accurate scraping it does need to know the URL that the HTML was retrieved from.  Here's an example where we use the Python ``requests`` library to perform the download:
 
 .. code:: python
 
@@ -503,24 +505,48 @@ In case you want to run a single unittest for a newly developed scraper
 
 FAQ
 ---
-- **How do I know if a website has a Recipe Schema?** Run in python shell:
+**What if the recipe site I want to extract information from is not listed above?**
+
+You can give it a try with the ``wild_mode`` option!
+
+If there is Schema/Recipe available it will work just fine.
 
 .. code:: python
 
-    from recipe_scrapers import scrape_me
-    scraper = scrape_me('<url of a recipe from the site>', wild_mode=True)
-    # if no error is raised - there's schema available:
+    scraper = scrape_me('https://www.feastingathome.com/tomato-risotto/', wild_mode=True)
+
+    scraper.host()
     scraper.title()
-    scraper.instructions()  # etc.
+    scraper.total_time()
+    scraper.image()
+    scraper.ingredients()
+    scraper.ingredient_groups()
+    scraper.instructions()
+    scraper.instructions_list()
+    scraper.yields()
+    scraper.to_json()
+    scraper.links()
+    scraper.nutrients()  # not always available
+    scraper.canonical_url()  # not always available
+    scraper.equipment()  # not always available
+    scraper.cooking_method()  # not always available
+    scraper.keywords()  # not always available
+    scraper.dietary_restrictions() # not always available
 
-Netiquette
-----------
 
-If you're using this library to collect large numbers of recipes from the web, please use the software responsibly and try to avoid creating high volumes of network traffic.
+**How do I know if a website has a Recipe Schema?**
 
-Python's standard library provides a ``robots.txt`` `parser <https://docs.python.org/3/library/urllib.robotparser.html>`_ that may be helpful to automatically follow common instructions specified by websites for web crawlers.
+Run in python shell:
 
-Another parser option -- particularly if you find that many web requests from ``urllib.robotparser`` are blocked -- is the `robotexclusionrulesparser <https://pypi.org/project/robotexclusionrulesparser/>`_ library.
+.. code:: pycon
+
+    Python 4.0.4 (main, Oct 26 1985, 09:00:32) [GCC 22.3.4] on linux
+    Type "help", "copyright", "credits" or "license" for more information.
+    >>> from recipe_scrapers import scrape_me
+    >>> scraper = scrape_me('<url of a recipe from the site>', wild_mode=True)
+    >>> # if no error is raised - there's schema available:
+    >>> scraper.title()
+    >>> scraper.instructions()  # etc.
 
 
 Special thanks to:
diff --git a/recipe_scrapers/_abstract.py b/recipe_scrapers/_abstract.py
index 393c9595..401bfc7d 100644
--- a/recipe_scrapers/_abstract.py
+++ b/recipe_scrapers/_abstract.py
@@ -63,9 +63,8 @@ class AbstractScraper:
                 setattr(self.__class__, name, current_method)
             setattr(self.__class__, "plugins_initialized", True)
 
-    @classmethod
-    def host(cls) -> str:
-        """Host domain of the recipe URL."""
+    def author(self):
+        """Author of the recipe."""
         raise NotImplementedError("This should be implemented.")
 
     def canonical_url(self):
@@ -75,44 +74,13 @@ class AbstractScraper:
             return urljoin(self.url, canonical_link["href"])
         return self.url
 
-    def title(self):
-        """Title of the recipe."""
-        raise NotImplementedError("This should be implemented.")
-
-    def category(self):
-        """Category of the recipe."""
-        raise NotImplementedError("This should be implemented.")
-
-    def total_time(self):
-        """Total time needed to prepare and cook the recipe in minutes."""
-        raise NotImplementedError("This should be implemented.")
-
-    def cook_time(self):
-        """Cooking time in minutes."""
-        raise NotImplementedError("This should be implemented.")
-
-    def prep_time(self):
-        """Preparation time in minutes."""
-        raise NotImplementedError("This should be implemented.")
-
-    def cooking_method(self):
-        """The method of cooking the recipe"""
-        raise NotImplementedError("This should be implemented.")
-
-    def yields(self):
-        """Total servings or items in the recipe."""
-        raise NotImplementedError("This should be implemented.")
-
-    def image(self):
-        """An image URL for the recipe."""
-        raise NotImplementedError("This should be implemented.")
-
-    def nutrients(self):
-        """Nutrients of the recipe."""
+    def site_name(self):
+        """Name of the website."""
         raise NotImplementedError("This should be implemented.")
 
-    def keywords(self):
-        """Keywords or tags used to describe the recipe"""
+    @classmethod
+    def host(cls) -> str:
+        """Host domain of the recipe URL."""
         raise NotImplementedError("This should be implemented.")
 
     def language(self):
@@ -146,6 +114,10 @@ class AbstractScraper:
         else:
             raise ElementNotFoundInHtml("Could not find language.")
 
+    def title(self):
+        """Title of the recipe."""
+        raise NotImplementedError("This should be implemented.")
+
     def ingredients(self):
         """Ingredients of the recipe."""
         raise NotImplementedError("This should be implemented.")
@@ -166,38 +138,70 @@ class AbstractScraper:
             if instruction
         ]
 
-    def ratings(self):
-        """Ratings of the recipe."""
+    def category(self):
+        """Category of the recipe."""
         raise NotImplementedError("This should be implemented.")
 
-    def ratings_count(self):
-        """Total number of ratings of the recipe."""
+    def yields(self):
+        """Total servings or items in the recipe."""
         raise NotImplementedError("This should be implemented.")
 
-    def author(self):
-        """Author of the recipe."""
+    def description(self):
+        """Description of the recipe."""
+        raise NotImplementedError("This should be implemented.")
+
+    def total_time(self):
+        """Total time needed to prepare and cook the recipe in minutes."""
+        raise NotImplementedError("This should be implemented.")
+
+    def cook_time(self):
+        """Cooking time in minutes."""
+        raise NotImplementedError("This should be implemented.")
+
+    def prep_time(self):
+        """Preparation time in minutes."""
         raise NotImplementedError("This should be implemented.")
 
     def cuisine(self):
         """Cuisine of the recipe."""
         raise NotImplementedError("This should be implemented.")
 
-    def description(self):
-        """Description of the recipe."""
+    def cooking_method(self):
+        """The method of cooking the recipe"""
         raise NotImplementedError("This should be implemented.")
 
-    def reviews(self):
-        """Reviews of the recipe."""
+    def ratings(self):
+        """Ratings of the recipe."""
+        raise NotImplementedError("This should be implemented.")
+
+    def ratings_count(self):
+        """Total number of ratings of the recipe."""
         raise NotImplementedError("This should be implemented.")
 
     def equipment(self):
         """Equipment needed for the recipe."""
         raise NotImplementedError("This should be implemented.")
 
+    def reviews(self):
+        """Reviews of the recipe."""
+        raise NotImplementedError("This should be implemented.")
+
+    def nutrients(self):
+        """Nutrients of the recipe."""
+        raise NotImplementedError("This should be implemented.")
+
     def dietary_restrictions(self):
         """The specified dietary restrictions or guidelines for which this recipe is suitable"""
         raise NotImplementedError("This should be implemented.")
 
+    def image(self):
+        """An image URL for the recipe."""
+        raise NotImplementedError("This should be implemented.")
+
+    def keywords(self):
+        """Keywords or tags used to describe the recipe"""
+        raise NotImplementedError("This should be implemented.")
+
     def links(self):
         """Links found in the recipe."""
         invalid_href = {"#", ""}
@@ -205,10 +209,6 @@ class AbstractScraper:
 
         return [link.attrs for link in links_html if link["href"] not in invalid_href]
 
-    def site_name(self):
-        """Name of the website."""
-        raise NotImplementedError("This should be implemented.")
-
     def to_json(self):
         """Recipe information in JSON format."""
         json_dict = {}
diff --git a/recipe_scrapers/_utils.py b/recipe_scrapers/_utils.py
index 6d6fe164..7bd445b7 100644
--- a/recipe_scrapers/_utils.py
+++ b/recipe_scrapers/_utils.py
@@ -1,4 +1,5 @@
 import html
+import inspect
 import math
 import re
 
@@ -311,3 +312,16 @@ def change_keys(obj, convert):
 def get_url_slug(url):
     path = url_path_to_dict(url).get("path")
     return path.split("/")[-1]
+
+
+def get_abstract_methods():
+    from ._abstract import AbstractScraper
+
+    return [
+        name
+        for name, value in AbstractScraper.__dict__.items()  # Attributes of the abstract scraper class..
+        if not name.startswith("_")  # ... that are not private ...
+        and inspect.isfunction(value)  # ... and are functions ...
+        and name not in {"links", "to_json"}  # ... and not excluded as special-cases.
+        or name == "host"  # ... explicitly include the `host` method.
+    ]
diff --git a/scripts/reorder_json_keys.py b/scripts/reorder_json_keys.py
new file mode 100644
index 00000000..863bbada
--- /dev/null
+++ b/scripts/reorder_json_keys.py
@@ -0,0 +1,31 @@
+import json
+import os
+
+from recipe_scrapers._utils import get_abstract_methods
+
+KEYS = get_abstract_methods()
+
+
+def reorder_json_keys(file_path):
+    with open(file_path, encoding="utf-8") as file:
+        data = json.load(file)
+
+    reordered_data = {key: data[key] for key in KEYS if key in data}
+
+    if list(data.keys()) != list(reordered_data.keys()):
+        with open(file_path, "w", encoding="utf-8") as file:
+            print(f"Re-ordering JSON keys for: {file_path}")
+            json.dump(reordered_data, file, indent=2, ensure_ascii=False)
+            file.write("\n")
+
+
+def process_directory(directory):
+    for root, _, files in os.walk(directory):
+        for file in files:
+            if file.endswith(".json"):
+                file_path = os.path.join(root, file)
+                reorder_json_keys(file_path)
+
+
+if __name__ == "__main__":
+    process_directory("tests/test_data")


Test patch:
diff --git a/tests/library/test_json_order.py b/tests/library/test_json_order.py
new file mode 100644
index 00000000..0b71b3a7
--- /dev/null
+++ b/tests/library/test_json_order.py
@@ -0,0 +1,36 @@
+import json
+import os
+import unittest
+
+from recipe_scrapers._utils import get_abstract_methods
+
+
+class TestJsonOrder(unittest.TestCase):
+
+    def test_json_key_order(self):
+        correct_order = get_abstract_methods()
+        json_directory = "tests/test_data/"
+
+        for root, dirs, files in os.walk(json_directory):
+            for filename in files:
+                if filename.endswith(".json"):
+                    with open(os.path.join(root, filename), encoding="utf-8") as f:
+                        data = json.load(f)
+
+                    filtered_data_keys = [key for key in correct_order if key in data]
+
+                    expected_keys = filtered_data_keys
+                    actual_keys = list(data.keys())
+
+                    try:
+                        self.assertEqual(expected_keys, actual_keys)
+                    except AssertionError as e:
+                        recommend_reorder_helper = (
+                            "\n\nTo automatically reorder the keys, run the following command:\n"
+                            "python scripts/reorder_json_keys.py\n"
+                        )
+                        raise AssertionError(
+                            f"File: {os.path.join(root, filename)}\n"
+                            f"{str(e)}"
+                            f"{recommend_reorder_helper}"
+                        ) from e
diff --git a/tests/library/test_utils.py b/tests/library/test_utils.py
index 5a5c7a9e..95b33f95 100644
--- a/tests/library/test_utils.py
+++ b/tests/library/test_utils.py
@@ -2,6 +2,7 @@ import unittest
 
 from recipe_scrapers._utils import (
     _extract_fractional,
+    get_abstract_methods,
     get_minutes,
     get_url_slug,
     url_path_to_dict,
@@ -116,3 +117,35 @@ class TestUtils(unittest.TestCase):
         input_url = "https://example.com/first/second/last"
         url_slug = get_url_slug(input_url)
         self.assertEqual("last", url_slug)
+
+    def test_get_abstract_methods(self):
+        abstract_methods = get_abstract_methods()
+        expected_methods = [
+            "author",
+            "canonical_url",
+            "site_name",
+            "host",
+            "language",
+            "title",
+            "ingredients",
+            "ingredient_groups",
+            "instructions",
+            "instructions_list",
+            "category",
+            "yields",
+            "description",
+            "total_time",
+            "cook_time",
+            "prep_time",
+            "cuisine",
+            "cooking_method",
+            "ratings",
+            "ratings_count",
+            "equipment",
+            "reviews",
+            "nutrients",
+            "dietary_restrictions",
+            "image",
+            "keywords",
+        ]
+        self.assertEqual((expected_methods), (abstract_methods))
diff --git a/tests/test_data/15gram.be/fifteengram.json b/tests/test_data/15gram.be/fifteengram.json
index fdde9d71..bf0ef251 100644
--- a/tests/test_data/15gram.be/fifteengram.json
+++ b/tests/test_data/15gram.be/fifteengram.json
@@ -1,9 +1,10 @@
 {
   "author": "15gram",
   "canonical_url": "https://15gram.be/recepten/mac-n-cheese-met-gehakt-en-pompoen",
+  "site_name": "15gram",
   "host": "15gram.be",
-  "description": "We nemen je mee op skivakantie! Of toch naar de après-ski maaltijd. De pompoenblokjes zijn al voorgesneden en mixen we door de saus. Daardoor kleurt die ook mooi oranje!",
-  "image": "https://image.15gram.be/uploads/recipes/5193_1706264487-1920x1280.jpg",
+  "language": "nl",
+  "title": "Mac 'n cheese met gehakt en pompoen",
   "ingredients": [
     "400 gr pompoenblokjes",
     "1 teentje knoflook",
@@ -42,10 +43,9 @@
     "Giet de pasta af en schik opnieuw in de pot. Meng met de roomsaus en het gehakt. Stort in een ovenschaal en strooi de rest van de kaas erover. Gratineer nog 5-10 min. in de oven voor een mooi kaaskorstje.",
     "TIP: Geef de saus wat extra punch met paprikapoeder, chilivlokken of bouillon."
   ],
-  "language": "nl",
-  "site_name": "15gram",
-  "title": "Mac 'n cheese met gehakt en pompoen",
+  "yields": "2 servings",
+  "description": "We nemen je mee op skivakantie! Of toch naar de après-ski maaltijd. De pompoenblokjes zijn al voorgesneden en mixen we door de saus. Daardoor kleurt die ook mooi oranje!",
   "total_time": 30,
   "cook_time": 30,
-  "yields": "2 servings"
+  "image": "https://image.15gram.be/uploads/recipes/5193_1706264487-1920x1280.jpg"
 }
diff --git a/tests/test_data/aldi.com.au/aldi.json b/tests/test_data/aldi.com.au/aldi.json
index 57408251..765d3e90 100644
--- a/tests/test_data/aldi.com.au/aldi.json
+++ b/tests/test_data/aldi.com.au/aldi.json
@@ -1,9 +1,10 @@
 {
   "author": "ALDI AU",
   "canonical_url": "https://www.aldi.com.au/recipes/breakfast-recipes/overloaded-cheese-toastie-recipe/",
+  "site_name": "Aldi",
   "host": "aldi.com.au",
-  "image": "https://www.aldi.com.au/fileadmin/_processed_/8/b/csm_1000340_WK23_PD_455x315__OVERLOADED-CHEESE-TOASTIE__1_RET_4506d46579.jpg",
-  "category": "Breakfast",
+  "language": "en-AU",
+  "title": "Overloaded Cheese Toastie Recipe",
   "ingredients": [
     "1 tbsp Remano pesto",
     "1 slice Westacre tasty cheese",
@@ -30,11 +31,10 @@
     "Place the sandwich in the press and cook for 3-4 minutes or until the cheese melts and the sandwich is golden brown.",
     "Allow to cool slightly before serving."
   ],
-  "language": "en-AU",
-  "site_name": "Aldi",
-  "title": "Overloaded Cheese Toastie Recipe",
-  "prep_time": 5,
-  "cook_time": 5,
+  "category": "Breakfast",
+  "yields": "1 serving",
   "total_time": 10,
-  "yields": "1 serving"
+  "cook_time": 5,
+  "prep_time": 5,
+  "image": "https://www.aldi.com.au/fileadmin/_processed_/8/b/csm_1000340_WK23_PD_455x315__OVERLOADED-CHEESE-TOASTIE__1_RET_4506d46579.jpg"
 }
diff --git a/tests/test_data/davidlebovitz.com/davidlebovitz.json b/tests/test_data/davidlebovitz.com/davidlebovitz.json
index 22e59f02..ec965704 100644
--- a/tests/test_data/davidlebovitz.com/davidlebovitz.json
+++ b/tests/test_data/davidlebovitz.com/davidlebovitz.json
@@ -53,7 +53,7 @@
     "Scrape the pâté into a small serving bowl and refrigerate for a few hours, until firm."
   ],
   "category": null,
-  "total_time": null,
   "description": "Adapted from Très Green, Très Clean, Très Chic by Rebecca Leffler Lentils double in volume when cooked, so 1 cup (160g) of dried lentils will yield close to the correct amount. They usually take about 20 to 30 minutes to cook until soft, but check the directions on the package for specific guidelines. If avoiding gluten, use tamari instead of soy sauce. For a vegan version, replace the butter with the same quantity of olive oil, for a total of 1/4 cup (60ml) of olive oil. The cognac or brandy is optional, but it does give the faux gras a little je ne sais quoi.",
+  "total_time": null,
   "image": "https://www.davidlebovitz.com/wp-content/uploads/2015/06/Faux-Gras-Lentil-Pate-8.jpg"
 }
diff --git a/tests/test_data/dobruchut.aktuality.sk/dobruchutaktualitysk.json b/tests/test_data/dobruchut.aktuality.sk/dobruchutaktualitysk.json
index 7b54d19e..62318094 100644
--- a/tests/test_data/dobruchut.aktuality.sk/dobruchutaktualitysk.json
+++ b/tests/test_data/dobruchut.aktuality.sk/dobruchutaktualitysk.json
@@ -1,11 +1,10 @@
 {
-  "host": "dobruchut.aktuality.sk",
+  "author": "Howardcooks",
   "canonical_url": "https://dobruchut.aktuality.sk/recept/79940/tvarohove-knedlicky-s-okamzitou-omackou/",
   "site_name": "DobrúChuť.sk",
-  "author": "Howardcooks",
+  "host": "dobruchut.aktuality.sk",
   "language": "sk",
   "title": "Tvarohové knedličky s okamžitou omáčkou",
-  "prep_time": 15,
   "ingredients": [
     "250 g hrudkovitý tvaroh",
     "1 ks vajce",
@@ -45,8 +44,9 @@
     "Dobrú chuť!",
     "Ďalšie overené recepty na chutné gnocchi nájdete TU!"
   ],
-  "total_time": 20,
   "yields": "2 servings",
-  "image": "https://img.aktuality.sk/foto/MHgyMzQ6MjU2MHgxNjc0L2ZpdC1pbi84ODB4NDAwL2ZpbHRlcnM6d2F0ZXJtYXJrKGh0dHA6Ly9sb2NhbGhvc3Q6ODEvaW1nL2RjaDEsLTEwLC0xMCw1MCkvaW1n/-FKw-ZNVSAbTY3mcAAS5LQ.jpg?st=9Dtr7drqJY-DJYKA1up51XKWiqAZ2hLVOvE3M5rbPyo&ts=1720619390&e=0",
-  "description": "Rýchle knedličky z tvarohu s ešte rýchlejšou syrovo-bylinkovou omáčkou."
+  "description": "Rýchle knedličky z tvarohu s ešte rýchlejšou syrovo-bylinkovou omáčkou.",
+  "total_time": 20,
+  "prep_time": 15,
+  "image": "https://img.aktuality.sk/foto/MHgyMzQ6MjU2MHgxNjc0L2ZpdC1pbi84ODB4NDAwL2ZpbHRlcnM6d2F0ZXJtYXJrKGh0dHA6Ly9sb2NhbGhvc3Q6ODEvaW1nL2RjaDEsLTEwLC0xMCw1MCkvaW1n/-FKw-ZNVSAbTY3mcAAS5LQ.jpg?st=9Dtr7drqJY-DJYKA1up51XKWiqAZ2hLVOvE3M5rbPyo&ts=1720619390&e=0"
 }
diff --git a/tests/test_data/kwestiasmaku.com/kwestiasmaku.json b/tests/test_data/kwestiasmaku.com/kwestiasmaku.json
index a094eb4d..c31971e1 100644
--- a/tests/test_data/kwestiasmaku.com/kwestiasmaku.json
+++ b/tests/test_data/kwestiasmaku.com/kwestiasmaku.json
@@ -38,9 +38,9 @@
     "Wstawić do nagrzanego piekarnika i piec przez 30 minut. Po upieczeniu doprawić solą i posiekaną natką pietruszki."
   ],
   "category": null,
-  "total_time": null,
   "yields": "2 servings",
   "description": "To sprawdzony sposób na pieczonego kalafiora :) smakowity w wersji z kurkumą i czarnuszką, ale równie dobry z innym zestawem ziół. Sprawdzą się oregano, rozmaryn, papryka, ale również garam masala oraz przyprawa curry. Warto dodać kurkumę dla koloru. W zasadzie do zapamiętania są tutaj parametry pieczenia, a mianowicie 200 stopni C - 30 minut. Kalafiora pieczemy na dużej blaszce. Kalafiora można posypać po upieczeniu zieleniną, można podać z pokruszoną fetą lub sosem czosnkowym. Sprawdzi się jako przekąska lub dodatek np. do sałatki.",
+  "total_time": null,
   "ratings": 4.77,
   "ratings_count": 52,
   "image": "https://www.kwestiasmaku.com/sites/v123.kwestiasmaku.com/files/pieczony-kalafior-01_0.jpg"
diff --git a/tests/test_data/littlesunnykitchen.com/littlesunnykitchen.json b/tests/test_data/littlesunnykitchen.com/littlesunnykitchen.json
index 871ab915..a1cd4c01 100644
--- a/tests/test_data/littlesunnykitchen.com/littlesunnykitchen.json
+++ b/tests/test_data/littlesunnykitchen.com/littlesunnykitchen.json
@@ -1,8 +1,8 @@
 {
-  "host": "littlesunnykitchen.com",
+  "author": "Diana",
   "canonical_url": "https://littlesunnykitchen.com/mexican-chicken-marinade/",
   "site_name": "Little Sunny Kitchen",
-  "author": "Diana",
+  "host": "littlesunnykitchen.com",
   "language": "en-US",
   "title": "Mexican Chicken Marinade",
   "ingredients": [
@@ -41,11 +41,14 @@
     "Place in the fridge and allow to marinate for 2-24 hours.",
     "Remove the chicken from the bag, and grill it or bake it until the internal temperature of the chicken reaches 165°F/74°C with an Instant read thermometer."
   ],
-  "total_time": 130,
-  "prep_time": 10,
   "category": "Main Course",
   "yields": "6 servings",
+  "description": "Make this fresh Mexican chicken marinade to season all of your favorite cuts of chicken. Made with lime, garlic, olive oil, oregano, and warm spices to give your chicken an amazing flavor!",
+  "total_time": 130,
+  "prep_time": 10,
   "cuisine": "American,Mexican",
+  "ratings": 4.95,
+  "ratings_count": 57,
   "nutrients": {
     "calories": "49 kcal",
     "carbohydrateContent": "2 g",
@@ -57,11 +60,8 @@
     "sugarContent": "1 g",
     "servingSize": "1 serving"
   },
-  "ratings": 4.95,
-  "ratings_count": 57,
+  "image": "https://littlesunnykitchen.com/wp-content/uploads/2021/05/Mexican-Chicken-Marinade-1.jpg",
   "keywords": [
     "Mexican Chicken Marinade"
-  ],
-  "image": "https://littlesunnykitchen.com/wp-content/uploads/2021/05/Mexican-Chicken-Marinade-1.jpg",
-  "description": "Make this fresh Mexican chicken marinade to season all of your favorite cuts of chicken. Made with lime, garlic, olive oil, oregano, and warm spices to give your chicken an amazing flavor!"
+  ]
 }
diff --git a/tests/test_data/receptyprevas.sk/receptyprevas.json b/tests/test_data/receptyprevas.sk/receptyprevas.json
index b242febb..08eca4a6 100644
--- a/tests/test_data/receptyprevas.sk/receptyprevas.json
+++ b/tests/test_data/receptyprevas.sk/receptyprevas.json
@@ -1,8 +1,8 @@
 {
-  "host": "receptyprevas.sk",
+  "author": "Martina",
   "canonical_url": "https://www.receptyprevas.sk/recepty/buchty-na-pare-babickin-recept-na-parene-buchty/",
   "site_name": "Recepty pre Vás",
-  "author": "Martina",
+  "host": "receptyprevas.sk",
   "language": "sk-SK",
   "title": "Buchty na pare, babičkin RECEPT na parené buchty",
   "ingredients": [
@@ -72,14 +72,14 @@
     "Vložíme buchty, nie príliš blízko seba, lebo parením narastú. Dávame pozor, para vie popáliť. Buchty paríme 8-10 minút. Potom pokrievku pomaly odstránime, aby nám kvapky pary nekvapli na buchty, a tak nepľasli. Opatrne ich vyberieme na tanier, popicháme ostrou špajdlou alebo ihlou. Na kvalitnej panvici opražíme maslo.",
     "Hotové buchty podávame ešte teplé, posypané s obľúbenými posýpkami - kakaovou, makovou či orechovou, poliate voňavým opraženým maslom. Ak buchty budeme konzumovať neskôr, potrieme ich olejom a prikryjeme, aby nevyschli. Keď vychladnú, môžeme ich zamraziť, a túto sladkú pochúťku máme hotovú raz-dva."
   ],
-  "total_time": 120,
+  "category": "Bezmäsitá strava,Detské recepty,Hlavné jedlá,Klasické jedlá,Múčne jedlá",
   "yields": "4 servings",
-  "image": "https://www.receptyprevas.sk/wp-content/uploads/2022/05/buchty_na_pare.jpg",
   "description": "Buchty na pare – parené buchty s lekvárom. Skvelá tradičná pochúťka pre deti, ale aj dospelých. Nech sa páči, dnes mám pre vás pripravený babičkin osvedčený recept. Recept je s podrobným fotopostupom, ktorý zvládnu aj začiatočníci. Sladké maškrtenie pre celú rodinu. Posýpky na parené buchty. Môže byť kakaová – Granko alebo kakao zmiešané s práškovým cukrom. Vynikajúca je posýpka orechová či maková – pomletý mak (orechy) zmiešame s práškovým cukrom. Dôležité je však voňavé opražené maslo, ktoré buchty a posýpku prepojí do dokonalej chuti. Dobrá rada. Buchty na pare paríme 8-10 minút. Tento čas potrebujeme, aby boli pekne nadýchané. Po naparení ich popicháme ostrou špajdlou alebo ihlou, uvoľní sa para. Buchty nesmú príliš dlho kysnúť, aby neprekysli. 1 porcia – 4 buchty (recept na 16 buchiet)",
-  "category": "Bezmäsitá strava,Detské recepty,Hlavné jedlá,Klasické jedlá,Múčne jedlá",
+  "total_time": 120,
   "cuisine": "Slovenská",
   "nutrients": {
     "servingSize": "1",
     "calories": "790"
-  }
+  },
+  "image": "https://www.receptyprevas.sk/wp-content/uploads/2022/05/buchty_na_pare.jpg"
 }
diff --git a/tests/test_data/rosannapansino.com/rosannapansino.json b/tests/test_data/rosannapansino.com/rosannapansino.json
index 3591395c..949bf800 100644
--- a/tests/test_data/rosannapansino.com/rosannapansino.json
+++ b/tests/test_data/rosannapansino.com/rosannapansino.json
@@ -33,7 +33,7 @@
     "Oil your hands to prevent the marshmallow from sticking to you. Knead in the sugar until you have the consistency of soft taffy and the fondant no longer sticks to your hands. If you are tinting the fondant, add food coloring now and knead until the color is fully incorporated. (Coat your hands with shortening so the colors don't stain them.)",
     "If you are not using the fondant right away, store it tightly wrapped or in a plastic bag at room temperature."
   ],
-  "total_time": null,
   "description": "A delicious and easy Marshmallow Fondant recipe!\nMakes 24 ounces",
+  "total_time": null,
   "image": "http://rosannapansino.com/cdn/shop/articles/1Y6A6281.jpg?v=1567109086"
 }
diff --git a/tests/test_data/varecha.pravda.sk/varechapravdask.json b/tests/test_data/varecha.pravda.sk/varechapravdask.json
index 76b380c4..6973f5a3 100644
--- a/tests/test_data/varecha.pravda.sk/varechapravdask.json
+++ b/tests/test_data/varecha.pravda.sk/varechapravdask.json
@@ -1,21 +1,10 @@
 {
-  "host": "varecha.pravda.sk",
+  "author": "charllote",
   "canonical_url": "varecha.pravda.sk",
   "site_name": "Varecha.sk",
-  "author": "charllote",
+  "host": "varecha.pravda.sk",
   "language": "sk",
   "title": "Palacinky (fotorecept)",
-  "keywords": [
-    "Bezmäsité jedlá",
-    "Chody",
-    "Dezerty",
-    "Múčne jedlá",
-    "Na sladko",
-    "Palacinky",
-    "Pečené",
-    "* Recepty z Pravdy"
-  ],
-  "prep_time": 10,
   "ingredients": [
     "mlieko, 500 ml",
     "múka hladká, 10 PL",
@@ -42,8 +31,19 @@
     "cesto nám postálo 20 minút skontrolujeme či ma správnu hustotu a môžeme sa pustiť do pečenia :) Vymastíme si panvicu s olejom úplne jemnučko nie veľa oleja a poriadne rozohrejeme panvicu a môžeme piecť.. naozaj je dosť dôležité na akej panvici palacinky pečieme ja som piekla na tefal panvici, čo sa aj odzrkadlilo lebo sa mi ani jedna palacinka nepotrhala ani ta prvá....",
     "upečené palacinky si ochutíme podľa vlastného gusta :) ja dávam do palacinky nutelu, čerstvé ovocie nakrájané jahody,maliny,čučoriedky a šľahačku, keď dávam šľahačku do vnútra je to super fakt a určite to vyskúšajte :) ozdobila som topingom , šľahačkou, ovocím a mätou... Dobrú chuť :)"
   ],
-  "total_time": 10,
   "yields": "15 servings",
+  "description": "Palacinky sa dajú robiť na milión spôsobov - na sladko, na slano, tenké, hrubé, plnené, zapekané, flambované, to samozrejme všetci vieme.... :) Vyskúšala som mnoho receptov ale nedarilo sa a nedávno som našla recept na palacinky ktoré sú naozaj jemné, nadýchané, chutné,nelepia sa ale to samozrejme závisí aj od panvice na ktorej palacinky pečieme :)",
+  "total_time": 10,
+  "prep_time": 10,
   "image": "https://varecha.pravda.sk/usercontent/images/velky-recept66949_1444894775.jpg",
-  "description": "Palacinky sa dajú robiť na milión spôsobov - na sladko, na slano, tenké, hrubé, plnené, zapekané, flambované, to samozrejme všetci vieme.... :) Vyskúšala som mnoho receptov ale nedarilo sa a nedávno som našla recept na palacinky ktoré sú naozaj jemné, nadýchané, chutné,nelepia sa ale to samozrejme závisí aj od panvice na ktorej palacinky pečieme :)"
+  "keywords": [
+    "Bezmäsité jedlá",
+    "Chody",
+    "Dezerty",
+    "Múčne jedlá",
+    "Na sladko",
+    "Palacinky",
+    "Pečené",
+    "* Recepty z Pravdy"
+  ]
 }