diff --git a/app/assets/images/pageflow/admin/icons/published_with_noindex.svg b/app/assets/images/pageflow/admin/icons/published_with_noindex.svg new file mode 100644 index 0000000000..8b40ea40ff --- /dev/null +++ b/app/assets/images/pageflow/admin/icons/published_with_noindex.svg @@ -0,0 +1,4 @@ + + diff --git a/app/assets/stylesheets/pageflow/admin/entries.scss b/app/assets/stylesheets/pageflow/admin/entries.scss index bf6b4f5359..9dd03e8394 100644 --- a/app/assets/stylesheets/pageflow/admin/entries.scss +++ b/app/assets/stylesheets/pageflow/admin/entries.scss @@ -22,6 +22,10 @@ $pageflow-published-revision-background-color: #beebb8 !default; .publication_state_indicator { height: 17px; } + + .tooltip_clue { + display: inline-block; + } } .legend { diff --git a/app/assets/stylesheets/pageflow/admin/publication_state_indicator.scss b/app/assets/stylesheets/pageflow/admin/publication_state_indicator.scss index 07a3c58b5b..2d7d7af106 100644 --- a/app/assets/stylesheets/pageflow/admin/publication_state_indicator.scss +++ b/app/assets/stylesheets/pageflow/admin/publication_state_indicator.scss @@ -22,4 +22,8 @@ $pageflow-publication-state-indicator-size: 25px !default; &.published_with_password_protection { background-image: image-url("#{$dir}/published_with_password.svg"); } + + &.published_with_noindex { + background-image: image-url("#{$dir}/published_with_noindex.svg"); + } } diff --git a/app/controllers/pageflow/editor/entry_publications_controller.rb b/app/controllers/pageflow/editor/entry_publications_controller.rb index 49f4f9c4f4..e76d55571f 100644 --- a/app/controllers/pageflow/editor/entry_publications_controller.rb +++ b/app/controllers/pageflow/editor/entry_publications_controller.rb @@ -39,7 +39,11 @@ def build_entry_publication(entry) end def entry_publication_params - params.fetch(:entry_publication, {}).permit(:published_until, :password, :password_protected) + params + .fetch(:entry_publication, {}) + .permit(:published_until, + :password, :password_protected, + :noindex) end def published_entries_quota(entry) diff --git a/app/helpers/pageflow/meta_tags_helper.rb b/app/helpers/pageflow/meta_tags_helper.rb index 96bfad1227..c72ea89179 100644 --- a/app/helpers/pageflow/meta_tags_helper.rb +++ b/app/helpers/pageflow/meta_tags_helper.rb @@ -9,7 +9,8 @@ def meta_tags_data_for_entry(entry) { keywords: entry.keywords, author: entry.author, - publisher: entry.publisher + publisher: entry.publisher, + noindex: entry.noindex? } end end diff --git a/app/models/concerns/pageflow/entry_publication_states.rb b/app/models/concerns/pageflow/entry_publication_states.rb index 8d97709aa4..133fd3f6ee 100644 --- a/app/models/concerns/pageflow/entry_publication_states.rb +++ b/app/models/concerns/pageflow/entry_publication_states.rb @@ -11,6 +11,9 @@ module EntryPublicationStates scope(:published_with_password_protection, -> { published.merge(Revision.with_password_protection) }) + scope(:published_without_noindex, + -> { published.merge(Revision.without_noindex) }) + scope(:not_published, lambda do includes(:published_revision) @@ -22,6 +25,8 @@ module EntryPublicationStates def publication_state if published_with_password_protection? 'published_with_password_protection' + elsif published? && published_revision.noindex? + 'published_with_noindex' elsif published? 'published_without_password_protection' else @@ -45,6 +50,10 @@ def published_until published? ? published_revision.published_until : nil end + def last_published_with_noindex? + !!revisions.publications.first&.noindex + end + module ClassMethods def with_publication_state(state) case state diff --git a/app/models/pageflow/entry.rb b/app/models/pageflow/entry.rb index 07a6b0fd71..f2995363b9 100644 --- a/app/models/pageflow/entry.rb +++ b/app/models/pageflow/entry.rb @@ -88,6 +88,7 @@ def publish(options = {}) revision.published_at = Time.now revision.published_until = options[:published_until] revision.password_protected = options[:password_protected] + revision.noindex = !!options[:noindex] end end end @@ -111,6 +112,7 @@ def restore(options) revision.published_at = nil revision.published_until = nil revision.password_protected = nil + revision.noindex = nil end end diff --git a/app/models/pageflow/entry_at_revision.rb b/app/models/pageflow/entry_at_revision.rb index 69016987dd..ca20880c3d 100644 --- a/app/models/pageflow/entry_at_revision.rb +++ b/app/models/pageflow/entry_at_revision.rb @@ -22,6 +22,7 @@ def initialize(entry, revision, theme: nil) :password_digest, :to_model, :to_key, :to_param, :persisted?, :to_json, :first_published_at, :published_until, :published?, + :last_published_with_noindex?, :type_name, to: :entry) @@ -35,6 +36,7 @@ def initialize(entry, revision, theme: nil) :locale, :author, :publisher, :keywords, :published_at, + :noindex?, :configuration, to: :revision) diff --git a/app/models/pageflow/revision.rb b/app/models/pageflow/revision.rb index 0ece5a5097..81587ca2bf 100644 --- a/app/models/pageflow/revision.rb +++ b/app/models/pageflow/revision.rb @@ -65,6 +65,8 @@ class Revision < ApplicationRecord scope(:with_password_protection, -> { where('password_protected IS TRUE') }) scope(:without_password_protection, -> { where('password_protected IS NOT TRUE') }) + scope(:without_noindex, -> { where('noindex IS NOT TRUE') }) + scope :editable, -> { where('frozen_at IS NULL') } scope :frozen, -> { where('frozen_at IS NOT NULL') } diff --git a/app/models/pageflow/sitemaps.rb b/app/models/pageflow/sitemaps.rb index 488533c344..4084644583 100644 --- a/app/models/pageflow/sitemaps.rb +++ b/app/models/pageflow/sitemaps.rb @@ -6,6 +6,7 @@ def self.entries_for(site:) site .entries .published_without_password_protection + .published_without_noindex .order('first_published_at DESC') ) end diff --git a/app/views/components/pageflow/admin/revisions_tab.rb b/app/views/components/pageflow/admin/revisions_tab.rb index 48da700265..8499c889c8 100644 --- a/app/views/components/pageflow/admin/revisions_tab.rb +++ b/app/views/components/pageflow/admin/revisions_tab.rb @@ -39,6 +39,14 @@ def build(entry) end end + if revision.noindex? + span(class: 'publication_state_indicator published_with_noindex') do + span(class: 'tooltip_bubble') do + t('pageflow.admin.entries.noindex') + end + end + end + if revision.password_protected? span(class: 'publication_state_indicator published_with_password_protection') do span(class: 'tooltip_bubble') do diff --git a/app/views/pageflow/editor/entries/_entry.json.jbuilder b/app/views/pageflow/editor/entries/_entry.json.jbuilder index 941ac831d8..a677b32901 100644 --- a/app/views/pageflow/editor/entries/_entry.json.jbuilder +++ b/app/views/pageflow/editor/entries/_entry.json.jbuilder @@ -6,6 +6,7 @@ json.default_file_rights entry.account.default_file_rights json.published(entry.published?) json.publishable(can?(:publish, entry.to_model)) json.password_protected(entry.password_digest.present?) +json.last_published_with_noindex(entry.last_published_with_noindex?) json.metadata do json.(entry, diff --git a/app/views/pageflow/editor/entry_publications/check.json.jbuilder b/app/views/pageflow/editor/entry_publications/check.json.jbuilder index ed0a67cdbd..3bf5f2e6cc 100644 --- a/app/views/pageflow/editor/entry_publications/check.json.jbuilder +++ b/app/views/pageflow/editor/entry_publications/check.json.jbuilder @@ -6,6 +6,7 @@ json.entry do json.(@entry_publication.entry, :published_until) json.published(@entry_publication.entry.published?) json.password_protected(@entry_publication.entry.password_digest.present?) + json.last_published_with_noindex(@entry_publication.entry.last_published_with_noindex?) end json.exhausted_html(render_html_partial('pageflow/editor/quotas/published_entries_exhausted', entry: @entry_publication.entry, diff --git a/app/views/pageflow/meta_tags/_entry.html.erb b/app/views/pageflow/meta_tags/_entry.html.erb index eb37474c65..3f85d4a61b 100644 --- a/app/views/pageflow/meta_tags/_entry.html.erb +++ b/app/views/pageflow/meta_tags/_entry.html.erb @@ -2,3 +2,4 @@ <% if keywords.present? %><% end %> <% if author.present? %><% end %> <% if publisher.present? %><% end %> + <% if noindex %><% end %> diff --git a/config/locales/new/noindex.de.yml b/config/locales/new/noindex.de.yml new file mode 100644 index 0000000000..19cc36f49a --- /dev/null +++ b/config/locales/new/noindex.de.yml @@ -0,0 +1,18 @@ +de: + pageflow: + editor: + templates: + publish_entry: + noindex: Suchmaschinen ausschließen + noindex_help: | + Meta Tag setzen, dass Crawler von Suchmaschinen wie Google + oder Bing anweist, die URL des veröffentlichten Beitrags + nicht in den Index aufzunehmen. + admin: + entries: + noindex: Aus Suchmaschinen ausschließen + activerecord: + values: + pageflow/entry: + publication_states: + published_with_noindex: Veröffentlicht aber aus Suchmaschinen ausgeschlossen diff --git a/config/locales/new/noindex.en.yml b/config/locales/new/noindex.en.yml new file mode 100644 index 0000000000..b0c7afefa5 --- /dev/null +++ b/config/locales/new/noindex.en.yml @@ -0,0 +1,18 @@ +en: + pageflow: + editor: + templates: + publish_entry: + noindex: Exclude from search engines + noindex_help: | + Set a meta tag that instructs crawlers of search engines + like Google or Bing to not include the published entry in + the index. + admin: + entries: + noindex: Exclude from search engines + activerecord: + values: + pageflow/entry: + publication_states: + published_with_noindex: Published but excluded from search engines diff --git a/db/migrate/20231128124523_add_noindex_to_revisions.rb b/db/migrate/20231128124523_add_noindex_to_revisions.rb new file mode 100644 index 0000000000..6d954bbaaf --- /dev/null +++ b/db/migrate/20231128124523_add_noindex_to_revisions.rb @@ -0,0 +1,5 @@ +class AddNoindexToRevisions < ActiveRecord::Migration[5.2] + def change + add_column :pageflow_revisions, :noindex, :boolean + end +end diff --git a/package/spec/editor/models/Entry-spec.js b/package/spec/editor/models/Entry-spec.js index 961596e77a..3dad1c41dd 100644 --- a/package/spec/editor/models/Entry-spec.js +++ b/package/spec/editor/models/Entry-spec.js @@ -123,6 +123,16 @@ describe('Entry', () => { expect(entry.getFileCollection(testContext.imageFileType).first().get('state')).toBe('processed'); }); + + it('updates last_published_with_noindex attribute', () => { + const entry = support.factories.entry(); + + entry.parse({ + last_published_with_noindex: true + }); + + expect(entry.get('last_published_with_noindex')).toEqual(true); + }) }); describe('file collection count attribute', () => { diff --git a/package/spec/editor/views/PublishEntryView-spec.js b/package/spec/editor/views/PublishEntryView-spec.js index e2322059e0..beebfcfcf3 100644 --- a/package/spec/editor/views/PublishEntryView-spec.js +++ b/package/spec/editor/views/PublishEntryView-spec.js @@ -1,14 +1,24 @@ import {PublishEntryView} from 'pageflow/editor'; import Backbone from 'backbone'; +import $ from 'jquery'; import {useFakeTranslations} from 'pageflow/testHelpers'; import {within} from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/extend-expect'; describe('PublishEntryView', () => { useFakeTranslations({ - 'pageflow.editor.templates.publish_entry.date': 'Published until date' + 'pageflow.editor.templates.publish_entry.unlimited': 'Unlimited', + 'pageflow.editor.templates.publish_entry.date': 'Published until date', + 'pageflow.editor.templates.publish_entry.noindex': 'Set noindex', + 'pageflow.editor.templates.publish_entry.noindex_help': '', + 'pageflow.editor.templates.publish_entry.publish': 'Publish' + }); + + afterEach(() => { + jest.useRealTimers(); }); it('sets published until date based on passed duration if not set yet', () => { @@ -68,4 +78,68 @@ describe('PublishEntryView', () => { expect(getByLabelText('Published until date')).toHaveValue('31.05.2022'); }); + + it('checks noindex if last published with noindex', async () => { + const entryPublication = { + publish: jest.fn() + }; + entryPublication.publish.mockReturnValue(new $.Deferred().promise()); + const view = new PublishEntryView({ + model: new Backbone.Model({ + last_published_with_noindex: true + }), + entryPublication, + account: new Backbone.Model(), + config: {} + }); + + const {getByLabelText} = within(view.render().el); + + expect(getByLabelText('Set noindex')).toBeChecked(); + }); + + it('does not pass noindex flag by default', async () => { + const entryPublication = { + publish: jest.fn() + }; + entryPublication.publish.mockReturnValue(new $.Deferred().promise()); + const view = new PublishEntryView({ + model: new Backbone.Model(), + entryPublication, + account: new Backbone.Model(), + config: {} + }); + + const user = userEvent.setup(); + const {getByRole, getByLabelText} = within(view.render().el); + await user.click(getByLabelText('Unlimited')); + await user.click(getByRole('button', {name: 'Publish'})); + + expect(entryPublication.publish).toHaveBeenCalledWith(expect.objectContaining({ + noindex: false + })); + }); + + it('passes noindex flag when check box checked', async () => { + const entryPublication = { + publish: jest.fn() + }; + entryPublication.publish.mockReturnValue(new $.Deferred().promise()); + const view = new PublishEntryView({ + model: new Backbone.Model(), + entryPublication, + account: new Backbone.Model(), + config: {} + }); + + const user = userEvent.setup(); + const {getByRole, getByLabelText} = within(view.render().el); + await user.click(getByLabelText('Unlimited')); + await user.click(getByLabelText('Set noindex')); + await user.click(getByRole('button', {name: 'Publish'})); + + expect(entryPublication.publish).toHaveBeenCalledWith(expect.objectContaining({ + noindex: true + })); + }); }); diff --git a/package/src/editor/models/Entry.js b/package/src/editor/models/Entry.js index 3fac44b946..433cdba550 100644 --- a/package/src/editor/models/Entry.js +++ b/package/src/editor/models/Entry.js @@ -143,7 +143,9 @@ export const Entry = Backbone.Model.extend({ parse: function(response, options) { if (response) { - this.set(_.pick(response, 'published', 'published_until', 'password_protected')); + this.set(_.pick(response, + 'published', 'published_until', + 'password_protected', 'last_published_with_noindex')); this._setFiles(response, { add: false, remove: false, diff --git a/package/src/editor/templates/publishEntry.jst b/package/src/editor/templates/publishEntry.jst index 3f1df04c22..4a3d554655 100644 --- a/package/src/editor/templates/publishEntry.jst +++ b/package/src/editor/templates/publishEntry.jst @@ -39,6 +39,18 @@ +