2009. szeptember 17., csütörtök

Statikus file-ok organizációja

Előszó

Egyszer már neki fogtam ennek a cikk megírásának, ellenben az idő ahogy múlt, maga az írás is idejét múlt lett, újabb ötletek és próbálkozások történtek a módszerrel, finomodtak a műveletek.

Most újra nekifogok, ezúttal gyorsan és hatékonyan, hogy ne járjon el felette az idő vasfoga.

Bevezetés

Sokan írtak mostnában arról, hogy ők hogyan is rendezik a CSS-en belül a tulajdonságokat, hogyan generálják saját CSS Framework-jükkel a file-okat. Gondoltam itt az idő, hogy én is megosszam a saját módszeremet.

Mikor kialakítottam ezt a rendszert, mindvégig az volt a szemem előtt, hogy a látogatóknak a lehető leggyorsabban jelenjen meg minden, illetőleg a lehető leggazdaságosabban tudjuk őket kiszolgálni – kapcsolatok és kérések minimalizálása, file méretek csökkentése –, ugyanakkor fejleszteni se legyen egy rémálom, hogy minden egy 100K-s CSS file-ban van. De ne ugorjunk ennyire előre, kezdjük a legelején.

Tehát az egész megoldást a következő szemszögekből kell megvizsgálni:

  • a látogató;
  • és a fejlesztő.

A látogató

A látogatónak csak egy fontos: amit meg akar nézni, azt gyorsan megkapja. Hogy az oldal betöltésénél ne kelljen várnia több másodpercet és a betöltődött oldal ne egye meg a gépét.

Először Stoyan Stefanov említette meg a "High Performance Web pages" prezentációjában, amit azóta a legtöbb Yahoo! YSlow-val, illetve front-end optimalizációval kapcsolatos blogban és prezentációban hangoztatnak, miszerint:

  • We won't tolerate slow pages
  • 500 ms slower = 20% drop in traffic (Google)
  • 100 ms slower = 1% drop in sales (Amazon)

Ebből is látszik, hogy mennyire nagy "teher" van a front-end web fejlesztőkön, Rajtunk!

A fejlesztő

Most nézzük meg, hogy számunkra mi az, ami fontos. A CSS, JavaScript és egyéb állományokat jól struktúráltan kell tárolni. Ennek két megközelítése lehet:

  • oldalanként szeparált könyvtárba;
  • típusonként (modulonként) könyvtárba.

Eleinte az első megközelítést alkalmaztam, ellenben túl sok refaktorálással járt, mikor egy-egy állomány az oldal fejlődésével "közös" kódra épült. Ilyenkor mindig ki kellett emelni a saját könyvtrából, elhelyezni a közös kódoknak fenntartott konyvtárban, minden hivatkozást átírni, stb. Egy szóval jellemezve: macerás volt.

A második esetben már alapból egységnyi modulokra szétbontva tároljuk a CSS file-okat, így jóval ritkább esetben fordul elő, hogy egy új oldal/funkció létrehozásakor refaktorálnunk kelljen az állományainkat.

A struktúra

A struktúra, amit jelenleg használunk a következő (az itt látható könyvtárak a statikus állományokat kiszolgáló domain gyökerétől értendők):

[css] [common] browser_hacks.css footer.css header.css layout.css [component] captcha.css … [css_module] date.css nick.css … [loader] commonLoader1.css … cssModuleLoader1.css … componentsLoader1.css … pagesLoader1.css … [page] [shared] comments.css index_index.css style.css [img] [js] [swf] [build] (style-pack.php)

Amint látható, a CSS, JavaScript és kép illetve flash file-ok teljesen elkülönítve vannak tárolva a fejlesztés alatt. Azért írtam, hogy a fejlesztés alatt, mert a publikálás során az összes állomány átkerül a [build] könyvtárba ömlesztve, ezzel is csökkentve a kiszolgáláskor szükséges útvonal és a hozzá tartozó feloldáshoz szükséges idő hosszát.

A [css] könyvtár

A [common] tartalmazza a teljesen általános, minden oldalon használt – vagy használható – stílusokat, mint pl. az oldal struktúra, fejléc, lábléc vagy az általános, minden böngészőben használandó "hack"-ek.

A nálunk használt keretrendszerben lehetőség van a programból előállított, több helyen használt egységek – úgynevezett blokkok és komponensek – használatára. Ezek elkülönített stíluslapjai találhatóak a [component] könyvtárban. Ilyen például a captcha megjelenítő, vagy egy értékelő form megjelenítő.

A [css_module] könyvtárban található egységek hasonlítanak a [component]-ben található stíluslapokhoz, ellenben az itt elhelyezett különálló modulok nem programból generált tartalmak. Ilyenek a beviteli mezők, a felhasználói nick-ek, gombok, linkek, dátumok, címkék, stb. megjelenítése.

Az összes oldalhoz tartozik egy külön CSS file, még akkor is, ha az adott oldal megjelenése ezt nem követeli meg, tehát kvázi egy üres CSS file-ról van szó. Ezeket a file-okat a [page] könyvtárban helyezzük el. Amennyiben két vag több oldal megjelenése struktúrálisan megegyezne, ellenben azok nem modul szinten azonosak – tehát nem lehet sem a [component], sem a [css_module] könyvtárakban logikusan elhelyezni –, a [page]-en belül található [shared] könyvtárba rakjuk. Ezeket a file-okat az összes őt használó page szintű CSS file-nak a legelején importáljuk, így könnyen nyomon lehet követni, hogy mely oldalak használják azt.

Utoljára hagytam a fejlesztés és publikálás szempontjából legfontosabb könyvtárat, a [loader]-t. Ebben a könyvtárban találhatóak azok a loader típusú CSS file-ok, melyek az összes korábbi elsődleges könyvtárban (common, component, css_module és page) található file-okat importálják magukba. Erre azért van szükség, mert sajnálatos módon az Internet Explorer 6-os nem képes, csak maximum 31 @import szabályt feldolgozni egy CSS file-ból, és mindezt maximum 4 szint mélységig. A fentebb részletezett struktúrával elértük azt, hogyaz ilyen import mélység maximum 3 mélységű legyen.

Speciális CSS file-ok

style.css 

A style.css gyakorlatilag nem tartalmaz mást, mint a [loader] könyvtárban található loader típusú CSS file-okat importálja be. Így a három mélységünk a style.css-től számolva a következő:

  1. loader file-ok;
  2. common, component, css_module és page file-ok;
  3. a page file-okban található esetleges shared file-ok.

browser_hacks.css 

Jelenleg nálam a következőket tartalmaza:

  • Safari 3 és 4 szöveg anti-alias megjelenítési bug;
  • Mozilla Firefox alatt button és gomb típusú input-okon :hover esetén nem jelenik meg a text-decoration formázás;
  • Általánosan a képeket inline-block-ra állítani;
  • Internet Explorer 6-nál korábbi böngészők alatt a linkek-re a kurzor beállítása;
  • Internet Explorer 7 és újabbak, illetve a modern böngészők alatt a hidden input-ok elrejtése;
  • Internet Explorer alatt az input-ok dupla padding hack-je.

Mintakód

html, body, input, select, textarea, button { text-shadow: rgba(0, 0, 0, 0.01) 0 0 0; } input[type=hidden] { display: none !important; } @-moz-document url-prefix() { button, input[type=button], input[type=submit] { display: inline-block; } } a { cursor: pointer; cursor: hand; } img { display: inline-block; } input { _overflow: visible; }

Oldal stíluslapok

Ezek a stíluslapok találhatóak a [page] könyvtárban. A HTML-ben minden oldal kap egy, az oldal tartalmát tökéletesen beazonosító azonosítót. Ezt általában a body elemre rakott id attribútum használatával érhetjük el. Ennek segítségével oldható meg, hogy – bár az összes oldal stíluslapja be van mindig töltve, – csak a megfelelő oldal stílusai legyenek a tartalmunkra érvényesítve. Ez az azonosító megegyezik a CSS file nevével. Amennyiben egy máshol definiált komponensnek kicsit másképp kell kinéznie az adott oldalon, úgy az ilyen kivételek is ebbe a file-ba kerülnek.

Hogy jobban szemléletessem, íme egy mintakód, melyben a belépő oldalon a beviteli mezőkben nagyobb a szövegméret, mint az általános:

#user_login span.textInput { height: 26px; } #user_login span.textInput input { font-size: 18px; height: 22px; }

Kódolási konvenciók

Akár egyedül, akár többen dolgoznak is egy projekten, fontos, hogy minden típusú állománynak meglegyen a saját (lefektetett) írásmódja (kódolási konvenció). Nálunk jelenleg a következő minta alapján történik mindez:

h1, #myDiv .className { display: inline-block; color: #000; /* Azert kell, mert IE-ben kulonben nem jelenik meg a tartalom. */ _overlay: inline; }

Kiválasztó(k)

  • Minden kiválasztó szabályt külön sorba kell írni, azokat a specifikációban meghatározott módon vesszővel elválasztva. A vesszőt a szabály végén, közvetlenül – szóköz nélkül – kell írni.
  • A HTML elemeket kisbetűvel írjuk, követve az XHTML szabványt.
  • A class elnevezéseknek és azonosítóknak beszédesnek, de nem hosszúnak (maximum 15-20 karakteresnek) kell lenniük, és követniük kell a camelCase írásmódot.

A camelCase írásmód alól kivételt képeznek nálunk a rendszer jellegéből adódólag az oldalakat beazonosító id-k.

Személy szerint a következő ponttal ellent megyek a Google Page Speed elveinek, miszerint minél kevesebb kiválasztót használjunk, helyette minden elemnek adjunk saját class elnevezést vagy id-t. Ennek az ajánlásnak az az oka, mivel a böngészők a CSS selectorokat nem elölről, hanem hátulról kezdik el feldolgozni, emiatt a #myDiv div.container div ul li a span kiválasztónál először az összes span-t keresi meg a DOM fában, majd vissza felé lépkedve ebből az elem csoportból szűri ki a mintára nem illeszkedő elemeket. Ebből következik, hogy ha a cél span-ra rakunk egy containerLinkSpan class-t, jóval gyorsabban meg fogja találni az illeszkedő elemeket a DOM-ban a böngésző.

A kérdés persze az, hogy vajon miért is teszik ezt a böngészők? Illetve akkor mi értelme van egyáltalán a selector-oknak a CSS-ben és miért kellett újabbakat létrehozni a CSS3-ban? Illetve, hogy a HTML méret növekedése és a DOM-beli attribútumok számának növelése lassítja jobban a betöltést, vagy a "felesleges" CSS selector-ok?

  • A HTML kódban a lehető legkevesebb class elnevezést és id-t kell használni, helyettük használjuk a kiválasztókat.

Nyitó kapcsoszárójel

  • A nyitó kapcsoszárójelet minden esetben az utolsó kiválasztó után, szóközzel elválasztva kell írni, majd utána sortörést rakni.

Szabályok

  • Minden szabály külön sorba irandó, a szabály végén a pontosvessző kirakása kötelező;
  • A sorokat indenteljük egy tabulátor karakterrel;
  • A tulajdonságot és az értékét – betűcsalád és file útvonal kivételével – csupa kisbetűvel írjuk;
  • A HEXA színkódokat szintén kisbetűvel írjuk, és ahol #RRGGBB formátumú színt írunk, ott írjunk #RGB-t;
  • Amennyiben hack-et írunk, úgy azt plusz egy tabulátorral beljebb indentáljuk és felette megjegyzésben odaírjuk, hogy miért kell az adott hack-et használni.

Publikáló script-ek

Ezeknek a script-eknek nem más a feladata, mint az általunk gondosan szétválogatott és az előre megbeszélt kódolási konvenciók által megírt, kommentekkel ellátott CSS és JavaScript file-jainkból a lehető legkisebb, minden felesleges karaktertől megtisztított – a CSS file-ok esetében a kérések minimalizálása érdekében az importálandó file-okkal összevont (merge) – végleges file-okat hozzák létre.

Verziózás

Fontos továbbá az is, hogy amennyiben a Yahoo! YSlow és Google Page Speed ajánlásokat követve statikus állományainkat kellően hosszú lejárati idővel szolgáljuk ki, a megfelelő állományokra hivatkozásokat – például a CSS file-okban a képek – ellássa egy megfelelő verziószámmal, melynek hatására a böngészők új file-ként értelmezik őket, így nem a böngésző cache-ből, hanem a szerverről kérik le azt.

A verziószámmal való ellátásra kétféle megoldásunk lehet:

  • a file után irandó query string-ben helyezzük el (my_image.png?1234);
  • a file nevét egészítjük ki (my_image_1234.png).

CSS file-ok publikálása

A publikáláskor a script-ünk a style.css file-t dolgozza fel. A lépések, melyeket végrehajt:

File merge-elések

Az @import-ok helyére magának a file-nak a tartalmát "másolja" be rekurívan.

preg_replace('/\s*@import\s+(url\()?"(?P<file>.*?)"(?(1)\));/xe', '…', $cssLine)

Útvonalak normalizálása

Az így keletkezett tartalomban a képekre hivatkozások útvonalát normalizálja.

preg_replace('/(?<!@import )\s*url\(([^\'"].+?)\)/e', '\' url(\'.….'\'', $cssFileContent);

Ezekután a script megkeresi az összes képre való hivatkozást…

/url\((.+?\.(?:gif|jpg|png))\)/

…majd az általunk kiválasztott módszerrel ellátja a verziószámmal, amely jöhet SVN, CVS vagy Git revízió számból, vagy a file utolsó módosításának timestamp-jéből is.

Tartalom minimalitzálása

  • Felesleges sor eleji és végi whitespace-ek és kommentek törlése;
  • Felesleges egynél hosszabb whitespace-ek cseréje egy space-re;
  • A szabalyokban lévő felesleges whitespace-ek törlése;
  • "a b a b" értékek átalakítása "a b"-re;
  • "a b c b" értékek átalakítása "a b c"-re;
  • Mértékegységgel rendelkező 0 értékek cseréje mértékegység nélkülire;
  • Felesleges whitespace-ek eltávolítása a kapcsoszárójelek és vesszők körül;
  • #rrggbb színkódokból #rgb-t csinálunk;
  • !important előtt felesleges a whitespace;
  • Szabályokban az utolsó pontosvessző felesleges, ezeket töröljük.

Mintakód

preg_replace( array( '/(^\s+|\s+$|\/\*.*?\*\/)/s', '/\s+/', '/(\w)\s*:\s*(.+?)\s*(?:;|(\}))\s*/', '/(\d+(?:[a-z]{2}|%)?)\s+(\d+(?:[a-z]{2}|%)?)\s+\\1\s+\\2/', '/(\d+(?:[a-z]{2}|%)?)\s+(\d+(?:[a-z]{2}|%)?)\s+(\d+(?:[a-z]{2}|%)?)\s+\\2/', '/(?<=\D)0(?:[a-z]{2}|%)?/', '/\s*([{},])\s*/', '/#([a-f0-9])\\1([a-f0-9])\\2([a-f0-9])\\3/i', '/\s+!important/', '/;\}/' ), array( '', ' ', '$1:$2;$3', '$1 $2', '$1 $2 $3', '0', '$1', '#$1$2$3', '!important', '}' ), $content);

Böngésző specifikus hack-ek alkalmazása

A feladata az általánosan ismert böngésző hiányosságok pótlása a tartalomból. Ennek a megoldásnak köszönhetően a fejlesztés során nem kell törődni a böngészők ilyen irányú hiányosságaival, a script a szabványosan megírt CSS tartalomból állítja elő számunkra a böngésző-független forrást.

Ezek a hack-ek jelenleg a következők:

  • A inline-block tulajdonságot Firefox 2.0 nem ismeri, helyette display: -moz-inline-box; értéket kell alkalmazni;
  • A min-height és min-width tulajdonságot az Internet Explorer 7 és korábbi böngészők nem ismerik;
  • A :hover-t nem támogatják az Internet Explorer 8-nál korábbi verziók, csak a elemeken, így ezekre JavaScript-es megoldáshoz létrehozunk egy .originalClassName.hover nevű selector-t is;
  • Az opacity tulajdonságot nem kezelik a Firefox 2.0 és korábbi és az Internet Explorer 8-nál korábbi böngészők.

Mintakód

preg_replace( array( '/\bdisplay\s*:\s*inline-block\b/is', '/\bmin-height\s*:\s(\d+[a-z]{2}|%)/is', '/\bmin-width\s*:\s(\d+[a-z]{2}|%)/is', '/([^},\n]*?(?:\s|>|\+|:[a-z-]+\()(?:[^a]|[a-z]{2,}|\*)(?:\.\w+)*):hover(\s+[^{,\n]+?)?(?=\s*[{,\n])/isx', '/\bopacity\s*:\s*(\d*\.\d+|\d+)/ise', ), array( 'display: -moz-inline-box; display: inline-block', 'min-height: $1; _height: $1', 'min-width: $1; _width: $1', '$1:hover$2, $1.hover$2', 'opacity($1)' ), $content);

opacity() metódus

function opacity($value) { $value = (string)max(min((float)$value, 1), 0); return '-moz-opacity: '.$value.';'. 'opacity: '.$value.';'. 'filter: alpha(opacity='.(int)($value * 100).')'; }

JavaScript file-ok publikálása

A cél az, hogy vonjuk össze a lehető legtöbb JavaScript file-t, azokat minimalizáljuk (pack) a lehető legjobban, majd helyezzük el az így kapott file-t a [build] könyvtárban. Természetesen a file-jainkat szeparáltan tároljuk a fejlesztés során, a kérdés mindig a kiszolgálás mikéntjében rejlik.

Itt két megközelítés lehetséges. Az egyiknél a fejlesztés során van könnyebb dolgunk, a másiknál pedig a látogatónak kedvezünk. Amint azt korábban is már írtam, nem szabad csak a látogatót figyelembe venni a döntések során, a fejlesztő is ugyanolyan fontos!

Ha eldöntöttük, hogy melyik módszert alkalmazzuk, már csak a pack-elés módját kell kiválasztani. Számos mások által megírt megoldás közül is választhatunk. A megoldások után írt érték egy 120KB-s jQuery file-on végrehajtott méretcsökkenést jelent.

  • A Mozilla Rhino például a változóneveket lerövidíti a lehető legkisebbre, ügyelve azok láthatóságára. (43%)
  • Használhatjuk Dean Edwards által írt JavaScript Packer-t. Ez egy JavaScript-ben írt tömörítő. A hátránya, hogy némely kódok – például régebbi jQuery plugin-ek, stb. – nem működnek vele, illetve mivel maga a kicsomagoló JavaScript-ben íródott, ezért mire a becsomagolt script-ünk a letöltés után mire elérhetővé – értelmezetté – válik, eléggé sokáig tart. (66%)
  • Yahoo! YUI Compressor: sajnos ez idáig nem volt még vele tapasztalatom, mivel korábban a fenti két megoldást ötvözve használtam, az átállás egy hosszabb folyamat lenne, mert a YUI Compressor-ral is le kellene tesztelni minden eddig megírt – több, mint 1MB – JavaScript file működését az összes általunk támogatott böngészőn. Ez nagyon nagy munka, de idővel meg kell majd lépnünk, mert a JavaScript Packer a legkülönfélébb helyenek tud hibákat produkálni, melyek felderítése időigényes. (53%)

Minden JavaScript file-t külön pack-elünk

Nem szükséges minden módosítás után build-elni, mivel minden file-t be tudunk húzni a HTML-ben, amire szükségünk van.

Példával illusztrálva, ha nekünk van egy jQuery-nk (jquery.js), hozzá plugin-ek (jquery.scrollto.js és jquery.autocomplete.js) és a saját függvényeinket tartalmazó file (base.js), akkor ezeket egyenként fogjuk betölteni a HTML-ünkben mind a fejlesztés, mint éles környezetben. Ennek előnye, hogy a módosításokat azonnal tesztelhetjük, hátránya, hogy megnöveljük a betöltődés idejét, mivel megnöveltük a lekérések számát (minimum 4 JavaScript file betöltése mindig).

JavaScript file-ok összevonása egy file-ba

Ekkor a HTML-ben csak egy file-ra hivatkozunk, emiatt minden módosítás után szükséges az összevont file újboli létrehozása. Ez megnehezíti a kód tesztelését, bár szerintem idővel hozzászokik az ember, illetve a programozáshoz és teszteléshez való hozzáállást is javítja. Megfigyeltem, hogy mikor ezt a módszert alkalmaztam, egy idő után kevesebb hibát vétettem a programokban.

Persze lehet a build-elést automatizálni, akár lekéréskor a fejlesztői környezetben generálni a pack-elt JavaScrip file-t, de ez a betöltési időt növeli meg még akkor is, ha éppen nem a JavaScript-jeinkkel dolgozunk.

Képek

Ha már a publikáló script-jeink elvégeznek helyettünk mindent, miért is ne foglalkozzanak az oldalon használt képek optimalizálásával is. Ezzel mind mi spórolhatunk sávszélességet, mind a látogatónak gyorsabban töltődnek azok be.

Mi a fejlesztés során akár használhatunk minden esetben 24 bit-es PNG-ket – Internet Explorer 6 miatt átlátszóság nélkül –, azokból a programunk generálhat majd GIF vagy PNG8 képeket, annak megfelelően, hogy melyik eredményezi a kisebb file méretet.

Fotók esetén természetesen a JPEG formátumot használjuk, ellenben ezeken a file-okon is lehet optimalizációt végezni. Akár megcsinálhatjuk azt is, hogy a JPEG-jeinket mindig 100%-os minőségben mentjük ki. A publikáló script fog azokból egy általánosan elfogadott minőséget generálni – mondjuk egy konfigurációs file-ban előre, képenként külön is meghatározható beállítások szerint.

Ami viszont lényeges file méret csökkenést okoz, ha a JPEG és PNG file-jaink header-jeit csonkítjuk meg a lehető legjobban. Erre léteznek külön programok is, mint pl. a jpegtran vagy az OptiPNG. Ezen programok haszbálatával akár 40-50%-kal is csökkenthetjük a képi állományaink összméretét.

14 megjegyzés:

  1. Szia!
    Nagyon érdekes kis cikk lett - mondom ezt egy backend fejlesztő szemével.
    Egyetlen dolog jutott az eszembe:
    a fájlverziózásra mennyivel lenne egyszerűbb/bonyolultabb az ETag HTTP header használata. Esetleg ezzel kapcsolatban vannak-e meglévő tapasztalaitok?

    VálaszTörlés
  2. @attila: két probléma is lenne ezzel kapcsolatban:
    - Lehet a webszerver és a böngésző között minden féle nem szabványoeszköz (akár maga a böngésző is), ami nem veszi pontosan figyelembe azt, amit te szerver oldalon mondasz, ami okozhatja azt, hogy egy változtatás után nem töltődik újra a fájl. Ezért mindig az a biztos, ha a verziózás a fájl nevében jut érvényre, akkor tuti nincs meglepetés. Pl. egyik nagyobb oldal esetén az swf-ek verziója eleinte GET paraméterben ment, és rendre előfordult, hogy nem töltődött x usernek be az új verzió.
    - Ezenkívül az Etag-os megoldásnak lenne még egy nagy hátránya: a böngészőnek folyamatosan feltételes HTTP kéréseket kell küldenie, hogy megnézze-e, hogy változott-e a fájl, ami nyilván okoz némi overheadet mind a usernek időben, mint a webszervernek erőforrás igényben. Ha viszont a fenti vagy hasonló verziózást használsz, akkor megadhatsz egy nagyobb jövőbeli értéket expire headerben, és akkor erre nincs szükség, mindenki jobban jár.

    VálaszTörlés
  3. @Felhő: köszi, hogy megválaszoltad a kérdést :)

    VálaszTörlés
  4. (szerintem) hiánypótló bejegyzés, érdemes volt elolvasni. köszönet érte!

    VálaszTörlés
  5. Nagyon jó cikk, sok olyan dolgot tanultam belőle amiről eddig csak hallottam, de gyakorlati példát nem láttam belőle. Köszönöm a cikket!

    VálaszTörlés
  6. A CSS optimalizálásánál az "a b a b" és az "a b c b" típusú szelektorok átalakítása biztosan rendben van?
    Elsősorban IE6 miatt gyakran használok olyant 2 szintű menüben, hogy "ul li ul li {", ezt nagyon nem kéne "ul li {"-re egyszerűsíteni. A második meg pláne nem triviális...

    Egyébként nagyon tetszenek az ötleteid!

    Norbi

    VálaszTörlés
  7. Norbi: az optimalizálásnál azok az egyszerűsítések kizárólag tulajdonság értékekre, pl. padding lesznek alkalmazva, a selectorokat érintetlenül hagyja.

    VálaszTörlés
  8. Kiegészítem még azzal az előző hozzászólásomat, hogy a regexp direkt úgy lett megoldva, hogy csak "szám és utána mértékegység" csoportokat nézze (\d+(?:[a-z]{2}|%)?), emiatt az "ul li ul li" selectorokat békén hagyja.

    VálaszTörlés
  9. izin simak ya admin buat artikelnya, postingan yg bermanfaat sekali. terimakasih atas informasinya sangat bermanfaat.

    VálaszTörlés
  10. jangan lupa, kita hidup ditanah. kita bukan langit

    VálaszTörlés
  11. membawa berkah dan membukakan pintu kesuksesan

    VálaszTörlés
  12. Adakah cara supaya postingan secara otomatis terkirim ke media sosial (tweeter, Facebook, dll) seperti punya google plus. thanks

    VálaszTörlés