Een wordpress-plug-in om content van andere sites op jouw site te plaatsen

Ingediend door Dirk Hornstra op 15-jul-2024 22:04

English version

Er moet een andere auto komen. De specs zijn niet heel uitgebreid,  namelijk een "benzine-wagen", een automaat, niet al te groot en ook niet teveel kilometers op de teller. Je hebt natuurlijk een site als gaspedaal.nl waar je kunt zoeken/filteren. Maar dat voldoet niet aan mijn criteria (waarbij ik bepaalde merken juist wil uitsluiten van de resultaten). En ook staan volgens mij niet alle garages op die site.

Ik ben dus zelf gaan zoeken naar websites van (vak)garages en kon daar bij een groot deel de URL vinden om het resultaat van mijn zoekactie terug te geven.
Alleen, ik heb het "druk". Overdag aan het werk. In eigen tijd met allemaal andere dingen bezig. Ik heb hier helemaal geen tijd voor, terwijl het wel bij mij wordt "gedropt". Eigenlijk zou degene die de auto zoekt ook dat werk moeten doen. Zij moet er in gaan rijden, dan moet ze zelf ook maar kijken of er wat tussen zit. Zij is de pensionado, ik niet ;)

Maar om haar nu 30+ URL's aan te leveren en te zeggen: loop dit elke dag maar door, dat is ook weer niet hoe ik het voor me zie.

Dus ik heb er nu toch een paar dagen van mijn spaarzame tijd in gestoken om dat te fixen. Ik heb op een subdomein een Wordpress-site opgestart en heb het standaard-template gebruikt wat er bij zit. Veel dingen eruit gestript, zodat alleen het overzicht van berichten zichtbaar blijft.
En een "plan van aanpak" uitgewerkt.

Namelijk:

  1. Wordpress installeren.
  2. De URL's die ik wil opvragen kun je (tot nu toe) met een GET-request afvangen (dus als je de URL in je adresbalk plakt, kom je meteen op de juiste plek uit). In het linkermenu in Wordpress heb je "Berichten", "Media", "Pagina's". Ik wil hier een item "URL beheer", zodat ik daar zelf de URL's kan invoeren en aanpassen.
  3. Via een bepaalde URL kun je het aanbod van deze pagina's opvragen. De HTML wordt opgeslagen in losse wp_post records. Zodat je in /wp-admin de inhoud kunt bekijken.
  4. Er wordt gekeken of er al een vorige versie is, zo nee: dan maken we een nieuwe versie aan, zo ja: dan wordt gekeken of de huidige versie afwijkt. Zo nee: dan doen we niets, wijkt de versie wel af, dan maken we een nieuwe revisie aan.
  5. Bij een toevoeging of nieuwe versie doorlopen we de HTML en halen de "losse auto's" eruit. Deze worden als post aangemaakt. Ook deze controleren we: bestaat deze auto al en zijn de gegevens identiek, dan doen we niets. Anders maken we deze aan of maken een nieuwe revisie.
  6. Wat we nu doen door een bepaalde URL aan te roepen, dat doen we ook "automatisch" door via CRON deze actie elk uur uit te laten voeren.

 

Het opbouwen van deze plug-in heeft inmiddels versie 1.0 gehaald, het werkt. De code van de plug-in kun je vinden op deze Github-URL. Tijdens het bouwen ben ik soms afgeweken van de plan van aanpak, hierbij de uitleg wat er dan anders is;

1. Installeer Wordpress en dan begint het echte werk!

De installatie leek de meest simpele stap, maar daar liep ik meteen  al tegen problemen aan: ik kon niet meer FTP-en! Blijkt dat KPN een update heeft doorgevoerd waardoor de firewall van je glasvezelmodem standaard dat verkeer blokkeert. Die instelling op "laag" gezet en het werkt weer.
Toen ik "even  niets kon" heb ik alvast wat websites erbij gezocht.

2. Item "URL beheer" als los menu in WP-Admin.

Ik kreeg URL beheer niet als een los item in deze lijst. Bij mijn aanroep komt deze als submenu onder het onderdeel "Instellingen". Op zich vond ik dat ook wel prima, dus niet extra tijd in gestoken om het toch anders te krijgen.

3. Opvragen van de HTML van de (vak-) garages.

Direct maar even het punt benoemen om de HTML in een los post-record op te slaan en het item welke op de website getoond wordt in een ander post-record op te slaan: dat is niet nodig. Bij een post-record kun je namelijk meta-data opslaan (in de tabel postmeta). Dus ik maak een nieuw post-record aan en in een gekoppeld meta-veld "original_html" sla ik de originele lowercase HTML op.

Die meta-records gebruik ik ook bij de URL's. Ik sla een veld 'lastsync' op, waarmee ik zorg dat een URL maximaal 1x per uur gecrawled kan worden. Er zou iets fout kunnen gaan waardoor je een externe URL ineens tig keer aan gaat roepen. Om te voorkomen dat de andere kant je blokkeert, zorg je dus zelf dat je het aantal requests tot het minimum beperkt. Zodra ik een URL opsla in wp-admin wordt dit veld leeg gemaakt. Op deze manier kan ik dus ook zelf snel zorgen dat een URL weer opgevraagd kan worden, mocht ik bijvoorbeeld wat aangepast hebben of niet zelf een uur wil wachten.

4/5. Vergelijken van HTML, wijkt het af dan een nieuwe revisie aanmaken.

Deze aanpak is gebaseerd op de volledige HTML en de aanname dat je zaken kunt vergelijken. Maar dat doen we niet. We filteren de blokken uit de HTML en maken daar onze losse artikelen van. Deze controleren we met het meta-veld "original html" en zo kunnen we bepalen of de auto al bestaat (doorgaan met de volgende) of dat dit een nieuwe auto is (aanmaken). Ook "verwijder" ik niet bestaande auto's nog niet van de site. Het is extra werk, nieuwe auto's komen bovenaan de site naar voren, dus het is een kwestie van regelmatig even de site bekijken. Dan hoef je niet eens zover naar beneden te scrollen.

6. CRON instellen.

Dit punt komt (zo'n beetje als enige?) overeen met het plan van aanpak. De taak is aangemaakt!

 

Er zijn een aantal sites die met een POST-request werken en die JSON terug geven. Ik ga nog kijken of ik mijn code ook dat kan laten verwerken.

Hierbij nog even de uitleg van de werking zoals dat nu met een GET-request gedaan wordt en waarbij die actie "gewone" HTML terug geeft:

Als je de URL hebt is het uitvoeren van een GET-request in Wordpress een fluitje van een cent:


            $http = new WP_Http;
            $args = array('method' => 'GET', 'headers' => [
                'Content-Type' => 'text/html'
            ]);
            $response = $http->request($url, $args);
            if ( is_wp_error( $response ) ) {
                echo "could not process " . $post->post_title . "<HR/>";
                continue;
            }

De HTML die je ontvangt staat in $response["body"]. Daar haal ik alle linebreaks en carriage-returns uit, zodat ik 1 lange tekst over houd.
Daar ga ik dan op zoek naar de match die bij deze URL hoort en zet die blokken HTML in een array.

Hierna kijk ik of er al een post is waarbij in de meta-data in "original_html" dezelfde lowercase (dus geen gedoe met hoofd- of kleine letters) HTML staat. Zo ja, dan hebben we deze auto al en gaan we vrolijk door met de volgende.

Bestaat deze advertentie nog niet, dan gaan we kijken of er in de tekst bepaalde "uitsluitingswoorden" voorkomen. Ik heb in wp-admin een categorie "Niet van toepassing" met slug "nvt" aangemaakt, zodat ik alle auto's die ik niet wil matchen in wp-admin kan bijhouden. Dus audi, bmw, maserati.


    private function GetIgnoreWords()
    {
        $nvtCategory = get_categories(array('hierarchical' => 'true', 'hide_empty' => 0, 'slug' => 'nvt'));
        return get_categories(array('hierarchical' => 'true', 'hide_empty' => 0, 'child_of' => $nvtCategory[0]->cat_ID));
    }

Hierna kan de post met de metadata aangemaakt worden:


                $postAsArray = array();
                $postAsArray['post_title'] = $post->post_title.'-'.date('Ymd').'-'.$counter;
                $postAsArray['post_author'] = 1;
                $postAsArray['post_excerpt']  = "";
                $postAsArray['post_content']  = $this->RemoveHtmlTags($item, $url);
                $postAsArray['_status']  = 'draft';
                $postAsArray['post_status']  = 'draft';
                $postID = wp_insert_post($postAsArray);
                add_post_meta( $postID, 'origin', $url );
                add_post_meta( $postID, 'original_html', $lowerCaseItem );

Je ziet hier een functie RemoveHtmlTags. Bij "clean code" leer je dat een functie maar 1 ding mag doen, maar ik moet hierbij toegeven dat dit een beetje een houtje-touwtje fix-it-all functie is. Maar goed, het doet zijn werk en omdat ik eigenlijk al helemaal geen tijd hiervoor had, vind ik het wel prima. Even een schematische uitleg wat deze functie doet:

  1. Zet achter elke > een linebreak en voor elke < een linebreak.
  2. Hiermee komen alle "tags" op losse regels te staan.
  3. Eerst doorlopen we deze regels en kijken we of er een <script of </script> in een regel voorkomt. Dan weet je dat de tussenliggende regels JavaScript bevatten en dat die weg kunnen. Dat doen we dus ook.Die regel met <script en </script> voegen we natuurlijk ook niet toe.
  4. Vervolgens doorlopen we weer alle regels en kijken we of er een "tag" op de regel staat. Die verwijderen we, behalve als het een <a, </a>, <img of </img> tag is. We willen linkjes namelijk wel behouden, zodat je vanuit onze wordpress-site snel kunt doorklikken naar de advertentie. Bij een afbeelding doen we nog een extra controle, namelijk of het attribuut src wel naar een http(s)... locatie verwijst. Als de URL relatief is (/plaatje.jpg) dan wordt het plaatje niet getoond, dan moet namelijk de URL van de originele site ervoor geplakt worden. Dat regelen we in deze functie.
  5. Dat JavaScript en die tags zijn verwijderd in de HTML. Maar met onze linebreaks wordt onze tekst "erg groot" op het scherm. Als laatste actie vervangen we alle linebreaks met spaties. Daardoor houden we een beknopte tekst per artikel.

 

Oh ja, dat linkje naar de site van de garage, je wilt natuurlijk wel dat dit in een "nieuw venster" geopend wordt. En nee, daarvoor hoef je niet de HTML door te spitten. En ook dat afbeeldingen de volle breedte van je scherm krijgen. Daarvoor hoef je alleen dit te doen in je plug-in:


function insert_my_head() {
    echo "<base target=\"_blank\" />";
    echo "<style>";
    echo "img{width:100%}";
    echo "</style>";
}
add_action('wp_head', 'insert_my_head');


Ook had ik dat artikelen meteen de status "publish" kregen. Goed om in eerste instantie de site te vullen, maar dat moet je niet willen. Maak ze eerst aan als "draft" zodat je zelf nog kunt bepalen of de auto goed getoond kan/mag worden.



Ben je benieuwd naar het resultaat? Je kunt de site bekijken op auto.koeminder.nl
Disclaimer: als er een auto gevonden is en die is uiteindelijk gekocht, dan zal de site weer offline gehaald worden. Dus geen site? Dan is de missie geslaagd!

 

English version by ChatGPT

Another car needs to be found. The specifications are not very extensive, namely a "gasoline car," an automatic, not too large, and also not too many miles on the odometer. You have a site like gaspedaal.nl where you can search/filter. But it doesn't meet my criteria (where I want to exclude certain brands from the results). And I also think not all dealerships are on that site.

So, I started looking for websites of (professional) dealerships and was able to find the URL for the search results on many of them.
Only, I am "busy." Working during the day. In my own time, I'm busy with all sorts of other things. I don't have time for this at all, while it's been "dropped" on me. Actually, the person looking for the car should do this work. She is going to drive it, so she should check if there is something suitable. She is retired, I'm not ;)

But to provide her with 30+ URLs and tell her: go through these every day, that's not how I envision it either.

So, I have now spent a few days of my scarce free time to fix this. I started a WordPress site on a subdomain and used the default template that comes with it. Stripped out many things, so only the overview of posts remains visible.
And worked out a "plan of approach."

  1. Install WordPress.
  2. The URLs I want to request can (so far) be intercepted with a GET request (so if you paste the URL in your address bar, you immediately get to the right place). In the left menu in WordPress, you have "Posts," "Media," "Pages." I want an item "URL management" here, so I can enter and adjust the URLs myself.
  3. You can request the offers from these pages via a certain URL. The HTML is stored in separate wp_post records. So you can view the content in /wp-admin.
  4. It checks if there is already a previous version, if not: then we create a new version, if yes: then we check if the current version differs. If not: then we do nothing, if the version does differ, then we create a new revision.
  5. When adding or creating a new version, we parse the HTML and extract the "individual cars." These are created as posts. We also check these: if the car already exists and the details are identical, we do nothing. Otherwise, we create it or create a new revision.
  6. What we do now by calling a certain URL, we also do "automatically" by executing this action every hour via CRON.

 

Building this plugin has now reached version 1.0, it works. You can find the code of the plugin on this Github-URL. During build, I sometimes deviated from the plan of approach, here is the explanation of what is different:

1. Install WordPress and then the real work begins!

The installation seemed the simplest step, but I immediately encountered problems: I could no longer FTP! It turns out that KPN performed an update that blocks that traffic by default due to the firewall on your fiber modem. Set that setting to "low" and it works again.
When I "couldn't do anything" for a while, I searched for some websites.

2. "URL management" item as a separate menu in WP-Admin.

I couldn't get URL management as a separate item in this list. In my call, it appears as a submenu under the "Settings" section. I found that fine, so I didn't spend extra time trying to get it differently.

3. Requesting the HTML of the (professional) dealerships.

I'll directly address the point of storing the HTML in a separate post record and the item displayed on the website in another post record: it's not necessary. In a post record, you can store metadata (in the postmeta table). So I create a new post record and store the original lowercase HTML in a linked meta field "original_html."

I also use those meta records for the URLs. I store a 'lastsync' field to ensure a URL can be crawled a maximum of once per hour. Something could go wrong causing you to call an external URL many times suddenly. To prevent the other side from blocking you, you ensure that you minimize the number of requests. As soon as I save a URL in wp-admin, this field is cleared. This way, I can quickly ensure that a URL can be requested again if, for example, I have made some adjustments or do not want to wait an hour.

4/5. Comparing HTML, if it differs, create a new revision.

This approach is based on the complete HTML and the assumption that you can compare things. But we don't do that. We filter the blocks from the HTML and make our individual articles from them. We check these with the meta field "original_html" and thus determine whether the car already exists (continue to the next one) or if it is a new car (create it). I also don't "delete" non-existing cars from the site yet. It's extra work, new cars appear at the top of the site, so it's a matter of regularly checking the site. Then you don't even have to scroll down that far.

6. Setting up CRON.

This point (more or less the only one?) matches the plan of approach. The task has been created!

There are a few sites that work with a POST request and return JSON. I will see if I can also process that with my code.

Here is an explanation of how it currently works with a GET request and where the action returns "regular" HTML:

If you have the URL, performing a GET request in WordPress is a piece of cake:


$http = new WP_Http;
$args = array('method' => 'GET', 'headers' => [
    'Content-Type' => 'text/html'
]);
$response = $http->request($url, $args);
if (is_wp_error($response)) {
    echo "could not process " . $post->post_title . "<HR/>";
    continue;
}

The HTML you receive is in $response["body"]. I remove all line breaks and carriage returns from it, so I end up with one long text.
I then search for the match corresponding to this URL and put those HTML blocks in an array.

After this, I check if there is already a post with the same lowercase (so no issues with uppercase or lowercase letters) HTML in the metadata in "original_html." If yes, then we already have this car and happily move on to the next one.

If this ad does not yet exist, we check if certain "exclusion words" are present in the text. I created a category "Not applicable" with the slug "nvt" in wp-admin, so I can track all cars I do not want to match in wp-admin. So audi, bmw, maserati.


private function GetIgnoreWords()
{
    $nvtCategory = get_categories(array('hierarchical' => 'true', 'hide_empty' => 0, 'slug' => 'nvt'));
    return get_categories(array('hierarchical' => 'true', 'hide_empty' => 0, 'child_of' => $nvtCategory[0]->cat_ID));
}

Next, the post with the metadata can be created:


$postAsArray = array();
$postAsArray['post_title'] = $post->post_title . '-' . date('Ymd') . '-' . $counter;
$postAsArray['post_author'] = 1;
$postAsArray['post_excerpt']  = "";
$postAsArray['post_content']  = $this->RemoveHtmlTags($item, $url);
$postAsArray['_status']  = 'draft';
$postAsArray['post_status']  = 'draft';
$postID = wp_insert_post($postAsArray);
add_post_meta($postID, 'origin', $url);
add_post_meta($postID, 'original_html', $lowerCaseItem);

You see a function RemoveHtmlTags here. In "clean code," you learn that a function should only do one thing, but I must admit this is a bit of a makeshift fix-it-all function. But it works, and because I had no time for this at all, I think it's fine. Here is a schematic explanation of what this function does:

  1. Add a line break after each > and before each <.
  2. This way, all "tags" are on separate lines.
  3. We first go through these lines and check if a line contains <script or </script>. Then you know that the lines in between contain JavaScript and can be removed. So we do that. Of course, we also do not add the line with <script and </script>.
  4. Next, we go through all the lines again and check if a "tag" is on the line. We remove it unless it is an <a, </a>, <img, or </img> tag. We want to keep links so you can quickly click through to the ad from our WordPress site. For an image, we do an extra check to see if the src attribute points to an http(s)... location. If the URL is relative (/image.jpg), the image is not displayed; the original site's URL must be prefixed. We handle this in this function.
  5.  The JavaScript and those tags have been removed from the HTML. But with our line breaks, our text becomes "very large" on the screen. As the final action, we replace all line breaks with spaces. This leaves us with a concise text per article.

 

Oh yes, that link to the dealership's site, of course, you want it to open in a "new window." And no, you don't have to sift through the HTML for that. And also to make sure images take the full width of your screen. For that, you only need to do this in your plugin:


function insert_my_head() {
    echo "<base target=\"_blank\" />";
    echo "<style>";
    echo "img{width:100%}";
    echo "</style>";
}
add_action('wp_head', 'insert_my_head');

I also had the articles set to "publish" status immediately. Good for initially filling the site, but you don't want that. First, create them as "draft" so you can decide if the car is displayed correctly.

Curious about the result? You can view the site at auto.koeminder.nl
Disclaimer: if a car is found and eventually bought, the site will be taken offline. So no site? Then the mission is accomplished!