Implement meta stuff for SEO

This commit is contained in:
Mikko Ahlroth 2019-01-26 16:58:37 +02:00
parent a9055d2bd8
commit 498e38a259
8 changed files with 160 additions and 7 deletions

View file

@ -30,6 +30,9 @@ config :mebe_2,
# Basic blog information
blog_name: "My awesome blog",
blog_author: "Author McAuthor",
blog_description: "Description for SEO.",
# Twitter username of blog author (without @) or nil for none
blog_author_twitter: nil,
# Absolute URL to the site, including protocol, no trailing slash
absolute_url: "http://localhost:2124",
# Default timezone to use for posts with time data

View file

@ -4,10 +4,10 @@ defmodule Mebe2.Engine.Models do
"""
defmodule PageData do
defstruct filename: nil,
title: nil,
defstruct filename: "",
title: "",
headers: [],
content: nil
content: ""
@type t :: %__MODULE__{
filename: String.t(),

View file

@ -18,10 +18,12 @@ defmodule Mebe2.Engine.Parser do
|> format()
end
@spec split_lines(String.t()) :: [String.t()]
def split_lines(pagedata) do
String.split(pagedata, ~R/\r?\n/)
end
@spec parse_raw([String.t()], PageData.t(), :content | :headers | :title) :: PageData.t()
def parse_raw(datalines, pagedata \\ %PageData{}, mode \\ :title)
def parse_raw([title | rest], pagedata, :title) do
@ -43,10 +45,12 @@ defmodule Mebe2.Engine.Parser do
%{pagedata | content: Enum.join(content, "\n")}
end
@spec render_content(PageData.t()) :: PageData.t()
def render_content(pagedata) do
%{pagedata | content: Earmark.as_html!(pagedata.content, @earmark_opts)}
end
@spec format(PageData.t()) :: Page.t() | Post.t()
def format(%PageData{
filename: filename,
title: title,

View file

@ -2,6 +2,8 @@ defmodule Mebe2.Web.Views.BaseLayout do
use Raxx.Layout,
layout: "base_layout.html.eex"
alias Mebe2.Web.Views.SinglePost
@doc """
Render title for this page, based on the given module. If the module has a title/1 function, it
will be used to get the title to use, but a postfix with the blog name will be added. The title
@ -10,9 +12,18 @@ defmodule Mebe2.Web.Views.BaseLayout do
@spec render_title(module, keyword()) :: String.t()
def render_title(module, vars) do
if function_exported?(module, :title, 1) do
"#{apply(module, :title, [vars])} "
"#{apply(module, :title, [vars])} #{Mebe2.get_conf(:blog_name)}"
else
""
Mebe2.get_conf(:blog_name)
end
end
@doc """
Get Schema.org type for this page, either Blog or BlogPosting.
"""
@spec schema_type(module, keyword) :: String.t()
def schema_type(module, vars)
def schema_type(SinglePost, _vars), do: "BlogPosting"
def schema_type(_module, _vars), do: "Blog"
end

View file

@ -1,11 +1,13 @@
<!DOCTYPE html>
<html>
<html itemscope itemtype="http://schema.org/<%= schema_type(__ENV__.module, binding()) %>">
<head>
<meta charset="utf-8" />
<title><%= render_title(__ENV__.module, binding()) %><%= Mebe2.get_conf(:blog_name) %></title>
<title><%= render_title(__ENV__.module, binding()) %></title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<%= Mebe2.Web.Views.Meta.html(__ENV__.module, binding()) %>
<%= if Mebe2.get_conf(:enable_feeds) do %>
<link
rel="alternate"

81
lib/web/views/meta.ex Normal file
View file

@ -0,0 +1,81 @@
defmodule Mebe2.Web.Views.Meta do
import Mebe2.Web.Views.BaseLayout, only: [render_title: 2]
use Raxx.View,
template: "meta.html.eex",
arguments: [:module, :vars]
alias Mebe2.Web.Views.SinglePost, as: PostView
alias Mebe2.Web.Views.Page, as: PageView
alias Mebe2.Engine.Models.Post, as: PostModel
@doc """
Get the description of this page for search engines. Posts use the "description" header, if
available. Fallback is to `:blog_description.`
"""
@spec description(module, keyword) :: String.t()
def description(module, vars)
def description(PostView, vars) do
%PostModel{} = post = Keyword.get(vars, :post)
Map.get(post.extra_headers, "description", Mebe2.get_conf(:blog_description))
end
def description(_module, _vars), do: Mebe2.get_conf(:blog_description)
@doc """
Get the author for Twitter, or empty string if not set.
"""
@spec twitter_author() :: String.t()
def twitter_author() do
case Mebe2.get_conf(:blog_author_twitter) do
nil -> ""
author -> "@#{author}"
end
end
@doc """
Get the OpenGraph type for this page. Pages and posts are type "article", others are type
"website".
"""
@spec og_type(module, keyword) :: String.t()
def og_type(module, vars)
def og_type(m, _vars) when m in [PostView, PageView], do: "article"
def og_type(_module, _vars), do: "website"
@doc """
Get the canonical URL for this page. Only pages and posts have this, it's not that useful for
other views so it's not defined for them.
"""
@spec url(PostView | PageView, keyword) :: String.t()
def url(module, vars)
def url(PostView, vars) do
%PostModel{} = post = Keyword.get(vars, :post)
Mebe2.get_conf(:absolute_url) <> Mebe2.Web.Views.Utils.get_post_path(post)
end
def url(PageView, vars) do
%Mebe2.Engine.Models.Page{} = page = Keyword.get(vars, :page)
Mebe2.get_conf(:absolute_url) <> Mebe2.Web.Views.Utils.get_page_path(page)
end
@doc """
Get the ISO 8601 timestamp for this post.
"""
@spec timestamp(PostView, keyword) :: String.t()
def timestamp(PostView, vars) do
%PostModel{} = post = Keyword.get(vars, :post)
DateTime.to_iso8601(post.datetime)
end
@doc """
Get the image, if any, for this post. Uses the "image" header.
"""
@spec image(PostView, keyword) :: String.t()
def image(PostView, vars) do
%PostModel{} = post = Keyword.get(vars, :post)
Map.get(post.extra_headers, "image", "")
end
end

View file

@ -0,0 +1,37 @@
<meta name="author" content="<%= Mebe2.get_conf(:blog_author) %>" />
<meta name="description" content="<%= description(module, vars) %>" />
<%# Schema.org markup for Google+ %>
<meta itemprop="name" content="<%= render_title(module, vars) %>" />
<meta itemprop="author" content="<%= Mebe2.get_conf(:blog_author) %>" />
<meta itemprop="headline" content="<%= render_title(module, vars) %>" />
<meta itemprop="description" content="<%= description(module, vars) %>" />
<%# Twitter Card data %>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="<%= twitter_author() %>" />
<meta name="twitter:title" content="<%= render_title(module, vars) %>" />
<meta name="twitter:description" content="<%= description(module, vars) %>" />
<meta name="twitter:creator" content="<%= twitter_author() %>" />
<%# Open Graph data %>
<meta property="og:title" content="<%= render_title(module, vars) %>" />
<meta property="og:type" content="<%= og_type(module, vars) %>" />
<meta property="og:description" content="<%= description(module, vars) %>" />
<meta property="og:site_name" content="<%= Mebe2.get_conf(:blog_name) %>" />
<%= if module == Mebe2.Web.Views.SinglePost do %>
<meta property="article:published_time" content="<%= timestamp(module, vars) %>" />
<meta itemprop="datePublished" content="<%= timestamp(module, vars) %>" />
<%# Large image must be at least 280x150px %>
<meta name="twitter:image:src" content="<%= image(module, vars) %>" />
<meta property="og:image" content="<%= image(module, vars) %>" />
<meta itemprop="image" content="<%= image(module, vars) %>" />
<% end %>
<%= if module in [Mebe2.Web.Views.SinglePost, Mebe2.Web.Views.Page] do %>
<meta property="og:url" content="<%= url(module, vars) %>" />
<% end %>

View file

@ -19,6 +19,21 @@ defmodule Mebe2.Web.Views.Utils do
"/#{dstr}/#{post.slug}"
end
@doc """
Get the relative path to a given page.
## Examples
iex> Mebe2.Web.Views.Utils.get_page_path(
...> %Mebe2.Engine.Models.Page{slug: "foo-bar"}
...> )
"/foo-bar"
"""
@spec get_page_path(Mebe2.Engine.Models.Page.t()) :: String.t()
def get_page_path(%Mebe2.Engine.Models.Page{} = page) do
"/#{page.slug}"
end
@doc """
Get the relative path to a given tag.