Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse Mastodon handle to add attribution and verification metadata #13

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Bridgetown SEO Tag adds the following meta tags to your site:
* Canonical URL
* Next and previous URLs on paginated pages
* [Open Graph](https://ogp.me/) title, description, site title, and URL (for Facebook, LinkedIn, etc.)
* Mastodon [verification](https://docs.joinmastodon.org/user/profile/#verification) and [attribution](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/)
* [Twitter Summary Card](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started) metadata

While you could theoretically add the necessary metadata tags yourself, Bridgetown SEO Tag provides a battle-tested template of crowdsourced best-practices.
Expand All @@ -75,6 +76,8 @@ The SEO tag will respect any of the following if included in your site's `site_m
* `description` - A longer description used for the description meta tag. Also used as fallback for documents that don't provide their own `description` and as part of the home page title tag if `tagline` is not defined.
* `author` - global author information (see [Advanced usage](https://github.com/bridgetownrb/bridgetown-seo-tag/wiki/Advanced-Usage#author-information))

* `mastodon` - Your Mastodon handle, to both verify your Mastodon profile, and link to your profile when someone shares your content on the network. If the page metadata contains a `mastodon` entry, it will take precedence over `site_metadata.yml`.

* `twitter` - You can add a single Twitter handle to be used in Twitter card tags, like "bridgetownrb". Or you use a YAML mapping with additional details:
* `twitter:card` - The site's default card type
* `twitter:username` - The site's Twitter handle
Expand Down
1 change: 1 addition & 0 deletions lib/bridgetown-seo-tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
module Bridgetown
class SeoTag < Liquid::Tag
autoload :AuthorDrop, "bridgetown-seo-tag/author_drop"
autoload :MastodonDrop, "bridgetown-seo-tag/mastodon_drop"
autoload :ImageDrop, "bridgetown-seo-tag/image_drop"
autoload :UrlHelper, "bridgetown-seo-tag/url_helper"
autoload :Drop, "bridgetown-seo-tag/drop"
Expand Down
2 changes: 1 addition & 1 deletion lib/bridgetown-seo-tag/author_drop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def resolved_author
@resolved_author = sources.find { |s| !s.to_s.empty? }
end

# If resolved_author is a string, attempts to find coresponding author
# If resolved_author is a string, attempts to find corresponding author
# metadata in `site.data.authors`
#
# Returns a hash representing additional metadata or an empty hash
Expand Down
2 changes: 1 addition & 1 deletion lib/bridgetown-seo-tag/drop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def author
end

# Returns a Drop representing the page's image
# Returns nil if the image has no path, to preserve backwards compatability
# Returns nil if the image has no path, to preserve backwards compatibility
def image
@image ||= ImageDrop.new(page: page, context: @context)
@image if @image.path
Expand Down
2 changes: 1 addition & 1 deletion lib/bridgetown-seo-tag/image_drop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def initialize(page: nil, context: nil)
@context = context
end

# Called path for backwards compatability, this is really
# Called path for backwards compatibility, this is really
# the escaped, absolute URL representing the page's image
# Returns nil if no image path can be determined
def path
Expand Down
84 changes: 84 additions & 0 deletions lib/bridgetown-seo-tag/mastodon_drop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Bridgetown
class SeoTag
# A drop representing the current page's mastodon handle
#
# Mastodon handle will be pulled from:
#
# 1. The page's `mastodon` key
# 2. The `mastodon` key in the site config
class MastodonDrop < Bridgetown::Drops::Drop
HANDLE_REGEX = /\A@?(?<username>[^@]+)@(?<server>[^@]+)\z/

# Initialize a new MastodonDrop
#
# page - The page hash (e.g., Page#to_liquid)
# site - The Bridgetown::Drops::SiteDrop
def initialize(page: nil, site: nil)
raise ArgumentError unless page && site

@mutations = {}
@page = page
@site = site
end

def mastodon_handle
"@#{username}@#{server}" if handle?
end
alias_method :to_s, :mastodon_handle

def mastodon_url
"https://#{server}/@#{username}" if handle?
end

# Make the drop behave like a hash
def [](key)
return mastodon_handle if key.to_sym == :mastodon
end

private

attr_reader :page, :site

# Finds the mastodon handle in page.metadata, or site.metadata
#
# Returns a string
def resolved_handle
return @resolved_handle if defined? @resolved_handle

sources = [page["mastodon"]]
sources << site.data.dig("site_metadata", "mastodon")
@resolved_handle = sources.find { |s| !s.to_s.empty? }
end

# Returns the username parsed from the resolved handle
def username
handle_hash["username"]
end

# Returns the server parsed from the resolved handle
def server
handle_hash["server"]
end

# Returns a hash containing username and server
# or an empty hash, if the handle cannot be parsed
def handle_hash
@handle_hash ||= case resolved_handle
when String
HANDLE_REGEX.match(resolved_handle)&.named_captures || {}
else
{}
end
end
# Since author_hash is aliased to fallback_data, any values in the hash
# will be exposed via the drop, allowing support for arbitrary metadata
alias_method :fallback_data, :handle_hash

def handle?
handle_hash != {}
end
end
end
end
2 changes: 1 addition & 1 deletion lib/bridgetown-seo-tag/url_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module UrlHelper
# Determines if the given string is an absolute URL
#
# Returns true if an absolute URL.
# Retruns false if it's a relative URL
# Returns false if it's a relative URL
# Returns nil if it is not a string or can't be parsed as a URL
def absolute_url?(string)
return false unless string
Expand Down
5 changes: 5 additions & 0 deletions lib/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
<meta property="twitter:title" content="{{ seo_tag.page_title }}" />
{% endif %}

{% if seo_tag.mastodon_handle %}
<meta name="fediverse:creator" content="{{ seo_tag.mastodon_handle }}" />
<link rel="me" href="{{ seo_tag.mastodon_url }}" />
{% endif %}

{% if site.metadata.twitter %}
<meta name="twitter:site" content="@{{ site.metadata.twitter.username | default: site.metadata.twitter | remove:'@' }}" />

Expand Down
2 changes: 1 addition & 1 deletion spec/bridgetown_seo_tag/author_drop_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
context "without an author name or handle" do
let(:page_meta) { { "author" => { "foo" => "bar" } } }

it "dosen't blow up" do
it "doesn't blow up" do
expect(subject["twitter"]).to be_nil
end
end
Expand Down
83 changes: 83 additions & 0 deletions spec/bridgetown_seo_tag/mastodon_drop_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

RSpec.describe Bridgetown::SeoTag::MastodonDrop do
let(:data) { {} }
let(:site_config) { {} }
let(:metadata_config) { { "mastodon" => "@handle@metadata.config" } }
let(:site) do
site = make_site(metadata_config, site_config)
site.data = site.data.merge(data)
site
end
let(:site_payload) { site.site_payload["site"] }

let(:page_meta) { { "title" => "page title" } }
let(:page) { make_page(page_meta) }
subject { described_class.new(page: page.to_liquid, site: site_payload.to_liquid) }

before do
Bridgetown.logger.log_level = :error
end

it "returns the mastodon handle for #to_s" do
expect(subject.to_s).to eql("@handle@metadata.config")
end

context "with mastodon handle in site metadata" do
it "returns the site metadata handle" do
expect(subject.mastodon_handle).to eql("@handle@metadata.config")
end
end

context "with mastodon handle in front matter default" do
let(:site_config) do
{
"defaults" => [
{
"scope" => { "path" => "" },
"values" => { "mastodon" => "@handle@frontmatter.defaults" },
},
],
}
end

it "uses the handle from the front matter default" do
site # init new config
defaults_page = Bridgetown::SeoTag::MastodonDrop.new(page: make_resource_page.to_liquid, site: site_payload.to_liquid)
expect(defaults_page["mastodon"]).to eql("@handle@frontmatter.defaults")
end
end

context "with mastodon override in page meta" do
let(:page_meta) { { "mastodon" => "@handle@page.meta" } }

it "uses the value defined in page metadata" do
expect(subject["mastodon"]).to eql("@handle@page.meta")
end

context "with an empty value" do
let(:metadata_config) { {} }
let(:page_meta) { { "mastodon" => "" } }

it "doesn't blow up" do
expect(subject["mastodon"]).to be_nil
end
end

context "with a hash value" do
let(:page_meta) { { "mastodon" => { "some" => "thing" } } }

it "doesn't blow up" do
expect(subject["mastodon"]).to be_nil
end
end

context "with an array value" do
let(:page_meta) { { "mastodon" => [] } }

it "doesn't blow up" do
expect(subject["mastodon"]).to be_nil
end
end
end
end