@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/?post_type=lh-portfolio-doc&#038;p=152901>
  a sioc:Post ;
  dc:title "Architecture" ;
  dcterms:identifier 152901 ;
  dc:modified "2026-10-31T15:39:34Z"^^xsd:dateTime ;
  dc:created "2026-10-31T12:14:21Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-shadow-taxonomies/architecture/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-shadow-taxonomies/">LH Shadow Taxonomies</a></li></ul><h3 class="wp-block-heading">Declaring a shadow taxonomy</h3><p class="wp-block-paragraph">A taxonomy becomes a shadow taxonomy when its registration arguments include <code>shadow</code>, a string or array naming what it shadows: <code>user</code>, <code>bp_group</code> or a post type name. <code>register_taxonomy()</code> keeps unknown arguments on the taxonomy object, which is where the plugin reads it.</p><pre class="wp-block-code"><code>register_taxonomy( 'group_tags', array( 'post', 'attachment' ), array(
    'label'  =&gt; 'Group Tags',
    'shadow' =&gt; 'bp_group',
) );</code></pre><p class="wp-block-paragraph">The map of shadowed type to taxonomies is built from <code>get_taxonomies()</code> and cached in a static variable, but only once <code>init</code> has run, so taxonomies registered late are not missed. Shadowable types are limited by <code>lh_shadow_taxonomies_return_object_whitelist</code> (default <code>user</code>, <code>bp_group</code>, <code>post</code>); in the lookup helpers an unknown type falls back to <code>post</code>. In practice each shadow taxonomy should shadow one type: the admin column handler is chosen by the first match of <code>bp_group</code>, then <code>user</code>, then a post type.</p><h3 class="wp-block-heading">Linking a term to its object</h3><p class="wp-block-paragraph">Each shadow term carries term meta <code>_lh_shadow_taxonomies-{type}-object_id</code> holding the ID of the object it stands for, e.g. <code>_lh_shadow_taxonomies-bp_group-object_id</code>. The public static helpers <code>LH_Shadow_taxonomies_plugin::get_shadowed_term( $object_id, $taxonomy, $type )</code> and <code>get_shadowed_object_id( $term_id, $type )</code> look the link up in either direction, returning <code>false</code> when there is none. Groups also get group meta <code>_lh_shadow_taxonomies-is_shadowed</code>, the time their terms were last synced successfully.</p><h3 class="wp-block-heading">Keeping terms in step</h3><figure class="wp-block-table"><table><thead><tr><th>Type</th><th>Created or updated on</th><th>Deleted on</th><th>Term name / slug / description</th></tr></thead><tbody><tr><td>Post type</td><td><code>wp_insert_post</code> (active statuses only, skipping revisions and autosaves), <code>untrashed_post</code></td><td><code>after_delete_post</code></td><td>title / post_name / excerpt</td></tr><tr><td>User</td><td><code>user_register</code>, <code>profile_update</code></td><td><code>delete_user</code> (before deletion, deliberately)</td><td>display_name / user_nicename / description</td></tr><tr><td>BuddyPress group</td><td><code>groups_create_group</code>, <code>groups_details_updated</code></td><td><code>groups_delete_group</code></td><td>name / slug / description</td></tr></tbody></table></figure><p class="wp-block-paragraph">Only posts in an active status are shadowed (default <code>publish</code>, filterable). Trashing a post leaves its term in place; only permanent deletion removes it. Term insert and update failures are written to the debug log when debugging is enabled for the plugin.</p><h3 class="wp-block-heading">Cron: backfill and reconciliation</h3><p class="wp-block-paragraph">Activation schedules a daily <code>lh_shadow_taxonomies_process</code> event and a one-off <code>lh_shadow_taxonomies_initial</code> event; deactivation clears both. Both run the same two passes, sharing a 20-second time limit:</p><ul class="wp-block-list"><li><strong>Backfill:</strong> for each shadow taxonomy, creates terms for one batch (default 50) of objects that have none yet. A large site therefore takes several daily runs to backfill fully.<ul class="wp-block-list"><li>Users and posts are found with a single prepared query per taxonomy: an <code>ID NOT IN ( subquery )</code> anti-join against termmeta, so already-shadowed IDs never come back to PHP and the query stays the same size however many objects are shadowed. Users are limited to members of the current site on multisite; posts to the active statuses.</li><li>BuddyPress groups go through <code>groups_get_groups()</code> instead, so plugins that limit groups to a site (such as LH BuddyPress Site Only Groups) still apply. The already-shadowed group IDs are passed as its <code>exclude</code> list, which is bounded by the site&#8217;s group count. The exclusion is not added to BuddyPress&#8217;s SQL, because BuddyPress caches that query under a key only group changes invalidate.</li><li>IDs returned by the blacklist filter are excluded in every case.</li></ul></li><li><strong>Reconciliation:</strong> re-fires <code>lh_shadow_taxonomies_term_assigned</code> for a batch of 50 existing term assignments per taxonomy, with <code>tagged_by</code> 0 and <code>tagged_at</code> empty, so listeners can pick up assignments made by bulk edits, imports or direct writes. The batch is always the lowest object IDs, and listeners must be idempotent.</li></ul><h3 class="wp-block-heading">Admin and term ordering</h3><p class="wp-block-paragraph">On <code>admin_init</code> every shadow taxonomy&#8217;s term list gets a Shadow column linking to the user&#8217;s profile edit screen, the group page or the post. The plugin also returns <code>false</code> on <code>wp_term_order_taxonomy_supported</code> for shadow taxonomies, so WP Term Order does not try to manage their order.</p><h3 class="wp-block-heading">Structure and storage</h3><p class="wp-block-paragraph">A single file, <code>lh-shadow-taxonomies.php</code>, holding the singleton <code>LH_Shadow_taxonomies_plugin</code> (namespace <code>lh_shadow_taxonomies</code>, text domain <code>lh-shadow-taxonomies</code>). Hooks are registered in <code>plugin_init()</code> on <code>plugins_loaded</code>, behind the <code>lh_shadow_taxonomies_init_plugin</code> filter. No options, custom tables, settings screens or abilities: all state is in term meta and BuddyPress group meta.</p>"""^^rdf:XMLLiteral ;
  sioc:content """Part of: LH Shadow TaxonomiesDeclaring a shadow taxonomyA taxonomy becomes a shadow taxonomy when its registration arguments include shadow, a string or array naming what it shadows: user, bp_group or a post type name. register_taxonomy() keeps unknown arguments on the taxonomy object, which is where the plugin reads it.register_taxonomy( 'group_tags', array( 'post', 'attachment' ), array(
    'label'  =&gt; 'Group Tags',
    'shadow' =&gt; 'bp_group',
) );The map of shadowed type to taxonomies is built from get_taxonomies() and cached in a static variable, but only once init has run, so taxonomies registered late are not missed. Shadowable types are limited by lh_shadow_taxonomies_return_object_whitelist (default user, bp_group, post); in the lookup helpers an unknown type falls back to post. In practice each shadow taxonomy should shadow one type: the admin column handler is chosen by the first match of bp_group, then user, then a post type.Linking a term to its objectEach shadow term carries term meta _lh_shadow_taxonomies-{type}-object_id holding the ID of the object it stands for, e.g. _lh_shadow_taxonomies-bp_group-object_id. The public static helpers LH_Shadow_taxonomies_plugin::get_shadowed_term( $object_id, $taxonomy, $type ) and get_shadowed_object_id( $term_id, $type ) look the link up in either direction, returning false when there is none. Groups also get group meta _lh_shadow_taxonomies-is_shadowed, the time their terms were last synced successfully.Keeping terms in stepTypeCreated or updated onDeleted onTerm name / slug / descriptionPost typewp_insert_post (active statuses only, skipping revisions and autosaves), untrashed_postafter_delete_posttitle / post_name / excerptUseruser_register, profile_updatedelete_user (before deletion, deliberately)display_name / user_nicename / descriptionBuddyPress groupgroups_create_group, groups_details_updatedgroups_delete_groupname / slug / descriptionOnly posts in an active status are shadowed (default publish, filterable). Trashing a post leaves its term in place; only permanent deletion removes it. Term insert and update failures are written to the debug log when debugging is enabled for the plugin.Cron: backfill and reconciliationActivation schedules a daily lh_shadow_taxonomies_process event and a one-off lh_shadow_taxonomies_initial event; deactivation clears both. Both run the same two passes, sharing a 20-second time limit:Backfill: for each shadow taxonomy, creates terms for one batch (default 50) of objects that have none yet. A large site therefore takes several daily runs to backfill fully.Users and posts are found with a single prepared query per taxonomy: an ID NOT IN ( subquery ) anti-join against termmeta, so already-shadowed IDs never come back to PHP and the query stays the same size however many objects are shadowed. Users are limited to members of the current site on multisite; posts to the active statuses.BuddyPress groups go through groups_get_groups() instead, so plugins that limit groups to a site (such as LH BuddyPress Site Only Groups) still apply. The already-shadowed group IDs are passed as its exclude list, which is bounded by the site&#8217;s group count. The exclusion is not added to BuddyPress&#8217;s SQL, because BuddyPress caches that query under a key only group changes invalidate.IDs returned by the blacklist filter are excluded in every case.Reconciliation: re-fires lh_shadow_taxonomies_term_assigned for a batch of 50 existing term assignments per taxonomy, with tagged_by 0 and tagged_at empty, so listeners can pick up assignments made by bulk edits, imports or direct writes. The batch is always the lowest object IDs, and listeners must be idempotent.Admin and term orderingOn admin_init every shadow taxonomy&#8217;s term list gets a Shadow column linking to the user&#8217;s profile edit screen, the group page or the post. The plugin also returns false on wp_term_order_taxonomy_supported for shadow taxonomies, so WP Term Order does not try to manage their order.Structure and storageA single file, lh-shadow-taxonomies.php, holding the singleton LH_Shadow_taxonomies_plugin (namespace lh_shadow_taxonomies, text domain lh-shadow-taxonomies). Hooks are registered in plugin_init() on plugins_loaded, behind the lh_shadow_taxonomies_init_plugin filter. No options, custom tables, settings screens or abilities: all state is in term meta and BuddyPress group meta.""" ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
