14

Perhaps this is an intentional user interface change, but it appears that the title attribute has been removed from the question link on the main page of at least several SE sites (cs, electronics, writers, etc.). Given that question titles are often less than entirely clear, this makes it more difficult to decide whether the question is of interest or not without following the link.

(While hover text may be undesirable for touch-based devices, the removal of the information reduces the quality of the interface for some users.)

This happens on multiple sites in multiple browsers with all userscripts disabled.

  • 1
    I noticed this today too and it's a real decrease in usability. I use that hover text all the time. – Monica Cellio Mar 7 '17 at 15:33
  • I can't actually remember what this feature looked like before. Can someone edit in a screenshot? – Steven M. Vascellaro May 17 '17 at 1:33
7

As this feature has been disabled for now due to the load it caused on the infrastructure I have created the following userscript to re-enable this functionality for those users that don't mind running userscripts in their browser.

Here is a direct install link. Please log issue on the Github repository

Or copy the below code in your Userscript manager:

// ==UserScript==
// @name         add title
// @namespace    https://meta.stackexchange.com/users/158100/rene
// @version      0.3
// @description  Add titles to links on the frontpage of an SE site
// @author       rene
// @match        *://*.stackexchange.com/
// @match        *://superuser.com/
// @match        *://serverfault.com/
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // build api url for an endpoint and its optional parameters
    function apiBuilder(endpoint, params) {
        var url = 'https://api.stackexchange.com/2.2/',
            urlPath = url + endpoint;
        params.key ='Kdg9mxpgwALz)u5ubehUFw((';
        if (params !== undefined)  {
            var query = [];
            for(var prop in params) {
                if (params.hasOwnProperty(prop)) {
                    query.push( prop + '=' + encodeURI(params[prop]));
                }
            }
            urlPath = urlPath + '?' + query.join('&');
        }
        return urlPath;
    }

    // build url for /sites api endpoint
    function apiSitesBuilder() {
        return apiBuilder(
            'sites', 
            {
                pagesize: 500,
                filter: '!*L6SijN-EORrs4rs'
            });
    }

    // build url for /Question endpoint
    function apiQuestionBuilder(site, qid) {
        return apiBuilder(
            'questions/' + qid, 
            {
                site: site,
                order: 'asc',
                page: 1,
                pagesize: 100,
                sort: 'activity',
                filter: '!w-2nxYBnAP3ZrgppIq'
            });
    }

    // do a get on the API for the given url
    // and invoke the callback with the JSON result
    function API () {

        var backlog = [],
            getfunction;

        // simply push the params on the queue
        function cacheget(url, callback) {
            backlog.push({ url: url, callback: callback});
        }

        // this makes the actual xhr call
        function realget(url, callback) {
            var xhr = new XMLHttpRequest();

            // handles pending calls by invoking realget
            // and resetting the getfunction when 
            // the backlog is cleared
            function handleBacklog() {
                var item = backlog.shift();
                if (item !== undefined) {
                    console.log('from cache');
                    // handle this single item
                    realget(item.url, item.callback);
                } 
                if (backlog.length === 0) {
                    // if the backlog is empty 
                    // use realget for the next call
                    getfunction = realget;
                }
            }

            xhr.addEventListener('error', function () {
                console.log(xhr.status);
            });

            xhr.addEventListener('load', function () {
                var response = JSON.parse(xhr.responseText);
                var backoff = response.backoff || 0;
                // backoff received
                if (backoff > 0) {
                    // start caching calls
                    console.log('backoff recv');
                    getfunction = cacheget;
                }
                if (response.error_id === 502) {
                    console.log(reponse.error_message);
                    getfunction = cacheget;
                    backoff = 120;
                }
                // process pending backlog
                setTimeout(handleBacklog, backoff * 1000);
                // invoke the callback
                callback(response);
            });
            xhr.open('GET', url);
            xhr.send();
        }

        // calls either xhr or the cache
        function get(url, callback)
        {
            getfunction(url, callback);
        }

        // initially we start with a realget
        getfunction = realget;

        // return the public api
        return {
            get: get
        };
    }

    var SEApi = new API(); // keep an instance

    // hook the mouseover event on the titles
    function bindMouseOver(api_site_parameter) {
        $('div.summary>h3>a').one('mouseover', function (e) {
            var questionTitleLink = $(this), 
                id = questionTitleLink.parent().parent().parent().prop('id'),
                idparts = id.split('-');
            if (idparts.length>2) {
                // call the api for the question to get the body
                SEApi.get(apiQuestionBuilder(api_site_parameter, idparts[2]), function (data) {
                    if (data.items && data.items.length > 0) {
                        // html encoding
                        var text = document.createElement('span');
                        text.innerHTML = data.items[0].body_markdown.substring(0,200);
                        // set title
                        questionTitleLink.prop(
                            'title', 
                            text.textContent);
                    }
                });
                $(this).prop('title', 'loading ' + id);
            }
        });
    }

    // match the  hostname against site-url to find api_parameter
    function findApiSiteParameter(items) {
        var i, site;
        for(i = 0; i < items.length; i = i + 1) {
            site = items[i];
            if (site.site_url.indexOf(document.location.hostname) !== -1) {
                bindMouseOver(site.api_site_parameter);
                return site.api_site_parameter;
            }
        }
        return null;
    }

    // cache site list
    var cachedSites = localStorage.getItem('SE-add-titles');
    if (cachedSites !== undefined) cachedSites = JSON.parse(cachedSites);

    var day = 86400000; // in ms
    if ((cachedSites === undefined || cachedSites === null ) || (cachedSites.items ) ||
       (cachedSites.cacheDate && (cachedSites.cacheDate + day) < Date.now() )) {
        // fetch sites
        SEApi.get(apiSitesBuilder(), function (data) {
            if (data.items && data.items.length) {
                var site = findApiSiteParameter(data.items);
                localStorage.setItem('SE-add-titles', JSON.stringify({ cachedDate: Date.now() , site: site  }));
            }
        });
    } else {
       bindMouseOver(cachedSites.site);
    }
})();

This is tested on Chrome and Edge with TamperMonkey and FireFox 51 with GreaseMonkey, all on Windows 10.

  • This would be an unwise thing to run, and would exhaust your localStorage quickly, causing more errors on the site. – Nick Craver Mar 13 '17 at 20:40
  • @NickCraver I only store the /sites list because that is the advice given on the API doc. It only fetches the text for titles that are hovered and then only once. That wouldn't be so bad I assume. no? – rene Mar 13 '17 at 20:42
  • yeah that list is growing though. Either way, what you want is a jQuery .one() handler here, rather than an unbind race which can trigger many times before the API returns. – Nick Craver Mar 13 '17 at 20:43
  • @NickCraver I didn't need the list so I now only store the api_parameter and used .one() instead of .on() and .off(). If you still feel it will be an issue I'm happy to delete this. – rene Mar 13 '17 at 20:54
  • 1
    It seems same now, if a user has this and hits request limits (IMO very unlikely), they have the option of turning it off. It comes down to the fact that this was rarely used and is very expensive in certain dimensions. If a minority of users want to user script it up, then by all means, that's what the API is there for. I wouldn't have provided help above if I thought otherwise :) – Nick Craver Mar 13 '17 at 20:56
8

We took the titles off as part of mitigating a DDoS attack on our servers - removing the title attribute has reduced the page size dramatically, meaning we could cope with the attack better.

There are no plans for re-enabling it in the short term.

  • 1
    This was a useful feature. Think it's wrong to fend off attacks by taking away features. – Shadow Wizard Mar 13 '17 at 19:37
  • 2
    The root cause of that attack is a botnet of IoT devices. Since logging in to SE through an IoT device is absolutely useless (unless the device acts like a screenreader, reading the text on the webpages out loud), why not disable the title only for unregistered users? – dorukayhan Mar 13 '17 at 19:45
  • @ShadowWizard due respect, you have a minimal view of the impact such a change makes in various situations and what bottlenecks it alleviates. If I told you it allowed us to scale to ~30% more traffic, does your opinion change? – Nick Craver Mar 13 '17 at 20:37
  • @dorukayhan It's not that simple, it's a code path ~13 methods removed and in a cross-site-code path. Accessing the request or user there would be prohibitively expensive. – Nick Craver Mar 13 '17 at 20:38
  • 1
    @Nick respect given, don't doubt it helps you fight attacks and reduce costs. But why was this feature in there in the first place, if it's so bad? What feature will be the next victim of the DDoS attacks? (And I'm sure the attacker(s) now really happy for being able to make a change for bad in SE.) – Shadow Wizard Mar 13 '17 at 21:03
  • 1
    @ShadowWizard Those responsible probably have no idea this change was made. It was something we considered moving for years, it's a ~30-40% bandwidth increase on many routes that almost no one uses, that's a little crazy from a scale standpoint. The fact that a specific instance was a final catalyst here is mostly a detail in the decision. I made this call, and I stand by it for the health and resilience of the entire network. – Nick Craver Mar 13 '17 at 21:23
  • 1
    @Nick wasn't aware you considered removing it before, this does change my view on this. Still, would be nice to announce such a change before it's done, as you take away a feature many people use on daily basis. Of course not everyone, not many, but still, some. – Shadow Wizard Mar 13 '17 at 21:25
  • 2
    @ShadowWizard totally fair, that wasn't high on the priority list under the removal circumstances here, but at least this meta post serves as a reference now. I'll make an effort to post to meta explicitly for future changes like this...but none are planned right now. – Nick Craver Mar 13 '17 at 21:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .