Nasıl bir SPA SEO taranabilir olun?


143

Google'ın talimatlarına göre google tarafından bir SPA'nın taranabilir hale getirilmesi üzerinde çalışıyorum . Birkaç genel açıklama olmasına rağmen, hiçbir yerde gerçek örneklerle daha ayrıntılı bir adım adım öğretici bulamadım. Bunu bitirdikten sonra çözümümü paylaşmak istiyorum, böylece diğerleri de onu kullanabilir ve muhtemelen daha da geliştirebilir.
Ben kullanıyorum MVCile Webapikontrolörler ve Phantomjs sunucu tarafında ve Durandal ile istemci tarafında push-stateetkin; Ayrıca , şiddetle tavsiye ettiğim istemci-sunucu veri etkileşimi için Breezejs kullanıyorum , ancak diğer platformları kullanan insanlara da yardımcı olacak genel bir açıklama yapmaya çalışacağım.


40
"bir konu" ile ilgili - bir web uygulaması programcısı onun SEO için nasıl taranabilir hale getirmek için bir yol bulmak zorunda, bu web üzerinde temel bir gereksinimdir. Bunu yapmak kendi başına programlama ile ilgili değildir, ancak stackoverflow.com/help/on-topic bölümünde açıklandığı gibi "programlama mesleğine özgü pratik, cevaplanabilir problemler" konusuyla ilgilidir . Tüm web üzerinde net çözüm bulunmayan birçok programcı için bir sorundur. Başkalarına yardım etmeyi umuyordum ve sadece burada açıklamak için saatler harcadım, olumsuz puanlar almak beni tekrar yardım etmeye motive etmiyor.
ışıltı

3
Vurgu programlama ve değil yılan yağı / gizli sos SEO voodoo / spam ise o zaman mükemmel topikal olabilir. Ayrıca, gelecekteki okuyuculara uzun vadede faydalı olma potansiyeline sahip oldukları cevapları da seviyoruz. Bu soru cevap çifti her iki testi de geçiyor gibi görünüyor. (Arka plan detaylarından bazıları, cevapta sunulmak yerine soruyu daha iyi ortaya çıkarabilir, ancak bu oldukça küçüktür)
Flekso

6
Oyları azaltmak için + 1'leyin. Q / a'nın blog yazısı olarak daha uygun olup olmadığına bakılmaksızın, soru Durandal ile ilgilidir ve cevap iyi araştırılmıştır.
RainerAtSpirit

2
SEO'nun günümüzde geliştiricilerin günlük yaşamında önemli bir parçası olduğunu ve kesinlikle stackoverflow bir konu olarak kabul edilmesi gerektiğini kabul ediyorum!
Kim D.

Tüm süreci kendiniz uygulamak dışında, temel olarak bu sorunu bir hizmet olarak ele alan SnapSearch snapsearch.io'yu deneyebilirsiniz .
CMCDragonkai

Yanıtlar:


121

Başlamadan önce lütfen google'ın neye ihtiyacı olduğunu , özellikle de güzel ve çirkin URL'leri kullandığınızdan emin olun . Şimdi uygulamayı görelim:

Müşteri Tarafı

İstemci tarafında, sunucu ile AJAX çağrıları aracılığıyla dinamik olarak etkileşime giren tek bir html sayfanız vardır. SPA bununla ilgili. aİstemci tarafındaki tüm etiketler uygulamamda dinamik olarak oluşturulur, daha sonra bu bağlantıların google'ın sunucudaki botuna nasıl görünür hale getirileceğini göreceğiz. Bu tür aher bir etiketin, google'ın botunun onu taraması pretty URLiçin hrefetikette bir olması gerekir. Sen istemiyoruz hrefbiz yüke yeni bir sayfa istemeyebilir, çünkü (daha sonra göreceksiniz, sunucu bunu ayrıştırmak mümkün istiyoruz olsa bile) kısmı üzerinde istemci tıklama kullanılacak yalnızca sayfanın bir bölümünde görüntülenecek bazı verileri almak üzere bir AJAX çağrısı yapmak ve URL'yi javascript (ör . HTML5 pushstateveya ile Durandaljs) kullanarak değiştirmek . Yani, ikimiz dehrefgoogle özelliği onclickve kullanıcı bağlantıyı tıkladığında işi yapan özelliktir . Şimdi, kullandığım için URL'de push-statehiçbir şey istemiyorum #, bu yüzden tipik bir aetiket şöyle görünebilir:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

'kategori' ve 'altKategori' muhtemelen 'iletişim' ve 'telefonlar' veya 'bilgisayarlar' gibi diğer ifadeler olacaktır ve elektrikli ev aletleri mağazası için 'dizüstü bilgisayarlar'. Açıkçası birçok farklı kategori ve alt kategori olacaktır. Gördüğünüz gibi, bağlantı doğrudan kategori, alt kategori ve ürüne yöneliktir, gibi belirli bir 'mağaza' sayfasına ekstra parametre olarak değil http://www.xyz.com/store/category/subCategory/product111. Çünkü daha kısa ve daha basit bağlantıları tercih ediyorum. 'Sayfalarımdan biriyle aynı ada sahip bir kategori olmayacağımı, yani'
Ben AJAX ( onclickbölüm) üzerinden veri yüklemek için google üzerinde arama içine girmeyeceğim , birçok iyi açıklamalar vardır. Burada bahsetmek istediğim tek önemli şey, kullanıcı bu bağlantıyı tıkladığında, tarayıcıdaki URL'nin şöyle görünmesini istediğidir:
http://www.xyz.com/category/subCategory/product111. Ve bu URL sunucuya gönderilmez! Unutmayın, bu istemci ve sunucu arasındaki tüm etkileşimin AJAX aracılığıyla yapıldığı, hiç bağlantı olmadığı bir SPA! tüm 'sayfalar' istemci tarafında uygulanır ve farklı URL sunucuya çağrı yapmaz (sunucunun başka bir siteden sitenize harici bağlantılar olarak kullanılması durumunda bu URL'lerin nasıl ele alınacağını bilmesi gerekir, bunu daha sonra sunucu tarafında göreceğiz). Şimdi, bu Durandal tarafından harika bir şekilde ele alınmaktadır. Şiddetle tavsiye ederim, ancak diğer teknolojileri tercih ediyorsanız bu bölümü de atlayabilirsiniz. Bunu seçerseniz ve aynı zamanda benim gibi Web için MS Visual Studio Express 2012 kullanıyorsanız, Durandal Başlangıç ​​Seti'ni yükleyebilir ve burada şöyle bir shell.jsşey kullanabilirsiniz:

define(['plugins/router', 'durandal/app'], function (router, app) {
    return {
        router: router,
        activate: function () {
            router.map([
                { route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
                { route: 'about', moduleId: 'viewmodels/about', nav: true }
            ])
                .buildNavigationModel()
                .mapUnknownRoutes(function (instruction) {
                    instruction.config.moduleId = 'viewmodels/store';
                    instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of push-state, only ! remains
                    return instruction;
                });
            return router.activate({ pushState: true });
        }
    };
});

Burada dikkat edilmesi gereken birkaç önemli nokta var:

  1. İlk rota (ile route:'') içinde fazladan veri olmayan URL içindir, yani http://www.xyz.com. Bu sayfada, AJAX kullanarak genel verileri yüklersiniz. Aslında abu sayfada hiç etiket olmayabilir . O google'ın bot onunla ne yapacağını bilecek böylece aşağıdaki etiketi eklemek isteyeceksiniz:
    <meta name="fragment" content="!">. Bu etiket, Google'ın botunun www.xyz.com?_escaped_fragment_=daha sonra göreceğimiz URL'yi dönüştürmesini sağlayacaktır .
  2. 'Yaklaşık' rota, web uygulamanızda isteyebileceğiniz diğer 'sayfalara' bağlantıya sadece bir örnektir.
  3. Şimdi, zor olan kısım 'kategori' güzergahı olmaması ve hiçbiri önceden tanımlanmış bir güzergahı olmayan birçok farklı kategori olabilir. Bu, devreye girdiği yerdir mapUnknownRoutes. Bu bilinmeyen rotaları 'mağaza' yoluna eşler ve ayrıca '!' pretty URLGoogle'ın arama motoru tarafından oluşturulması durumunda URL'den . 'Store' yolu 'fragment' özelliğindeki bilgileri alır ve verileri almak, görüntülemek ve URL'yi yerel olarak değiştirmek için AJAX çağrısı yapar. Uygulamamda, bu tür her arama için farklı bir sayfa yüklemiyorum; Sayfanın yalnızca bu verilerin alakalı olduğu bölümünü değiştiriyorum ve URL'yi yerel olarak değiştiriyorum.
  4. pushState:trueHangisinin Durandal'a push state URL'lerini kullanma talimatı verdiğine dikkat edin .

Müşteri tarafında ihtiyacımız olan bu. Karma URL'lerle de uygulanabilir (Durandal'da bunun pushState:trueiçin basitçe kaldırın ). Daha karmaşık olan kısım (en azından benim için ...) sunucu kısmıydı:

Sunucu Tarafı

Ben kullanıyorum MVC 4.5ile sunucu tarafında WebAPIkontrolörleri. Sunucu aslında 3 URL'lerin türlerini işlemek gerekir: google tarafından oluşturulan olanları - her ikisi prettyve uglyhem de müşterinin tarayıcısında göründüğünü o aynı biçimde bir 'basit' URL. Bunu nasıl yapacağımıza bakalım:

Güzel URL'ler ve 'basit' olanlar önce sunucu tarafından varolmayan bir denetleyiciye başvurmaya çalışıyormuş gibi yorumlanır. Sunucu benzer bir şey görür http://www.xyz.com/category/subCategory/product111ve 'kategori' adlı bir denetleyici arar. Bu yüzden web.configbunları belirli bir hata işleme denetleyicisine yönlendirmek için aşağıdaki satırı ekliyorum:

<customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
</customErrors><br/>

Şimdi, böyle bir şey URL'yi dönüştürür: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. URL AJAX üzerinden veri yükleyecek istemciye gönderilmesini istiyorum, bu yüzden burada hile herhangi bir denetleyiciye başvuruyormuş gibi varsayılan 'dizin' denetleyicisi çağırmak için; Tüm 'kategori' ve 'altKategori' parametrelerinden önce URL'ye bir karma ekleyerek bunu yaparım ; karma URL, varsayılan 'index' denetleyicisi dışında herhangi bir özel denetleyici gerektirmez ve veriler istemciye gönderilir; bu, daha sonra karma değerini kaldırır ve verileri AJAX aracılığıyla yüklemek için karma işleminden sonra bilgileri kullanır. Hata işleyici denetleyici kodu:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

using System.Web.Routing;

namespace eShop.Controllers
{
    public class ErrorController : ApiController
    {
        [HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
        public HttpResponseMessage Handle404()
        {
            string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
            string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
            var response = Request.CreateResponse(HttpStatusCode.Redirect);
            response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
            return response;
        }
    }
}


Peki ya Çirkin URL'ler ? Bunlar, Google'ın botu tarafından oluşturulur ve kullanıcının tarayıcıda gördüğü tüm verileri içeren düz HTML döndürmelidir. Bunun için phantomjs kullanıyorum . Phantom, tarayıcının istemci tarafında - ancak sunucu tarafında - yaptığı işi yapan başsız bir tarayıcıdır. Başka bir deyişle, fantom (diğer şeylerin yanı sıra) bir URL aracılığıyla bir web sayfasını nasıl alacağınızı bilir, içindeki tüm javascript kodunu çalıştırmayı (AJAX çağrıları yoluyla veri almayı da içerir) dahil eder ve size yansıtan HTML'yi geri verir. DOM. MS Visual Studio Express kullanıyorsanız birçoğu bu bağlantı üzerinden hayalet yüklemek istersiniz .
Ancak önce sunucuya çirkin bir URL gönderildiğinde onu yakalamamız gerekir; Bunun için, 'App_start' klasörüne aşağıdaki dosyayı ekledim:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace eShop.App_Start
{
    public class AjaxCrawlableAttribute : ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;

            if (request.QueryString[Fragment] != null)
            {

                var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");

                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
}

Bu, 'App_start' öğesinde de 'filterConfig.cs' den çağrılır:

using System.Web.Mvc;
using eShop.App_Start;

namespace eShop
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
}

Gördüğünüz gibi, 'AjaxCrawlableAttribute' çirkin URL'leri 'HtmlSnapshot' adlı bir denetleyiciye yönlendirir ve işte bu denetleyici:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace eShop.Controllers
{
    public class HtmlSnapshotController : Controller
    {
        public ActionResult returnHTML(string url)
        {
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);

            var startInfo = new ProcessStartInfo
            {
                Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output;
            return View();
        }

    }
}

İlişkili viewçok basit, sadece bir kod satırı:
@Html.Raw( ViewBag.result )
Denetleyicide görebileceğiniz gibi, fantom, createSnapshot.jsoluşturduğum bir klasörün altında adlandırılan bir javascript dosyası yükler seo. İşte bu javascript dosyası:

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();

page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () { });

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        var result = page.content;
        //result = result.substring(0, 10000);
        console.log(result);
        //console.log(results);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);

İlk önce :-) temel kodu aldığım sayfa için Thomas Davis'e teşekkür etmek istiyorum .
Burada tuhaf bir şey fark edeceksiniz: phantom, checkLoaded()işlev true olana kadar sayfayı yeniden yüklemeye devam eder . Neden? Bunun nedeni, özel SPA'mın tüm verileri almak ve sayfamdaki DOM'a yerleştirmek için birkaç AJAX çağrısı yapması ve fantom, DOM'un HTML yansımasını geri döndürmeden önce tüm çağrıların ne zaman tamamlandığını bilememesi. Burada yaptığım son AJAX çağrısından sonra ekliyorum <span id='compositionComplete'></span>, bu etiket varsa DOM'un tamamlandığını biliyorum. Bunu Durandal'ın compositionCompleteolayına yanıt olarak yapıyorum , buraya bakındaha fazlası için. Eğer bu 10 saniye içinde olmazsa, vazgeçerim (en fazla sadece bir saniye sürmelidir). Döndürülen HTML, kullanıcının tarayıcıda gördüğü tüm bağlantıları içerir. <script>HTML anlık görüntüsünde bulunan etiketler doğru URL'ye başvurmadığından komut dosyası düzgün çalışmaz . Bu çok javascript phantom dosyasında değiştirilebilir, ancak HTML snapshort sadece google tarafından abağlantıları almak ve javascript çalıştırmak için kullanılan çünkü bu gerekli olduğunu sanmıyorum ; bu bağlantıları yapmak referansı oldukça URL ve bir tarayıcıda HTML anlık görmeye çalışırsanız aslında, eğer javascript hatası alacağını ancak tüm bağlantılar düzgün çalışması ve güzel bir URL'ye bu kez bir kez daha sunucuya yönlendirir tam çalışma sayfası elde.
Budur. Artık sunucu, hem sunucuda hem de istemcide push-state etkinken hem güzel hem de çirkin URL'lerin nasıl işleneceğini biliyor. Tüm çirkin URL'ler, fantom kullanılarak aynı şekilde ele alınır, böylece her arama türü için ayrı bir denetleyici oluşturmaya gerek yoktur.
Değiştirmeyi tercih edebileceğiniz bir şey, genel bir 'category / subCategory / product' çağrısı yapmak değil, bağlantının şöyle görünmesi için bir 'mağaza' eklemektir http://www.xyz.com/store/category/subCategory/product111. Bu, çözümümdeki tüm geçersiz URL'lerin gerçekten 'dizin' denetleyicisine çağrı yapıyormuş gibi ele alınmasını önler ve bunların web.configyukarıda gösterilen I eki olmadan daha sonra 'mağaza' denetleyicisi içinde ele alınabileceğini düşünüyorum .


Ben hızlı bir sorum var, ben şimdi bu çalışma var ive düşünüyorum ama ben sitemi google göndermek ve google, site haritaları, vb bağlantılar vermek zaman google mysite.com/# vermek gerekir ? ya da sadece mysite.com ve google , meta etiketine sahip olduğum için escaped_fragment'a ekleyecek mi?
ccorrin

ccorrin - bilgim dahilinde en iyi şekilde google bir şey vermek gerekmez; google'ın botu sitenizi bulur ve güzel URL'ler arar (herhangi bir URL içermeyebileceğinden ana sayfada meta etiketi de eklemeyi unutmayın). escaped_fragment öğesini içeren çirkin URL her zaman yalnızca google tarafından eklenir - bunu asla HTML'lerinizin içine koymamalısınız. ve destek için teşekkürler :-)
ışıltı

teşekkürler Bjorn & Sandra :-) Bu belgenin daha iyi bir sürümü üzerinde çalışıyorum, bu da süreci daha hızlı hale getirmek ve url'nin içerdiği daha yaygın kullanımda yapmak için sayfaları nasıl önbelleğe alacağınız hakkında bilgi içerecektir. kontrolörün adı; Hazır olur olmaz
yayınlarım

Bu harika bir açıklama !!. Uyguladım ve localhost geliştirici cihazımda bir cazibe gibi çalışıyor. Sorun, site donuyor ve bir süre sonra bir 502 hatası alıyorum çünkü Azure Web sitelerine dağıtırken. Phantomjs'un Azure'a nasıl dağıtılacağı hakkında herhangi bir fikriniz var mı? ... Teşekkürler ( testypv.azurewebsites.net/?_escaped_fragment_=home/about )
yagopv 17:13

Azure web siteleriyle ilgili hiçbir deneyimim yok, ancak aklıma gelen şey, sayfanın tamamen yüklenmesi için kontrol sürecinin hiçbir zaman yerine getirilmemesidir, böylece sunucu sayfayı tekrar tekrar yüklemeye çalışır. belki de sorun burasıdır (bu kontroller için bir zaman sınırı olmasına rağmen orada olmayabilir)? 'gerçek dönüşü' koymaya çalışın; 'checkLoaded ()' öğesindeki ilk satır olarak görün ve fark yaratıp yaratmadığına bakın.
beamish


4

İşte 14 Ağustos'ta Londra'da ev sahipliği yaptığım Ember.js Eğitim sınıfımdan bir screencast kaydının bağlantısı. Hem istemci tarafı uygulamanız hem de sunucu tarafı uygulamanız için bir stratejinin ana hatlarını çizmenin yanı sıra, bu özelliklerin uygulanmasının JavaScript Tek Sayfalı Uygulamanıza JavaScript'i kapalı olan kullanıcılar için nasıl zarif bir bozulma sağlayacağını canlı bir şekilde gösterir. .

Web sitenizi taramaya yardımcı olmak için PhantomJS kullanır.

Kısacası, gerekli adımlar şunlardır:

  • Taramak istediğiniz web uygulamasının barındırılan bir sürümüne sahip olduğunuzda, bu sitenin üretimdeki TÜM verilerinizin olması gerekir
  • Web sitenizi yüklemek için bir JavaScript uygulaması (PhantomJS Script) yazın
  • Taranacak URL listesine index.html (veya “/“) ekleyin
    • Tarama listesine eklenen ilk URL'yi açın
    • Sayfayı yükle ve DOM'unu oluştur
    • Yüklenen sayfada kendi sitenize bağlantı veren tüm bağlantıları bulun (URL filtreleme)
    • Bu bağlantı henüz taranmamışsa "taranabilir" URL'lerin listesine ekleyin
    • Oluşturulan DOM'u dosya sistemindeki bir dosyaya depolayın, ancak önce TÜM komut dosyası etiketlerini kaldırın
    • Sonunda, taranan URL'lerle bir Site Haritası.xml dosyası oluşturun

Bu adım tamamlandıktan sonra, HTML'nizin statik sürümünü o sayfadaki noscript etiketinin bir parçası olarak sunmak arka ucunuza bağlıdır. Bu, uygulamanız başlangıçta tek sayfalık bir uygulama olmasına rağmen Google ve diğer arama motorlarının web sitenizdeki her bir sayfayı taramasına olanak tanır.

Tüm ayrıntılarla ekran görüntüsüne bağlantı:

http://www.devcasts.io/p/spas-phantomjs-and-seo/#


0

Prerender adlı servisle SPA'nızı önceden hazırlamak için kendi hizmetinizi kullanabilir veya oluşturabilirsiniz. Onun web sitesi prerender.io ve onun github projesinde kontrol edebilirsiniz (PhantomJS kullanır ve sizin için web sitenize render).

Başlamak çok kolay. Yalnızca tarayıcı isteklerini hizmete yönlendirmeniz gerekir ve bunlar işlenen html'yi alır.


2
Bu bağlantı soruyu cevaplayabilse de, cevabın temel kısımlarını buraya eklemek ve bağlantıyı referans olarak sağlamak daha iyidir. Bağlantı verilen sayfa değişirse, yalnızca bağlantı yanıtları geçersiz olabilir. - Yorumdan
timgeb

2
Haklısın. Yorumumu güncelledim ... Umarım şimdi daha kesin olur.
gabrielperales

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.