Betinget logikk i WordPress er ikke «litt PHP rundt malen». Det er kontrakten mellom spørringen, malhierarkiet og det du faktisk viser til brukeren. I 2026, med block themes og Full Site Editing, lever den kontrakten fortsatt i PHP for plugins, mu-plugins, enqueue-logikk og alle steder der en malfil eller et block pattern ikke strekker til.
Denne guiden er skrevet for utviklere som allerede kjenner The Loop, men som fortsatt møter rare false-positiver med is_front_page(), tomme resultater utenfor løkken, eller cache som serverer feil variant til innloggede brukere. Vi går fra taksonomisjekker til query-pitfalls, kapasitetskontroller, FSE-grenser, caching og hjelpere du kan unit-teste.
Hvor conditional tags faktisk leser fra
Conditional tags i WordPress leser hovedsakelig den globale $wp_query (eller den spørringen du sender inn). De sier ikke «hvilken fil som kjører». De svarer på spørsmål som: er denne forespørselen et enkeltinnlegg, et kategoriarkiv, forsiden, et søk?
Det har to konsekvenser:
- Kall dem for tidlig (før
wp-hooken har bygget hovedspørringen), og du får false eller uforutsigbare verdier. - Kall dem inne i en sekundær
WP_Queryuten å sende den spørringen inn, og du leser fortsatt hovedspørringen.
Dokumentasjonen for conditional tags er eksplisitt på at mange sjekker forutsetter at hovedspørringen er klar. I praksis: tema-setup i after_setup_theme er for tidlig for is_singular(). wp_enqueue_scripts, template_redirect og malfiler er trygge nok for de fleste tilfeller. For plugins som må forgrene seg tidlig, lytt på wp eller senere, eller injiser logikken via hooks der konteksten allerede finnes.
is_singular og is_front_page: vanlige fallgruver
is_singular uten post type
is_singular() uten argument er true for ethvert enkelt objekt i hovedspørringen: innlegg, side, CPT. Det er nyttig når du vil laste felles CSS for alle singles. Det er farlig når du egentlig mente «bare vanlige innlegg»:
// For bredt hvis du bare vil treffe blogginnlegg.
if ( is_singular() ) {
wp_enqueue_script( 'share-buttons' );
}
// Eksplisitt.
if ( is_singular( 'post' ) ) {
wp_enqueue_script( 'share-buttons' );
}is_single() er snevrere (ikke sider, ikke attachments i vanlig bruk), men for CPT er is_singular( 'product' ) mer lesbart enn en blanding av is_singular() og manuelle get_post_type()-sjekker.
is_front_page versus is_home
Norske redaksjonsnett bruker ofte «statisk forside + innleggsside». Da er:
is_front_page()true på den valgte forsidesiden (eller bloggindekset hvis «dine siste innlegg» er satt som forside).is_home()true på innleggsindeksen, som kan være en egen side, ikke forsiden.
En klassisk feil er å putte hero-logikk bak is_home() og tro at den treffer forsiden. En annen er å bruke is_front_page() || is_home() uten å vite hvilken av de to som skal ha hvilken layout. Skill dem i kode og i kommentar:
if ( is_front_page() && ! is_home() ) {
// Statisk forside.
}
if ( is_home() && ! is_front_page() ) {
// Innleggsside som ikke er forsiden.
}Query flags etter custom queries
Hvis du kjører query_posts() (unngå det) eller glemmer wp_reset_postdata() etter en sekundær loop, kan conditional tags fortsatt se riktig hovedspørring, mens the_post()-baserte hjelpere ser feil post. Symptom: is_singular( 'post' ) er true, men in_category( 'nyheter' ) treffer feil innlegg. Roten er nesten alltid global postkontekst, ikke selve conditional taggen.
in_the_loop: når postkontekst finnes
in_the_loop() forteller om du er inne i hovedløkken for den aktuelle spørringen. Mange funksjoner som the_title(), the_content() og implisitte varianter av in_category() / has_term() forventer at gjeldende post er satt via the_post().
Utenfor løkken:
// Usikkert: avhenger av hvilken post som tilfeldig ligger i global $post.
if ( in_category( 'nyheter' ) ) {
// ...
}
// Eksplisitt post-ID.
if ( has_term( 'nyheter', 'category', $post_id ) ) {
// ...
}I widgets, shortcodes, REST-callbacks og block render-callbacks er du ofte utenfor The Loop. Da skal alle taksonomi- og meta-sjekker ta et eksplisitt postobjekt eller ID. En praktisk regel på WPPoland-prosjekter: hvis funksjonen kan kalles fra mer enn én kontekst, er tredje argument på has_term() obligatorisk.
function wppoland_post_has_news_category( int $post_id ): bool {
return has_term( 'nyheter', 'category', $post_id );
}current_user_can og innloggingsgrener
Kapasitetssjekker er betinget logikk på brukeren, ikke på spørringen. Typisk mønster:
if ( current_user_can( 'edit_post', $post_id ) ) {
// Vis «rediger»-lenke, intern merknad, A/B-flagg for redaksjon.
}Tre punkter som ofte glemmes:
is_user_logged_in()er billig, men for grov. En abonnent og en redaktør er begge «innlogget».current_user_can( 'manage_options' )er ikke det samme som «er administrator på dette nettstedet» i multisite.- Aldri cache HTML som inneholder kapasitetsgrener som om den var anonym. Se caching-seksjonen under.
For plugins: hold capability-strenger i konstanter eller én policy-klasse, slik at du ikke sprer magiske strenger i malene. Det gjør også tester enklere fordi du kan mocke policyen uten å bootstrappe hele brukersystemet i hvert scenario.
Malhierarki versus PHP-conditionals
Template hierarchy er WordPress sin declarative routing: single-product.php, taxonomy-product_cat.php, front-page.php, home.php, og i block themes tilsvarende HTML-maler under templates/. Når hierarkiet allerede skiller kontekst, trenger du ikke å gjenskape den med if ( is_singular( 'product' ) ) inne i en generisk single.php.
Bruk hierarkiet når:
- hele layouten er annerledes (WooCommerce-produkt vs blogginnlegg),
- du vil at en juniorutvikler skal finne filen uten å lese en 200-linjers
if/else, - du jobber i et klassisk tema der filnavn fortsatt er kontrakten.
Bruk PHP-conditionals når:
- bare én seksjon endres (disclaimer, schema, enqueue),
- konteksten krysser flere maltyper (felles script på singular CPT A og B),
- logikken lever i et plugin som ikke eier malfilene.
En sunn fordeling på større codebases: hierarki for makrostruktur, små helpers for mikrobeslutninger. Hvis single.php inneholder mer enn to nivåer med nøstede conditionals for post type og taksonomi, er det et signal om å splitte mal eller flytte beslutningen til en navngitt helper.
Block themes og FSE: hva PHP fortsatt eier
I et block theme erstattes mange PHP-maler av HTML-maler og template parts. Conditional tags forsvinner ikke. De flytter seg:
- til
functions.php/ et theme-plugin forwp_enqueue_scripts, - til block render-callbacks (
render_callback) der dynamiske blokker bygges, - til hooks som
body_class,render_block,pre_render_block.
Begrensninger du må designe rundt:
- En ren HTML-mal kan ikke kalle
has_term()direkte. Du trenger en dynamisk blokk, et pattern med PHP via plugin, eller enqueue/body_class-logikk utenfor malen. theme.jsonog style variations uttrykker design-tokens, ikke forretningsregler som «vis mva-tekst bare påproduct_cat: mat».- Site Editor lar redaktører bytte template parts visuelt. Hardkodede PHP-grener i et gammelt child theme kan da «forsvinne» fra synlig UI mens de fortsatt kjører i hooks. Dokumenter hvor beslutningen lever.
Praktisk mønster for FSE-prosjekter:
add_filter(
'render_block',
static function ( string $block_content, array $block ): string {
if ( ( $block['blockName'] ?? '' ) !== 'core/group' ) {
return $block_content;
}
if ( empty( $block['attrs']['className'] ) || ! str_contains( $block['attrs']['className'], 'is-news-only' ) ) {
return $block_content;
}
if ( ! is_singular( 'post' ) || ! has_term( 'nyheter', 'category', get_queried_object_id() ) ) {
return '';
}
return $block_content;
},
10,
2
);Klassen is-news-only blir kontrakten mellom editor og kode. Redaktøren ser gruppen; PHP bestemmer synlighet uten å duplisere hele malen.
Taksonomier: in_category versus has_term
Tilbake til kjernen i tittelen. in_category() er en tynn wrapper rundt native category. Den forstår ikke product_cat, post_tag (bruk has_tag() / has_term()), eller egne taksonomier.
if ( in_category( 'nyheter' ) ) {
// Bare category.
}
if ( has_term( 'jeans', 'product_cat' ) ) {
// Fungerer for WooCommerce og andre CPT-taksonomier.
}Foretrekk has_term() i ny kode, også for vanlige kategorier. Da er API-et likt på tvers av innholdstyper, og du kan bytte taksonominavn uten å bytte funksjon. Bruk term-ID når du har den: det unngår slug-oppslag og er mer stabilt ved rename av slug.
Hierarki: barn er ikke forelder
has_term( 'frukt', 'category', $post_id ) er false hvis innlegget bare er merket eple, selv om eple er barn av frukt. Kjernen gjør eksakt match. For «innlegg i denne grenen av treet» trenger du en hjelper:
function wppoland_post_in_descendant_category( int $post_id, int $parent_term_id ): bool {
$descendants = get_term_children( $parent_term_id, 'category' );
if ( is_wp_error( $descendants ) ) {
return false;
}
$term_ids = array_map( 'intval', $descendants );
$term_ids[] = $parent_term_id;
return has_term( $term_ids, 'category', $post_id );
}get_term_children() er memoized via object cache i vanlige oppsett, men på store trær bør du ikke kalle den inne i en loop over tusenvis av poster uten å cache resultatet per parent-ID i requesten.
Sidekontekst versus postdata
Skill disse to:
| Behov | Funksjon |
|---|---|
| Er vi på kategoriarkivet «nyheter»? | is_category( 'nyheter' ) |
| Er det aktuelle innlegget i «nyheter»? | in_category() / has_term() |
| Er vi på et egendefinert taksonomiarkiv? | is_tax( 'product_cat', 'jeans' ) |
| Er produktet merket «jeans»? | has_term( 'jeans', 'product_cat', $post_id ) |
WooCommerce-fellen: is_product_category() inne i en generisk Loop over produkter på forsiden er false, fordi du ikke er på kategoriarkivet. Bruk has_term() med produkt-ID.
Caching: page cache versus innloggede brukere
Page cache (Nginx FastCGI cache, Cloudflare Cache Everything, plugin full-page cache) lagrer HTML for en URL. Conditional tags som leser spørringen er vanligvis trygge: samme URL, samme is_singular(), samme arkiv. Conditional tags som leser brukeren er det ikke.
Regler som holder i produksjon:
- Anonym HTML skal ikke inneholde grener styrt av
is_user_logged_in()ellercurrent_user_can()med mindre du har separate cache-varianter (Cookie,Cache-Control: private, bypass forwordpress_logged_in_*). - Object cache (Redis/Memcached) er fin for
has_term()og term-trær. Den erstatter ikke page-cache-disiplin. - Fragment cache / ESI / client-side hydrate er alternativene når du trenger «rediger»-lenker på ellers cachet side.
DONOTCACHEPAGE-konstanter og plugin-hooks finnes, men bruk dem smalt. En hel side satt til uncacheable fordi én widget sjekker kapasitet er en dyr løsning.
Testmatrise før lansering: anonym, abonnent, redaktør, på forside, single, kategoriarkiv. Hvis anonym og redaktør får samme HTML på en URL som skal vise ekstra UI, har du en cache-bug, ikke en conditional-bug.
Unit-testbare hjelpere
Global state ($wp_query, $post, current user) gjør direkte kall til is_singular() vanskelig å teste. Pakk beslutningene:
final class Wppoland_Request_Context {
public function __construct(
private WP_Query $query,
) {}
public function is_news_singular(): bool {
if ( ! $this->query->is_singular( 'post' ) ) {
return false;
}
$post_id = (int) $this->query->get_queried_object_id();
return has_term( 'nyheter', 'category', $post_id );
}
}I WP_UnitTestCase (eller en integrasjonstest med Brain Monkey for isolasjon) bygger du en WP_Query, factory-poster med termer, og assert-er på hjelperen. Unngå å assert-e på HTML fra hele temaet når du egentlig tester én boolsk beslutning.
Retningslinjer:
- Ta
WP_Post|intogWP_Querysom argumenter der det er mulig. - Returner bool eller små value objects, ikke echo.
- Hold side effects (enqueue, header, redirect) i et tynt adapter-lag som kaller hjelperen.
- Én helper, én grunn.
wppoland_should_show_vat_notice( $post_id )er lettere å navngi i en feilrapport enn et 40-linjers if-tre ifunctions.php.
Ytelse i praksis
Én has_term() mot object cache er billig. Det som blir dyrt:
- rekursive
get_term_children()per rad i en stor admin-tabell, - N+1 der du resolver slug til term i en loop i stedet for å bruke ID,
- conditional enqueue som treffer
filemtimepå disk for filer du ikke trenger.
Mål før du optimaliserer: Query Monitor på en single, et arkiv og forsiden. Hvis conditional-grenen ikke synes i slow queries, er lesbarhet viktigere enn mikrooptimalisering.
Sjekkliste før du merger
- Er beslutningen makro (malhierarki / FSE-template) eller mikro (helper / hook)?
- Kjører sjekken etter at hovedspørringen er klar?
- Er post-ID eksplisitt når koden kan leve utenfor The Loop?
- Er brukergrener holdt utenfor anonym page cache?
- Finnes det en unit-test eller minst en WP_UnitTestCase for hjelperen?
- Bruker du
has_term()(eller bevisstin_category()) konsistent?
Oppsummering
Betinget logikk i WordPress er tre lag: query-flagg (is_singular, is_front_page, is_category), postdata (has_term, in_the_loop), og bruker (current_user_can). Malhierarki og FSE flytter hvor du skriver beslutningen, ikke om du trenger den. Cache straffer deg bare når du blander brukergrener inn i anonym HTML. Testbare hjelpere med eksplisitte argumenter er det som skiller en SOP fra enda en rotete functions.php.
Forskjellen på has_term(), is_category() og in_category() avgjør om en mal oppfører seg likt i arkivet og i enkeltinnlegget. Vi rydder gjerne opp i slik betinget logikk, se WordPress-utvikling for maler og taksonomier.







