Projektipäevik #7: Vundamendi kasutuselevõtt – Block Bindings, Service kaardid ja arhiivileht

Sissejuhatus

Eelmistes postitustes ehitasime vundamendi – CPT UI + ACF, Service ja Team CPT-d, meta-väljad. Aga me pole neid veel tegelikult kasutanud. Andmed istuvad andmebaasis, frontend ei näe neid.

Selles postituses anname vundamendile mõtte: ehitame Teenused-arhiivilehe (/services/), mis kuvab Service CPT postituste kaarte koos pisipildi, pealkirja, ikooni ja hinnaga. Block Bindings API sillab visuaalsed Block Editor patternid ACF meta-väljadega.

Postitus on kirjutatud nii, et õpilane saab seda samm-sammult järgi teha. Iga sammu juures on Miks, Kuidas ja Mida kontrollida.

Mida me täpsemalt ehitame

  • patterns/card-service.php – üksiku teenuse kaart (kasutame Query Loop’i sees)
  • templates/archive-service.html – arhiivileht, mis renderdub /services/ URL-il
  • functions.php täiendus – register_post_meta() ACF väljade jaoks

Eeldused

  • WordPress 6.5+ (Block Bindings API)
  • CPT UI + ACF pluginad aktiivsed (#3)
  • Service CPT registreeritud (URL slug “services”, has_archive true)
  • ACF Field Group “Service Fields” – väljad price_from (Number), icon (Text), featured (True/False)
  • Vähemalt 1 Service postitus loodud koos täidetud ACF väljadega
  • DDEV keskkond

Samm 1: register_post_meta() functions.php-sse

Miks?

WP Block Bindings API saab lugeda core/post-meta source’iga ainult registreeritud meta-välju. ACF salvestab oma väljad wp_postmeta tabelisse, aga ei registreeri neid REST-API jaoks. Tulemus: Block Bindings ei näe ACF välju, frontend kuvab tühja.

Lahendus: registreeri meta-väljad ka PHP-koodi kaudu, atribuudiga show_in_rest: true. Duubeldus on talutav – ACF UI on andmesisestuseks, register_post_meta() on Block Editor’i jaoks tuvastatavus.

Kuidas?

Ava wp-content/themes/fse-theme/functions.php. Lisa pärast olemasolevat fse_theme_register_block_styles() funktsiooni järgmine:

/**
 * Register ACF meta fields for REST API + Block Bindings.
 */
function fse_theme_register_meta_fields() {
    // Service CPT
    register_post_meta( 'service', 'price_from', [
        'type'         => 'number',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'service', 'icon', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'service', 'featured', [
        'type'         => 'boolean',
        'single'       => true,
        'show_in_rest' => true,
    ] );

    // Team CPT
    register_post_meta( 'team', 'job_title', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'team', 'email', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'team', 'linkedin', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
}
add_action( 'init', 'fse_theme_register_meta_fields' );

Mida kontrollida?

Salvesta fail. Ava brauser, navigeeri /wp-json/wp/v2/service?_fields=id,title,meta. Peaksid vastusena nägema JSON-i, mis sisaldab meta objekti koos price_from, icon, featured väljadega:

{
  "id": 144,
  "title": { "rendered": "SEO Optimization" },
  "meta": {
    "price_from": 199,
    "icon": "🎯",
    "featured": true
  }
}

Kui meta objekt on tühi → register_post_meta ei töötanud. Kontrolli typo’sid, faili salvestust, ja DDEV ümberkäivitust (ddev restart).

Terve functions.php pärast seda sammu (versioon 3)

Pärast register_post_meta lisamist näeb functions.php välja nii. Muudatused võrreldes versiooniga 2 (#6 lõpus): uus funktsioon fse_theme_register_meta_fields() + selle hook’ile haakimine.

<?php
/**
 * fse-theme functions.php
 *
 * Theme support + block styles + pattern categories + page excerpt UI + meta-väljade registreerimine.
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Theme support deklaratsioonid.
 */
function fse_theme_setup() {
    add_theme_support( 'post-thumbnails' );

    add_theme_support( 'html5', [
        'search-form',
        'comment-form',
        'comment-list',
        'gallery',
        'caption',
        'style',
        'script',
    ] );

    add_theme_support( 'wp-block-styles' );

    add_post_type_support( 'page', 'excerpt' );
}
add_action( 'after_setup_theme', 'fse_theme_setup' );

/**
 * Block style: Secondary Button.
 */
function fse_theme_register_block_styles() {
    register_block_style( 'core/button', [
        'name'  => 'secondary',
        'label' => __( 'Secondary', 'fse-theme' ),
    ] );
}
add_action( 'init', 'fse_theme_register_block_styles' );

/**
 * UUS: Register ACF meta fields for REST API + Block Bindings.
 */
function fse_theme_register_meta_fields() {
    // Service CPT
    register_post_meta( 'service', 'price_from', [
        'type'         => 'number',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'service', 'icon', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'service', 'featured', [
        'type'         => 'boolean',
        'single'       => true,
        'show_in_rest' => true,
    ] );

    // Team CPT
    register_post_meta( 'team', 'job_title', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'team', 'email', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
    register_post_meta( 'team', 'linkedin', [
        'type'         => 'string',
        'single'       => true,
        'show_in_rest' => true,
    ] );
}
add_action( 'init', 'fse_theme_register_meta_fields' );

/**
 * Pattern kategooriad.
 */
function fse_theme_register_pattern_categories() {
    register_block_pattern_category( 'fse-theme-hero', [
        'label' => __( 'FSE Theme – Hero', 'fse-theme' ),
    ] );

    register_block_pattern_category( 'fse-theme-card', [
        'label' => __( 'FSE Theme – Cards', 'fse-theme' ),
    ] );

    register_block_pattern_category( 'fse-theme-section', [
        'label' => __( 'FSE Theme – Sections', 'fse-theme' ),
    ] );
}
add_action( 'init', 'fse_theme_register_pattern_categories' );

Samm 2: card-service.php pattern

Miks pattern?

Service kaart kasutatakse kahes kohas: arhiivilehel (kogu nimekiri) ja tulevikus avalehel (Featured Services Query Loop). Otsustusmaatriksi (vt #4) järgi 2+ kohta sama struktuuriga, varieeruv andmestik → pattern Query Loop’i sees.

Kuidas? — kaks teed (vt Case 14)

Tee A (Site Editor): Patterns → Create new → “Card Service” → ehita visuaalselt → CBT export. Töötab, aga Block Bindings markup tuleb lisada Code Editor’is.

Tee B (otsekoodi): Loo fail VS Code’is. Kiirem, kui struktuur on selge. Meie valisime Tee B, sest:

  • Eelmise patterni (page-hero.php) tegime Site Editor’s – kogemus käes
  • Block Bindings JSON-süntaks on raskem UI-st klõpsata kui otse kirjutada
  • Otsekoodi puhul ei ole CBT-ekspordi lõksu (JSON-attributes-strip, page-hetro typo)

Kuidas? — sammude koodina

Loo fail wp-content/themes/fse-theme/patterns/card-service.php järgmise sisuga:

<?php
/**
 * Title: Card Service
 * Slug: fse-theme/card-service
 * Categories: featured
 * Description: Teenuse kaart. Kasutatakse Query Loop'i sees archive-service.html-is ja avalehel Featured Services sektsioonis.
 */
?>
<!-- wp:group {"tagName":"article","style":{"spacing":{"padding":{"top":"var:preset|spacing|l","right":"var:preset|spacing|l","bottom":"var:preset|spacing|l","left":"var:preset|spacing|l"},"blockGap":"var:preset|spacing|s"},"border":{"radius":"8px","width":"1px","color":"#e5e5e5"}},"layout":{"type":"constrained"}} -->
<article class="wp-block-group" style="border-color:#e5e5e5;border-width:1px;border-radius:8px;padding:var(--wp--preset--spacing--l)">

    <!-- wp:post-featured-image {"isLink":true,"aspectRatio":"16/9"} /-->

    <!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"icon"}}}},"fontSize":"x-large"} -->
    <p class="has-x-large-font-size">🎯</p>
    <!-- /wp:paragraph -->

    <!-- wp:post-title {"level":2,"isLink":true,"fontSize":"large"} /-->

    <!-- wp:post-excerpt {"moreText":""} /-->

    <!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap"}} -->
    <div class="wp-block-group">
        <!-- wp:paragraph -->
        <p>Alates</p>
        <!-- /wp:paragraph -->

        <!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"price_from"}}}},"style":{"typography":{"fontWeight":"700"}}} -->
        <p style="font-weight:700">0</p>
        <!-- /wp:paragraph -->

        <!-- wp:paragraph -->
        <p>€</p>
        <!-- /wp:paragraph -->
    </div>
    <!-- /wp:group -->

</article>
<!-- /wp:group -->

Block Bindings markup’i selgitus

Vaata icon paragraph blokki:

<!-- wp:paragraph {
  "metadata": {
    "bindings": {
      "content": {                    ← seo `content` atribuut ...
        "source": "core/post-meta",  ← ... meta-väljaga ...
        "args": {"key": "icon"}      ← ... mille võtmega "icon"
      }
    }
  }
} -->
<p>🎯</p>     ← fallback tekst, kuvatakse kui meta tühi
<!-- /wp:paragraph -->

Frontend’s WP loeb meta-välja icon, asendab paragraph’i sisu sellega. Kui meta-väli on tühi, jääb fallback tekst.

Mida kontrollida?

Pärast faili loomist, käivita terminalis:

ddev exec wp eval-file wp-content/themes/fse-theme/flush-pattern-cache.php
ddev exec wp cache flush

Seejärel ava WP admin → Appearance → Editor → Patterns. Tee hard refresh (Cmd+Shift+R). Peaksid nägema “Card Service” patternit “Featured” kategoorias.

Samm 3: archive-service.html template

Miks?

WP template hierarchy järgi: URL /services/ otsib kõigepealt archive-service.html, siis archive.html, siis index.html. Loome esimese, et meil oleks täielik kontroll arhiivilehe üle.

Pedagoogiline lõks: miks ei kasuta PageHero pattern reference’it?

PageHero pattern kasutab wp:post-title ja wp:post-excerpt blokke. Need loevad praeguse postituse andmeid. Aga CPT-arhiivilehel POLE praegust postitust – on hoopis nimekiri postitustest.

PageHero töötab:

  • Page-template’is (page.html) – Meist, Kontakt, Privaatsus
  • home.html-is (page_for_posts) – Blogi listing, sest Posts page on tegelikult Page

PageHero EI tööta:

  • CPT-arhiivilehel (archive-service.html) – pole alus-Page’i

Lahendus: inline hero arhiivi-template’is, kasutame wp:query-title blokki (loeb CPT label’i).

Kuidas?

Loo fail wp-content/themes/fse-theme/templates/archive-service.html:

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">

    <!-- Inline hero (CPT-arhiivi jaoks, mitte PageHero pattern) -->
    <!-- wp:group {"tagName":"section","align":"full","backgroundColor":"bg-gray","style":{"spacing":{"padding":{"top":"var:preset|spacing|xxxxxl","bottom":"var:preset|spacing|xxxxxl"}}},"layout":{"type":"default"}} -->
    <section class="wp-block-group alignfull has-bg-gray-background-color has-background">
        <!-- wp:query-title {"type":"archive","textAlign":"center","level":1,"showPrefix":false} /-->
        <!-- wp:paragraph {"align":"center"} -->
        <p class="has-text-align-center">Kõik teenused, mida pakume.</p>
        <!-- /wp:paragraph -->
    </section>
    <!-- /wp:group -->

    <!-- Query Loop, kasutab meie card-service patternit -->
    <!-- wp:query {"query":{"perPage":9,"postType":"service","order":"asc","orderBy":"date","inherit":false},"align":"wide","layout":{"type":"constrained"}} -->
    <div class="wp-block-query alignwide">
        <!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
            <!-- wp:pattern {"slug":"fse-theme/card-service"} /-->
        <!-- /wp:post-template -->

        <!-- wp:query-pagination -->
            <!-- wp:query-pagination-previous /-->
            <!-- wp:query-pagination-numbers /-->
            <!-- wp:query-pagination-next /-->
        <!-- /wp:query-pagination -->

        <!-- wp:query-no-results -->
            <!-- wp:paragraph -->
            <p>Teenuseid ei leitud.</p>
            <!-- /wp:paragraph -->
        <!-- /wp:query-no-results -->
    </div>
    <!-- /wp:query -->

</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

Query Loop’i seadete selgitus

  • "postType": "service" – päring tagastab AINULT Service CPT postitusi
  • "perPage": 9 – kuni 9 kaarti lehekülje kohta (3×3 grid)
  • "order": "asc" – vanemad enne (newest first oleks “desc”)
  • "inherit": false – ei päri template’ist (sest custom päring)
  • align: "wide" – 1312px lai (theme.json wideSize)

Post Template sees on AINULT <!-- wp:pattern slug="fse-theme/card-service" --> – pattern reference, mis tähendab et iga muudatus card-service.php-s kajastub kõikjal.

Mida kontrollida?

Salvesta fail. Käivita terminalis:

ddev exec wp eval-file wp-content/themes/fse-theme/flush-pattern-cache.php
ddev exec wp cache flush

Ava brauser: https://[sinu-sait].ddev.site/services/. Peaksid nägema:

  • Header (template-part)
  • Hall sektsioon “Services” (CPT label) + “Kõik teenused, mida pakume.”
  • 3-veeruline grid kaartidest, igaüks: pisipilt + 🎯 ikoon + H2 pealkiri + excerpt + “Alates X €”
  • Footer

Lõks, mida me kohtasime: ACF + Block Bindings

Esimese katsega kasutasime Block Bindings source’iks acf/field (ACF 6.2+ peaks toetama). Frontend’s tagastas binding TÜHJA – isegi kui ACF UI andmebaasis on väärtus.

Põhjus pole 100% kindel. Võimalik, et:

  • ACF Block Bindings adapter vajab ACF Pro plugin’it (free versioonis ei tööta)
  • Field Group seades on eraldi lüliti
  • Args.key formaat erineb (field_key vs field_name)

Lahendus: register_post_meta() + core/post-meta source. WP-natiivne, töötab ACF versioonist sõltumata, lihtsam debugida.

Õpilasele: kui sa ehitad seda projekti uuesti, järgi otse Samm 1-st (register_post_meta). Ära raisaka aega acf/field source’iga.

Veaotsing

Kaart kuvab “Alates 0 €” (fallback)

  • Kontrolli functions.php – kas register_post_meta() on sees? Salvestatud?
  • Kontrolli REST-API: /wp-json/wp/v2/service/144?_fields=meta – kas meta on seal nähtav?
  • Kontrolli ACF: kas tegelikult on Service postitusele väärtus täidetud? ddev exec wp post meta get 144 price_from

Pattern ei ilmu admin’is

  • Pattern cache flush: ddev exec wp eval-file wp-content/themes/fse-theme/flush-pattern-cache.php
  • Object cache flush: ddev exec wp cache flush
  • Hard refresh brauseris: Cmd+Shift+R (Mac) või Ctrl+Shift+R
  • Kontrolli PHP-header’i süntaksit (Title, Slug, Categories ühel real)

CPT label kuvub inglise keeles (“Services” mitte “Teenused”)

CPT UI → Edit Service → Labels → muuda “Services” → “Teenused”, “Service” → “Teenus”. URL on eraldi (rewrite slug = “teenused” annab /teenused/).

Õppetundide kokkuvõte

  • Vundament saab kasutuselevõetud: CPT + ACF + Pattern + Template + Block Bindings tervikuna
  • Block Bindings API sillab patternid ja meta-väljad – WP-natiivne, deklaratiivne
  • core/post-meta vajab register_post_meta() – ACF ei tee seda automaatselt
  • CPT-arhiivilehel ei tööta PageHero pattern (post-title/excerpt blokid) – kasuta query-title + inline hero
  • Pattern reference Query Loop’i sees tagab muudatuse propagatsiooni (mitte inline-imine)
  • Site Editor vs otsekoodi: mõlemad on legitiimsed, vali rütmi järgi

Mis tuleb järgmiseks

Avaleht “Featured Services” – sama card-service pattern, aga Query Loop filtriga featured = true. See on meta_query kasutus – kuidas filtreerida postitusi ACF-välja järgi. Tuleb järgmises postituses.

Tagantjärele saame ehitada ka single-service.html (üksiku teenuse leht ServiceHero + Body + FAQ), archive-team.html (Meeskond, sama muster), page.html (Meist, Kontakt, Privaatsus – PageHero pattern reference’iga).

Kõik need õppetundid on detailselt talletatud project/casestudy.md Case 14-16-s. Õpilase soovituslik järjekord:

Hea õppimist.