favicon для мобильных сайтов и не только

Сегодня я вам расскажу, как современные web-разработчики должны правильно писать вот такую строчку:

<link rel="shortcut icon" href="/favicon.ico">
Читать далее...
Семантический веб

Когда я опубликовал предыдущий пост и решил добавить ссылку на него в Google+, я обратил внимание на то, что Google выбирает не тот текст со страницы, который я хотел бы видеть. Ну кому интересно каждый раз смотреть на облако тегов?

Скриншот с неправильной выборкой

В итоге я решил разобраться и попытаться указать гуглу (а может и еще кому) нужный кусок. С гуглом получилось, с другими пока нет. И заодно я поднял целый пласт недостающих знаний по HTML5, а именно Microdata. Я, конечно, не раз встречал это слово в спецификации, но никак не мог сообразить, где именно это может использоваться.

Итак, представляю вашему вниманию HTML Microdata. Эта спецификация отвечает за дополнительную семантическию разметку страницы. Но для начала разберемся, что такое семантическая разметка. Как подсказывает нам википедия, семантика — серия научных дисциплин и концепций о смысловой интерпретации в различных символьных системах. Таким образом, семантическая разметка — это способ придания смысла кускам страницы.

Как вы, должно быть, знаете, в HTML5 появился целый набор специальных тегов для семантичекой разметки. Например, header пригодится, чтобы объявить кусок страницы заголовком, а article — статьей, новостью или чем-то подобным. Но что, если нам нужно точнее указывать значение элементов на странице? Например, вот это — имя автора статьи, рядом список ключевых слов, а потом еще какая-то информация, которая к статье относится весьма опосредованно. Зачем? Ну мы-то визуально всё это различим, а вот поисковики и скрипты на всяких сайтах наверняка нет. И вот в этом случае придется иметь дело с Microdata.

По спецификации у нас есть буквально несколько дополнительных атрибутов для тегов — itemscope, itemtype и itemprop. Но сами по себе эти атрибуты никакой семантики не добавляют. Создатели спецификации решили, что не дело им указывать какие типы/значения могут быть у контента. Оно и правильно, пусть другие этим занимаются. Так ведь нашлось кому заняться: в 2011 году Google, Bing и Yahoo объединились, чтобы создать список типов данных на странице, а еще через некоторое время к ним присоединился Яндекс. Так что теперь у нас есть Schema.org, прошу любить и жаловать.

Теперь можно со спокойной душой добавить всякой семантической красоты на свою страницу и наслаждаться жизнью. Практически весь специализированный вывод на страницах результатов поисковиков получается благодаря этой разметке. Скажу честно, это не единственный способ, есть еще microformats, но, по утверждению того же Google, предпочтительно использовать Microdata, т.к. этот формат гораздо более гибкий. Кстати, у Google и Яндекса есть инструменты для тестирования подобной разметки: раз и два.

Ладно, может, это всё и никому не нужная ерунда, но по крайней мере у меня получилось указать Google+, какая именно часть страницы является статьей и что нужно показывать.

Скриншот с правильной выборкой

А еще хочется бросить камень в огород сказать пару слов по поводу Facebook. Эти ребята решили выделиться: для поддержки Facebook нужно использовать свой формат данных — Open Graph. И использовать его нужно только одним заранее определенным способом: с помощью добавления тегов meta в заголовок страницы. Придется, видимо, подумать, как это сделать, если я хочу красивое описание у ссылок на мой сайт в Facebook.

Пример использования Microdata можно посмотреть прям тут (Ctrl+U в хроме или Firefox). Вопросы и пожелания — добро пожаловать в комментарии.

Загрузка файлов на Rackspace

Я всё-таки дописал этот функционал, хотя он отнял приличное количество времени. Попытаюсь поделиться своим опытом. Не буду рассказывать как реализовать получение списка файлов, т.к. в большей части это перекликается с самой загрузкой.

Oсторожно, сейчас будет простыня.

Во-первых, нам нужна форма загрузки:

<form class="well form-inline upload-form" action="/vault/fileupload" enctype="multipart/form-data" method="post">
  <strong>Загрузить: </strong>
  <input type="hidden" name="container" id="file-container" />
  <input type="file" name="file" id="file-upload"
    style="position: absolute; top: -100px; left: -100px;" />
  <button class="btn" type="button" id="file-button"> Выбрать </button>
  <input type="text" name="name" id="file-name" placeholder="Под именем" />
  <button class="btn" type="submit"> Отправить </button>
</form>

Мне захотелось кастомную кнопку выбора файлов, для этого оригинальную кнопку мы прячем, а клик по ней эмулируем:

$('#file-button').click(function () {
    $('#file-upload').click();
    return false;
});

Стоит обратить внимание на способ, которым кнопка спрятана. Если просто указать display: none, то опера перестанет воспринимать поддельные клики и окошко с выбором файла перестанет появляться. А если указать visibility: hidden, то старое поле будет занимать положенное ему место и получится дырка на форме.

Итак, форму нарисовали, можно писать обработчик:

vaultFileUpload :: AppHandler ()
vaultFileUpload = do
  tmpDir <- liftIO getTemporaryDirectory
  response <- handleFileUploads tmpDir uploadPolicy partUploadPolicy processForm
  writeBS $ pack $ encode response

Тут всё просто: получаем имя временной папки, обрабатываем форму и выводим результат, поэтому посмотрим внимательнее на функцию обработки:

response <- handleFileUploads tmpDir uploadPolicy partUploadPolicy processForm

Функция handleFileUploads принимает на вход 4 параметра: временная папка, глобальная политика загрузки, функция, возвращающая по описанию части локальную политику загрузки, и, наконец, функция для обработки полученных данных. Т.е. мы загружаем все или только некоторые файлы во временную папку и вызываем обработчик:

processForm ((_, Left uploadError) : []) = 
  return $ ServiceError $ show uploadError

processForm ((_, Right path) : []) = do
  container <- getPostParam "container"
  name <- getPostParam "name"
  if container == Nothing || name == Nothing
    then return $ ServiceError "Container or name not defined"
    else liftIO $ curlDo $ UploadFile path (unpack $ fromMaybe "" container) (unpack $ fromMaybe "" name)

processForm [] = return $ ServiceError "No files were transmitted"
processForm _ = return $ ServiceError "Too many files"

Интересен только второй случай, когда успешно загружен только один файл. Мы получаем два POST-параметра (имя контейнера Rackspace CloudFiles и имя, под которым файл должен быть загружен) и вызываем нашу функцию curlDo с описанием нужной операции. UploadFile — это конструктор моего типа ServiceAction, где просто перечислены все необходимые операции:

data ServiceAction
  = GetContainers
  | GetContainerItems String
  | UploadFile FilePath String String

Переходим к curlDo.

curlDo :: ServiceAction -> IO ServiceResponse
curlDo action = withCurlDo $ do
    h <- initialize
    response <- curl h "https://auth.api.rackspacecloud.com/v1.0"
      [CurlHttpHeaders 
        [ "X-Auth-Key: "  ++ rackspaceAuthKey
        , "X-Auth-User: " ++ rackspaceAuthUser
        ]
      ]
    
    let headers = M.map (dropWhile (== ' ')) $ M.fromList $ respHeaders response
    case respStatus response of
      204 -> processAction action 
        (headers M.! "X-Storage-Url") (headers M.! "X-CDN-Management-Url") 
        (headers M.! "X-Auth-Token")
      _ -> return $ ServiceError "Can't authenticate"

Мы отправляем аутентификационные данные на Cloud-сервер и проверяем их правильность. В случае удачи вызывается processAction с действием, которое нужно выполнить. Ну и кроме того передаются некоторые заголовки из ответа сервера — там указано, к каким серверам дальше обращаться.

В функции processAction несколько паттернов, по одному на каждую выполняемую операцию из типа ServiceAction. Вот тот, что используется для операции загрузки файла:

processAction (UploadFile filePath container name) url _ =
  uploadFile filePath container name url

Как видите, ничего особенного, просто вызов. Переходим к самому интересному — uploadFile. Каркас этой функции выглядит так:

uploadFile filePath container name url token = do
  h <- initialize
  response <- withBinaryFile filePath ReadMode processFile
  case respStatus response of
    201 -> return ServiceSuccess
    _ -> return $ ServiceDebug $ show $ respStatus response

Сначала мы инициализируем handle от curl c помощью initialize, затем оборачиваем в withBinaryFile работу с файлом (отправку) и отдаем результат в зависимости от кода, возвращенного сервером. Отправка файла реализуется через processFile:

processFile fh = do
  fileSize <- hFileSize fh
  curl h (url ++ "/" ++ container ++ "/" ++ name)
    [ CurlPut True
    , CurlHttpHeaders ["X-Auth-Token: " ++ token]
    , CurlReadFunction readFunction
    , CurlInFileSize $ fromInteger fileSize
    ]

Тут задаются опции для вызова curl. Самое интересное и сложное — это функция readFunction, которая отвечает за чтение файла и передачу данных в curl. Т.к. функция будет вызываться из libcurl, написана она весьма специфически с использованием библиотеки Foreign:

readFunction :: Ptr CChar -> CInt -> CInt -> Ptr () -> IO (Maybe CInt)
readFunction ptr size nmemb _ = do
  actualSize <- hGetBuf fh ptr $ fromInteger $ toInteger (size * nmemb)
  return $ if (actualSize > 0) then Just $ fromInteger $ toInteger actualSize else Nothing

hGetBuf читает из файла fh в область по указателю pts (size * nmemb) байт и возвращает количество действительно прочитанных байт. Ну и сама функция должна вернуть Maybe CInt, причем Nothing возвращается в случае, если ничего не прочитано и читать дальше не надо.

Если собрать весь код функции в одно место, получится как-то так:

uploadFile filePath container name url token = do
  h <- initialize
  let 
    processFile fh = do
      let 
        readFunction :: Ptr CChar -> CInt -> CInt -> Ptr () -> IO (Maybe CInt)
        readFunction ptr size nmemb _ = do
          actualSize <- hGetBuf fh ptr $ fromInteger $ toInteger (size * nmemb)
          return $ if (actualSize > 0) then Just $ fromInteger $ toInteger actualSize else Nothing

      fileSize <- hFileSize fh
      curl h (url ++ "/" ++ container ++ "/" ++ name)
        [ CurlPut True
        , CurlHttpHeaders ["X-Auth-Token: " ++ token]
        , CurlReadFunction readFunction
        , CurlInFileSize $ fromInteger fileSize
        ]

  response <- withBinaryFile filePath ReadMode processFile
  case respStatus response of
    201 -> return ServiceSuccess
    _ -> return $ ServiceDebug $ show $ respStatus response

Вот такими нехитрыми действиями можно пересылать файлы с клиента через промежуточный сервер на сервера Rackspace CloudFiles. Все исходники лежат на GitHub, где и подлежат и вдумчивому изучению (trollface).

HTML5 Audio

На рынке браузеров сложилась довольно интересная ситуация с поддержкой тега <audio>. Оставим в стороне IE6–8, в которых вообще нет поддержки нативного воспроизведения аудио, и посмотрим на весь остальной зоопарк. Самый распространенный формат mp3 поддерживают Google Chrome, Safari, IE9-10. Firefox и Opera решили отказаться от него, видимо, из-за лицензионных ограничений. Зато эти браузеры поддерживают ogg, а IE и Safari — нет. Т.е. если хочешь поддерживать все браузеры, то будь добр сконвертировать свое аудио в mp3 и ogg. А еще не забудь прикрутить flash-плеер для совсем старых браузеров, ну или хотя бы ссылочку приложи на файлы, чтобы пользователи могли тебя услышать.

Кстати, точно так же обстоит дело с поддержкой еще двух форматов: WebM и AAC. WebM поддерживают Chrome, Firefox и Opera, а AAC — Chrome, IE и Safari. Получаем две воинствующие группировки IE-Safari и Firefox–Opera, которые работают с непересекающимися множествами форматов. Ну и Chrome, которому всё равно и он играет все, что ему подсунешь.

Табличка для тех, кто совсем ничего не понял:

 MP3OGGAACWebM
Chrome
Firefox
Opera
Safari
Internet Explorer

HTML5 audio вы можете видеть в предыдущем посте.