summaryrefslogtreecommitdiff
path: root/app/helpers/social_helper.rb
blob: e66884a400c6f0d004ffa35bc8a9bbf777448ddd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
module SocialHelper
  # The club's name is a proper noun and identical in both locales.
  OG_SITE_NAME = "Chaos Computer Club".freeze

  # Facebook's og:locale wants a language_TERRITORY tag, not a bare
  # language code. Anything not listed falls back to the default locale's
  # tag rather than emitting something no consumer recognises.
  OG_LOCALES = { :de => "de_DE", :en => "en_GB" }.freeze

  OG_DEFAULT_IMAGE = "/images/social_default.png".freeze

  # Previews render unpublished drafts behind nothing but a token in the
  # URL, so these controllers emit no social metadata at all.
  PREVIEW_CONTROLLERS = %w[shared_previews pages].freeze

  # Search result pages are the classic noindex case. Previews join them
  # for the reason above.
  NOINDEX_CONTROLLERS = (PREVIEW_CONTROLLERS + %w[search]).freeze

  # og:image must be an absolute URL with a scheme; a path is ignored.
  # request.base_url rather than a routing helper, so default_url_options
  # cannot inject a locale prefix into a static asset path.
  def og_absolute_url(path)
    "#{request.base_url}#{path}"
  end

  def social_meta?
    !PREVIEW_CONTROLLERS.include?(controller_name)
  end

  def robots_directive
    return nil unless NOINDEX_CONTROLLERS.include?(controller_name)

    # nofollow on previews as well, so a crawler that reaches one does not
    # walk out of it into whatever the draft links to. Search pages omit it,
    # since following result links is the one useful thing a crawler can do
    # there.
    PREVIEW_CONTROLLERS.include?(controller_name) ? "noindex, nofollow" : "noindex"
  end

  # The headline asset's card if it has one, else the site default.
  # has_variant?
  #
  # The query suffix defeats indefinite crawler caching: FileAttachment
  # deliberately keeps public URLs stable across a file replacement, so
  # without it Facebook would serve the superseded card forever.
  def og_image_url
    asset = @page&.persisted? ? @page.headline_asset : nil

    if asset&.has_variant?(:og)
      og_absolute_url("#{asset.upload.url(:og)}?v=#{asset.upload_updated_at.to_i}")
    else
      og_absolute_url(OG_DEFAULT_IMAGE)
    end
  end

  # Both files are exactly 1200x630, so these are constants either way.
  def og_image_width
    FileAttachment::OG_WIDTH
  end

  def og_image_height
    FileAttachment::OG_HEIGHT
  end

  def og_image_alt
    asset = @page&.persisted? ? @page.headline_asset : nil
    asset&.has_variant?(:og) ? asset.name.to_s : OG_SITE_NAME
  end

  def og_title
    @page&.title.presence || OG_SITE_NAME
  end

  # Public controllers are deliberately not pinned to the default locale,
  # so Globalize follows I18n.locale here and the abstract arrives in the
  # language being served. Abstracts hold markup, hence strip_tags.
  def og_description
    text = strip_tags(@page&.abstract.to_s).squish
    return truncate(text, :length => 200, :separator => " ") if text.present?

    t("layouts.social_meta.site_description")
  end

  # A rendered page is a piece of content; the aggregate views (search,
  # tags, gallery) have no @page and are the site itself.
  def og_type
    @page&.persisted? ? "article" : "website"
  end

  def og_published_time
    @page&.published_at&.iso8601
  end

  # request.path rather than a routing helper: it is already the locale's
  # own canonical form -- unprefixed for German, /en/ for English -- and
  # dropping the query string is what makes it canonical.
  def og_canonical_url
    og_absolute_url(request.path)
  end

  def og_locale
    OG_LOCALES.fetch((@page&.effective_lang || I18n.locale).to_sym,
                     OG_LOCALES[I18n.default_locale.to_sym])
  end

  # Only locales in which this page genuinely has a translation, so a
  # crawler is not told about a variant that would fall back.
  def og_locale_alternates
    return [] unless @page

    (@page.translated_locales.map(&:to_sym) - [og_locale_key]).filter_map do |locale|
      OG_LOCALES[locale]
    end
  end

  private

    def og_locale_key
      (@page&.effective_lang || I18n.locale).to_sym
    end
end