diff options
Diffstat (limited to 'app/helpers')
| -rw-r--r-- | app/helpers/social_helper.rb | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/app/helpers/social_helper.rb b/app/helpers/social_helper.rb new file mode 100644 index 00000000..e66884a4 --- /dev/null +++ b/app/helpers/social_helper.rb | |||
| @@ -0,0 +1,122 @@ | |||
| 1 | module SocialHelper | ||
| 2 | # The club's name is a proper noun and identical in both locales. | ||
| 3 | OG_SITE_NAME = "Chaos Computer Club".freeze | ||
| 4 | |||
| 5 | # Facebook's og:locale wants a language_TERRITORY tag, not a bare | ||
| 6 | # language code. Anything not listed falls back to the default locale's | ||
| 7 | # tag rather than emitting something no consumer recognises. | ||
| 8 | OG_LOCALES = { :de => "de_DE", :en => "en_GB" }.freeze | ||
| 9 | |||
| 10 | OG_DEFAULT_IMAGE = "/images/social_default.png".freeze | ||
| 11 | |||
| 12 | # Previews render unpublished drafts behind nothing but a token in the | ||
| 13 | # URL, so these controllers emit no social metadata at all. | ||
| 14 | PREVIEW_CONTROLLERS = %w[shared_previews pages].freeze | ||
| 15 | |||
| 16 | # Search result pages are the classic noindex case. Previews join them | ||
| 17 | # for the reason above. | ||
| 18 | NOINDEX_CONTROLLERS = (PREVIEW_CONTROLLERS + %w[search]).freeze | ||
| 19 | |||
| 20 | # og:image must be an absolute URL with a scheme; a path is ignored. | ||
| 21 | # request.base_url rather than a routing helper, so default_url_options | ||
| 22 | # cannot inject a locale prefix into a static asset path. | ||
| 23 | def og_absolute_url(path) | ||
| 24 | "#{request.base_url}#{path}" | ||
| 25 | end | ||
| 26 | |||
| 27 | def social_meta? | ||
| 28 | !PREVIEW_CONTROLLERS.include?(controller_name) | ||
| 29 | end | ||
| 30 | |||
| 31 | def robots_directive | ||
| 32 | return nil unless NOINDEX_CONTROLLERS.include?(controller_name) | ||
| 33 | |||
| 34 | # nofollow on previews as well, so a crawler that reaches one does not | ||
| 35 | # walk out of it into whatever the draft links to. Search pages omit it, | ||
| 36 | # since following result links is the one useful thing a crawler can do | ||
| 37 | # there. | ||
| 38 | PREVIEW_CONTROLLERS.include?(controller_name) ? "noindex, nofollow" : "noindex" | ||
| 39 | end | ||
| 40 | |||
| 41 | # The headline asset's card if it has one, else the site default. | ||
| 42 | # has_variant? | ||
| 43 | # | ||
| 44 | # The query suffix defeats indefinite crawler caching: FileAttachment | ||
| 45 | # deliberately keeps public URLs stable across a file replacement, so | ||
| 46 | # without it Facebook would serve the superseded card forever. | ||
| 47 | def og_image_url | ||
| 48 | asset = @page&.persisted? ? @page.headline_asset : nil | ||
| 49 | |||
| 50 | if asset&.has_variant?(:og) | ||
| 51 | og_absolute_url("#{asset.upload.url(:og)}?v=#{asset.upload_updated_at.to_i}") | ||
| 52 | else | ||
| 53 | og_absolute_url(OG_DEFAULT_IMAGE) | ||
| 54 | end | ||
| 55 | end | ||
| 56 | |||
| 57 | # Both files are exactly 1200x630, so these are constants either way. | ||
| 58 | def og_image_width | ||
| 59 | FileAttachment::OG_WIDTH | ||
| 60 | end | ||
| 61 | |||
| 62 | def og_image_height | ||
| 63 | FileAttachment::OG_HEIGHT | ||
| 64 | end | ||
| 65 | |||
| 66 | def og_image_alt | ||
| 67 | asset = @page&.persisted? ? @page.headline_asset : nil | ||
| 68 | asset&.has_variant?(:og) ? asset.name.to_s : OG_SITE_NAME | ||
| 69 | end | ||
| 70 | |||
| 71 | def og_title | ||
| 72 | @page&.title.presence || OG_SITE_NAME | ||
| 73 | end | ||
| 74 | |||
| 75 | # Public controllers are deliberately not pinned to the default locale, | ||
| 76 | # so Globalize follows I18n.locale here and the abstract arrives in the | ||
| 77 | # language being served. Abstracts hold markup, hence strip_tags. | ||
| 78 | def og_description | ||
| 79 | text = strip_tags(@page&.abstract.to_s).squish | ||
| 80 | return truncate(text, :length => 200, :separator => " ") if text.present? | ||
| 81 | |||
| 82 | t("layouts.social_meta.site_description") | ||
| 83 | end | ||
| 84 | |||
| 85 | # A rendered page is a piece of content; the aggregate views (search, | ||
| 86 | # tags, gallery) have no @page and are the site itself. | ||
| 87 | def og_type | ||
| 88 | @page&.persisted? ? "article" : "website" | ||
| 89 | end | ||
| 90 | |||
| 91 | def og_published_time | ||
| 92 | @page&.published_at&.iso8601 | ||
| 93 | end | ||
| 94 | |||
| 95 | # request.path rather than a routing helper: it is already the locale's | ||
| 96 | # own canonical form -- unprefixed for German, /en/ for English -- and | ||
| 97 | # dropping the query string is what makes it canonical. | ||
| 98 | def og_canonical_url | ||
| 99 | og_absolute_url(request.path) | ||
| 100 | end | ||
| 101 | |||
| 102 | def og_locale | ||
| 103 | OG_LOCALES.fetch((@page&.effective_lang || I18n.locale).to_sym, | ||
| 104 | OG_LOCALES[I18n.default_locale.to_sym]) | ||
| 105 | end | ||
| 106 | |||
| 107 | # Only locales in which this page genuinely has a translation, so a | ||
| 108 | # crawler is not told about a variant that would fall back. | ||
| 109 | def og_locale_alternates | ||
| 110 | return [] unless @page | ||
| 111 | |||
| 112 | (@page.translated_locales.map(&:to_sym) - [og_locale_key]).filter_map do |locale| | ||
| 113 | OG_LOCALES[locale] | ||
| 114 | end | ||
| 115 | end | ||
| 116 | |||
| 117 | private | ||
| 118 | |||
| 119 | def og_locale_key | ||
| 120 | (@page&.effective_lang || I18n.locale).to_sym | ||
| 121 | end | ||
| 122 | end | ||
