Repository: hhursev/recipe-scrapers
Pull request #1105 (2024-04-28 13:01:58)

Add README inclusion test
This PR adds a new test that checks the available scrapers list in `README.rst` for all of the supported hosts in `SCRAPERS`. I think that this is useful because I often forget to add the README entries when creating a scraper, and this will force developers to add their new scrapers to the list. Currently the comparison is agnostic to `www.`.

After creating the test I noticed a number of scrapers which are not in the list, so I went through and added all of these so that the test would pass.

If people are happy with this PR, I will extend the test to do a comparison the other way (to check that all the listed scrapers have an associated scraper, to ensure that if scrapers are removed, they are also removed from the list).

jknndy: Nice feature! Any ideas on how we could cover multiple top level domains in one listing? (Hellofresh/others)

It may be best to leave it this way but it just feels a little redundant. Thoughts?
mlduff: @jknndy I've accepted your feedback and made a few changes, thanks heaps for the review.
Is there a way to make a completely custom error message, because to me this seems a little confusing (mainly the `... is not False` part):
```
AssertionError: ['davidlebovitz.com'] is not false : Missing scrapers in README.rst: ['davidlebovitz.com']
```

Would you also be happy for me to extend this test to enforce alphabetical order, and also ensure that no scrapers are included in the list that shouldn't be?
mlduff: >Nice feature! Any ideas on how we could cover multiple top level domains in one listing? (Hellofresh/others)
>
>It may be best to leave it this way but it just feels a little redundant. Thoughts?

To me it makes the most sense to leave it as is, because it most accurately describes what domains are going to work without having `wild_mode=True`. I'd be interested to hear in the alternatives (would you include `http://hellofresh.*/` in the README instead?).
mlduff: Also @jknndy I was also wondering what you think about moving the list into its own file, rather than having everything sitting in the initial README? Would just having a "Click here to see the list of available scrapers" work better? At the moment you have to scroll past the entire list to see the bottom part of the README.
jayaddison: It could be worth validating both the name part and also the hyperlink part of each entry; sometimes they can go out of sync during editing (for example: https://github.com/hhursev/recipe-scrapers/pull/1104/commits/03ff4f32dcaac087172722b7352f32b7a1dc05df)
jayaddison: > > It could be worth validating both the name part and also the hyperlink part of each entry; sometimes they can go out of sync during editing (for example: [03ff4f3](https://github.com/hhursev/recipe-scrapers/commit/03ff4f32dcaac087172722b7352f32b7a1dc05df))
> 
> @jayaddison Yep that's a good idea, could this even be done as a separate test case, responsible for checking domains matching on each line?

Two assertions within a single test case, I think - one that checks that the hyperlink name is accurate, and one that checks that the href is correct.
jknndy: > Also @jknndy I was also wondering what you think about moving the list into its own file, rather than having everything sitting in the initial README? Would just having a "Click here to see the list of available scrapers" work better?

While I agree there could be benefits to moving the coverage list into its own file I think the extra step adds some accessibility loss and someone quickly skimming the read me could miss it. On the counter point this would allow us some more flexibility in the testing / auto generation of read me enteries. Check out #820 for some old thoughts on this (most likely outdated by now) 

> At the moment you have to scroll past the entire list to see the bottom part of the README.

What do you think about adding a hyper link above the scraper list that says something like "contribution & development information" that scrolls past the list of the contribution header? I'm not sold on either idea being an improvement but would like to hear what you / others think?
mlduff: > While I agree there could be benefits to moving the coverage list into its own file I think the extra step adds some accessibility loss and someone quickly skimming the read me could miss it

@jknndy I personally feel that it wouldn't be unreasonable to expect someone to see a link placed under a level 1 heading saying "Supported Websites" that would be placed near the top, but I get what you are saying (as it is certainly quite hard to miss the massive list 😂 ). I also understand that I am new here so I'm more than happy to go with your preference.

> What do you think about adding a hyper link above the scraper list that says something like "contribution & development information" that scrolls past the list to the contribution header?

I think this could work, but to be honest probably not required, but would be interested to hear some other opinions. Would it impact the accessibility to just move this information above the list?

mlduff: @jayaddison Just saw your previous message - that is a good idea, disregard the idea about the sub-level domains. To confirm, would you rather me proceed with this than the verbose, list everything thing I mentioned?
mlduff: @jayaddison @jknndy I'm working through adding the new format now, should each of the sub TLDS (`.com.au`, `.co.uk`, `.at`, etc.) be hyperlinks to their respective sites? So clicking on `.com.au` would take you to https://www.hellofresh.com.au/
mlduff: Also, to keep you both updated - this version I am working on will enforce alphabetical order, based on the full domain name (as determined by Python's `sort`/`sorted`) as I think this would greatly increase readability of the list.
jknndy: > should each of the sub TLDS (`.com.au`, `.co.uk`, `.at`, etc.) be hyperlinks to their respective sites? So clicking on `.com.au` would take you to https://www.hellofresh.com.au/

Yep! That's exactly what I had in mind. 

I agree with your comment about enforcing an alphabetical list as well
mlduff: I've pushed what I have so far, but I have a bit of work to do on it still, so other than the general approach please don't worry about the specifics too much at the moment. I hope to have it in a reviewable state tomorrow sometime.
mlduff: @jknndy I'm a bit stuck with `PracticalSelfReliance` - it's host definition takes an entire domain (rather than just a suffix) - as such my logic falls over as I can't distinguish that both `creativecanning.com` and `practicalselfreliance.com` are both full domains. Do you have any suggestions on proceeding here?

I was thinking of comparing the first section (after splitting on `.`), and if they are different then I know that they are both full domain names in their own right. This however would fall over if it was something like `recipes.creativecanning.com` and `recipes.practicalselfreliance.com`. Would it be possible to change the `host()` signature to specify sub-level/full domain vs just top-level domain? Or do you have any other ideas?

I'm really keen to proceed with getting this test in, as I have already uncovered numerous instances of no-longer supported sites being included in the README, as well as instances of sites being out of order or being omitted completely.
mlduff: @jknndy My most recent commit has what I think is a good solution - checking the default output of `host` if `""` is passed as the argument. If `""` is returned, then we know the whole domain name is defined by the `domain` argument. It isn't perfect, as it won't catch cases where the values being returned are a mix of TLD and complete changes, but I think it is good enough? My test also won't handle things where the country/language code precedes the domain, as I think that will just become too complicated (and I think it is best to just have everything in alphabetical order regardless).
jayaddison: > I had to make a change to way `host()` works, where you either pass a different top level domain, or pass an entire string if the changes to the host are more than just a different top level domain. I was thinking if you aren't happy with this we could have a different approach where `host` can have the following keyword arguments:

Mmm, I'm not too keen on this alternative; I'll add a comment explaining some of the reasons for that soon.  I think it'd be possible to use an alternative approach where we scan through `SCRAPERS`, group by `host()`, and extract common prefixes -- but I'd also like to check that that's feasible, so don't take my word for it (perhaps I've not anticipated some obstacle related to that).
mlduff: @jayaddison I figured out a way to allow subdomains to be configured through `host()` without having to have the whole domain set each time - I think that is all the cases covered now.
jayaddison: I think the format of the README presented here is good, yep :+1: - thanks @mlduff 

  * Sorted, allowing manual scanning and for us to know where to place additional scrapers.
  * Searchable easily in most browsers and text editors (as long as the name-plus-secondary-TLD is not in the search string).
  * Reduces repetition by grouping related domains.

Additional documentation would be useful, yep.

Even though this functionality currently all exists within the test suite, I'm beginning to think of it as `get_scraper_index` -- an indexed directory of scrapers and their supported domain names.

I'm surprised at the number of tricky scenarios to handle when attempting an alternative implementation.  Even so, I might suggest some refactoring -- either as a pull request on your repo, or possibly after we merge this.
jayaddison: @mlduff could you add a test to check that the total count of primary domains in the index matches the total number of scrapers?  (I think that should be true?)
mlduff: > @mlduff could you add a test to check that the total count of primary domains in the index matches the total number of scrapers? (I think that should be true?)

@jayaddison If by the number of scrapers you mean the number of unique scraper classes included in the list, I don't think so. Reason being that for scraper classes that have multiple domains associated with them, but the variance is more than the top-level-domain, each domain is treated as primary (as it would be confusing to start grouping on different things other than TLDs). An example of this is `PracticalSelfReliance`:
```
PracticalSelfReliance.host(): PracticalSelfReliance,
PracticalSelfReliance.host(domain="creativecanning.com"): PracticalSelfReliance,
```
The default domain is `practicalselfreliance.com`, meaning you will have two totally different domains. The same applies for `KptnCook`, where the subdomains are varying (not the TLDs), and as such both are treated as primary.
mlduff: > Even though this functionality currently all exists within the test suite, I'm beginning to think of it as get_scraper_index -- an indexed directory of scrapers and their supported domain names.

@jayaddison So would this be a function available to import from `recipe_scrapers`? And would the output be the same as what `get_supported_scrapers()` currently outputs in the test? If so, I'd be happy to make this refactor.

I was also planning on some changes to improve the error messaging to help users know if a domain name is spelt wrong, or if it is just out of order. Will write up the documentation too. I was thinking of making a dedicated section in between "Create the test" and "Open a pull request" in `how-to-develop-scraper.md` called "Update the README" with in-depth instructions on how to do so.
jayaddison: > > @mlduff could you add a test to check that the total count of primary domains in the index matches the total number of scrapers? (I think that should be true?)
> 
> @jayaddison If by the number of scrapers you mean the number of unique scraper classes included in the list, I don't think so. Reason being that for scraper classes that have multiple domains associated with them, but the variance is more than the top-level-domain, each domain is treated as primary (as it would be confusing to start grouping on different things other than TLDs). An example of this is `PracticalSelfReliance`:

Agreed, there were some obstacles to this idea in the existing host mapping.  I made some adjustments in #1118 - does that unblock the idea, or is it still impractical?

> > Even though this functionality currently all exists within the test suite, I'm beginning to think of it as get_scraper_index -- an indexed directory of scrapers and their supported domain names.
> 
> @jayaddison So would this be a function available to import from `recipe_scrapers`? And would the output be the same as what `get_supported_scrapers()` currently outputs in the test? If so, I'd be happy to make this refactor.

Maybe in future, yep - let's begin with it as an internal utility function until we're confident that it works correctly, and also while we think (or wait for requests) about what kind of functionality it would provide to users.  At the moment it'd be useful for checking the `README` scraper list, but I'm not sure what else yet.

The reason to be a bit cautious is that things we add to the public interface of the library can be difficult to adjust or remove - and if we don't know that it's going to be used, then let's reduce the work for ourselves.
jayaddison: @mlduff I've pushed a commit here with a reimplementation / refactor that I attempted locally.  The main item I'd draw attention to is the common-longest-prefix code.

Feel free to revert the commit; it's partly a way for me to demonstrate some alternative implementation ideas I'd had.  I'm not really sure it is more concise, so my commit message might have been a bit misleading.
mlduff: @jayaddison I like you changes for the whole, although a bit confused on a few aspects:

What exactly do we want the return value of `get_scraper_index` to be? From what I have seen, your implementation currently returns a dictionary, where the keys are the primary domain, and the values are a tuple of the scraper class itself (and **all** subdomains).
I'm not sure this is what you were going for, as your comment says "Index the primary domain and include their secondary domains minus the shared prefix". To me, this indicates that you aren't intending to include the primary domain's suffix, so I'm not sure what your desired output is?

I'm assuming that the current output of `get_scraper_index` is what you want, and the comment is just slightly off or I misunderstood it. To this end, I'm currently tidying up and fixing the now broken tests.
mlduff: @jayaddison I have just pushed a commit which fixes the tests, incorporating your changes. One thing I would like to point out are the cases of (`mobkitchen.co.uk`, `mob.co.uk`) and (`sharing.kptncook.com`, `mobile.kptncook.com`). For the `Mob` case, I followed the same approach you did for `CreativeCanning` and `PracticalSelfReliance` as they are different domains. However, for the `KptnCook` case, I kept them with the same class, and added handling to check if they actually had a shared prefix - if they don't, treat them both as primary domains (this is the behavior I had before your change).

What do you think of the above?
jknndy: > Side note - not sure why the weight watchers tests are failing?

Haven't had time to look over all the changes yet but did take a look at this, not sure exactly whats causing the change but the newly expected output is closer to matching the actual information on the site. So a good change in the end?

mlduff: @jayaddison To recap, is there anything else you want me to do to get this PR ready to merge? Keen to get it in to avoid more merge conflicts.

Patch:
diff --git a/README.rst b/README.rst
index b79aaa5a..a6150fd6 100644
--- a/README.rst
+++ b/README.rst
@@ -89,18 +89,19 @@ Some Python HTTP clients that you can use to retrieve HTML include `requests <ht
 Scrapers available for:
 -----------------------
 
+- `https://101cookbooks.com/ <https://101cookbooks.com/>`_
 - `https://15gram.be <https://15gram.be>`_
+- `https://www.750g.com <https://www.750g.com>`_
 - `https://aberlehome.com/ <https://aberlehome.com>`_
-- `https://claudia.abril.com.br/ <https://claudia.abril.com.br>`_
 - `https://abuelascounter.com/ <https://abuelascounter.com>`_
 - `https://www.acouplecooks.com <https://acouplecooks.com/>`_
-- `https://aflavorjournal.com/ <https://aflavorjournal.com/>`_
 - `https://addapinch.com/ <https://addapinch.com/>`_
 - `http://www.afghankitchenrecipes.com/ <http://www.afghankitchenrecipes.com/>`_
-- `https://akispetretzikis.com/ <https://akispetretzikis.com/>`_
+- `https://aflavorjournal.com/ <https://aflavorjournal.com/>`_
 - `https://ah.nl/ <https://ah.nl/>`_
-- `https://alittlebityummy.com/ <https://alittlebityummy.com/>`_
+- `https://akispetretzikis.com/ <https://akispetretzikis.com/>`_
 - `https://alexandracooks.com/ <https://alexandracooks.com/>`_
+- `https://alittlebityummy.com/ <https://alittlebityummy.com/>`_
 - `https://allrecipes.com/ <https://allrecipes.com/>`_
 - `https://allthehealthythings.com/ <https://allthehealthythings.com/>`_
 - `https://alltommat.se/ <https://alltommat.se/>`_
@@ -112,13 +113,13 @@ Scrapers available for:
 - `https://www.arla.se/ <https://www.arla.se/>`_
 - `https://www.atelierdeschefs.fr/ <https://www.atelierdeschefs.fr/>`_
 - `https://averiecooks.com/ <https://www.averiecooks.com/>`_
-- `https://barefeetinthekitchen.com/ <https://barefeetinthekitchen.com/>`_
-- `https://barefootcontessa.com/ <https://barefootcontessa.com>`_
 - `https://www.bakels.com.au/ <https://www.bakels.com.au/>`_
 - `https://baking-sense.com/ <https://baking-sense.com/>`_
 - `https://bakingmischief.com/ <https://bakingmischief.com/>`_
+- `https://barefeetinthekitchen.com/ <https://barefeetinthekitchen.com/>`_
+- `https://barefootcontessa.com/ <https://barefootcontessa.com>`_
 - `https://bbc.com/ <https://bbc.com/food/recipes>`_
-- `https://bbc.co.uk/ <http://bbc.co.uk/food/recipes>`_
+    - `.co.uk <https://bbc.co.uk/food/recipes>`_
 - `https://bbcgoodfood.com/ <https://bbcgoodfood.com>`_
 - `https://bestrecipes.com.au/ <https://bestrecipes.com.au>`_
 - `https://bettybossi.ch/ <https://bettybossi.ch>`_
@@ -127,8 +128,8 @@ Scrapers available for:
 - `https://bigoven.com/ <https://bigoven.com>`_
 - `https://blueapron.com/ <https://blueapron.com>`_
 - `https://bluejeanchef.com/ <https://bluejeanchef.com/>`_
-- `https://bonappetit.com/ <https://bonappetit.com>`_
 - `https://www.bodybuilding.com/ <https://www.bodybuilding.com/>`_
+- `https://bonappetit.com/ <https://bonappetit.com>`_
 - `https://bongeats.com/ <https://bongeats.com/>`_
 - `https://bowlofdelicious.com/ <https://bowlofdelicious.com/>`_
 - `https://breadtopia.com/ <https://breadtopia.com/>`_
@@ -141,15 +142,16 @@ Scrapers available for:
 - `https://chefkoch.de/ <https://chefkoch.de>`_
 - `https://www.chefnini.com/ <https://www.chefnini.com/>`_
 - `https://chefsavvy.com/ <https://chefsavvy.com/>`_
+- `https://claudia.abril.com.br/ <https://claudia.abril.com.br>`_
 - `https://closetcooking.com/ <https://closetcooking.com>`_
 - `https://comidinhasdochef.com/ <https://comidinhasdochef.com/>`_
+- `https://cook-talk.com/ <https://cook-talk.com/>`_
 - `https://cookeatshare.com/ <https://cookeatshare.com/>`_
 - `https://cookieandkate.com/ <https://cookieandkate.com/>`_
+- `https://cooking.nytimes.com/ <https://cooking.nytimes.com>`_
 - `https://cookingcircle.com/ <https://cookingcircle.com/>`_
 - `https://cookinglight.com/ <https://cookinglight.com/>`_
 - `https://cookpad.com/ <https://cookpad.com/>`_
-- `https://cookstr.com/ <https://cookstr.com>`_
-- `https://cook-talk.com/ <https://cook-talk.com/>`_
 - `https://www.coop.se/ <https://www.coop.se/>`_
 - `https://copykat.com/ <https://copykat.com>`_
 - `https://www.costco.com/ <https://www.costco.com>`_
@@ -171,17 +173,18 @@ Scrapers available for:
 - `https://www.eatingwell.com/ <https://www.eatingwell.com>`_
 - `https://www.eatliverun.com/ <https://www.eatliverun.com/>`_
 - `https://eatsmarter.com/ <https://eatsmarter.com/>`_
-- `https://eatsmarter.de/ <https://eatsmarter.de/>`_
+    - `.de <https://eatsmarter.de/>`_
+- `https://eattolerant.de/ <https://eattolerant.de/>`_
 - `https://www.eatwell101.com <https://www.eatwell101.com>`_
 - `https://eatwhattonight.com/ <https://eatwhattonight.com/>`_
 - `https://elavegan.com/ <https://elavegan.com/>`_
 - `https://emmikochteinfach.de/ <https://emmikochteinfach.de/>`_
-- `https://ethanchlebowski.com/ <https://ethanchlebowski.com>`_
+- `https://en.wikibooks.org/ <https://en.wikibooks.org>`_
 - `https://epicurious.com/ <https://epicurious.com>`_
+- `https://www.errenskitchen.com/ <https://www.errenskitchen.com/>`_
+- `https://ethanchlebowski.com/ <https://ethanchlebowski.com>`_
 - `https://www.evolvingtable.com/ <https://www.evolvingtable.com/>`_
 - `https://www.familyfoodonthetable.com/ <https://www.familyfoodonthetable.com/>`_
-- `https://www.errenskitchen.com/ <https://www.errenskitchen.com/>`_
-- `https://recipes.farmhousedelivery.com/ <https://recipes.farmhousedelivery.com/>`_
 - `https://www.farmhouseonboone.com/ <https://www.farmhouseonboone.com/>`_
 - `https://www.fattoincasadabenedetta.it/ <https://www.fattoincasadabenedetta.it/>`_
 - `https://felix.kitchen <https://felix.kitchen>`_
@@ -192,20 +195,19 @@ Scrapers available for:
 - `https://food.com/ <https://www.food.com>`_
 - `https://food52.com/ <https://www.food52.com>`_
 - `https://foodandwine.com/ <https://www.foodandwine.com>`_
-- `https://foodnetwork.com/ <https://www.foodnetwork.com>`_
+- `https://foodfidelity.com/ <https://foodfidelity.com>`_
+- `https://foodnetwork.co.uk/ <https://www.foodnetwork.co.uk>`_
+    - `.com <https://www.foodnetwork.com>`_
 - `https://foodrepublic.com/ <https://foodrepublic.com>`_
 - `https://www.forksoverknives.com/ <https://www.forksoverknives.com/>`_
 - `https://forktospoon.com/ <https://forktospoon.com/>`_
-- `https://fredriksfika.allas.se/ <https://fredriksfika.allas.se/>`_
-- `https://www.750g.com <https://www.750g.com>`_
+- `https://franzoesischkochen.de/ <https://franzoesischkochen.de/>`_
 - `https://www.gesund-aktiv.com/ <https://www.gesund-aktiv.com>`_
-- `https://giallozafferano.it/ <https://giallozafferano.it>`_
 - `https://gimmesomeoven.com/ <https://www.gimmesomeoven.com/>`_
 - `https://godt.no/ <https://godt.no/>`_
+- `https://gonnawantseconds.com/ <https://gonnawantseconds.com>`_
 - `https://goodfooddiscoveries.com/ <https://goodfooddiscoveries.com/>`_
 - `https://goodhousekeeping.com/ <https://www.goodhousekeeping.com/>`_
-- `https://recietas.globo.com/ <https://www.receitas.globo.com/>`_
-- `https://gonnawantseconds.com/ <https://gonnawantseconds.com>`_
 - `https://gourmettraveller.com.au/ <https://gourmettraveller.com.au>`_
 - `https://gousto.co.uk/ <https://gousto.co.uk>`_
 - `https://www.grandfrais.com/ <https://www.grandfrais.com>`_
@@ -216,19 +218,15 @@ Scrapers available for:
 - `https://handletheheat.com/ <https://handletheheat.com/>`_
 - `https://www.hassanchef.com/ <https://www.hassanchef.com/>`_
 - `https://headbangerskitchen.com/ <https://www.headbangerskitchen.com/>`_
+- `https://healthyeating.nhlbi.nih.gov/ <https://healthyeating.nhlbi.nih.gov>`_
 - `https://heatherchristo.com/ <https://heatherchristo.com/>`_
 - `https://www.heb.com/ <https://www.heb.com/recipe/landing>`_
 - `https://hellofresh.com/ <https://hellofresh.com>`_
-- `https://hellofresh.co.uk/ <https://hellofresh.co.uk>`_
-- `https://www.hellofresh.de/ <https://www.hellofresh.de/>`_
-- `https://www.hellofresh.fr/ <https://www.hellofresh.fr/>`_
-- `https://www.hellofresh.nl/ <https://www.hellofresh.nl/>`_
-- `https://www.hellofresh.ie/ <https://www.hellofresh.ie/>`_
+    - `.at <https://www.hellofresh.at/>`_ `.be <https://www.hellofresh.be/>`_, `.ca <https://www.hellofresh.ca/>`_, `.ch <https://www.hellofresh.ch/>`_, `.co.nz <https://www.hellofresh.co.nz/>`_, `.co.uk <https://hellofresh.co.uk>`_, `.com.au <https://www.hellofresh.com.au/>`_, `.de <https://www.hellofresh.de/>`_, `.dk <https://www.hellofresh.dk/>`_, `.es <https://www.hellofresh.es/>`_, `.fr <https://www.hellofresh.fr/>`_, `.ie <https://www.hellofresh.ie/>`_, `.it <https://www.hellofresh.it/>`_, `.lu <https://www.hellofresh.lu/>`_, `.nl <https://www.hellofresh.nl/>`_, `.no <https://www.hellofresh.no/>`_, `.se <https://www.hellofresh.se/>`_
 - `https://www.hersheyland.com/ <https://www.hersheyland.com/>`_
 - `https://www.homechef.com/ <https://www.homechef.com/>`_
 - `https://hostthetoast.com/ <https://hostthetoast.com/>`_
 - `https://www.ica.se/ <https://www.ica.se/>`_
-- `https://receitas.ig.com.br/ <https://receitas.ig.com.br>`_
 - `https://www.im-worthy.com/ <https://www.im-worthy.com>`_
 - `https://inbloombakery.com/ <https://inbloombakery.com/>`_
 - `https://indianhealthyrecipes.com <https://www.indianhealthyrecipes.com>`_
@@ -247,6 +245,7 @@ Scrapers available for:
 - `https://justbento.com/ <https://justbento.com>`_
 - `https://www.justonecookbook.com/ <https://www.justonecookbook.com>`_
 - `https://kennymcgovern.com/ <https://kennymcgovern.com>`_
+- `https://keukenliefde.nl/ <https://keukenliefde.nl>`_
 - `https://www.kingarthurbaking.com <https://www.kingarthurbaking.com>`_
 - `https://kitchenaid.com.au/ <https://kitchenaid.com.au/blogs/kitchenthusiast/tagged/blog-category-recipes>`_
 - `https://www.kitchensanctuary.com/ <https://www.kitchensanctuary.com>`_
@@ -254,15 +253,14 @@ Scrapers available for:
 - `https://kochbar.de/ <https://kochbar.de>`_
 - `https://kochbucher.com/ <https://kochbucher.com/>`_
 - `http://koket.se/ <http://koket.se>`_
-- `https://www.kptncook.com/ <https://www.kptncook.com>`_
 - `https://kristineskitchenblog.com/ <https://kristineskitchenblog.com>`_
 - `https://kuchnia-domowa.pl/ <https://www.kuchnia-domowa.pl/>`_
 - `https://kuchynalidla.sk/ <https://www.kuchynalidla.sk/>`_
 - `https://www.kwestiasmaku.com/ <https://www.kwestiasmaku.com/>`_
 - `https://www.latelierderoxane.com <https://www.latelierderoxane.com/blog/recettes/>`_
 - `https://leanandgreenrecipes.net <https://leanandgreenrecipes.net>`_
-- `https://lecremedelacrumb.com/ <https://lecremedelacrumb.com/>`_
 - `https://www.lecker.de <https://www.lecker.de/rezepte>`_
+- `https://lecremedelacrumb.com/ <https://lecremedelacrumb.com/>`_
 - `https://lekkerensimpel.com <https://lekkerensimpel.com>`_
 - `https://leukerecepten.nl/ <https://www.leukerecepten.nl>`_
 - `https://lifestyleofafoodie.com <https://lifestyleofafoodie.com>`_
@@ -273,18 +271,12 @@ Scrapers available for:
 - `https://madensverden.dk/ <https://madensverden.dk/>`_
 - `https://www.madewithlau.com/ <https://www.madewithlau.com/>`_
 - `https://madsvin.com/ <https://madsvin.com/>`_
-- `https://marleyspoon.com.au/ <https://marleyspoon.com.au/>`_
 - `https://marleyspoon.com/ <https://marleyspoon.com/>`_
-- `https://marleyspoon.de/ <https://marleyspoon.de/>`_
-- `https://marleyspoon.at/ <https://marleyspoon.at/>`_
-- `https://marleyspoon.be/ <https://marleyspoon.be/>`_
-- `https://marleyspoon.nl/ <https://marleyspoon.nl/>`_
-- `https://marleyspoon.se/ <https://marleyspoon.se/>`_
+    - `.at <https://marleyspoon.at/>`_, `.be <https://marleyspoon.be/>`_, `.com.au <https://marleyspoon.com.au/>`_, `.de <https://marleyspoon.de/>`_, `.nl <https://marleyspoon.nl/>`_, `.se <https://marleyspoon.se/>`_
 - `https://marmiton.org/ <https://marmiton.org/>`_
 - `https://www.marthastewart.com/ <https://www.marthastewart.com/>`_
 - `https://matprat.no/ <https://matprat.no/>`_
 - `https://www.mccormick.com/ <https://www.mccormick.com/>`_
-- `https://www.modernhoney.com/ <https://www.modernhoney.com/>`_
 - `https://meljoulwan.com/ <https://meljoulwan.com/>`_
 - `https://www.melskitchencafe.com/ <https://www.melskitchencafe.com/>`_
 - `http://mindmegette.hu/ <http://mindmegette.hu/>`_
@@ -292,10 +284,13 @@ Scrapers available for:
 - `https://ministryofcurry.com/ <https://ministryofcurry.com/>`_
 - `https://misya.info/ <https://misya.info>`_
 - `https://www.mob.co.uk/ <https://www.mob.co.uk/>`_
+- `https://mobile.kptncook.com/ <https://mobile.kptncook.com/>`_
+- `https://mobkitchen.co.uk/ <https://mobkitchen.co.uk/>`_
+- `https://www.modernhoney.com/ <https://www.modernhoney.com/>`_
+- `https://www.momontimeout.com/ <https://www.momontimeout.com/>`_
 - `https://momswithcrockpots.com/ <https://momswithcrockpots.com>`_
 - `https://monsieur-cuisine.com/ <https://monsieur-cuisine.com>`_
 - `http://motherthyme.com/ <http://motherthyme.com/>`_
-- `https://www.momontimeout.com/ <https://www.momontimeout.com/>`_
 - `https://www.moulinex.fr/ <https://www.moulinex.fr/>`_
 - `https://www.mundodereceitasbimby.com.pt/ <https://www.mundodereceitasbimby.com.pt/>`_
 - `https://mybakingaddiction.com/ <https://mybakingaddiction.com>`_
@@ -305,35 +300,32 @@ Scrapers available for:
 - `https://mykoreankitchen.com/ <https://mykoreankitchen.com>`_
 - `https://www.myplate.gov/ <https://www.myplate.gov/>`_
 - `https://myrecipes.com/ <https://myrecipes.com>`_
-- `https://healthyeating.nhlbi.nih.gov/ <https://healthyeating.nhlbi.nih.gov>`_
-- `https://nibbledish.com/ <https://nibbledish.com>`_
 - `https://www.nhs.uk/healthier-families/ <https://www.nhs.uk/healthier-families/>`_
-- `https://www.nosalty.hu/ <https://www.nosalty.hu>`_
+- `https://nibbledish.com/ <https://nibbledish.com>`_
+- `https://norecipes.com/ <https://norecipes.com/>`_
 - `https://www.notenoughcinnamon.com/ <https://www.notenoughcinnamon.com/>`_
 - `https://nourishedbynutrition.com/ <https://nourishedbynutrition.com/>`_
 - `https://www.nrk.no/ <https://www.nrk.no/>`_
 - `https://www.number-2-pencil.com/ <https://www.number-2-pencil.com/>`_
 - `https://nutritionbynathalie.com/blog <https://nutritionbynathalie.com/blog>`_
 - `https://nutritionfacts.org/ <https://nutritionfacts.org/>`_
-- `https://cooking.nytimes.com/ <https://cooking.nytimes.com>`_
 - `https://ohsheglows.com/ <https://ohsheglows.com>`_
 - `https://omnivorescookbook.com <https://omnivorescookbook.com>`_
 - `https://www.onceuponachef.com <https://www.onceuponachef.com>`_
 - `https://onesweetappetite.com/ <https://onesweetappetite.com>`_
 - `https://owen-han.com/ <https://owen-han.com>`_
-- `https://101cookbooks.com/ <https://101cookbooks.com/>`_
 - `https://www.paleorunningmomma.com/ <https://www.paleorunningmomma.com>`_
 - `https://www.panelinha.com.br/ <https://www.panelinha.com.br>`_
 - `https://paninihappy.com/ <https://paninihappy.com>`_
 - `https://www.persnicketyplates.com/ <https://www.persnicketyplates.com/>`_
-- `https://pinchofyum.com/ <https://pinchofyum.com/>`_
 - `https://www.pickuplimes.com/ <https://www.pickuplimes.com/>`_
+- `https://pinchofyum.com/ <https://pinchofyum.com/>`_
 - `https://www.pingodoce.pt/ <https://www.pingodoce.pt>`_
 - `https://pinkowlkitchen.com/ <https://pinkowlkitchen.com/>`_
 - `https://www.platingpixels.com/ <https://www.platingpixels.com/>`_
 - `https://plowingthroughlife.com/ <https://plowingthroughlife.com/>`_
-- `https://potatorolls.com/ <https://potatorolls.com/>`_
 - `https://popsugar.com/ <https://popsugar.com>`_
+- `https://potatorolls.com/ <https://potatorolls.com/>`_
 - `https://practicalselfreliance.com/ <https://practicalselfreliance.com>`_
 - `https://pressureluckcooking.com/ <https://pressureluckcooking.com/>`_
 - `https://www.primaledgehealth.com/ <https://www.primaledgehealth.com/>`_
@@ -345,31 +337,38 @@ Scrapers available for:
 - `https://rainbowplantlife.com/ <https://rainbowplantlife.com/>`_
 - `https://realfood.tesco.com/ <https://realfood.tesco.com>`_
 - `https://realsimple.com/ <https://www.realsimple.com>`_
-- `https://recept.se/ <https://recept.se/>`_
+- `https://receitas.globo.com/ <https://www.receitas.globo.com/>`_
+- `https://receitas.ig.com.br/ <https://receitas.ig.com.br>`_
 - `https://www.receitasnestle.com.br <https://www.receitasnestle.com.br>`_
+- `https://recept.se/ <https://recept.se/>`_
 - `https://www.recipegirl.com/ <https://www.recipegirl.com/>`_
 - `https://reciperunner.com/ <https://www.reciperunner.com>`_
+- `https://recipes.farmhousedelivery.com/ <https://recipes.farmhousedelivery.com/>`_
+- `https://recipes.timesofindia.com/ <https://recipes.timesofindia.com/>`_
 - `https://recipetineats.com/ <https://www.recipetineats.com/>`_
 - `https://redhousespice.com/ <https://redhousespice.com/>`_
 - `https://reishunger.de/ <https://www.reishunger.de/>`_
 - `https://rezeptwelt.de/ <https://rezeptwelt.de>`_
 - `https://ricetta.it/ <https://ricetta.it>`_
+- `https://ricette.giallozafferano.it/ <https://ricette.giallozafferano.it>`_
 - `https://www.ricetteperbimby.it/ <https://www.ricetteperbimby.it/>`_
 - `https://rosannapansino.com <https://rosannapansino.com>`_
 - `https://rutgerbakt.nl/ <https://rutgerbakt.nl/>`_
 - `https://www.saboresajinomoto.com.br/ <https://www.saboresajinomoto.com.br/>`_
-- `https://sallysbakingaddiction.com <https://sallysbakingaddiction.com/>`_
 - `https://sallys-blog.de <https://sallys-blog.de/>`_
+- `https://sallysbakingaddiction.com <https://sallysbakingaddiction.com/>`_
 - `https://saltpepperskillet.com/ <https://saltpepperskillet.com/>`_
 - `https://www.saveur.com/ <https://www.saveur.com/>`_
 - `https://www.savorynothings.com/ <https://www.savorynothings.com/>`_
 - `https://seriouseats.com/ <https://seriouseats.com>`_
+- `https://sharing.kptncook.com/ <https://sharing.kptncook.com/>`_
 - `https://simple-veganista.com/ <https://simple-veganista.com/>`_
+- `https://simply-cookit.com/ <https://simply-cookit.com>`_
 - `https://simplyquinoa.com/ <https://simplyquinoa.com>`_
-- `https://simplyrecipes.com/ <https://simplyrecipes.co>`_
+- `https://simplyrecipes.com/ <https://simplyrecipes.com>`_
 - `https://simplywhisked.com/ <https://simplywhisked.com>`_
-- `https://simply-cookit.com/ <https://simply-cookit.com>`_
 - `https://skinnytaste.com/ <https://www.skinnytaste.com>`_
+- `https://smulweb.nl/ <https://smulweb.nl>`_
 - `https://sobors.hu/ <https://sobors.hu>`_
 - `https://www.southerncastiron.com/ <https://www.southerncastiron.com>`_
 - `https://southernliving.com/ <https://southernliving.com/>`_
@@ -384,34 +383,34 @@ Scrapers available for:
 - `https://www.sunset.com/ <https://www.sunset.com/>`_
 - `https://sweetcsdesigns.com/ <https://www.sweetcsdesigns.com/>`_
 - `https://sweetpeasandsaffron.com/ <https://sweetpeasandsaffron.com/>`_
-- `https://www.tasteatlas.com/ <https://www.tasteatlas.com/>`_
-- `https://www.thecookierookie.com/ <hhttps://www.thecookierookie.com/>`_
 - `https://www.taste.com.au/ <https://www.taste.com.au/>`_
+- `https://www.tasteatlas.com/ <https://www.tasteatlas.com/>`_
 - `https://tasteofhome.com <https://tasteofhome.com>`_
 - `https://tastesbetterfromscratch.com <https://tastesbetterfromscratch.com>`_
 - `https://tastesoflizzyt.com <https://tastesoflizzyt.com>`_
 - `https://tasty.co <https://tasty.co>`_
 - `https://tastykitchen.com/ <https://tastykitchen.com>`_
 - `https://theclevercarrot.com/ <https://theclevercarrot.com>`_
+- `https://www.thecookierookie.com/ <https://www.thecookierookie.com/>`_
 - `https://thecookingguy.com/ <https://thecookingguy.com>`_
 - `https://theexpertguides.com/ <https://theexpertguides.com>`_
 - `https://thehappyfoodie.co.uk/ <https://thehappyfoodie.co.uk>`_
-- `https://www.thekitchenmagpie.com/ <https://www.thekitchenmagpie.com>`_
 - `https://thekitchencommunity.org/ <https://thekitchencommunity.org/>`_
+- `https://www.thekitchenmagpie.com/ <https://www.thekitchenmagpie.com>`_
 - `https://thekitchn.com/ <https://thekitchn.com/>`_
 - `https://www.themagicalslowcooker.com/ <https://www.themagicalslowcooker.com/>`_
 - `https://themodernproper.com/ <https://themodernproper.com/>`_
 - `https://www.thepalatablelife.com <https://www.thepalatablelife.com/>`_
-- `https://thesaltymarshmallow.com/ <https://thesaltymarshmallow.com/>`_
 - `https://thepioneerwoman.com/ <https://thepioneerwoman.com>`_
 - `https://therecipecritic.com/ <https://therecipecritic.com>`_
+- `https://thesaltymarshmallow.com/ <https://thesaltymarshmallow.com/>`_
 - `https://thespruceeats.com/ <https://thespruceeats.com/>`_
 - `https://thevintagemixer.com/ <https://thevintagemixer.com>`_
 - `https://thewoksoflife.com/ <https://thewoksoflife.com/>`_
 - `https://thinlicious.com/ <https://thinlicious.com/>`_
-- `https://timesofindia.com/ <https://timesofindia.com/>`_
-- `https://tine.no/ <https://tine.no>`_
 - `https://tidymom.net <https://tidymom.net>`_
+- `https://tine.no/ <https://tine.no>`_
+- `https://tofoo.co.uk <https://tofoo.co.uk>`_
 - `https://tudogostoso.com.br/ <https://www.tudogostoso.com.br/>`_
 - `https://twopeasandtheirpod.com/ <http://twopeasandtheirpod.com>`_
 - `https://uitpaulineskeuken.nl/ <https://uitpaulineskeuken.nl>`_
@@ -430,12 +429,11 @@ Scrapers available for:
 - `https://whatsgabycooking.com/ <https://whatsgabycooking.com>`_
 - `https://whole30.com/ <https://whole30.com/>`_
 - `https://www.wholefoodsmarket.com/ <https://www.wholefoodsmarket.com/>`_
-- `https://www.wholefoodsmarket.co.uk/ <https://www.wholefoodsmarket.co.uk/>`_
+    - `.co.uk <https://www.wholefoodsmarket.co.uk/>`_
 - `https://www.williams-sonoma.com/ <https://www.williams-sonoma.com/>`_
-- `https://www.womensweekly.com.au/ <https://www.womensweekly.com.au/>`_
-- `https://woop.co.nz/ <https://woop.co.nz/>`_
+- `https://womensweeklyfood.com.au/ <https://womensweeklyfood.com.au/>`_
 - `https://woolworths.com.au/shop/recipes <https://www.woolworths.com.au/shop/recipes/>`_
-- `https://en.wikibooks.org/ <https://en.wikibooks.org>`_
+- `https://woop.co.nz/ <https://woop.co.nz/>`_
 - `https://yemek.com/ <https://yemek.com>`_
 - `https://yummly.com/ <https://yummly.com>`_ (*)
 - `https://www.zaubertopf.de <https://www.zaubertopf.de>`_
diff --git a/docs/how-to-develop-scraper.md b/docs/how-to-develop-scraper.md
index 5ac4f2f5..2456460c 100644
--- a/docs/how-to-develop-scraper.md
+++ b/docs/how-to-develop-scraper.md
@@ -205,7 +205,13 @@ Where `ClassName` is the name that you used earlier to generate the scraper.
 > [!TIP]
 > It is also recommended that you manually test the scraper with a couple of different recipes from the website, to check that there aren't any special cases the scraper will need to handle. You don't need to create test cases for each of these.
 
-## 6. Open a pull request
+## 6. Update the README
+
+Add the website's domain to the supported scraper list in README.rst, ensuring alphabetical order.
+
+If your site supports multiple top level domains (e.g. `.com.au`, `.co.ul`, `.at`, etc.) then list these on an indented entry under the primary domain (the default value of `host()` when no arguments are provided). For an example of this, check out the `hellofresh` listings.
+
+## 7. Open a pull request
 
 Once you have finished developing the scraper and test case, you can commit the files to git and push them to GitHub. You should also update the README.rst to list the site, alphabetically, under the [Scrapers available for:](https://github.com/hhursev/recipe-scrapers#scrapers-available-for) header.
 
diff --git a/recipe_scrapers/__init__.py b/recipe_scrapers/__init__.py
index 37ebbf86..98fd2494 100644
--- a/recipe_scrapers/__init__.py
+++ b/recipe_scrapers/__init__.py
@@ -199,6 +199,7 @@ from .minimalistbaker import Minimalistbaker
 from .ministryofcurry import MinistryOfCurry
 from .misya import Misya
 from .mob import Mob
+from .mobkitchen import MobKitchen
 from .modernhoney import ModernHoney
 from .momontimeout import MomOnTimeout
 from .momswithcrockpots import MomsWithCrockPots
@@ -591,8 +592,8 @@ SCRAPERS = {
     Minimalistbaker.host(): Minimalistbaker,
     MinistryOfCurry.host(): MinistryOfCurry,
     Misya.host(): Misya,
-    Mob.host(domain="mob.co.uk"): Mob,
-    Mob.host(domain="mobkitchen.co.uk"): Mob,
+    Mob.host(): Mob,
+    MobKitchen.host(): MobKitchen,
     MomsWithCrockPots.host(): MomsWithCrockPots,
     MonsieurCuisine.host(): MonsieurCuisine,
     MotherThyme.host(): MotherThyme,
diff --git a/recipe_scrapers/mob.py b/recipe_scrapers/mob.py
index b83a2291..c35097ce 100644
--- a/recipe_scrapers/mob.py
+++ b/recipe_scrapers/mob.py
@@ -14,8 +14,8 @@ class Mob(AbstractScraper):
         )["props"]["pageProps"]["recipe"]
 
     @classmethod
-    def host(cls, domain="mob.co.uk"):
-        return domain
+    def host(cls):
+        return "mob.co.uk"
 
     def author(self):
         chefs = self.recipe_json.get("chefs", [])
diff --git a/recipe_scrapers/mobkitchen.py b/recipe_scrapers/mobkitchen.py
new file mode 100644
index 00000000..b37864a3
--- /dev/null
+++ b/recipe_scrapers/mobkitchen.py
@@ -0,0 +1,9 @@
+# mypy: allow-untyped-defs
+
+from .mob import Mob
+
+
+class MobKitchen(Mob):
+    @classmethod
+    def host(cls):
+        return "mobkitchen.co.uk"
diff --git a/recipe_scrapers/weightwatchers.py b/recipe_scrapers/weightwatchers.py
index 27d79858..c61c8ef0 100644
--- a/recipe_scrapers/weightwatchers.py
+++ b/recipe_scrapers/weightwatchers.py
@@ -9,7 +9,7 @@ from ._utils import get_minutes, get_yields, normalize_string
 class WeightWatchers(AbstractScraper):
     @classmethod
     def host(cls):
-        return "www.weightwatchers.com"
+        return "weightwatchers.com"
 
     def author(self):
         return "WeightWatchers"
diff --git a/recipe_scrapers/weightwatcherspublic.py b/recipe_scrapers/weightwatcherspublic.py
index 5461439a..1e553cfb 100644
--- a/recipe_scrapers/weightwatcherspublic.py
+++ b/recipe_scrapers/weightwatcherspublic.py
@@ -8,7 +8,7 @@ from .weightwatchers import WeightWatchers
 class WeightWatchersPublic(WeightWatchers):
     @classmethod
     def host(cls):
-        return "www.weightwatchers.com"
+        return "weightwatchers.com"
 
     def _find_data_container(self):
         return self.soup.find("div", {"class": "HorizontalList_list__GESs0"})


Test patch:
diff --git a/tests/library/test_readme.py b/tests/library/test_readme.py
new file mode 100644
index 00000000..fc229a81
--- /dev/null
+++ b/tests/library/test_readme.py
@@ -0,0 +1,162 @@
+import re
+import unittest
+from collections import defaultdict
+from typing import Dict, List, Optional, Tuple
+
+from recipe_scrapers import SCRAPERS, AbstractScraper
+
+START_LIST = "-----------------------"
+END_LIST = "(*) offline saved files only"
+
+ScraperIndex = Dict[str, Tuple[AbstractScraper, List[str]]]
+
+
+def get_scraper_domains():
+    scraper_domains = defaultdict(list)
+    for domain, scraper in SCRAPERS.items():
+        primary_domain = scraper.host()
+        if domain == primary_domain:
+            scraper_domains[scraper].insert(0, domain)
+        else:
+            scraper_domains[scraper].append(domain)
+    return scraper_domains
+
+
+def get_scraper_index() -> ScraperIndex:
+    scraper_index: ScraperIndex = {}
+    for scraper_instance, domains in get_scraper_domains().items():
+        shared_prefix = get_shared_prefix(domains)
+
+        if not shared_prefix:
+            # Treat all as primary domains
+            for domain in domains:
+                scraper_index[domain] = (scraper_instance, [domain])
+            continue
+
+        # Index the primary domain and include their secondary domains minus the shared prefix
+        primary_domain = scraper_instance.host()
+        secondary_domains = [
+            domain[len(shared_prefix) :] if domain.startswith(shared_prefix) else domain
+            for domain in domains
+            if domain != shared_prefix
+        ]
+        scraper_index[primary_domain] = (scraper_instance, secondary_domains)
+
+    # Produce the index sorted by primary domain name
+    return scraper_index
+
+
+def get_shared_prefix(domains: List[str]) -> str:
+    """
+    Find the longest-common-prefix of the domains
+    """
+    if not domains:
+        return ""
+
+    shared_prefix = domains[0]
+    for domain in domains[1:]:
+        while not domain.startswith(shared_prefix):
+            shared_prefix = shared_prefix[:-1]
+            if not shared_prefix:
+                return ""
+
+    if "." in shared_prefix:
+        shared_prefix, _ = shared_prefix.rsplit(".", 1)
+
+    return shared_prefix
+
+
+def get_secondary_domains(
+    scraper_index: ScraperIndex, primary_domain: str
+) -> List[str]:
+    _, suffixes = scraper_index[primary_domain]
+    return [suffix for suffix in suffixes if not primary_domain.endswith(suffix)]
+
+
+def parse_primary_line(line: str) -> Optional[Tuple[str, str]]:
+    match = re.search(
+        r"^- `https?://(?:www\.)?([^/\s]+)[^<]*<https?://(?:www\.)?([^/\s]*)[^>]*>`_(?: \(\*\))?$",
+        line,
+    )
+    if match:
+        groups = match.groups()
+        if len(groups) == 2:
+            return groups
+    return None
+
+
+def parse_secondary_line(line: str) -> List[Tuple[str, str]]:
+    return re.findall(r"`(\.[^\s]+)\s<https?://(?:www\.)?([^/>]+)[^>]*>`_", line)
+
+
+def get_list_lines() -> List[str]:
+    list_lines: List[str] = []
+    with open("README.rst") as f:
+        started_list = False
+        for line in f:
+            stripped_line = line.strip()
+            if stripped_line == START_LIST:
+                started_list = True
+                continue
+
+            if not started_list or not stripped_line:
+                continue
+
+            if stripped_line == END_LIST:
+                break
+
+            list_lines.append(line)
+    return list_lines
+
+
+class TestReadme(unittest.TestCase):
+
+    def test_includes(self):
+        scraper_index = get_scraper_index()
+        primary_domains = sorted(scraper_index.keys())
+        lines = get_list_lines()
+        current_line_index = 0
+
+        for primary_host in primary_domains:
+            current_line = lines[current_line_index]
+            parse_result = parse_primary_line(current_line)
+
+            if not parse_result:
+                self.fail(f"Invalid line: {current_line}")
+
+            name_host, value_host = parse_result
+            self.assertEqual(
+                name_host,
+                value_host,
+                "The name and value hyperlink portions have different hosts.",
+            )
+            self.assertEqual(
+                name_host,
+                primary_host,
+                f"The host ({name_host}) doesn't match the expected host ({primary_host})",
+            )
+
+            current_line_index += 1
+            secondary_hosts = get_secondary_domains(scraper_index, primary_host)
+
+            if secondary_hosts:
+                current_line = lines[current_line_index]
+                parse_result = parse_secondary_line(current_line)
+
+                if not parse_result:
+                    self.fail(f"Invalid line: {current_line}")
+
+                sorted_secondary_hosts = sorted(secondary_hosts)
+                for i, secondary_host in enumerate(sorted_secondary_hosts):
+                    if i >= len(parse_result):
+                        self.fail(
+                            f"Missing top level domain(s) for primary domain {primary_host}"
+                        )
+
+                    top_level_domain = parse_result[i][0]
+                    self.assertEqual(
+                        secondary_host,
+                        top_level_domain,
+                        f"Expected top level domain {secondary_host}, got {top_level_domain} for primary domain {primary_host}",
+                    )
+                current_line_index += 1
diff --git a/tests/test_data/weightwatchers.com/weightwatcherspublic.json b/tests/test_data/weightwatchers.com/weightwatcherspublic.json
index ea7a00dd..eea3b312 100644
--- a/tests/test_data/weightwatchers.com/weightwatcherspublic.json
+++ b/tests/test_data/weightwatchers.com/weightwatcherspublic.json
@@ -1,23 +1,23 @@
 {
-  "author": null,
+  "author": "WeightWatchers",
   "canonical_url": "https://www.weightwatchers.com/de/rezept/kartoffelgulasch/562a9b02873e1afb2a3c4c13",
   "site_name": null,
   "host": "weightwatchers.com",
   "language": "en",
   "title": "Kartoffelgulasch",
   "ingredients": [
-    "800 g Kartoffeln vorwiegend festkochend",
-    "2 Stück, mittelgroß Zwiebel/n",
-    "2 Stück, rot Paprika",
-    "2 Stück, grün Paprika",
-    "1 EL, gehackt Petersilie",
-    "2 Stück, gelb Paprika",
+    "800 g Kartoffeln; vorwiegend festkochend",
+    "2 Stück Zwiebel/n; mittelgroß",
+    "2 Stück Paprika; rot",
+    "2 Stück Paprika; grün",
+    "1 EL Petersilie; gehackt",
+    "2 Stück Paprika; gelb",
     "250 g Tomaten, frisch",
     "4 Stück Wiener Würstchen",
     "2 TL Pflanzenöl, Rapsöl/Sonnenblumenöl",
     "2 EL Tomatenmark",
-    "250 ml Gemüsebouillon/Gemüsebrühe, zubereitet (1 TL Instantpulver)",
-    "1 TL Oregano gehackt",
+    "250 ml Gemüsebouillon/Gemüsebrühe, zubereitet; (1 TL Instantpulver)",
+    "1 TL Oregano; gehackt",
     "1 TL Paprikapulver",
     "1 Prise(n) Salz/Jodsalz",
     "1 Prise(n) Pfeffer",
@@ -26,18 +26,18 @@
   "ingredient_groups": [
     {
       "ingredients": [
-        "800 g Kartoffeln vorwiegend festkochend",
-        "2 Stück, mittelgroß Zwiebel/n",
-        "2 Stück, rot Paprika",
-        "2 Stück, grün Paprika",
-        "1 EL, gehackt Petersilie",
-        "2 Stück, gelb Paprika",
+        "800 g Kartoffeln; vorwiegend festkochend",
+        "2 Stück Zwiebel/n; mittelgroß",
+        "2 Stück Paprika; rot",
+        "2 Stück Paprika; grün",
+        "1 EL Petersilie; gehackt",
+        "2 Stück Paprika; gelb",
         "250 g Tomaten, frisch",
         "4 Stück Wiener Würstchen",
         "2 TL Pflanzenöl, Rapsöl/Sonnenblumenöl",
         "2 EL Tomatenmark",
-        "250 ml Gemüsebouillon/Gemüsebrühe, zubereitet (1 TL Instantpulver)",
-        "1 TL Oregano gehackt",
+        "250 ml Gemüsebouillon/Gemüsebrühe, zubereitet; (1 TL Instantpulver)",
+        "1 TL Oregano; gehackt",
         "1 TL Paprikapulver",
         "1 Prise(n) Salz/Jodsalz",
         "1 Prise(n) Pfeffer",
@@ -55,12 +55,12 @@
   "yields": "4 servings",
   "description": "Das Rezept zaubert ein saftiges, würziges Gericht auf den Tisch und schmeckt garantiert.",
   "total_time": 40,
-  "cook_time": null,
+  "cook_time": 0,
   "prep_time": 40,
   "nutrients": {
-    "calories": "137 kcal"
+    "points": "13 Points&reg; value"
   },
-  "image": "https://cmx.weightwatchers.com/assets-proxy/weight-watchers/image/upload/t_WINE_EXTRALARGE/h7wo0hbnwcleucj30sbw.jpg",
+  "image": "https://cmx.weightwatchers.com/assets-proxy/weight-watchers/image/upload/q_auto/h7wo0hbnwcleucj30sbw.jpg?auto=webp",
   "keywords": [
     "Ohne Meeresfrüchte",
     "Ohne Fisch",