• Projektipäevik #8: Otsinguleht – kontekstipõhine snippet ja bold otsingusõna ümber

    Sissejuhatus

    Otsing on iga sisuga lehe vajalik osa. Vaikimisi WordPress’i otsing töötab (URL-il ?s=keyword), aga vaikimisi tulemustelehel on kolm puudust: pole dedikeeritud template’i, excerpt’is võib otsingusõna puududa, ja otsingusõna pole esile tõstetud.

    Selles postituses ehitame otsingulehe (templates/search.html), mis kuvab pealkirjad ja kontekstipõhise snippet’i koos bold-itud otsingusõnaga. UTF-8 turvaline (eesti tähed Ä, Õ, Ü, Ö töötavad).

    Postitus on kirjutatud nii, et õpilane saab seda samm-sammult järgi teha. Tähelepanu: see ülesanne paljastab WP filtrite kihti, mis on FSE-arendaja initsiatsiooni-rituaal. Soovitan lugeda lõpuni.

    Eesmärk

    Frontend’i tulemus, mida tahame:

    • Hero ülaosas: “Search results for: ‘otsingusõna’”
    • Otsinguvorm uuesti otsida
    • Tulemused: igaüks H2 pealkiri + excerpt-snippet
    • Pealkirjas ja snippet’is on otsingusõna paksult (bold)
    • Snippet näitab konteksti (~80 tähemärki enne ja pärast), mitte vaikimisi esimesi 55 sõna
    • “No results” sõnum, kui midagi ei leita
    • Eesti tähed töötavad otsingus

    Eeldused

    • WordPress 6.x (Block Editor)
    • FSE block-teema (templates/ kaust olemas)
    • Vähemalt mõned postitused, mille sisus on otsitav sõna
    • DDEV või muu kohalik keskkond

    Samm 1: search.html template

    Miks?

    WP template hierarchy järgi: kui URL sisaldab ?s=keyword, WP otsib search.html template’i. Kui pole, kasutab index.html fallback’ina. index.html aga ei tea otsingu kontekstist midagi – see näitab postitusi nagu peavoolus.

    Dedikeeritud search.html annab:

    • Selge “Search results” pealkirja (`wp:query-title` blokiga)
    • Võimaluse uuesti otsida (`wp:search` blokk)
    • “No results” tagavara, kui midagi ei leita (`wp:query-no-results` blokk)
    • Kontrolli tulemuste-kaartide üle

    Kuidas?

    Loo fail wp-content/themes/[teema]/templates/search.html:

    <!-- wp:template-part {"slug":"header","tagName":"header"} /-->
    
    <!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
    <main class="wp-block-group">
    
        <!-- Hero: Search results title -->
        <!-- wp:group {"tagName":"section","align":"full","backgroundColor":"bg-gray",...} -->
        <section>
            <!-- wp:query-title {"type":"search","textAlign":"center","level":1,"showPrefix":false} /-->
            <!-- wp:paragraph {"align":"center"} -->
            <p>Allpool postitused ja lehed, mis sinu otsingule vastavad.</p>
            <!-- /wp:paragraph -->
            <!-- wp:search {"placeholder":"Otsi uuesti…","buttonText":"Otsi"} /-->
        </section>
        <!-- /wp:group -->
    
        <!-- Query Loop with inherit:true loeb URL'ist ?s=keyword -->
        <!-- wp:query {"query":{"perPage":10,"inherit":true,...},"align":"wide"} -->
        <div class="wp-block-query alignwide">
            <!-- wp:post-template -->
                <!-- wp:group {"tagName":"article",...} -->
                <article>
                    <!-- wp:post-title {"level":2,"isLink":true,"fontSize":"large"} /-->
                    <!-- wp:post-excerpt {"moreText":"Vaata edasi","showMoreOnNewLine":true} /-->
                </article>
                <!-- /wp:group -->
            <!-- /wp:post-template -->
    
            <!-- wp:query-pagination -->...<!-- /wp:query-pagination -->
    
            <!-- wp:query-no-results -->
                <!-- wp:paragraph {"align":"center"} -->
                <p>Sellele otsingule midagi ei leitud. Proovi mõnd teist sõna.</p>
                <!-- /wp:paragraph -->
            <!-- /wp:query-no-results -->
        </div>
        <!-- /wp:query -->
    
    </main>
    <!-- /wp:group -->
    
    <!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

    Olulisemad blokid

    • wp:query-title type="search" – loeb URL-ist ?s väärtuse ja kuvab “Search results for: ‘keyword’”
    • wp:search – otsinguvorm. Asukoht hero sees, et kasutaja saaks uuesti otsida
    • wp:query inherit:true – Query Loop pärib otsingu-päringut URL’ist, ei vaja oma postType
    • wp:query-no-results – kuvatakse, kui päring tagastab tühja

    Mida kontrollida?

    Salvesta fail. Ava brauser: https://[sait]/?s=test (asenda “test” mingi sõnaga, mis on sinu postitustes). Peaksid nägema struktuuri (hero, otsinguvorm, tulemused), aga otsingusõna POLE veel paksult – seda lisame järgmistes sammudes.

    Samm 2: Kontekstipõhine excerpt

    Miks?

    WordPress’i vaikimisi excerpt on esimesed 55 sõna sisust. Aga kui otsingusõna on lõigus 200, näeb kasutaja excerpt’is hoopis teist teksti – ei saa aru, miks see tulemus kuvatakse.

    Lahendus: filter, mis OTSINGU TULEMUSTE puhul leiab otsingusõna asukoha sisus ja kuvab ~80 tähemärki enne ja pärast. Annab kohese konteksti.

    Kuidas?

    Ava functions.php. Lisa:

    /**
     * Search: kontekstipõhine excerpt.
     */
    function fse_theme_search_contextual_excerpt( $excerpt, $post ) {
        if ( ! is_search() || ! $post instanceof WP_Post ) return $excerpt;
    
        $query = get_search_query();
        if ( empty( $query ) ) return $excerpt;
    
        $content = wp_strip_all_tags( $post->post_content );
        if ( empty( $content ) ) return $excerpt;
    
        $pos = mb_stripos( $content, $query );
        if ( $pos === false ) return $excerpt;
    
        $start   = max( 0, $pos - 80 );
        $length  = mb_strlen( $query ) + 160;
        $snippet = mb_substr( $content, $start, $length );
    
        if ( $start > 0 ) $snippet = '… ' . $snippet;
        if ( $start + $length < mb_strlen( $content ) ) $snippet .= ' …';
    
        return $snippet;
    }
    add_filter( 'get_the_excerpt', 'fse_theme_search_contextual_excerpt', 10, 2 );

    Olulised detailid

    • is_search() – ainult otsingulehel (mujal vaikimisi excerpt)
    • $post instanceof WP_Post – kaitse, kui filter saab veidra objekti
    • mb_stripos, mb_substr, mb_strlen – UTF-8 turvaline (eesti tähed!)
    • Kui otsingusõna pole sisus (võib olla pealkirjas), tagastame vaikimisi excerpt’i
    • Ellipsis (… ja …) annavad lugejale teada, et see on katkend

    Mida kontrollida?

    Tee hard refresh otsingulehel. Vaata excerpt’i tulemustes – kas see on nüüd otsingusõna ümbrusest, mitte vaikimisi esimesed 55 sõna? Veendu, et hard refresh oli (Cmd+Shift+R Mac, Ctrl+Shift+R muu), sest WP cache’ib agressiivselt.

    Samm 3: Bold pealkirjas

    Miks?

    Tulemuste skannitavus paraneb dramaatiliselt, kui otsingusõna on tulemustes esile tõstetud. Kasutaja näeb kohe, miks see konkreetne postitus oli kuvatud.

    Kuidas?

    Lisa functions.php-sse:

    /**
     * Search: bold otsingusõna pealkirjas.
     */
    function fse_theme_search_highlight_title( $title ) {
        if ( ! is_search() || ! in_the_loop() ) return $title;
    
        $query = get_search_query();
        if ( empty( $query ) ) return $title;
    
        return preg_replace(
            '/(' . preg_quote( $query, '/' ) . ')/iu',
            '<strong>$1</strong>',
            $title
        );
    }
    add_filter( 'the_title', 'fse_theme_search_highlight_title' );

    Detailid

    • in_the_loop() – kaitse, et navi-menüü, header’i ja muude kohtade pealkirjades ei bold’i. Ainult Query Loop’i sees
    • preg_quote – escape regex erimärke kasutaja sisendis (kui ta otsib “v.*”, ei taha me regex’i tõlgendust)
    • /u flag – UTF-8 mode, vajalik eesti tähtede (Ä, Õ, Ü, Ö) jaoks
    • /i flag – case-insensitive, et “Vundamendi” ja “vundamendi” mõlemad leiaks

    Mida kontrollida?

    Hard refresh. Vaata otsingulehel pealkirju – kas otsingusõna on neis paksult? Proovi otsinguid eri sõnaga, sh suure ja väikse algustähega.

    Samm 4: Bold excerpt’s

    NB! See samm on keerulisem kui eelnevad. Eelmistel Samm 1–3 oli muster: Miks → Kuidas → Mida kontrollida. Selles sammus on lisaks kolmas kiht: enne õiget lahendust näeme kaks loomulikku katset, mis ei tööta. See on tahtlik — tutvustab WP filtrite kihtide ehitust. Loe hoolikalt ja ära kiirusta.

    Loomulik (aga vale) lähenemine

    Sa võiksid arvata: “Eelmine filter töötas pealkirjale (the_title), nüüd lisan sama excerpt’ile (the_excerpt või get_the_excerpt).” Loogika ütleb seda.

    Aga see EI TÖÖTA. Miks?

    Kaks lõksu järjest

    Lõks #1: wp:post-excerpt blokk EI käivita the_excerpt filtrit. Blokk-renderdamise kood kasutab otse get_the_excerpt() funktsiooni. Niisiis kõik the_excerpt filtrid on FSE block-themes-is tähelepanuta.

    Lõks #2: Hea, niisiis kasutame get_the_excerpt filtrit. Filter käivitub korralikult ja tagastab tagasi <strong>tekst</strong>. AGA pärast filtrit kutsub blokk wp_trim_words(), mis kasutab seest wp_strip_all_tags()kogu HTML kaob.

    Tagajärg: <strong> tag, mille filter sisestas, lihtsalt strip’itakse ära enne renderdamist. Frontend näeb plaint teksti.

    Õige lahendus: render_block_core/post-excerpt

    FSE blokkide puhul on kõige hilisem filter ehk see, mis käivitub kõige hilisemana, kõige ohutum koht muudatuste tegemiseks. Iga blokk on render_block_core/{block-name} filtri all – see käivitub pärast kogu blokk-renderdamist.

    Lisa functions.php-sse:

    /**
     * Search: bold otsingusõna wp:post-excerpt bloki HTML-is.
     *
     * Käivitub PÄRAST wp_trim_words() HTML-stripimist.
     * See on ainus koht, kus <strong> säilib.
     */
    function fse_theme_search_highlight_excerpt_block( $block_content, $block ) {
        if ( ! is_search() ) return $block_content;
    
        $query = get_search_query();
        if ( empty( $query ) ) return $block_content;
    
        return preg_replace(
            '/(' . preg_quote( $query, '/' ) . ')/iu',
            '<strong>$1</strong>',
            $block_content
        );
    }
    add_filter( 'render_block_core/post-excerpt', 'fse_theme_search_highlight_excerpt_block', 10, 2 );

    Mida kontrollida?

    Hard refresh. Vaata frontend’s – otsingusõna peaks nüüd olema paks ka excerpt’s. Kui ei – kontrolli source’i (View Page Source), kas <strong> on HTML-is.

    Terve functions.php pärast seda sammu (versioon 4 – LÕPLIK)

    Lõpuks näeb functions.php välja nii. Muudatused võrreldes versiooniga 3 (#7 lõpus): kolm uut filter-funktsiooni otsingu-tulemuste lehe jaoks:

    <?php
    /**
     * fse-theme functions.php
     *
     * Theme support + block styles + pattern categories + page excerpt UI +
     * meta-väljade registreerimine + otsingu-filtrid.
     */
    
    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' );
    
    /**
     * Register ACF meta fields for REST API + Block Bindings.
     */
    function fse_theme_register_meta_fields() {
        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,
        ] );
    
        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' );
    
    /**
     * UUS: Search – kontekstipõhine excerpt.
     */
    function fse_theme_search_contextual_excerpt( $excerpt, $post ) {
        if ( ! is_search() || ! $post instanceof WP_Post ) {
            return $excerpt;
        }
    
        $query = get_search_query();
        if ( empty( $query ) ) {
            return $excerpt;
        }
    
        $content = wp_strip_all_tags( $post->post_content );
        if ( empty( $content ) ) {
            return $excerpt;
        }
    
        $pos = mb_stripos( $content, $query );
        if ( $pos === false ) {
            return $excerpt;
        }
    
        $start   = max( 0, $pos - 80 );
        $length  = mb_strlen( $query ) + 160;
        $snippet = mb_substr( $content, $start, $length );
    
        if ( $start > 0 ) {
            $snippet = '… ' . $snippet;
        }
        if ( $start + $length < mb_strlen( $content ) ) {
            $snippet .= ' …';
        }
    
        return $snippet;
    }
    add_filter( 'get_the_excerpt', 'fse_theme_search_contextual_excerpt', 10, 2 );
    
    /**
     * UUS: Search – bold otsingusõna excerpt-bloki HTML-is.
     */
    function fse_theme_search_highlight_excerpt_block( $block_content, $block ) {
        if ( ! is_search() ) {
            return $block_content;
        }
    
        $query = get_search_query();
        if ( empty( $query ) ) {
            return $block_content;
        }
    
        return preg_replace(
            '/(' . preg_quote( $query, '/' ) . ')/iu',
            '<strong>$1</strong>',
            $block_content
        );
    }
    add_filter( 'render_block_core/post-excerpt', 'fse_theme_search_highlight_excerpt_block', 10, 2 );
    
    /**
     * UUS: Search – bold otsingusõna pealkirjas.
     */
    function fse_theme_search_highlight_title( $title ) {
        if ( ! is_search() || ! in_the_loop() ) {
            return $title;
        }
    
        $query = get_search_query();
        if ( empty( $query ) ) {
            return $title;
        }
    
        return preg_replace(
            '/(' . preg_quote( $query, '/' ) . ')/iu',
            '<strong>$1</strong>',
            $title
        );
    }
    add_filter( 'the_title', 'fse_theme_search_highlight_title' );
    
    /**
     * 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' );
    

    See on kogu functions.php evolutsiooni lõpp-punkt projekti praegusele seisule. Edaspidi (nt avalehe Featured Services, single-service.html), võib lisada veel funktsioone, aga need 4 versiooni katavad kõik tähtsamad õppetunnid.

    Reegel filtri valimisel FSE blokkidega

    See on suur õppetund kogu projektist. Tee selge reegel oma peas:

    Tahad muuta…Kasuta filtrit
    Sisu enne renderdamistget_the_content, get_the_excerpt, the_title
    Renderdatud HTML-i (incl. inline HTML säilumist)render_block_core/{block-name}
    Excerpt’i HTML (sealhulgas pärast wp_trim_words)render_block_core/post-excerpt
    Vana classic style templatethe_excerpt (FSE-s ei käivitu)

    Õpilastele: kui sa lisad HTML-i excerpt-blokis ja see kaob, vahetage filter get_the_excerptrender_block_core/post-excerpt. See on FSE-spetsiifiline lõks, mida vanades teemades polnud.

    UTF-8 turvalisus (eesti tähed)

    Kuna projekt on eesti keeles, on kohustuslik kasutada UTF-8 turvalisi funktsioone:

    • mb_strlen (mitte strlen) – pikkus tähemärkides, mitte baitides
    • mb_stripos (mitte stripos) – case-insensitive otsing
    • mb_substr (mitte substr) – substring tähemärkides
    • Regex’is /u flag – UTF-8 mode

    Ilma nendeta: kui otsid “Õppimine”, saad katki snippet’i (ä, õ, ü on 2-baidised) ja regex käitub valesti.

    Veaotsing

    Excerpt näitab vaikimisi (mitte snippet)

    • Kontrolli, kas otsingusõna ON sisus (mitte ainult pealkirjas)
    • ddev exec wp cache flush
    • Hard refresh brauseris
    • Kontrolli, kas filter on registreeritud: get_the_excerpt (mitte the_excerpt)

    Bold pole excerpt’s näha (aga pealkirjas on)

    • Lõks #2 – wp_trim_words strip’b HTML
    • Lisa render_block_core/post-excerpt filter (Samm 4)

    Eesti tähed katki (“Õppimine” annab segase tulemuse)

    • Asenda strlen, stripos, substrmb_strlen, mb_stripos, mb_substr
    • Lisa /u flag regex’i
    • Kontrolli, kas mbstring laiend on PHP-s aktiivne: ddev exec php -m | grep mbstring

    Õppetundide kokkuvõte

    • search.html on WP standardne template’i nimi – tee see kausta templates/
    • wp:query-title type="search" kuvab “Search results for: ‘query’”
    • wp:query inherit:true pärib otsingu-päringu URL’ist – ei vaja oma postType
    • wp:query-no-results blokk käsitleb “midagi ei leitud” elegantselt
    • Kontekstipõhine snippet annab kasutajale konteksti, miks tulemus on relevantne
    • FSE-s ei kasuta the_excerpt, kasuta get_the_excerpt
    • HTML-i lisamine excerpt’i: ainult render_block_core/post-excerpt filter säilib
    • UTF-8 turvalisus on eesti keelse projekti puhul kohustuslik

    Mis tuleb järgmiseks

    Otsingulehe ehitamine paljastas kihti, mis on FSE-arendaja igapäev. Sama muster (sisu-filter vs blokk-renderdamise filter) tuleb tagasi alati, kui sa tahad lisada midagi blokkidesse, mida WP vaikimisi ei luba.

    Järgmised loomulikud sammud projektis:

    • page.html template (Meist, Kontakt, Privaatsuspoliitika jaoks – PageHero pattern reference’iga)
    • single-service.html template (üksiku teenuse leht ServiceHero + Body + FAQ)
    • Avaleht Featured Services (Query Loop filtriga meta_query: featured=true)
    • Otsinguvormi lisamine header’isse, et oleks alati kättesaadav

    Kõik need õppetunnid on detailselt talletatud project/casestudy.md-s. Soovituslik lugemise järjekord:

    Hea õppimist.

  • 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.

  • Projektipäevik #6: Blog Home template – 6 lõksu Query Loop’iga

    Sissejuhatus

    Eelmises postituses ehitasime PageHero pattern’i – H1 + alapealkiri tsentrissa, dünaamiline post-title + post-excerpt. Aeg anda sellele esimene päris kasutuskoht: blogilisting.

    Eesmärk: templates/home.html – WP template hierarchy järgi blogilistingu jaoks (kui pole, kasutab index.html fallback’ina). Sisaldama PageHero pattern reference + 3-veeruline grid kaartidega + pagination.

    Ehitamine läks pisut põnevamalt kui ootasime – tabasime 6 lõksu. Igaüks neist on tüüpiline WP FSE arendamise kogemus.

    Esimene samm: Add new template → Blog Home

    Site Editor → Templates → vajutame “+” → ilmub dialoog. WP pakub valida, millise template-tüübi jaoks loome:

    • Blog Home ← see ongi home.html
    • Single Posts
    • All Archives
    • Author Archives

    “Blog Home” valime – WP teab, et see vastab page_for_posts seades olevale lehele. Praktikas: kui Settings → Reading → Posts page = “Blogi”, siis selle lehe frontend rendib home.html.

    Query Loop variant: “Image, Date, & Title”

    Tühjas template’is lisame Query Loop’i. WP küsib esimese asjana “Choose a pattern for the query loop or start blank”, siis variandi:

    • Title & Date
    • Title & Excerpt
    • Title, Date, & Excerpt
    • Image, Date, & Title ← see Figma-kaardi struktuuri lähedasem

    Valime Image-i, lisame ise Excerpt’i. Mitte ühtegi variandi ei ole täpne, aga see annab kõige rohkem blokke (3/5 vajalikust).

    Lõks #1: “Inherit query from template” on nüüd “Query Type: Default”

    Otsisin sidebar’s “Inherit query from template” toggle’it. Polnud kuskil. WP 6.x-s see seade nimetati ümber:

    OLI:  Inherit query from template  [toggle ON/OFF]
    ON:   Query Type  [Default | Custom]
    • Default = päri template’ist (= inherit ON) ✅ vaikimisi õige
    • Custom = oma päring (post-type, perPage, taxQuery)

    Niisiis vaikimisi on juba korras – pole vaja midagi muuta. Aga **otsing võtab aega**, kui sa otsid vana sõnastust.

    Lõks #2: Grid layout on Post Template’is, mitte Query Loop’s

    Tahame 3-veerulist grid’i. Query Loop sidebar’s näeme “Layout” sektsiooni: Inner blocks use content width, Content width, Wide width, Justification. Aga **kus Grid valik?**

    Vastus: Grid layout pole Query Loop bloki seade, vaid Post Template bloki seade (Query Loop SEES). Niisiis:

    • List View → ava Query Loop → vali Post Template
    • Post Template’i toolbar’s (üleval) on kaks layout ikooni: Stack (vertikaalne) ja Grid
    • Vali Grid → sidebar’s ilmub “Columns” → vali 3

    Pärast: kaardid hakkavad 3 veergu paigutuma. Mobile’l läheb automaatselt 1 veerg, sest WP Grid layout on responsive.

    Lõks #3: 720px-laius on liiga kitsas – vajame wide width

    Kaardid paigutusid 3 veergu, aga grid oli kogu sisu-laiuses ~720px (theme.json contentSize). 3 kaardi jagu 720px = ~230px iga, **liiga kitsas**.

    Figma desktop: viewport 1440px, paddings 64px mõlemal pool → sisu 1312px. See vastab theme.json’i wideSize: 1312px.

    Lahendus: align:"wide" Query Loop’ile (või Post Template’ile – mõlemad toimivad sarnaselt). Toolbar’s align ikoon → “Wide width”.

    Pärast: Query Loop laieneb 1312px-ni, 3 kaarti jagu (umbes 420px iga + gap). Mahuvad ilusti.

    NB! Ära muuda theme.json contentSize väärtust laiemaks. Tekst peaks jääma kitsamana loetavuse huvides (max ~80 tähemärki rea kohta = ~720px). contentSize = tekstid; wideSize = grid’id, banner’id.

    Lõks #4: Read More duplikaat

    Lisasime Post Template’i Post Excerpt’i järele Read More bloki (“Loe edasi” tekstiga). Frontend’s nägi kaart välja nii:

    [pilt]
    May 22, 2026
    Projektipäevik #4: FSE filosoofia...
    Aha-hetk Hero'ga: 1 kasutus pole pattern...
    Read more       ← Post Excerpt'i sisene automaatne link
    Loe edasi       ← meie eraldi Read More blokk

    Kaks “Read More” elementi. Põhjus:

    • Post Excerpt blokk lisab vaikimisi automaatse “Read more” lingi excerpt’i järele
    • Meie lisasime VEEL ühe eraldi Read More bloki

    Lahendus: kustuta eraldi Read More blokk, kasuta Post Excerpt’i sisemise lingi atribuuti:

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

    Reegel: kui sama elementi annab teha kahel viisil (eraldi blokk vs atribuut), eelista atribuuti. Vähem blokke, lihtsam markup.

    Lõks #5: WCAG heading hierarhia – Post Title peab olema H2

    Mu esimene soovitus oli Post Title H3. Aga klient märkas kohe: “See ei tohiks H3 olla, kuna lehel on H1 ja vahepeal H2 ei ole.”

    Õigus. WCAG 2.1 reegel: heading-leveleid ei tohi vahele jätta. H1 → H3 (H2 puudub) on viga. Screen reader’id ja Google tõlgendavad valesti.

    Lehel:

    • H1: “Projektipäevik” (PageHero)
    • H2: postituse pealkiri (kaardid otse H1 all → H2-tase, mitte H3)

    Kui meil oleks vahepeal H2 sektsioonid (“Featured posts”, “Latest”), siis kaardid sees võiksid olla H3. Aga praegu pole – niisiis kõik kaardid on H2.

    Lõks #6 (suurim): Pattern ja Template-Part inline’imine

    CBT eksport tegi home.html faili. Avasin selle ja vaatasin – ja **plahvatasin**. Faili sees oli kõik järgnev inline’itud:

    • Header (15 rida “Simple header with tagline” core-pattern’i markup’i otse failis)
    • PageHero (5 rida Group + post-title + post-excerpt otse failis)

    Mitte ükski neist polnud reference. Iga muudatus parts/header.html-s või patterns/page-hero.php-s EI kajastu home.html-l. Kogu pattern-filosoofia idee – tsentraliseeritud muudatused – on katki.

    Miks see juhtub? Site Editor’s pattern’i lisamisel inline’b ta blokid template’i sisse (vaikimisi non-synced pattern’itega). See on WP-i mudel:

    • Synced patterns = jagatud sisu, kõik kasutuskohad sünkis → reference säilib
    • Non-synced patterns = struktuuriline mall, sisu varieerub kasutuskohas → inline’itakse

    Aga template’is ME EI TAHA inline’imist. Me tahame dünaamilist taaskasutust – kui PageHero pattern muutub, peab muutus kajastuma kõikjal.

    Lahendus: kirjutame pattern reference’i ja template-part reference’i KÄSITSI markup’i:

    <!-- wp:template-part {"slug":"header","tagName":"header"} /-->
    
    <!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
    <main class="wp-block-group">
        <!-- wp:pattern {"slug":"fse-theme/page-hero"} /-->
    
        <!-- wp:query {"query":{"inherit":true,...},"align":"wide","layout":{"type":"constrained"}} -->
        <div class="wp-block-query alignwide">
            <!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
                <!-- wp:post-featured-image {"isLink":true,"aspectRatio":"16/9"} /-->
                <!-- wp:post-date /-->
                <!-- wp:post-title {"level":2,"isLink":true} /-->
                <!-- wp:post-excerpt {"moreText":"Loe edasi"} /-->
            <!-- /wp:post-template -->
    
            <!-- wp:query-pagination -->
                <!-- wp:query-pagination-previous /-->
                <!-- wp:query-pagination-numbers /-->
                <!-- wp:query-pagination-next /-->
            <!-- /wp:query-pagination -->
        </div>
        <!-- /wp:query -->
    </main>
    <!-- /wp:group -->
    
    <!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

    Kaks võtmerada:

    • <!-- wp:template-part {"slug":"header"} /--> – Header tuleb parts/header.html-st
    • <!-- wp:pattern {"slug":"fse-theme/page-hero"} /--> – PageHero tuleb patterns/page-hero.php-st

    WP loeb need igal page-render’il, leiab vastava faili, ja renderdab selle sisu. Muudatused propageeruvad automaatselt.

    Boonus: “Author” indikaator Templates vaates

    Site Editor’s Templates kaardivaates iga eelvaate all on “Author” väli. Selle väärtus paljastab:

    • Teema nimi (nt “FSE Theme”) → template on koodis (templates/*.html, Git’is, deploy’iv)
    • Kasutaja nimi (admin, juhokalberg) → template on ainult andmebaasis (wp_posts, ei lähe Git’i)

    Praktiline tagajärg:

    • Author = kasutaja nimi → tee CBT eksport, et liiguks Git’i
    • Author = teema nimi → kõik korras

    Sama loogika kehtib Template Parts’idele ja Patterns’idele.

    Boonus: “Posts Page” notice tähendus

    Kui avada admin’s Page’i, mis on määratud page_for_posts-iks (näiteks Projektipäevik / Blogi), näeb teavitust:

    You are currently editing the page that shows your latest posts.

    See tähendab: Frontend’s selle Page sisu EI rendita – selle asemel home.html template. Aga Page’i title ja excerpt ON KASUTUSEL (PageHero pattern loeb need).

    Niisiis Page’is olev sisu pole oluline. Tähtsaim: Page Title (“Projektipäevik”) ja Excerpt (“Mõtted, õppetunnid ja juhendid.”) on korras.

    functions.php täiendus: Page Excerpt UI

    Aga väike lõks selles töös: kui üritasin Blogi Page’ile Excerpt-välja sisestada (kirjeldus “Mõtted, õppetunnid ja juhendid.”), siis Site Editor’i ja klassikalise Block Editor’i sidebar’s Excerpt-paneeli polnud üldse näha.

    Põhjus: WordPress’i vaikimisi Pages’idel pole Excerpt-välja UI-d. Andmebaasi veerg post_excerpt on olemas, aga UI on peidetud. Vajalik lisada teema-toetus:

    Mida lisada functions.php-sse

    Olemasolevasse fse_theme_setup() funktsiooni lisa üks rida:

    add_post_type_support( 'page', 'excerpt' );

    Pärast lisamist – salvesta. Ava klassikaline Pages → Blogi → Edit. Parem sidebar peaks nüüd näitama Excerpt-paneeli. Kui ei näe, ⋮ → Preferences → Panels → Excerpt → SISSE.

    Terve functions.php pärast seda etappi

    Versioon 2 – ainus muudatus võrreldes versiooniga 1 (#3 lõpus) on add_post_type_support( 'page', 'excerpt' ) rida fse_theme_setup() funktsioonis:

    <?php
    /**
     * fse-theme functions.php
     *
     * Theme support + block styles + pattern categories + page excerpt UI.
     */
    
    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' );
    
        // UUS: Page Excerpt UI sisselülitamine.
        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' );
    
    /**
     * 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' );
    

    Õppetundide kokkuvõte

    • WP 6.x ümberkirjutamised: “Inherit query” → “Query Type: Default”. Otsing võtab aega.
    • Grid layout on Post Template’is, mitte Query Loop’s. Toolbar’s Stack ↔ Grid valik.
    • Wide width (1312px) grid’idele, contentSize (720px) tekstidele. Ära ühista neid.
    • Read More duplikaat: Post Excerpt’il on sisene link – kasuta atribuuti moreText:"Loe edasi", ära lisa eraldi Read More blokki.
    • Heading hierarhia (WCAG): H1 → H2 → H3. Ei tohi vahele jätta. Post Title kaartides = H2.
    • Site Editor inline’b non-synced pattern’eid template’isse. Pattern reference (<!-- wp:pattern slug -->) tuleb käsitsi markup’i kirjutada.
    • Template-part reference alati: header/footer mitte kunagi inline.
    • Author indikaator Site Editor’s näitab, kus element elab (Git vs DB).
    • Posts Page notice on kasulik teavitus: Page sisu ei rendita, aga title + excerpt loetakse.

    Kõik 6 lõksu + boonused on detailselt dokumenteeritud project/casestudy.md Case 11–12-s. Järgmistele arendajatele soovitan: pärast CBT-eksporti loe template fail HARLIKULT üle. Lihtne checklist:

    • Header – template-part reference?
    • Footer – olemas?
    • Pattern reference (mitte inline)?
    • Main wrapper sisaldab kõike content’i?
    • Read More duplikaat puudub?
    • align width on Query Loop / Post Template’il?
    ). Tagajärg: template ei ole enam inline-toimetatav.

    Reegel: kui tahad template’i Site Editor’is inline-toimetatav, ÄRA linnuta “Localize Text”. Aktsepteeri, et template ei ole i18n-itud. Page-põhise saidi puhul (üks keel) ei ole see kaotus.

    Localize Images katkine URL

    Sama plugin’i “Localize Images” valik genereerib pildi URL’iks assets/images/<pildinimi>.jpg, aga ei loo kausta ega kopeeri pilti tegelikult (vähemalt selle versiooniga). Tulemus: katkine URL.

    Lahendus: kasuta otse /wp-content/uploads/... URL’i (klient saab media library kaudu asendada), VÕI kopeeri pilt käsitsi assets/images/-i.

    Pattern cache

    WordPress kasutab pattern’idele cache’i (WP_Theme::get_block_patterns()). Kui lisad uue PHP faili kausta patterns/, WP võib seda mitte näha, kuni cache flushid:

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

    flush-pattern-cache.php sisu (asub teema juurkaustas):

    <?php
    $theme = wp_get_theme();
    $theme->delete_pattern_cache();
    _register_theme_block_patterns();
    echo "Pattern cache flushed.\n";

    Boonusõppetund: semantiline HTML FSE-s

    Veebistandardite ja screen reader’ite jaoks tahaks puhast struktuuri:

    <header>...</header>
    <main>
      <section>...</section>
    </main>
    <footer>...</footer>

    Group block rendib vaikimisi <div>. Et saada semantilised tag’id, kasuta tagName atribuuti:

    <!-- wp:template-part {"slug":"header","tagName":"header"} /-->
    
    <!-- wp:group {"tagName":"main"} -->
    <main class="wp-block-group">
        <!-- wp:post-content {"align":"full"} /-->
    </main>
    <!-- /wp:group -->
    
    <!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

    WordPress lisab paratamatult kaks div-wrapper’it (.wp-site-blocks ja .entry-content), mida ei tasu eemaldada – eemaldamine vajab fragile output-buffering hack’i, mis WP-update’iga katki läheb. Aktsepteerime need ja keskendume sellele, et semantiline puu (header/main/section/footer) on õigetes kohtades. See on, mida loevad nii screen reader’id kui Google.

    Kokkuvõte

    FSE filosoofia ühe lauses: anna kliendile võimalikult palju Page-vaate kaudu hallatavat sisu; jäta teema-koodi ainult see, mis on tõeliselt korduv (template part) või dünaamiline mall (pattern).

    Praktilised reeglid, mis jäid külge:

    • 1 koht = ei vaja pattern’it. Hero esilehel → Page sisu.
    • Klient muudab Page’is, mitte template’is. Anna Page’i nii palju, kui võimalik.
    • Localize Text VÄLJAS, kui tahad template’i inline-toimetatavaks.
    • Pattern cache flush uue pattern faili lisamisel.
    • Semantiline HTML Group block’i tagName atribuudiga, aktsepteeri WP-core div-id.

    Täielik dokument koos lehe-haaval analüüsi ja failistruktuuriga elab repositories: project/fse-filosoofia.md. See on minu (ja Claude’i) otsuste vundament järgmistele sammudele.

    Järgmises postituses asume Avaleht Page’i ehitama ja PageHero patternit koostama. Vundament on käes – nüüd lehed.


    Praktiline lisa (#5 ja #6 järelmõte): millal kasutada pattern’it, millal mitte

    Pärast Projektipäevik #5 ja #6 ehitamist on selge: filosoofia on alus, aga otsus “kas see ON pattern” tuleb teha **iga elemendi kohta eraldi**. Lisan siia kolm asja, mida tahaksin olla algusest peale teadnud.

    1. Kontrast-tabel: mis elemendid Figmas kuuluvad kuhu

    ElementKasutuskohadSisuOtsus
    Hero (esileht)1 (avaleht)unikaalneAvaleht Page’i sisu – MITTE pattern
    PageHero5 (Meist, Kontakt, Privaatsus, Teenused-arhiiv, Blogi)varieerub iga lehe järgiPattern + post-title/post-excerpt (dünaamiline)
    HeaderKÕIKIDEL lehtedelsamaTemplate part (parts/header.html)
    FooterKÕIKIDEL lehtedelsamaTemplate part (parts/footer.html)
    Service cardTeenused-arhiiv + Avalehe Featuredsama struktuur, andmed CPT-stPattern Query Loop’i sees + Service CPT + ACF
    Team cardMeist-lehe Meeskondsama struktuur, andmed CPT-stPattern Query Loop’i sees + Team CPT + ACF
    Blog cardBlogi-listing + Single-postituse “Related”sama struktuur, andmed wp_posts’istPattern Query Loop’i sees (built-in posts)
    FAQ sectionTeenuse single-lehtunikaalne sisuService single-template’is – MITTE pattern
    Final CTAEsileht + Teenuse single + (potentsiaalselt rohkemgi)sama tekst kõikjalPattern reference
    Filters (Blogi listing)1 (Blogi listing)unikaalneTemplate sees – MITTE pattern

    Märkad mustrit: 3+ kasutuskohta + struktuuriline taaskasutus = pattern. 1 koht = ALATI Page sisu või template-sisu. 2 kohta on piiripealne – sõltub, kas sisu varieerub.

    2. Viis antimustrit (mida MITTE teha)

    ❌ “Iga komponent on pattern”: Avaleht Hero kahjustab klienti, kes tahab muuta teksti. Pattern on PHP-failis lukus – vaja arendajat ja git-commit’i.

    ❌ “Pattern lihtsuse pärast”: Kui klient küsib “kuhu muuta teksti”, ja vastad “ava VS Code, leia patterns/hero-home.php” – vale arhitektuur. Klient peab toimetama UI-st.

    ❌ “Pattern selleks, et oleks Git’is”: Page/Post sisu IS andmebaasis ja seda saab eksportida (näiteks wp post update või WXR-eksport). Ei pea sundima patternisse, et “kood Git’is”.

    ❌ “Loon mitu väikest pattern’it suurte asemel”: Card-Icon + Card-Title + Card-Excerpt eraldi pattern’idena → halduskulu kasvab, taaskasutuse väärtus ei lisandu. Eelista ühte korralikku Card-pattern’it.

    ❌ “Pattern, aga inline’itud template’is”: Site Editor’s pattern’i lisamine template’i inline’b markup’i (kui pattern pole synced). Template’i fail sisaldab kogu pattern’i markup’i, mitte reference’it. Tulemus: pattern’i muutused EI propageeru template’i (vt #6 Lõks 6). Lahendus: kirjuta käsitsi <!-- wp:pattern slug --> markup’is.

    3. Neli küsimust enne uue pattern’i loomist

    1. Mitu kasutuskohta? 1 → MITTE pattern (Page/Post sisu). 2+ → võibolla.
    2. Kas sisu varieerub kasutuskohas?
      • Jah, struktuuriliselt sama, andmed varieeruvad → pattern + post-data blokid (post-title, post-excerpt)
      • Jah, ka struktuur võib varieeruda → mitu pattern variantsiooni VÕI inline igas kohas
      • Ei, kõikjal sama → pattern reference
    3. Kas klient muudab teksti?
      • Jah → Page sisu VÕI post-data sees (Page Title, Page Excerpt jne)
      • Ei (staatiline brand-tekst) → pattern PHP-failis OK
    4. Kas template-sisene kasutus on võimalik?
      • Jah, ainult 1 template’s → otse template sees, mitte pattern
      • Jah, mitu template’s + Page’is võimalik → pattern reference

    Kõik need õppetunnid on detailselt talletatud project/casestudy.md Case 13-s.

    Järgmine etapp: CPT + ACF setup (Service, Team), pluss Avaleht Page sisu ehitamine. Pattern’imuusika hakkab tõsiselt mängima, kui meil on Query Loop’iga dünaamilist sisu päringult.

  • Projektipäevik #3: Vundament – pluginad, CPT-d, meta-väljad

    Kolmas postitus projektipäeviku sarjas. Eelmistes lugesime, mis on theme.json ja meie disainisüsteem ning mis on meie plaan ja arhitektuuri-otsused. Nüüd on viimaks tehtud vundamenditöö – kõik see, ilma milleta esilehe ehitama ei saa hakata.

    Mida vundament katab

    “Etapp 0” meie plaanis koosneb kuuest sammust:

    1. CPT UI + ACF pluginate installimine
    2. Service ja Team Custom Post Type’ide seadistamine
    3. Meta-väljade lisamine ACF kaudu
    4. Õhuke teema functions.php loomine
    5. (järgmine: patterns/ kausta loomine + Hero pattern)
    6. Testandmete loomine

    Selles postituses katab esimesed neli. Vaatame samme ükshaaval ja peatume olulistel õppetundidel.

    1. Pluginate paigaldamine

    Otsus, et CPT-d (Custom Post Type’d) lähevad plugin’isse, mitte teema functions.php-isse, oli teadlik (vaata #2 postitust). Põhjus: kui sa vahetad teemat, kas sait jääks tööle? CPT-d on sisu-loogika – peavad jääma alles isegi kui teema muutub.

    Praktikas valisime kaks levinumat tasuta plugin’it:

    • Custom Post Type UI (CPT UI) – GUI CPT-de registreerimiseks
    • Advanced Custom Fields (ACF) – meta-väljade haldamiseks

    Kompromissi-otsus oli, et õpetuslikus kontekstis pole praegu eesmärk PHP CPT-koodi õpetada. CPT UI annab kiire stardi GUI kaudu. Hiljem saab näidata, kuidas CPT UI-st kood eksportida ja oma plugin’i kolida – see on omaette õppetund “GUI vs koodipõhine konfiguratsioon”.

    2. CPT-d: Service ja Team

    CPT UI vorm võib esmapilgul tunduda hirmutav – kümneid välju, igaüks oma seadega. Aga enamus on intuitiivne, kui mõistad mõnda võtmemõistet:

    • Post type slug (service) – sisemine koodi-nimi, ei muutu hiljem
    • Custom Rewrite Slug (services) – URL-is nähtav
    • Has Archive – kas tekib koondleht (/services/)
    • Supports – millised toimetamise-väljad on saadaval (title, editor, thumbnail, excerpt)
    • Show in REST API – kriitiline, ilma selleta Block Editor ei tööta

    Service vs Team – vahed

    SeadeServiceTeam
    URL slugservicesteam
    Has ArchiveTrue (/services/ koondleht)False (kuvame Meist-lehel)
    ExcerptJah (lühikirjeldus kaardile)Ei
    Menu ikoondashicons-portfoliodashicons-groups

    Üks asi, mille üle veel mõtleme: /team/ URL renderdub praegu tühjalt (ainult footer), sest has_archive: false. WP ei loo Team arhiivilehte, ja sait langeb fallback’iks index.html-le. Pole hetkel probleem – Team kuvatakse Meist-lehel Query Loop’i kaudu.

    3. ACF meta-väljad – ja “Show in REST API” lõks

    Iga CPT vajab lisaks vaikimisi väljadele ka kohandatud välju:

    • Service Fields: price_from (Number), icon (Text), featured (True/False)
    • Team Member Fields: job_title (Text), email (Email), linkedin (URL)

    ACF Field Group on jaotatud kolme tabi vahel: Location Rules, Presentation, Group Settings. Iga üksiku Field on omakorda neljas tabis: General, Validation, Presentation, Conditional Logic. UI loogika selgus pärast paari katse-eksitust.

    Kõige olulisem lüliti: Show in REST API

    ACF Field Group Group Settings tab → lüliti Show in REST API. Vaikimisi VÄLJAS. Kui jätad nii, siis Block Editor ei näe ACF välju – ehk meie tulevased patternid ei saa neid kuvada. Lülita alati sisse.

    Sama oluline on Location Rules: iga grupp peab teadma, millise CPT juures ilmuda (Post Type = Service VÕI Team Member). Vastasel juhul grupp kas ei ilmu kuskil või kõikjal.

    4. Õhuke teema functions.php

    Klassikalises WP teemas oli functions.php “kõik-ühes-failis”. FSE teemas on see palju õhem. Lihtne otsustamise reegel: “kui sa eemaldad teemat, kas sisu jääks alles?” Kui jah → on plugin. Kui ainult visuaal muutub → on teemas.

    Meie teema functions.php teeb kolm asja:

    • add_theme_support() – deklareerib, mida teema toetab (post-thumbnails, HTML5)
    • register_block_style() – lisab Button blokile uue stiili “Secondary”
    • register_block_pattern_category() – loob 3 kategooriat patternide grupeerimiseks

    Kõik kasutab WordPressi hookide süsteemi – funktsioonid “liidetakse” sündmustele (after_setup_theme, init). See on WP-i tuum: mitte impereratiivne kood, vaid sündmuse-põhine “kui see juhtub, käivita too”.

    Terve functions.php pärast seda etappi

    Selle etapi lõpus näeb functions.php välja nii (PHP-võhikutele: salvesta see fail teema kausta nimega functions.php):

    <?php
    /**
     * fse-theme functions.php
     *
     * Theme support + block styles + pattern categories.
     * NB! CPT-d ja meta-väljad on plugin'ites (CPT UI + ACF), mitte siin.
     */
    
    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_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' );
    
    /**
     * 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' );
    

    See on versioon 1. Iga järgmise blogipostituse (kus muudame functions.php) näitame nii lisatava juppi kui kogu sel hetkel tervet faili.

    Vahepealse õppetund: slug ei tohi alata numbriga

    Ehitamise käigus jooksime kokku samasse lõksu, mida juba kirjeldasime esimeses postituses – aga seekord font-size slug’idega. Kõik h1, h2, h3 olid frontend’is sama suurusega (14px). Põhjus: WP normaliseeris CSS muutuja nimi (5xl5-xl), aga styles.elements.h2 viide jäi 5xl-iks – muutuja ei eksisteerinud, brauser kasutas fallback’i.

    Lahendus: ümbernimetatud kõik 2xl, 3xl, … 7xl kujule xxl, xxxl, … xxxxxxxl. Sama muster mis spacing’utel. Reegel: theme.json slug ei alga kunagi numbriga.

    Mis nüüd näeme

    Loodud on testandmed: üks Service (“SEO Optimization”) koos kõigi ACF väljadega ja üks Team Member (“Maria Saar”). Frontend’is näevad mõlemad korralikud välja:

    • H1 pealkiri suur ja silmatorkav (Inter font, 56px-ni)
    • H2/H3 alapealkirjad selgelt väiksemad
    • Body text lugemissõbralik (14px, 1.6 line-height)
    • Listid bullet-stiilis

    Üks väike asi veel: post-title on praegu sinine, sest index.html template’is on isLink: true ja link’i värv on accent-default. Tehniline detail, mille parandame, kui ehitame proper single.html template’i.

    Mis tuleb järgmisena

    Etapp 0 viimane samm: patterns/ kausta loomine ja esimene pattern (Hero-Home). Siis hakkame ehitama esilehte – kuus sektsiooni, igaüks oma pattern:

    1. Hero (juba olemas project/-s, vaja kolida teema kausta)
    2. Values – “Miks meie”
    3. Featured Services – kasutab äsja loodud CPT-d
    4. BlogPreview – 3 viimast postitust
    5. Testimonials
    6. Final CTA

    Iga sektsioon = eraldi õppetund. Iga pattern = uus kontseptsioon (Query Loop, Post Template, nested patterns, jne). Järgmises postituses tutvustame Hero patternit ja räägime, mis vahe on theme pattern’il ja template part’il.

  • Projektipäevik #2: Plaan ja arhitektuuri-otsused

    See on esimene postitus uues postituste sarjas “Projektipäevik”, kus dokumenteerime samm-sammult selle WordPress FSE teema ehitamist. Sari on mõeldud nii õpilastele kui ka tulevikus uutele meeskonnaliikmetele – et nad mõistaksid, miks projekt sellisena välja näeb, mitte ainult kuidas ta töötab.

    Mida me ehitame

    WordPress Full Site Editing (FSE) teema nimega fse-theme, mille disain pärineb Figma wireframidest. Lehte saab ehitada otse Site Editor’is, ilma et peaks teadma PHP-d või muutma malle koodi tasandil. Kogu kujundus tuleb theme.json-ist ja patternidest.

    Sait sisaldab 8 erinevat lehte (esileht, teenused, üksiku teenuse leht, meist, kontakt, blogi, blogipostitus, privaatsuspoliitika) ning kahte custom post type’i (Service ja Team). Disainisüsteem koosneb 38 muutujast: värvid, vahed, fondi-suurused, raadiused.

    Pedagoogiline lähenemine

    See projekt on ennekõike õppematerjal. Iga samm – alates theme.json seadistamisest kuni pattern-i loomiseni – on dokumenteeritud nii, et õpilane saab aru:

    • Miks me seda teeme (FSE-loogika, parimad tavad)
    • Kuidas see samm-sammult käib
    • Mida kontrollida, et veenduda õnnestumises

    Töövoog: AI-assistent (Claude/Cowork) selgitab ja dokumenteerib → kasutaja teeb. Ei lähe automaatselt edasi. Iga lõpetatud samm dokumenteeritakse projekti repos failis project/casestudy.md.

    Põhilised arhitektuuri-otsused

    1. Fluid spacing – üks väärtus, kohaneb ekraaniga

    FSE-l pole praegu native UI’d, mille kaudu määrata blokile erinevat padding’ut mobiilis ja desktopis. Lahenduseks valisime CSS clamp() funktsiooni spacingSizes preset’ides:

    { "slug": "xxxxxxl", "size": "clamp(40px, 3vw + 16px, 96px)" }

    Mobiilis ~40px, desktopis 96px, vahepeal sujuv üleminek. Põhimõte: mida väiksem spacing, seda väiksem muutus. UI-tihendid (4–16px) jäävad fikseerituks, sektsiooni-padding’ud (32px+) kahanevad mobiilis. Üks padding-väärtus footer’is töötab kõigil ekraanidel – pole vaja media query’sid.

    2. Custom Post Type’d plugin’is, mitte teemas

    WordPressi ametlik soovitus: “If you switch themes, you shouldn’t lose your content.” Kui registreeriksid Service CPT teema functions.php-s, siis teema vahetusel kaoks menüüpunkt “Teenused” admin’ist (postitused küll jääksid DB-sse, aga “orvuks”).

    Õige eraldus:

    • Teema – kujundus (CSS, templates, layout, theme.json)
    • Plugin – funktsionaalsus (CPT-d, meta-väljad, integratsioonid)

    Valisime CPT UI plugin’i CPT-de registreerimiseks (kõige levinum, 5M+ installi) ja ACF meta-väljade jaoks. Põhjus: õpetuslikus kontekstis pole praegu eesmärk PHP CPT-koodi õpetada – keskendume FSE-le. CPT UI annab kiire stardi GUI kaudu.

    3. Page-by-page lähenemine

    Kahe peamise lähenemise vahel – “ehita kõik komponendid valmis, siis lehed” vs “leht-haaval otsast lõpuni” – valisime teise. Põhjus: kasutaja näeb kohe töötavat tulemust, iga sektsioon on selge sammuga seotud, ja õpilane saab visuaalselt jälgida progressi.

    Esimene leht: esileht, mis koosneb 6 sektsioonist: Hero (juba olemas), Values, Featured Services, Blog Preview, Testimonials, Final CTA. Igale sektsioonile vastab üks (või kaks) pattern’it.

    4. Dokumenteerimine kahes kohas

    • project/casestudy.md – tehnilised case study’d (probleem → lahendus formaadis), versioonihalduses Git’i kaudu
    • WordPress’i blogi (see postituste sari) – “miks” ja “kuidas” narratiivina, avalik, läbi WP admin’i loetav

    Mis on praegu valmis

    • theme.json – disainisüsteem (38 muutujat) + fluid spacing’ud
    • parts/header.html ja parts/footer.html – template parts
    • CPT UI ja ACF pluginad installitud ja aktiveeritud
    • Inter fondid integreeritud

    Mis tuleb järgmisena

    1. CPT-de seadistamine CPT UI kaudu – Service ja Team
    2. Meta-väljade lisamine ACF kaudu – hind, ametinimetus, jne
    3. Õhuke teema functions.php – ainult block styles + pattern categories
    4. Esimesed patternid: card-value, values-grid (“Miks meie” sektsioon)
    5. Edasi punkti haaval iga sektsiooni kaudu kuni esileht on koos

    Järgmises postituses – CPT-de seadistamine ja miks me valisime just need konkreetsed sätted (slug, supports, has_archive jne).

  • Projektipäevik #1: theme.json – disainisüsteemi süda

    Projektipäeviku esimene postitus tutvustab theme.json faili – WordPress Full Site Editing teema tähtsaimat üksikfaili. See on koht, kus elab kogu disainisüsteem: värvid, vahed, fondi-suurused, raadiused, varjud. Kogu kujundus voolab siit edasi nii Site Editor’i UI-sse kui ka frontendi.

    Mis on theme.json?

    Kuni WordPress 5.9 oli FSE veel eksperimentaalne, ja teemastiilide haldamine käis CSS-failide ja PHP-koodi kaudu. Alates 5.9-st on theme.json ametlik viis defineerida:

    • Settings – mis disaini-väärtused on saadaval (värvi-palett, spacingu skaala, fondi-perekonnad)
    • Styles – kuidas vaikimisi asjad välja näevad (body taust, h1-h6, button’id, lingid)
    • Block-spetsiifilised stiilid – eraldi seaded igale bloki-tüübile
    • Template parts – header, footer, jne registreerimine

    Üks theme.json-i seade võib mõjutada nii Block Editor’i UI-d kui ka avaldatud lehte. WordPress genereerib selle põhjal CSS muutujad ja Block Editor’i konfiguratsiooni automaatselt.

    Meie projekti disainisüsteem

    Figma’st tõime üle 38 muutujat nelja kategooriasse:

    Värvid (11 tk) – settings.color.palette

    Mitte kasutame “Background”, “Text”, “Accent” – vaid täpsemad nimed: bg-default, bg-gray, bg-dark, text-default, text-muted, text-light, text-inverse, accent-default, border-default, border-strong, placeholder. Iga slug muutub CSS muutujaks: var(--wp--preset--color--bg-default).

    Vahed (10 tk) – settings.spacing.spacingSizes

    Spacingute skaala mahajääb xs-ist (4px) kuni xxxxxxl-ini (96px). Suuremad väärtused (xl ja edasi) on tehtud fluid’iks CSS clamp() funktsiooniga:

    { "slug": "xxxxxxl", "size": "clamp(40px, 3vw + 16px, 96px)" }

    Tulemus: mobile-vaates 40px, desktop’is 96px, vahepeal sujuv üleminek. Mida väiksem spacing, seda väiksem muutus – UI-tihendid (4-16px) jäävad fikseerituks, sektsiooni-padding’ud (32px+) kahanevad mobiilis. Üks padding-väärtus, töötab kõigil ekraanidel.

    Fondi-suurused (12 tk) – settings.typography.fontSizes

    Sarnaselt spacingule: xs (12px) → xxxxxxxl (56px). Samuti fluid clamp() väärtustega. Vaikimisi seadame Inter-perekonna ja line-height’i relaxed (1.6).

    Raadiused (4 tk) – settings.custom.radius

    sm (4px), md (6px), lg (8px), pill (999px). Custom muutujad on tarbitavad var(--wp--custom--radius--md) kaudu.

    Õppetund: slug ei tohi alata numbriga

    Ehitamise käigus jooksime kokku ühe peidetud lõksuga. Algselt olid font-size slug’id numbri-prefiksiga: 2xl, 3xl, 5xl, 7xl – Tailwind’i stiilis. Aga frontend’is jäid kõik h1, h2, h3 ja paragraafid täpselt ühe suurusega (14px).

    Põhjus

    WordPress normaliseerib slug’i CSS muutuja nimes: kui slug algab numbriga ja siis tuleb täht, lisatakse sidekriips:

    theme.json slug:  "5xl"
    WP CSS muutuja:   --wp--preset--font-size--5-xl    (sidekriipsuga!)

    AGA: kui sa kasutad seda muutujat styles.elements.h2-s nii:

    "fontSize": "var(--wp--preset--font-size--5xl)"

    …siis WordPress ei normaliseeri seda viidet. Tulemus: muutuja, millele h2 viitab, ei eksisteeri. Brauser kasutab fallback’i (body font-size = 14px). Kõik pealkirjad jäävad 14px.

    Lahendus

    Vältida numbri-prefiksiga slug’e. Asendasime kõik vormid mitme x-iga:

    Vana slugUus slugSuurus
    2xlxxl24px
    3xlxxxl28px
    4xlxxxxl32px
    5xlxxxxxl40px
    6xlxxxxxxl48px
    7xlxxxxxxxl56px

    Sama lõksu märkasime varem ka spacing’utes – sealgi 2xl-vormid ei töötanud, ja xxl-vorm lahendas selle. Niisiis: kogu projektis ühtne reegel – slug ei algagi numbriga.

    theme.json kihiline süsteem

    Üks oluline mõiste mõista: theme.json-il on kolm “kihti”, mille väärtused omakorda kombineeruvad:

    1. WordPress core – vaikimisi värvid, fondid, vahed (sa võid need vaikimisi välja lülitada defaultPalette: false jne kaudu)
    2. Teema theme.json – see, mida meie defineerime
    3. Kasutaja kohandused – kui kasutaja muudab Site Editor’is Styles’i

    Hilisem kiht võidab varasema. Meie näiteks lülitame välja WordPress’i vaikimisi värvi-paleti, sest see segab Figma disainisüsteemi. Selleks: "defaultPalette": false.

    Mida theme.json EI tee

    • Ei sisalda templates’eid (need on templates/*.html failid)
    • Ei sisalda patterneid (need on patterns/*.php failid)
    • Ei sisalda CPT-d (need lähevad plugin’isse)
    • Ei sisalda dünaamilist loogikat (PHP – see on functions.php)

    theme.json on ainult deklaratiivne konfiguratsioon: “millised väärtused on, kuidas vaikimisi näevad”. Kogu ülejäänud loogika elab teistes failides.

    Edasi: arhitektuuri-otsused

    Järgmises postituses (Projektipäevik #2) räägime laiematest arhitektuuri-otsustest: kus CPT-d elavad (plugin vs teema), miks valisime page-by-page lähenemise, ja kuidas dokumenteerime kogu protsessi.

  • Projektipäevik #0: Mida me teeme – sissejuhatus FSE-teema ehitamise teekonda

    Tere tulemast Projektipäeviku juurde. See on dokumenteeritud teekond ühe konkreetse WordPress’i teema ehitamisest – nullist, Figma wireframide järgi, kasutades WordPress’i kõige uuemat FSE (Full Site Editing) arhitektuuri.

    Iga sammu – õigeid ja valesid – kirjutan üles. Eesmärk on, et need postitused töötaksid ka case-study’na õpilastele: kuidas FSE-mõtteviisile mõelda, kuhu iga element kuulub, ja millised lõksud teel ootavad.

    Mis projekt see on

    Ehitame täielikult tehtud WordPress’i teema nimega fse-theme. Lähtekohaks on Figma wireframe-fail (file key Hh3rceSDR02v3xZlGX0Za1), mille kavandid katavad 8 erinevat lehte:

    • Avaleht (front-page) – Hero, Values, Featured Services, BlogPreview, Testimonials, FinalCTA
    • Teenused-arhiiv (services listing) – PageHero + ServiceCards grid
    • Üksiku teenuse leht – ServiceHero (breadcrumb + CTA), Body + Sidebar (hinnaga), FAQ
    • Meist – PageHero + Story + Values + TeamGrid
    • Kontakt – PageHero + Form + Info
    • Blogi-listing – PageHero + Filters + Featured + Grid + Pagination
    • Blogipostitus – PostHead + Cover + Content + Related
    • Privaatsuspoliitika – PageHero + TOC + Sisu sektsioonid

    Mobile + Desktop variandid igale, 16 kavandit kokku.

    Miks just FSE?

    WordPress 6.0+ tutvustas Full Site Editing – uus paradigma, kus kogu sait (template’id, header, footer, värvid, fondid) on toimetatav Block Editor’is, JSON-failidena ja patternitena. Klassikaline PHP get_header() / the_content() / WordPress Customizer asendub 5-kihilise süsteemiga:

    1. Template (templates/*.html) – lehe luustik
    2. Template part (parts/*.html) – korduvad osad
    3. Pattern (patterns/*.php) – disainimallid
    4. Page/Post sisu (wp_posts) – konkreetse lehe sisu
    5. CPT entry + ACF meta – dünaamiline andmestik

    Iga element vajab teadlikku otsust: millisesse kihti see kuulub? Selle filosoofiaga lahkame me Projektipäevik #4-s põhjalikult.

    Mida me valmis teeme

    Lõpptulemus on installitav ja kasutatav WordPress teema, mis sisaldab:

    • theme.json – disainisüsteem (värvid, fondid, fluid spacing, layout-laiused)
    • style.css – teema metaandmed
    • functions.php – õhuke (theme support, block styles, pattern categories)
    • parts/ – header.html, footer.html
    • templates/ – front-page, page, single, archive-service, single-service, home, single (blog), 404, index
    • patterns/ – page-hero, values-grid, service-hero, faq-section, team-grid, testimonials, blog-preview, related-posts, card-service, card-team, card-blog, card-value, final-cta, contact-form (~14 patternit)
    • assets/fonts/ – Inter font (Variable)

    Plus tugimaterjalid (mitte teemas, vaid project/ kaustas):

    • casestudy.md – iga õpitud lõks Case-formaadis (eeldatav lõppmaht 15–20 Case’i)
    • fse-filosoofia.md – arhitektuuri filosoofia
    • wordpress-fse-arhitektuur.md – tehniline otsuste pohi
    • PATTERN-LOOMISE-JUHEND.md – iga patterni struktuur

    Kuidas me ehitame (töömeetod)

    Töö käib paaris Claude’iga (AI-assistant) järgmisel mustril:

    • Claude selgitab ja dokumenteerib. Mitte automaatselt ära ei tee, vaid juhendab samm-sammult.
    • Mina (inimene) ehitan – Site Editor’s, klassikalises Block Editor’s, terminal’is, koodifailides.
    • Iga sammu kohta: miks + kuidas + mida kontrollida.
    • Iga lõks dokumenteeritaksecasestudy.md-sse uus Case, ja Projektipäeviku postitusena blogis.

    See pole “Claude tegi mulle teema valmis” lugu. See on “Claude õpetas mulle, kuidas FSE’d ehitada – ja iga apsakas on talletatud, et õpilased ei peaks samu vigu tegema”. Tegelik kood on inimese tehtud, kommentaarid ja dokumentatsioon on Claude’i abiga vormistatud.

    Etapilik roadmap

    Etapp 0 – Vundament

    • 0.1 CPT UI + ACF pluginate installimine (sisu vs kujundus eraldus)
    • 0.2 Service + Team CPT-de seadistamine GUI-st
    • 0.3 ACF meta-väljade lisamine (price_from, featured, job_title jne)
    • 0.4 Õhuke functions.php (block styles + pattern categories, MITTE CPT-d)
    • 0.5 Esimene pattern (PageHero)
    • 0.6 Testandmed

    Etapp 1 – Esileht

    • 1.1 Hero (Avaleht Page sisu, mitte pattern – 1 kasutuskoht!)
    • 1.2 Values sektsioon (card-value + values-grid patternid)
    • 1.3 Featured Services (Query Loop pattern Service CPT-ga)
    • 1.4 BlogPreview (Query Loop pattern wp_posts’ist)
    • 1.5 Testimonials
    • 1.6 FinalCTA
    • 1.7 Esilehe template (front-page.html) kokkupanek

    Etapp 2 – Muud lehed

    • Teenused-arhiiv → Single teenus → FAQ
    • Meist (Story + Values + TeamGrid)
    • Kontakt (vorm + info)
    • Blogi-listing (home.html) → Single postitus → Related
    • Privaatsuspoliitika (TOC + sisu)
    • 404

    Mida iga Projektipäeviku postitus sisaldab

    Üks postitus = üks etapp / üks õppetund / üks komponent. Iga postituses on:

    • Plaan: mis selle etapi eesmärk on ja miks
    • Tegevus: mis valmis sai (failid, blokid, seaded)
    • Lõksud: mis nähtus, mis ootuspärasega ei ühti – koos põhjuse ja lahendusega
    • Reegel: üldistus – mida sa peaksid teadma tulevikus
    • Mis järgmiseks: ühenduslik järgmise postitusega