Skip to content
Merged
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
28 changes: 28 additions & 0 deletions config/default/block.block.drevops_related_posts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
uuid: 0672b3ef-3a1c-433f-b72c-7662a0727809
langcode: en
status: true
dependencies:
content:
- 'block_content:civictheme_component_block:c1c2b47c-4c96-4f9e-9b6e-2b1f5f3d7e10'
module:
- block_content
- system
theme:
- drevops
id: drevops_related_posts
theme: drevops
region: content_bottom
weight: -1
provider: null
plugin: 'block_content:c1c2b47c-4c96-4f9e-9b6e-2b1f5f3d7e10'
settings:
id: 'block_content:c1c2b47c-4c96-4f9e-9b6e-2b1f5f3d7e10'
label: 'Related posts'
label_display: '0'
provider: block_content
view_mode: full
visibility:
request_path:
id: request_path
negate: false
pages: "/blog/*\r\n/services/*"
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ third_party_settings:
group_metadata:
children:
- field_c_p_list_topics
- field_c_p_list_topics_from_page
- field_c_p_list_site_sections
label: Metadata
region: content
Expand Down Expand Up @@ -313,7 +314,7 @@ content:
third_party_settings: { }
field_c_p_list_site_sections:
type: entity_reference_autocomplete
weight: 8
weight: 9
region: content
settings:
match_operator: CONTAINS
Expand All @@ -333,7 +334,7 @@ content:
third_party_settings: { }
field_c_p_list_topics_from_page:
type: boolean_checkbox
weight: 6
weight: 8
region: content
settings:
display_label: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ third_party_settings:
label: 'CivicTheme Three Columns'
context_mapping: { }
is_contained: false
vertical_spacing: auto
components:
5cf72ad7-c313-4cf3-9d4e-0573bbfa84a0:
uuid: 5cf72ad7-c313-4cf3-9d4e-0573bbfa84a0
Expand Down
2 changes: 1 addition & 1 deletion config/default/views.view.civictheme_automated_list.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ display:
type: full
options:
offset: 0
pagination_heading_level: h4
items_per_page: 12
total_pages: null
id: 0
Expand All @@ -95,7 +96,6 @@ display:
offset: false
offset_label: Offset
quantity: 5
pagination_heading_level: h4
exposed_form:
type: basic
options:
Expand Down
22 changes: 18 additions & 4 deletions docs/related-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@ Excluding the site navigation, most pages on this site had exactly one inbound i

## An Automated list can follow the page's own topics

The Automated list paragraph carries a **Use topics of the current page** checkbox (`field_c_p_list_topics_from_page`). With it on, the list ignores the Topics chosen on the paragraph and matches the topics of the node it is placed on instead, which turns the same component into a related-content list. Nothing new was built to do this: it is the Automated list, configured differently.
The Automated list paragraph carries a **Use topics of the current page** checkbox (`field_c_p_list_topics_from_page`), under *Content* → *Filters*. With it on, the list ignores the Topics chosen on the paragraph and matches the topics of the node being viewed instead, which turns the same component into a related-content list. Nothing new was built to do this: it is the Automated list, configured differently.

`drevops_civictheme_automated_list_view_alter()` in `web/themes/custom/drevops/includes/automated_list.inc` swaps the Topics contextual argument just before the view runs. CivicTheme fires that alter through the theme manager as well as the module handler, which is why this lives in the theme alongside the rest of the component's theming rather than in a module.

Points worth knowing before changing it:

- **The page being viewed is already excluded.** The view's fourth contextual filter is `nid` with `not` set, and it takes its value from the route, so a related list never lists the post it sits on. `_civictheme_automated_list__update_view()` passes only three arguments, which is what leaves that one to its route default.
- **The topics argument is position 1.** The arguments are ordered content type, topics, site sections. Reordering them in the view would silently point the swap at the wrong filter.
- **A post with no topics gets `none`, not `all`.** Without that, an empty topic set would fall through to the view's `all` default and advertise the whole site as related.
- **An empty result hides the heading.** `drevops_preprocess_paragraph__civictheme_automated_list()` clears the title when the list found nothing, so a post whose topics nothing else shares does not render a heading introducing empty space.
- **A page with no topics gets `none`, not `all`.** Without that, an empty topic set would fall through to the view's `all` default and advertise the whole site as related.
- **An empty result collapses the component.** `drevops_preprocess_paragraph__civictheme_automated_list()` clears the title, the rows and the vertical spacing when the list found nothing, so a page whose topics nothing else shares renders no heading and takes no height.

`do_base_deploy_add_related_lists()` puts one of these lists on every existing blog post. It is skipped for a post that already has one, so it is safe to re-run.
## One block, placed by path

**Related posts** is a Component block holding a single Automated list with that option on. `block.block.drevops_related_posts` puts it in the `content_bottom` region of the `drevops` theme, above the Signup block, and one **Pages** visibility condition decides where it appears:

```
/blog/*
/services/*
```

Points worth knowing before changing it:

- **It has to be one condition.** Visibility conditions are ANDed, so a *Content type* condition for blog posts alongside a *Pages* condition for the service pages would match nothing. Both audiences are expressed as paths in the single condition.
- **The condition matches the alias.** `RequestPath` resolves the current path to its alias before comparing, so `/node/12` is judged as `/blog/…` and reaches the same verdict as the aliased URL.
- **`/services` is not `/services/*`.** The services landing page carries no topics; leaving it outside the pattern keeps the block off a page that has nothing to put under it. Every service detail page beneath it does carry topics.
- **The block itself is content.** `do_base_deploy_add_related_posts_block()` creates it against the fixed UUID that `block.block.drevops_related_posts` names, and skips when that UUID is already present. Config import runs before deploy hooks, so on a site built from scratch the placement lands one step ahead of the block it points at and starts rendering once the hook has run.

## Topic pages

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@p0 @civictheme @civictheme_automated_list
Feature: Automated list following the page's own topics
@p0 @drevops @related_posts
Feature: Related posts block

As a site visitor
I want a post to link to others on the same topic
I want a page to link to posts on the same topic
So that I can keep reading without going back to a paginated listing

Background:
Expand All @@ -18,24 +18,16 @@ Feature: Automated list following the page's own topics
| [TEST] Post Other Topic | published | [relative:-3 days] | [TEST] Other Topic |
| [TEST] Post Without Topic | published | [relative:-4 days] | |

And the following fields for the paragraph "civictheme_automated_list" exist in the field "field_c_n_components" within the "blog" "node" identified by the field "title" and the value "[TEST] Post Being Read":
| field_c_p_title | [TEST] Related posts |
| field_c_p_list_type | civictheme_automated_list__block1 |
| field_c_p_list_content_type | blog |
| field_c_p_list_limit_type | limited |
| field_c_p_list_limit | 3 |
| field_c_p_list_topics_from_page | 1 |

@api
Scenario: The list shows posts sharing the page's topic
Scenario: The block lists posts sharing the topics of the post it is on
Given I am an anonymous user

When I visit the "blog" content page with the title "[TEST] Post Being Read"
Then I should see the text "[TEST] Related posts"
Then I should see the text "Related posts"
And I should see "[TEST] Post Same Topic"

@api
Scenario: The list leaves out the page it is on and posts on other topics
Scenario: The block leaves out the page it is on and posts on other topics
Given I am an anonymous user

When I visit the "blog" content page with the title "[TEST] Post Being Read"
Expand All @@ -44,19 +36,37 @@ Feature: Automated list following the page's own topics
And I should see 1 ".ct-promo-card" elements

@api
Scenario: A post whose topics nothing shares shows no heading
Given the following fields for the paragraph "civictheme_automated_list" exist in the field "field_c_n_components" within the "blog" "node" identified by the field "title" and the value "[TEST] Post Without Topic":
| field_c_p_title | [TEST] Related posts |
| field_c_p_list_type | civictheme_automated_list__block1 |
| field_c_p_list_content_type | blog |
| field_c_p_list_limit_type | limited |
| field_c_p_list_limit | 3 |
| field_c_p_list_topics_from_page | 1 |
Scenario: A page whose topics nothing shares shows no heading
Given I am an anonymous user

When I visit the "blog" content page with the title "[TEST] Post Without Topic"
Then I should not see the text "Related posts"

@api
Scenario: The block reaches pages under the services path
Given the following "civictheme_page" content:
| title | status | field_c_n_topics |
| [TEST] Service Page | 1 | [TEST] Shared Topic |

And the "civictheme_page" content "[TEST] Service Page" has the path alias "/services/test-service-page"
And I am an anonymous user

When I visit the "blog" content page with the title "[TEST] Post Without Topic"
Then I should not see the text "[TEST] Related posts"
When I go to "/services/test-service-page"
Then I should see the text "Related posts"
And I should see "[TEST] Post Being Read"
And I should see "[TEST] Post Same Topic"

@api
Scenario: The block stays off pages outside its visibility list
Given the following "civictheme_page" content:
| title | status | field_c_n_topics |
| [TEST] Plain Page | 1 | [TEST] Shared Topic |

And the "civictheme_page" content "[TEST] Plain Page" has the path alias "/test-plain-page"
And I am an anonymous user

When I go to "/test-plain-page"
Then I should not see the text "Related posts"

@api
Scenario: A post links to the topic pages it belongs to
Expand Down
106 changes: 78 additions & 28 deletions web/modules/custom/do_base/do_base.deploy.php
Original file line number Diff line number Diff line change
Expand Up @@ -1280,30 +1280,17 @@ function _do_base_alias_topic_term(TermInterface $term): void {
}

/**
* Adds a related-content list to the foot of every blog post.
* Builds the Related posts block that the block layout places in a region.
*/
function do_base_deploy_add_related_lists(?array &$sandbox = NULL): ?string {
$query = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', 'blog');
function do_base_deploy_add_related_posts_block(): string {
$uuid = 'c1c2b47c-4c96-4f9e-9b6e-2b1f5f3d7e10';

return Helper::entity($sandbox, 10)->batchQuery($query, static function (NodeInterface $node): void {
_do_base_add_related_list($node);
}, status: Reporter::UPDATED);
}
$existing = \Drupal::service('entity.repository')->loadEntityByUuid('block_content', $uuid);

/**
* Appends an Automated list configured to follow the post's own topics.
*/
function _do_base_add_related_list(NodeInterface $node): void {
if (!$node->hasField('field_c_n_components')) {
return;
}
if ($existing !== NULL) {
Helper::reporter()->skipped('The Related posts block already exists.');

foreach ($node->get('field_c_n_components')->referencedEntities() as $existing) {
if ($existing instanceof ParagraphInterface && $existing->bundle() === 'civictheme_automated_list' && !$existing->get('field_c_p_list_topics_from_page')->isEmpty() && (bool) $existing->get('field_c_p_list_topics_from_page')->value) {
return;
}
return Helper::report();
}

$paragraph = Paragraph::create([
Expand All @@ -1316,16 +1303,79 @@ function _do_base_add_related_list(NodeInterface $node): void {
'field_c_p_list_column_count' => 3,
'field_c_p_list_topics_from_page' => 1,
]);
$paragraph->setParentEntity($node, 'field_c_n_components');
$paragraph->save();

$components = $node->get('field_c_n_components')->getValue();
$components[] = [
'target_id' => $paragraph->id(),
'target_revision_id' => $paragraph->getRevisionId(),
];
$block_content = \Drupal::entityTypeManager()->getStorage('block_content')->create([
'uuid' => $uuid,
'type' => 'civictheme_component_block',
'info' => 'Related posts',
'moderation_state' => 'published',
]);
$block_content->get('field_c_b_components')->appendItem($paragraph);
$block_content->save();

Helper::reporter()->created('Created the Related posts block.');

return Helper::report();
}

/**
* Strips the related-content list from the foot of every blog post.
*
* @param array|null $sandbox
* Batch sandbox, matching the nullable reference the batch helper takes.
*
* @return string|null
* Summary once every post is stripped, or NULL while batching.
*/
function do_base_deploy_remove_related_lists(?array &$sandbox = NULL): ?string {
// The query doubles as the idempotency guard: a post loses its list on the
// first pass and no longer matches on the next.
$query = \Drupal::entityQuery('node')
->condition('type', 'blog')
->condition('field_c_n_components.entity:paragraph.field_c_p_list_topics_from_page', 1);

return Helper::entity($sandbox, 10)->batchQuery($query, static function (NodeInterface $node): void {
_do_base_remove_related_list($node);
}, status: Reporter::UPDATED);
}

/**
* Drops the page-topics Automated lists a post carries.
*/
function _do_base_remove_related_list(NodeInterface $node): void {
$dropped = [];

foreach ($node->get('field_c_n_components')->referencedEntities() as $paragraph) {
if ($paragraph instanceof ParagraphInterface && _do_base_is_related_list($paragraph)) {
$dropped[(int) $paragraph->id()] = $paragraph;
}
}

$node->set('field_c_n_components', $components);
if ($dropped === []) {
return;
}

$items = $node->get('field_c_n_components')->getValue();
$kept = array_filter($items, static fn (array $item): bool => !isset($dropped[(int) $item['target_id']]));

$node->set('field_c_n_components', array_values($kept));
$node->setNewRevision(FALSE);
$node->save();

// Dropping the reference does not delete the paragraph, and the orphan left
// behind still holds a revision row per node revision it was ever part of.
foreach ($dropped as $paragraph) {
$paragraph->delete();
}
}

/**
* Tells whether a paragraph is an Automated list following the page's topics.
*/
function _do_base_is_related_list(ParagraphInterface $paragraph): bool {
if ($paragraph->bundle() !== 'civictheme_automated_list' || !$paragraph->hasField('field_c_p_list_topics_from_page')) {
return FALSE;
}

return (bool) $paragraph->get('field_c_p_list_topics_from_page')->value;
}
6 changes: 4 additions & 2 deletions web/themes/custom/drevops/includes/automated_list.inc
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ function drevops_preprocess_paragraph__civictheme_automated_list(array &$variabl
return;
}

// A post whose topics nothing else shares would otherwise leave a heading
// introducing an empty space.
// The list is placed as a block on every page in its visibility list, so a
// page whose topics nothing else shares has to collapse to nothing: the
// heading and its spacing would otherwise render an empty section.
if (_drevops_automated_list_is_empty($variables['rows'] ?? NULL)) {
$variables['title'] = NULL;
$variables['rows'] = NULL;
$variables['vertical_spacing'] = NULL;
}
}

Expand Down
Loading