Skip to content
Open
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
32 changes: 29 additions & 3 deletions docs/site.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ $site->configure(function() {
});
```

This approach of passing a function to `conifigure()` has a couple advantages:
This approach of passing a function to `configure()` has a couple advantages:

- You don't have to define your own `Site` subclass.
- Wrapping your config code in a closure avoids polluting the global namespace, but also allows it to remain in the familiar functions.php rather than in a random library class, where newcomers to your codebase may not know to look.
Expand Down Expand Up @@ -101,7 +101,7 @@ $site->configure();

## Disabling defaults

If you want to *only* run your custom config callback without funning Conifer's default config code, you can pass `false` as the second argument to `configure()`, which tells Conifer to disable its defaults:
If you want to *only* run your custom config callback without running Conifer's default configuration code, pass `false` as the second argument to `configure()`:

```php
$site->configure(function() { /* ... */ }, false);
Expand All @@ -119,6 +119,32 @@ $site->configure(function() {

This closes all comments on the frontend, hides all existing comments on all posts on the frontend and within WP Admin, and removes comment management pages from WP Admin.

## Disabling Tags

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably mention the $this->disable_tags(); function as well?


Many sites do not need tag support on every post type. Use `disable_tags_for_post_types()` to remove the built-in tag taxonomy from the post types you specify:

```php
$site->configure(function() {
$postTypes = ['robot', 'android', 'cyborg'];

$this->disable_tags_for_post_types($postTypes);
});
```

This removes tag support and the `post_tag` taxonomy association only from the listed post types. Tags remain available on all other post types.

You can also remove tags for all post types using the included `disable_tags` function.

```php
$site->configure(function() {
//...

// Tags will be disabled for all registered post types
$this->disable_tags();

//...
});

## Directory Cascades

A **Directory Cascade** is an ordered list of directories where Conifer looks for Twig views (`*.twig` files), JS files, or stylesheets. Each type of asset (views, JS, CSS) has its own set of directories - its own *cascade* - where Conifer looks for that type of file. Each cascade has its own getter and setter on the `Site` class:
Expand Down Expand Up @@ -158,7 +184,7 @@ $site->configure(function() {

### Finding generic file paths

Under the hood, the script and style cascades call the generic `file_file()` method, which simply traverses a list of directories looking for a relative file path within each. It returns the full path of the first existing file it finds:
Under the hood, the script and style cascades call the generic `find_file()` method, which traverses a list of directories looking for a relative file path within each. It returns the full path of the first existing file it finds:

```php
// directory tree:
Expand Down
75 changes: 75 additions & 0 deletions lib/Conifer/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,13 @@ public function configure_defaults()
Button::register('button');

Integrations\YoastIntegration::demote_metabox();
// TODO moar integrations!
}

/**
* Register default Post Class Maps for default Conifer classes
*
* @todo Terms/Users
*/
public function configure_default_classmaps()
{
Expand Down Expand Up @@ -530,6 +533,7 @@ public function add_default_twig_helpers()
public function configure_default_admin_dashboard_widgets()
{
add_action('wp_dashboard_setup', function () {
// TODO widget API?
wp_add_dashboard_widget(
'conifer_guide',
__('Welcome to Conifer'),
Expand All @@ -546,6 +550,7 @@ function () {
public function remove_conifer_widget()
{
add_action('wp_dashboard_setup', function () {
// TODO widget API?
remove_meta_box('conifer_guide', 'dashboard', 'normal');
});
}
Expand Down Expand Up @@ -826,4 +831,74 @@ public function disable_comments()
add_filter('comments_open', '__return_false', 20);
add_filter('pings_open', '__return_false', 20);
}

/**
* Removes the tag taxonomy from the specified post types.
*
* @param array $postTypes Post types to remove the tag taxonomy from.
* @return void
*/
public function disable_tags_for_post_types(array $postTypes): void
{
// Attempt to disable tags for already registered taxonomies.
foreach ($postTypes as $postType) {
$this->disable_tags_for_post_type($postType);
}

// If called before init, core taxonomies may not be registered yet; retry after init.
if (did_action('init') && !doing_action('init')) {
return;
}

add_action('init', function () use ($postTypes) {
foreach ($postTypes as $postType) {
$this->disable_tags_for_post_type($postType);
}
}, PHP_INT_MAX);
}

/**
* Removes the tag taxonomy from all post types except the provided post types.
*
* @param array $excludedPostTypes Post types to exclude from tag removal.
* Defaults to ['tribe_events'].
* @return void
*/
private function disable_tags(array $excludedPostTypes = ['tribe_events']): void
{
$types = get_post_types([], 'names');

$filteredPostTypes = array_filter($types, function ($type) use ($excludedPostTypes) {
return !in_array($type, $excludedPostTypes, true);
});

// Hide Tags from the admin menu.
// ? This is intended to be a universal solution, but if we provide a public facing function to disable tag support for specific post types, we should
// ? probably reconsider globally removing the tags menu item. If downstream someone explicitly calls disable_tags_for_post_types(['post'])
// ? they may not expect the menu item to be removed for all post types.
add_action('admin_menu', function () {
Comment thread
amerk-sitecrafting marked this conversation as resolved.
remove_submenu_page('edit.php', 'edit-tags.php?taxonomy=post_tag');
});

// Taxonomies are registered during init, so remove associations after it runs.
if (did_action('init')) {
$this->disable_tags_for_post_types($filteredPostTypes);
return;
}

add_action('init', function () use ($filteredPostTypes) {
$this->disable_tags_for_post_types($filteredPostTypes);
});
}

/**
* Removes the tag taxonomy from a single post type.
*
* @param string $postType Post type to remove the tag taxonomy from.
* @return bool True if the taxonomy association was removed, false otherwise.
*/
private function disable_tags_for_post_type(string $postType): bool
{
return unregister_taxonomy_for_object_type('post_tag', $postType);
}
}
91 changes: 91 additions & 0 deletions test/integration/SiteTagRemovalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/**
* Test tag removal helpers on the Conifer\Site class.
*
* @copyright 2026 SiteCrafting, Inc.
* @package Conifer
*/

namespace Conifer\Integration;

class SiteTagRemovalTest extends Base
{
private const POST_TYPE_ONE = 'conifer_tag_test_one';
private const POST_TYPE_TWO = 'conifer_tag_test_two';
private const EXCLUDED_POST_TYPE = 'tribe_events';

/**
* Post types associated with post_tag before each test.
*
* @var string[]
*/
private array $postTypesWithTags = [];

public function setUp(): void
{
parent::setUp();

$this->postTypesWithTags = array_filter(
get_post_types([], 'names'),
function (string $postType): bool {
return is_object_in_taxonomy($postType, 'post_tag');
}
);

register_post_type(self::POST_TYPE_ONE, [
'public' => true,
'taxonomies' => ['post_tag'],
]);
register_post_type(self::POST_TYPE_TWO, [
'public' => true,
'taxonomies' => ['post_tag'],
]);
register_post_type(self::EXCLUDED_POST_TYPE, [
'public' => true,
'taxonomies' => ['post_tag'],
]);
}

public function tearDown(): void
{
foreach ([self::POST_TYPE_ONE, self::POST_TYPE_TWO, self::EXCLUDED_POST_TYPE] as $postType) {
unregister_post_type($postType);
}

foreach ($this->postTypesWithTags as $postType) {
register_taxonomy_for_object_type('post_tag', $postType);
}

parent::tearDown();
}

public function test_disable_tags_for_post_types_removes_tags_only_from_specified_post_types()
{
$this->site->disable_tags_for_post_types([self::POST_TYPE_ONE]);

$this->assertFalse(is_object_in_taxonomy(self::POST_TYPE_ONE, 'post_tag'));
$this->assertTrue(is_object_in_taxonomy(self::POST_TYPE_TWO, 'post_tag'));
}

public function test_disable_tags_excludes_tribe_events_by_default()
{
$this->site->configure(function () {
$this->disable_tags();
}, false);

$this->assertFalse(is_object_in_taxonomy(self::POST_TYPE_ONE, 'post_tag'));
$this->assertTrue(is_object_in_taxonomy(self::EXCLUDED_POST_TYPE, 'post_tag'));
}

public function test_disable_tags_honors_custom_excluded_post_types()
{
$excludedPostTypes = [self::POST_TYPE_ONE];
$this->site->configure(function () use ($excludedPostTypes) {
$this->disable_tags($excludedPostTypes);
}, false);

$this->assertTrue(is_object_in_taxonomy(self::POST_TYPE_ONE, 'post_tag'));
$this->assertFalse(is_object_in_taxonomy(self::POST_TYPE_TWO, 'post_tag'));
}
}
Loading