Wielu z nas posiada dynamiczne strony i blogi, na których zamieszczamy własnoręcznie stworzone treści. Co oczywiste, zależy nam, aby nasze teksty (strony) jak najlepiej się pozycjonowały i miały jak najwięcej wyświetleń. Warto więc trzymać się kilkunastu prostych zasad SEO, aby nasze treści były jak najlepsze zarówno dla użytkowników, jak i wyszukiwarek.
O tym czym jest SEO możesz przeczytać tutaj.
Sama wiedza o SEO nie wystarczy
Ale halo! Co wy mówicie? Przecież poznam te zasady i już będę mógł je stosować! No tak, będziesz mógł! Problem pojawia się wtedy, gdy przyjdzie pilnować ręcznie zasad w rodzaju:
- wyrazy poprawiające czytelność tekstu powinny stanowić przynajmniej 30% objętości tekstu,
- żaden akapit nie powinien mieć więcej niż 150 słów,
- tekst nie powinien zawierać zbyt wielu zdań dłuższych niż 25 słów.
Wtedy zaczynają się schody. Bo przecież nikt nie będzie liczył tego za każdym razem ręcznie. Ale przecież żyjemy w XXI wieku! A na dodatek mamy żyłkę programistyczną! Dlaczego więc nie napisać sobie skryptu, który sprawdzi nasz tekst za nas i zwróci nam raport SEO? I właśnie tak powstał WK-SeoTester, czyli nasz wirtuozerski sprawdzacz tekstów pod kątem SEO.
Właśnie niemu poświęcimy kilka wpisów, tworząc swojego rodzaju mini serię, w której opowiemy o kluczowych zasadach SEO dla tworzonych treści i pokażemy jak rozwiązaliśmy te kwestie. Zapraszamy serdecznie do śledzenia tej serii oraz do dyskusji, może pewne rzeczy rozwiązalibyście inaczej?
A to nie czasem wynajdowanie koła na nowo?
Pod pewnymi względami może i tak! Jeżeli posiadasz np. stronę zbudowaną na WordPressie to oczywiście istnieją wtyczki, których funkcjonalność będzie podobna do naszego Seo Testera i w takim wypadku najprościej i najlepiej taką wtyczkę sobie po prostu zainstalować i z niej korzystać. Przykładem takiej wtyczki jest szeroko znany Yoast SEO.
Jednak co jeżeli posiadamy stronę zbudowaną w oparciu o rozwiązanie dedykowane i nie mamy dostępnych gotowych rozwiązań? Albo dostępne moduły nie dostarczają interesujących nas funkcjonalności? Wtedy bardzo często przyjdzie nam napisać sobie własny SeoTester.
A może po prostu chcemy podjąć się ciekawego wyzwania i doszlifować nasze umiejętności? Każdy powód jest dobry, aby napisać kawałek eleganckiego kodu i podziałać w kierunku samorozwoju. Jesteśmy świadomi, że takie rozwiązania jak opisane poniżej mogą już istnieć, jednak potrzebowaliśmy czegoś 'skrojonego na miarę'.
Co w tym tygodniu?
Uff, skoro wstęp do tematu mamy już za sobą to czas powiedzieć czym zajmiemy się w tym tygodniu.
- Omówienie ogólnych założeń projektu
- Parser HTML - czyli funkcja, która przetworzy kod HTML na użyteczne dla nas dane
Na samym początku stworzymy sobie solidną bazę, do której w kolejnych częściach serii będziemy dodawać poszczególne funkcjonalności.
Założenia projektu
Jak w przypadku każdego projektu warto zacząć od rozpisania sobie najważniejszych jego założeń. Stworzymy sobie pewnego rodzaju szkielet, uporządkujemy wszystko i łatwiej będzie nam wtedy wypuścić z rąk dobrej jakości produkt.
Język
Zdecydowaliśmy, że najlepszym językiem do napisania naszej 'wtyczki' będzie JavaScript. Dlaczego? Z kilku powodów. Po pierwsze tester ma najwięcej sensu kiedy będzie działał na froncie czyli po stronie przeglądarki użytkownika, dając mu natychmiastową informację zwrotną. Zwyczajnie nie ma sensu angażować w ten proces serwera. Dodatkowo w ten sposób uniezależniamy się od technologii, z której korzysta projekt. Nieważne czy serwer będzie działał w oparciu o PHP, Node.js, Pythona, czy inny język. Stworzony tester będzie mógł zostać zawsze wykorzystany.
Kolejnym atutem JSa jest jego możliwość interakcji z DOM. Co nam to daje? Bardzo proste parsowanie kodu HTML treści, którą będziemy chcieli zbadać i ocenić pod kątem SEO.
Forma
Tester zostanie napisany obiektowo, będzie to dla niego najbardziej optymalna forma. Oczywiście można go równie dobrze zrobić funkcyjnie i ekstremalnie prosto, jednak zależy nam na dobrej organizacji i reużywalności kodu. Dzięki temu będzie go też można łatwo wykorzystać kilkukrotnie na jednej podstronie.
Piszemy w czystym JavaScript (ES6), taka forma będzie do celów pokazowych najbardziej uniwersalna. Będzie to też dobra forma wyjściowa do zaadoptowania skryptu do pracy w aplikacji opartej na Vue, React czy Angularze (u nas SeoTester jest komponentem Vue).
Działanie
Założenie jest bardzo proste.
- Tester otrzymuje treść do analizy w postaci stringa zawierającego kod HTML
- Z otrzymanego kodu HTML 'wyciągamy':
- treść i liczbę wszystkich akapitów (tylko elementy <p>, nie liczymy treści w elementach <div>*)
- treść i liczbę nagłówków od <h1> do <h4>
- treść liczbę odnośników (elementy <a>)
- liczbę obrazków i ich atrybuty alt
- liczbę i treść wszystkich zdań tekstu
- liczbę i treść wszystkich słów tekstu
- Następnie skrypt może wykonywać zadane testy, które bazują na danych uzyskanych z podanego kodu HTML
*nie bierzemy pod uwagę elementów <div>, jako że według zasad SEO treść tekstowa powinna być podzielona i umieszczona w akapitach i tego założenia powinniśmy się trzymać
Testy
Każdy test, który stworzymy może działać dowolnie bazując na dostępnych danych. Ważne jednak, aby każdy jeden zwracał komunikat o wynikach w jednolitej formie. Najprościej będzie w postaci obiektu.
Test będzie mógł zwrócić jeden z trzech stanów: "OK", "AVG" i "NOK" w zależności od otrzymanego wyniku. Co ważne, test jest zobowiązany do zwrócenia jakiejkolwiek odpowiedzi. Na podstawie właściwości status zostanie za niego przyznane odpowiednio 1, 0.5 lub 0 punktów. Dodatkowo można w przyszłości wzbogacić test o wskaźnik jego ważności, ponieważ nie wszystkie dobre praktyki SEO są równie kluczowe. Jednak na potrzeby tego wpisu pominiemy ten aspekt.
Ważną dla użytkownika właściwością będzie msg, która zawierać będzie m.in. wskazówki co należy poprawić w danej części naszego tekstu, aby uzyskać lepszy wynik.
Podsumowanie
Tworzymy skrypt, który przyjmie dane w postaci kodu HTML, przeprowadzi testy i zwróci wyniki. Kwestię prezentacji graficznej wyników zostawiamy Wam, nie będziemy pokrywać tego tematu.
To zaczynamy!
Co na początek? Cały nasz Tester zmieści się w jednym pliku (no może dwóch, ale o tym później), dlatego zacznijmy od utworzenia pliku .js i dodania szkieletu klasy.
Dodaliśmy też od razu funkcję constructor(). Co to? Jest to funkcja, która zostanie automatycznie wywołana w momencie utworzenia nowej instancji naszej klasy. Czyli po prostu jest to kod, który wykona się w momencie utworzenia obiektu testera. Po co nam on? W nim zawrzemy wywołanie parsera, jako że będzie to swoiste początkowe przygotowanie do pracy.
Parser HTML
Parser w naszym skrypcie będzie miał za zadanie wyciąganie istotnych dla nas danych, tak jak to opisaliśmy nieco wyżej w akapicie 'Działanie'. Będzie stanowił osobną funkcję, ponieważ będziemy go potrzebować do aktualizacji danych testera po każdej zmianie kodu wejściowego.
Najprostsze sposoby są najlepsze
Zgodnie z założeniami na start dostajemy kod HTML. Ale znaczniki HTML są nam niepotrzebne, wręcz tylko nam przeszkadzają. Musimy więc w jakiś sposób oczyścić z nich podany string. Jak to zrobić? Może przeszukać tekst wyrażeniem regularnym i usunąć każdy ciąg pomiędzy znakami '<' i '>'? Można, aczkolwiek stracimy wtedy np. obrazki, które wytniemy w całości jako że tag <img> jest znacznikiem samozamykającym.
Zdecydowanie więcej możliwości daje nam drugi sposób. Jak pewnie wiecie, JavaScript posiada bardzo wiele sposobów na interakcję z DOMem strony. Posiada dostęp do jej elementów, może na nie wpływać, czy też odczytywać ich własności. Ale jak z tego skorzystać skoro dostajemy kod HTML w postaci ciągu tekstowego? Wykonamy proste obejście tworząc sobie wirtualny element HTML. Nie będziemy go dołączać do treści strony, dlatego nie będzie on widoczny w przeglądarce. Będzie jednak posiadał wszystkie właściwości elementu DOM, które bardzo się nam przydadzą.
Ustawiając właściwość innerHTML tego elementu na podany testerowi string, automatycznie zmuszamy przeglądarkę do skonwertowania stringa HTML na drzewo obiektów DOM. Proste? I to jak! Właściwie przeglądarka załatwiła wszystko za nas :).
Na koniec zapisujemy sobie utworzony element pod właściwością obiektu, aby móc później z niego swobodnie korzystać.
Na czynniki pierwsze
Przyszedł czas 'wyjąć' interesujące nas rzeczy z podanego kodu. Pracować będziemy oczywiście na utworzonym przed chwilą elemencie DOM dostępnym zawsze pod this.html. Skorzystać możemy np. z funkcji querySelectorAll(), którą można wywołać na dowolnym elemencie, nie tylko na obiekcie document.
Interesują nas następujące elementy:
- paragrafy,
- nagłówki h1-h4,
- obrazy,
- odnośniki.
Do celów analizy SEO możemy spokojnie pominąć pozostałe elementy, takie jak znaczniki cytatów, czy wstawki kodu. Interesuje nas tylko treść pisana przez nas, dodatkowo te pozostałe elementy nie stanowią kluczowej treści tekstu. Dla każdego rodzaju elementu utworzymy tablicę, w której będziemy przechowywać wszystkie elementy danego rodzaju pobrane z this.html.
Dlaczego nie wyciągnąć od razu całego tekstu z this.html.innerHTML? Nie możemy tego zrobić, ponieważ zwrócony nam zostanie wtedy każdy tekst jaki istnieje w tym elemencie, także chociażby tekst z embedowanych wstawek kodu czy podpisy obrazków. Krótko mówiąc wypaczylibyśmy sobie w ten sposób wyniki testów.
Korzyści z używania DOM
Dzięki temu, że nasze parsowanie oparliśmy o operacje na DOMie, możemy też dostosować pod siebie pobierane elementy. Na wstawce powyżej widać, że nie pobieramy wszystkich elementów <a>, pomijamy te o klasie wk-image__anchor. Dlaczego? Do analizy SEO zależy nam na odnośnikach prowadzących na inne podstrony lub strony w internecie. Tymczasem odnośniki o tej klasie na naszym blogu służą do wyświetlania obrazów w ich oryginalnym rozmiarze.
Konwencja nazewnicza
Tablice przechowujące pobrane elementy nazwaliśmy tak jak tagi elementów, które przechowują. Wprawdzie odpowiedniejsza byłaby forma mnoga w nazwie (sugerująca typ tablicowy), jednak w tym wypadku nazwy pojedyncze wydają się nam przyjaźniejsze w odbiorze.
Pobranie i przetworzenie zdań tekstu
Mamy już pobrane wszystkie interesujące nas elementy. Do pełni szczęścia (i spełnienia założeń) musimy jeszcze w jakiś sposób wyjąć tekst z każdego elementu i złożyć to wszystko w jeden string. Kiedy będziemy mieli string ze zdaniami łatwo będzie go przetworzyć na tablicę pojedynczych wyrazów.
Czyli co, pętla po wszystkich elementach i doklejamy kolejne zdania? No niestety nie do końca. Musimy wpierw przetworzyć zawartość tekstową każdego elementu. Po co? Musimy się pozbyć chociażby dodatkowych spacji i niepotrzebnych znaków. Ale po kolei. Na potrzeby przetwarzania zdań stworzymy osobną funkcję extractSentences().
Myślę, że nie ma co się długo rozwodzić nad działaniem tego kodu, wykorzystuje on wyrażenia regularne do dokonywania zmian w tekście. W razie pytań zapraszamy do sekcji 'Komentarze' :).
Jedyna linijka warta głębszego wyjaśnienia to ta, która wykrywa miejsca w których kończy się jedno zdanie i zaczyna następne. Jak to działa? Szukamy po prostu grupy znaków spełniających regułę kropka poprzedzona małą literą, za którą stoi duża litera poprzedzona spacją. Oczywiście zakładamy i oczekujemy, że osoba pisząca tekst będzie pisać poprawnie z zachowaniem zasady spacji po znaku kończącym zdanie oraz rozpoczynania zdania dużą literą.
Nie można w tym wypadku pójść na łatwiznę i ciąć tekst na każdej kropce, ponieważ złapie się na to każdy skrót w środku zdania. Zaproponowany sposób jest najbardziej optymalny i zapewnia satysfakcjonującą skuteczność.
Funkcja zlepka $5$
Warto też objaśnić dlaczego pomiędzy zdania wstawiamy zlepek '$5$'. Skoro już znaleźliśmy miejsca pomiędzy zdaniami musimy je w jakiś sposób oznaczyć, żeby później móc rozbić string ze zdaniami w tablicę pojedynczych zdań. Musi to jednak być zlepek znaków, który ma bardzo nikłe szanse na pojawienie się przypadkowo w tekście. Dlatego akurat '$5$', aczkolwiek w zasadzie może to być cokolwiek, byle niepowtarzalne.
Adnotacja: Rozbijanie zdań można oczywiście przeprowadzić na zupełnie innej zasadzie, chociażby funkcją match().
Funkcja w akcji
Warto po prostu pokazać jak to działa! Naszym tekstem testowym będzie:
Drogi Marszałku, Wysoka Izbo. PKB rośnie. Nie chcę państwu niczego sugerować, ale rozszerzenie naszej kompetencji w wypracowaniu postaw uczestników wobec zadań stanowionych np. przez organizację. Proszę państwa, zmiana istniejących kryteriów, etc. umożliwia w określaniu odpowiednich warunków administracyjno-finansowych. Do tej 'sprawy' jest ważne zadanie w tym zakresie umożliwia w restrukturyzacji przedsiębiorstwa... Gdy za 4 lata? Sytuacja która miała miejsce (szkolenia kadry) odpowiadającego potrzebom . Jednakowoż, zawiązanie #koalicji ukazuje - nam efekt form oddziaływania. Wagi i realizacji kierunków postępowego wychowania!
Nie ma sensu, ale zawiera wszystko co potrzebujemy do sprawdzenia działania kodu. Po przepuszczeniu go przez funkcję extractSentences() (tekst musiał być przekazany jako innerText dowolnego elementu [tu stworzony pod zmienną e, analogicznie jak na początku funkcji parse()], żeby funkcja zadziałała poprawnie) otrzymaliśmy taki rezultat.
Z dowolnego tekstu dostajemy tablicę jego zdań w postaci samych wyrazów rozdzielonych pojedynczą spacją. Dzięki temu nasza dalsza praca będzie o wiele prostsza i przyjemniejsza.
Łączenie elementów składowych
Funkcja parse() powinna jeszcze zająć się utworzeniem tablic ze zdaniami i wyrazami z tekstu. Dzięki przygotowanej przed chwilą drugiej funkcji jest to całkiem proste.
Dla wygody najpierw stworzymy sobie dodatkową tablicę, która będzie połączeniem tablic przechowujących paragrafy i nagłówki (nie bierzemy pod uwagę odnośników, ponieważ stanowią część paragrafów). Dzięki temu załatwimy wszystko jedną pętlą. Korzystamy z tzw. spread operatora, który pozwala nam wyciągnąć wszystkie elementy z danej tablicy i umieścić w drugiej. Nie możemy tu skorzystać z concat(), ponieważ tablice elementów HTML to tablice NodeList, na których nie da się wykonać tej funkcji.
Funkcja extractSentences() zwraca tablicę zdań, więc wystarczy tylko wciąż dołączać do tablicy zdań nowe elementy. To samo dzieje się w przypadku tablicy wyrazów, dodatkowo jednak w locie tablica zdań zamieniana jest na tablicę wyrazów.
Czyli gotowe?
Tak! Wystarczy teraz w naszym konstruktorze wywołać funkcję parse() i gotowe!
Funkcję parse() będziemy jeszcze wywoływać przy każdej potrzebie aktualizacji danych wejściowych.
Wybaczcie, że nie pokażemy tego kodu tutaj w całości, ale nie chcemy powielać treści i robić zbyt długiego tasiemca. Końcowy rezultat po dzisiejszym wpisie można zobaczyć tutaj.
Podsumowanie
I to by było na tyle w tej części! Mamy solidną bazę, przygotowaliśmy sobie wszystkie dane do dalszej pracy. Teraz tylko pisać odpowiednie testy. Mamy nadzieję, że wszystko objaśniliśmy przystępnie i nie zagmatwaliśmy zbytnio :). Zapraszamy do dyskusji i zadawania pytań! Oraz oczywiście do kolejnych wpisów z tej mini serii!
Komentarze