diff --git a/Gemfile b/Gemfile index 868bb9a920..192a627d09 100644 --- a/Gemfile +++ b/Gemfile @@ -13,3 +13,6 @@ gem 'base64' gem 'bigdecimal' gem "logger", "~> 1.7" + +gem 'ruby-magic', '~> 0.6.0' + diff --git a/Gemfile.lock b/Gemfile.lock index 9c13e2747e..7855cf2b7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,6 +96,7 @@ GEM rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) mercenary (0.4.0) + mini_portile2 (2.8.9) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (6.0.2) @@ -105,6 +106,8 @@ GEM ffi (~> 1.0) rexml (3.4.2) rouge (4.5.2) + ruby-magic (0.6.0) + mini_portile2 (~> 2.8) safe_yaml (1.0.5) sass-embedded (1.89.2) google-protobuf (~> 4.31) @@ -178,6 +181,7 @@ DEPENDENCIES jekyll-redirect-from jekyll-watch logger (~> 1.7) + ruby-magic (~> 0.6.0) webrick (~> 1.8) BUNDLED WITH diff --git a/_layouts/default.html b/_layouts/default.html index 20e2ab6fe8..1f56e59dd1 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -15,7 +15,12 @@ - + {% assign page_image = page.image | default: '/assets/share-image.jpg' %} + {% assign is_page_image_legacy = page_image | is_legacy_cover_image %} + {% if is_page_image_legacy != true %} + {% assign page_image = page_image | ensure_magic_type: 'jpeg,png,gif', 'Some websites (like Reddit) seem to fail to render cover images in formats other than basic ones.', 'webp:We know that WebP is one of the best formats for the Web. But for now, we ban the use of WebP images only in the context of cover images.' %} + {% endif %} + diff --git a/_plugins/ensure_magic_type.rb b/_plugins/ensure_magic_type.rb new file mode 100644 index 0000000000..ae5a6f7281 --- /dev/null +++ b/_plugins/ensure_magic_type.rb @@ -0,0 +1,111 @@ +# Plugin for Jekyll that ensures known magic types. +# `ensure_magic_type` always returns the passed value. +# Though, it will raise an exception if the types don't match. +# +# Usage: +# {% assign page_image = page.image | ensure_magic_type: 'webp', 'WebP are preferred.', 'jpg:JPGs are way too big!' %} +# +# The first argument is a list of "known types". It's a comma-separated string of the below @@known_types keys. +# +# The last arguments are error contexts. +# If the error context begins with `:`, it will be printed to add context to the exception. +# If the error don't begin with this prefix, it will be used as a general context to explain why checks are made. + +require 'magic' + +module Jekyll + module EnsureMagicTypeFilter + @@known_types = { + "jpeg" => "JPEG image data", + "jxl" => "JPEG XL codestream", + "png" => "PNG image data", + "gif" => "GIF image data", + "webp" => "Web/P image", + "avif" => "AVIF Image", + } + + def ensure_magic_type(input, type, *error_contexts) + types = type.split(",") + types.each do |type| + if not @@known_types.key? type + raise Exception.new "\"#{type}\" is not a \"known type\" registered in `_plugins/ensure_magic_type.rb`. Please add the type in the `@@known_types` hash before testing." + end + end + + site_config = @context.registers[:site].config + site_source = site_config["source"] + + input_path = File.join(site_source, input) + + magic = Magic.new + magic_of_input = magic.file(input_path) + magic_of_input_split = magic_of_input.split(", ") + + detected_type = false + detected_type_key = nil + detected_type_text = "" + @@known_types.each do |known_type_name, known_type_magic| + if magic_of_input_split.include? known_type_magic + detected_type = true + detected_type_key = known_type_name + detected_type_text = known_type_name + break + end + end + + raise_exception = false + if detected_type + if not types.include? detected_type_key + raise_exception = true + end + else + raise_exception = true + detected_type_text = magic_of_input + end + + if raise_exception + msg = "\"#{input}\" is not of type \"#{types.join(", ")}\" (is of type \"#{detected_type_text}\")" + + if error_contexts.size > 0 + generic_error_context = nil + specific_error_context = nil + + error_contexts.each do |error_context| + starts_with_known_type = false + @@known_types.keys.each do |known_type_key| + prefix = "#{known_type_key}:" + if error_context.start_with? prefix + starts_with_known_type = true + if detected_type && known_type_key == detected_type_text + specific_error_context = error_context[prefix.length..] + end + break + end + end + + if not starts_with_known_type + if generic_error_context != nil + raise Exception.new "Multiple generic errors given." + end + generic_error_context = error_context + end + end + + + if generic_error_context != nil + msg += "\n#{generic_error_context}" + end + + if detected_type and specific_error_context != nil + msg += "\n[#{detected_type_text}]: #{specific_error_context}" + end + end + raise Exception.new msg + end + + input + end + end +end + +Liquid::Template.register_filter(Jekyll::EnsureMagicTypeFilter) diff --git a/_plugins/is_legacy.rb b/_plugins/is_legacy.rb new file mode 100644 index 0000000000..fbb4db7997 --- /dev/null +++ b/_plugins/is_legacy.rb @@ -0,0 +1,277 @@ +require "uri" + +module Jekyll + module IsLegacyCoverImageFilter + @@legacy_covers = [ + # WebP images. + "/assets/showcase/badaboom.webp", + "/assets/showcase/beat-invaders-header.webp", + "/assets/showcase/beat-invaders.webp", + "/assets/showcase/blastronaut-1.webp", + "/assets/showcase/blastronaut-2.webp", + "/assets/showcase/blastronaut-3.webp", + "/assets/showcase/blastronaut-header.webp", + "/assets/showcase/brotato-1.webp", + "/assets/showcase/brotato-2.webp", + "/assets/showcase/brotato-3.webp", + "/assets/showcase/brotato-header.webp", + "/assets/showcase/callbreak.webp", + "/assets/showcase/cassette-beasts-1.webp", + "/assets/showcase/cassette-beasts-2.webp", + "/assets/showcase/cassette-beasts-3.webp", + "/assets/showcase/cassette-beasts-header.webp", + "/assets/showcase/city-game-studio-header.webp", + "/assets/showcase/city-game-studio.webp", + "/assets/showcase/cruelty-squad-header.webp", + "/assets/showcase/delta-v-rings-of-saturn-header.webp", + "/assets/showcase/delta-v-rings-of-saturn.webp", + "/assets/showcase/dogwalk/header.webp", + "/assets/showcase/dome-keeper-header.webp", + "/assets/showcase/dome-keeper.webp", + "/assets/showcase/dungeondraft.webp", + "/assets/showcase/endoparasitic-header.webp", + "/assets/showcase/endoparasitic.webp", + "/assets/showcase/ex-zodiac-header.webp", + "/assets/showcase/ex-zodiac.webp", + "/assets/showcase/fist-of-the-forgotten-header.webp", + "/assets/showcase/fist-of-the-forgotten.webp", + "/assets/showcase/gnome-1.webp", + "/assets/showcase/gnome-2.webp", + "/assets/showcase/gnome-3.webp", + "/assets/showcase/gnome-header.webp", + "/assets/showcase/gravity-ace-header.webp", + "/assets/showcase/gravity-ace.webp", + "/assets/showcase/haiki-header.webp", + "/assets/showcase/haiki.webp", + "/assets/showcase/halls-of-torment-1.webp", + "/assets/showcase/halls-of-torment-2.webp", + "/assets/showcase/halls-of-torment-3.webp", + "/assets/showcase/halls-of-torment-4.webp", + "/assets/showcase/halls-of-torment.webp", + "/assets/showcase/hive-time.webp", + "/assets/showcase/human-diaspora-header.webp", + "/assets/showcase/human-diaspora.webp", + "/assets/showcase/kingdoms-of-the-dump-header.webp", + "/assets/showcase/lumencraft-1.webp", + "/assets/showcase/lumencraft-3.webp", + "/assets/showcase/lumencraft-header.webp", + "/assets/showcase/madness-endless-header.webp", + "/assets/showcase/of-life-and-land-1.webp", + "/assets/showcase/of-life-and-land-2.webp", + "/assets/showcase/of-life-and-land-3.webp", + "/assets/showcase/of-life-and-land-4.webp", + "/assets/showcase/of-life-and-land-header.webp", + "/assets/showcase/outskirts-godot-environment-demo.webp", + "/assets/showcase/pingo-adventure-header.webp", + "/assets/showcase/pingo-adventure.webp", + "/assets/showcase/precipice-header.webp", + "/assets/showcase/precipice.webp", + "/assets/showcase/primal-light-1.webp", + "/assets/showcase/primal-light-2.webp", + "/assets/showcase/primal-light-header.webp", + "/assets/showcase/resolutiion-1.webp", + "/assets/showcase/resolutiion-2.webp", + "/assets/showcase/resolutiion-header.webp", + "/assets/showcase/rogue-state-revolution-header.webp", + "/assets/showcase/rogue-state-revolution.webp", + "/assets/showcase/rpg-in-a-box-header.webp", + "/assets/showcase/rpg-in-a-box.webp", + "/assets/showcase/sandals-1.webp", + "/assets/showcase/sandals-2.webp", + "/assets/showcase/sandals-header.webp", + "/assets/showcase/tail-quest-header.webp", + "/assets/showcase/tail-quest.webp", + "/assets/showcase/tailquest-1.webp", + "/assets/showcase/tailquest-2.webp", + "/assets/showcase/until-then-1.webp", + "/assets/showcase/until-then-2.webp", + "/assets/showcase/until-then-3.webp", + "/assets/showcase/until-then-4.webp", + "/assets/showcase/until-then-header.webp", + "/assets/showcase/usagi-shima-1.webp", + "/assets/showcase/usagi-shima-4.webp", + "/assets/showcase/usagi-shima-5.webp", + "/assets/showcase/usagi-shima-header.webp", + "/assets/showcase/usagi-shima-scr03.webp", + "/assets/showcase/usagi-shima.webp", + "/assets/showcase/wrought-flesh-1.webp", + "/assets/showcase/wrought-flesh-header.webp", + "/storage/blog/2024-cherry-picks/2024-cherry-picks.webp", + "/storage/blog/covers/about-official-console-ports.webp", + "/storage/blog/covers/android-and-vulkan.webp", + "/storage/blog/covers/august-2025-update-godot-xr-community.webp", + "/storage/blog/covers/beyond-100000-you-re-breathtaking.webp", + "/storage/blog/covers/big-2023.webp", + "/storage/blog/covers/cassette-beasts.webp", + "/storage/blog/covers/community-poll-2024.webp", + "/storage/blog/covers/december-2024-update-godot-xr-community.webp", + "/storage/blog/covers/design-of-the-skeleton-modifier-3d.webp", + "/storage/blog/covers/dev-snapshot-godot-3-6-beta-3.webp", + "/storage/blog/covers/dev-snapshot-godot-3-6-beta-4.webp", + "/storage/blog/covers/dev-snapshot-godot-3-6-beta-5.webp", + "/storage/blog/covers/dev-snapshot-godot-4-1-beta-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-1-beta-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-1-beta-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-beta-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-beta-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-beta-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-beta-4.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-beta-5.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-beta-6.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-dev-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-dev-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-dev-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-dev-4.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-dev-5.webp", + "/storage/blog/covers/dev-snapshot-godot-4-2-dev-6.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-beta-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-beta-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-beta-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-dev-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-dev-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-dev-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-dev-4.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-dev-5.webp", + "/storage/blog/covers/dev-snapshot-godot-4-3-dev-6.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-beta-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-beta-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-beta-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-beta-4.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-dev-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-dev-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-dev-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-dev-4.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-dev-5.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-dev-6.webp", + "/storage/blog/covers/dev-snapshot-godot-4-4-dev-7.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-beta-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-beta-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-beta-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-beta-4.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-beta-5.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-beta-6.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-beta-7.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-dev-1.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-dev-2.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-dev-3.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-dev-4.webp", + "/storage/blog/covers/dev-snapshot-godot-4-5-dev-5.webp", + "/storage/blog/covers/dev-snapshot-godot-4-6-dev-1.webp", + "/storage/blog/covers/development-fund-perks.webp", + "/storage/blog/covers/ensuring-quality-godot-documentation.webp", + "/storage/blog/covers/february-2025-update-godot-xr-community.webp", + "/storage/blog/covers/funding-breakdown-and-hiring-process.webp", + "/storage/blog/covers/gamescom-cover-2024.webp", + "/storage/blog/covers/gamescom-latam-2024.webp", + "/storage/blog/covers/godot-3-6-finally-released.webp", + "/storage/blog/covers/godot-4-0-sets-sail.webp", + "/storage/blog/covers/godot-4-1-is-here.webp", + "/storage/blog/covers/godot-4-2-arrives-in-style.webp", + "/storage/blog/covers/godot-4-3-a-shared-effort.webp", + "/storage/blog/covers/godot-4-4-a-unified-experience.webp", + "/storage/blog/covers/godot-4-5-making-dreams-accessible.webp", + "/storage/blog/covers/godot-and-android-and-vulkan.webp", + "/storage/blog/covers/godot-awards-stayathomedev.webp", + "/storage/blog/covers/godot-editor-horizon-store-early-access-release.webp", + "/storage/blog/covers/godot-foundation-update-2024.webp", + "/storage/blog/covers/godot-jam.webp", + "/storage/blog/covers/godot-jetbrains.webp", + "/storage/blog/covers/godot-openxr-vendors-plugin-400.webp", + "/storage/blog/covers/godot-sdk-integrations.webp", + "/storage/blog/covers/godot-showcase-the-somar-project.webp", + "/storage/blog/covers/godot-showreel-2023.webp", + "/storage/blog/covers/godot-showreel-2024.webp", + "/storage/blog/covers/godot-xr-update-sep-2023.webp", + "/storage/blog/covers/godot_peluche_2025_5.webp", + "/storage/blog/covers/godotcon-2023-we-need-you.webp", + "/storage/blog/covers/godotcon-2023.webp", + "/storage/blog/covers/godotcon-2024-post.webp", + "/storage/blog/covers/godotcon-2024.webp", + "/storage/blog/covers/godotcon-boston-2025-android-xr.webp", + "/storage/blog/covers/godotcon-boston-2025.webp", + "/storage/blog/covers/godotfest-2025.webp", + "/storage/blog/covers/godotsharp-packages-net8.webp", + "/storage/blog/covers/gpu-sync-progress-report.webp", + "/storage/blog/covers/introducing-new-forum.webp", + "/storage/blog/covers/introducing-reverse-z.webp", + "/storage/blog/covers/live-from-godotcon-boston-web-net-prototype.webp", + "/storage/blog/covers/maintenance-release-godot-3-5-3.webp", + "/storage/blog/covers/maintenance-release-godot-4-0-4.webp", + "/storage/blog/covers/maintenance-release-godot-4-1-1.webp", + "/storage/blog/covers/maintenance-release-godot-4-1-2.webp", + "/storage/blog/covers/maintenance-release-godot-4-1-3.webp", + "/storage/blog/covers/maintenance-release-godot-4-2-1.webp", + "/storage/blog/covers/maintenance-release-godot-4-2-2-and-4-1-4.webp", + "/storage/blog/covers/maintenance-release-godot-4-4-1.webp", + "/storage/blog/covers/march-2025-update-godot-xr-community.webp", + "/storage/blog/covers/migrating-animations-from-godot-4-0-to-4-3.webp", + "/storage/blog/covers/moat_logo_large_colorful_over_screenshot.webp", + "/storage/blog/covers/october-2024-xr-progress-update.webp", + "/storage/blog/covers/progress-report-new-ufbx-importer.webp", + "/storage/blog/covers/progress-report-parallax2d.webp", + "/storage/blog/covers/progress-report-state-of-particles.webp", + "/storage/blog/covers/progress-report-web-export-in-4-3.webp", + "/storage/blog/covers/progress-update-csharp-2.webp", + "/storage/blog/covers/release-candidate-godot-3-5-3-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-3-6-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-0-4-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-1-1-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-1-2-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-1-3-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-1-4-and-4-2-2-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-1-4-and-4-2-2-rc-2.webp", + "/storage/blog/covers/release-candidate-godot-4-1-4-and-4-2-2-rc-3.webp", + "/storage/blog/covers/release-candidate-godot-4-1-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-1-rc-2.webp", + "/storage/blog/covers/release-candidate-godot-4-1-rc-3.webp", + "/storage/blog/covers/release-candidate-godot-4-2-1-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-2-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-2-rc-2.webp", + "/storage/blog/covers/release-candidate-godot-4-3-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-3-rc-2.webp", + "/storage/blog/covers/release-candidate-godot-4-3-rc-3.webp", + "/storage/blog/covers/release-candidate-godot-4-4-1-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-4-1-rc-2.webp", + "/storage/blog/covers/release-candidate-godot-4-4-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-4-rc-2.webp", + "/storage/blog/covers/release-candidate-godot-4-4-rc-3.webp", + "/storage/blog/covers/release-candidate-godot-4-5-1-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-5-1-rc-2.webp", + "/storage/blog/covers/release-candidate-godot-4-5-rc-1.webp", + "/storage/blog/covers/release-candidate-godot-4-5-rc-2.webp", + "/storage/blog/covers/rendering-priorities-jan-2024.webp", + "/storage/blog/covers/rendering-priorities-july-2023.webp", + "/storage/blog/covers/rendering-priorities-september-2024.webp", + "/storage/blog/covers/review-gamescom-2024.webp", + "/storage/blog/covers/statement-on-godloader-malware-loader.webp", + "/storage/blog/covers/uid-changes-coming-to-godot-4-4.webp", + "/storage/blog/covers/upcoming-serious-web-performance-boost.webp", + "/storage/blog/covers/usagi-shima.webp", + "/storage/blog/gamescom/2023/gamescom-cover.webp", + "/storage/blog/gdc/2024/gdc-thumbnail.webp", + "/storage/blog/gdc/2024/gdc2.webp", + "/storage/blog/godotcon-us-2025/looking-back-godotcon-2025.webp", + + # SVG images. + "/storage/app/uploads/public/57f/218/5f6/57f2185f638af084238366.svg", + "/storage/app/uploads/public/57e/f83/c4e/57ef83c4e718d700719754.svg", + "/storage/app/uploads/public/5a9/681/3c1/5a96813c154bd636778829.svg", + "/storage/app/uploads/public/5dc/019/1f9/5dc0191f92385004371530.svg", + ] + + def is_legacy_cover_image(input) + input_elements = Array(input) + + input_elements.each do |input_element| + uri = URI(input_element) + if not @@legacy_covers.include? uri.path + return false + end + end + + true + end + end +end + +Liquid::Template.register_filter(Jekyll::IsLegacyCoverImageFilter) diff --git a/assets/share-image.jpg b/assets/share-image.jpg new file mode 100644 index 0000000000..9559a1fde9 Binary files /dev/null and b/assets/share-image.jpg differ diff --git a/assets/share-image.webp b/assets/share-image.webp deleted file mode 100644 index 2d375174ff..0000000000 Binary files a/assets/share-image.webp and /dev/null differ