Rejestracja CPT w motywie

gabriela

Artykuł z 21 kwietnia, 2026

Jak rejestrować Custom Post Types w motywie, bez użycia wtyczki

Custom Post Type (CPT) to jeden z fundamentów każdego niestandardowego projektu w WordPressie. Jeśli klient potrzebuje czegoś więcej niż posty i strony – np. Oferty pracy, Portfolio, Członkowie zespołu czy Opinie – właśnie po to sięgamy po CPT.

W tym wpisie zawarta jest instrukcja, jak rejestrować CPT w sposób czysty i spójny z konwencjami przyjętymi w projekcie.

Zanim jednak przejdziesz dalej, warto zapoznać się też z oficjalną dokumentacją WordPressa – znajdziesz tam pełną listę wszystkich dostępnych parametrów funkcji register_post_type(): developer.wordpress.org/reference/functions/register_post_type

Gdzie umieścić kod?

Nie wrzucamy rejestracji CPT bezpośrednio do functions.php. Zamiast tego tworzymy osobny plik w folderze includes motywu:

kadence-child/
├── functions.php
└── includes/
    └── custom-post-type-projects.php

Następnie w functions.php podpinamy go jedną linijką:

require_once get_template_directory() . '/includes/custom-post-type-projects.php';

Każdy CPT rejestrujemy w tej samej konwencji – jako oddzielne pliki w folderze includes.

Zasada: slug po angielsku, URL po polsku (bądź w innych językach)

Slug używany przy rejestracji CPT – ten podawany jako pierwszy argument register_post_type() – zawsze piszemy po angielsku.

Dlaczego? Bo ten identyfikator pojawia się wszędzie w kodzie: w zapytaniach WP_Query, w funkcjach pomocniczych, w warunkach is_singular(), w nazwach hooków. Poza tym angielski jest standardowym językiem pisania kodu – nazwy zmiennych, funkcji, klas i identyfikatorów piszemy po angielsku niezależnie od tego, w jakim języku jest strona. Angielski slug jest neutralny, czytelny dla każdego dewelopera i nie sprawia problemów z kodowaniem znaków.

// ✅ Dobrze
register_post_type( 'projects', $args );
register_post_type( 'team-members', $args );
register_post_type( 'job-offers', $args );

// ❌ Źle – polskie znaki i język ojczysty w identyfikatorze
register_post_type( 'projekty', $args );
register_post_type( 'członkowie-zespołu', $args );

To samo dotyczy nazwy funkcji – register_post_type_projects(), nie register_post_type_projekty().

Jeśli jednak zależy Ci, żeby adres URL na stronie wyglądał po polsku (np. /projekty/nazwa-projektu/ zamiast /projects/nazwa-projektu/), możesz to ustawić niezależnie przez parametr rewrite. Slug rejestracji i slug w URL to dwie osobne rzeczy – nie musisz wybierać jednego z nich.

Ważne: ustawienie polskiego slugu na sztywno w rewrite ma sens tylko na stronach jednojęzycznych. Jeśli strona jest wielojęzyczna (WPML, Polylang itp.), każda wersja językowa potrzebuje własnego slugu – i każda z tych wtyczek ma do tego dedykowany interfejs. W takim przypadku w rewrite zostawiasz domyślny angielski slug (lub pomijasz ten parametr), a tłumaczenia slugów konfigurujesz po stronie wtyczki językowej.

Anatomia funkcji rejestrującej CPT

Poniżej pełny przykład dla fikcyjnego CPT „Projekty”. Przeanalizuj każdy parametr – komentarze wyjaśniają, co robi i kiedy go zmieniać.

if ( ! function_exists( 'register_post_type_projects' ) ) {

    function register_post_type_projects() {

        $labels = array(
            // Nazwa w liczbie mnogiej – widoczna np. w bocznym menu WP
            'name'               => 'Projekty',

            // Nazwa w liczbie pojedynczej – pojawia się np. w "Edytuj Projekt"
            'singular_name'      => 'Projekt',

            // Etykieta w bocznym menu admina
            'menu_name'          => 'Projekty',

            // Nazwa na górnym pasku administracyjnym (Admin Bar)
            'name_admin_bar'     => 'Projekt',

            // Tekst przycisku "Dodaj nowy" na liście wpisów
            'add_new'            => 'Dodaj nowy',

            // Tytuł strony edytora przy tworzeniu nowego wpisu
            'add_new_item'       => 'Dodaj nowy projekt',

            // Etykieta przy tworzeniu nowego wpisu
            'new_item'           => 'Nowy projekt',

            // Tytuł strony edytora przy edycji istniejącego wpisu
            'edit_item'          => 'Edytuj projekt',

            // Tekst linku "Zobacz" prowadzącego na frontend
            'view_item'          => 'Zobacz projekt',

            // Etykieta linku "Wszystkie..." w podmenu
            'all_items'          => 'Wszystkie projekty',

            // Placeholder pola wyszukiwania w panelu admina
            'search_items'       => 'Szukaj projektów',

            // Etykieta nadrzędnego wpisu (używana tylko gdy hierarchical => true)
            'parent_item_colon'  => 'Nadrzędny projekt:',

            // Komunikat wyświetlany gdy lista wpisów jest pusta
            'not_found'          => 'Nie znaleziono projektów',

            // Komunikat wyświetlany gdy kosz jest pusty
            'not_found_in_trash' => 'Nie znaleziono projektów w koszu',
        );

        $args = array(
            'labels'             => $labels,

            // Czy CPT jest widoczny publicznie – na frontendzie i w adminie.
            // Ustawiając false ukrywasz CPT całkowicie (np. dane wewnętrzne).
            'public'             => true,

            // Czy wpisy można odpytywać przez URL (?post_type=projects).
            // Zazwyczaj identyczne z 'public'.
            'publicly_queryable' => true,

            // Czy CPT pojawia się w panelu administracyjnym.
            'show_ui'            => true,

            // Czy CPT jest dostępny w opcjach tworzenia menu (Wygląd → Menu).
            'show_in_nav_menus'  => true,

            // Czy CPT pojawia się jako pozycja w bocznym menu admina.
            // Możesz też podać slug istniejącego menu – wtedy CPT staje się jego podpozycją,
            // np. 'tools.php' umieszcza go pod "Narzędzia".
            'show_in_menu'       => true,

            // Czy CPT jest dostępny przez WP REST API.
            // ZAWSZE ustawiaj true jeśli używasz edytora blokowego (Gutenberg)!
            'show_in_rest'       => true,

            // Ikona w bocznym menu admina.
            // Pełna lista: https://developer.wordpress.org/resource/dashicons/
            // Możesz też podać URL do własnej ikony (SVG lub obraz 20x20 px).
            'menu_icon'          => 'dashicons-portfolio',

            // Pozycja w menu bocznym. null = domyślna (poniżej Komentarzy).
            // Inne przydatne wartości: 5 (po Wpisach), 20 (po Stronach), 25 (po Komentarzach).
            'menu_position'      => null,

            // Czy CPT ma własną zmienną zapytania (?projects=slug).
            'query_var'          => true,

            // Model uprawnień. 'post' dziedziczy uprawnienia standardowych wpisów.
            // Zmień na 'page' jeśli CPT ma być hierarchiczny.
            'capability_type'    => 'post',

            // false = CPT płaski (jak posty).
            // true  = CPT hierarchiczny (jak strony) – wpisy mogą mieć dzieci.
            'hierarchical'       => false,

            // Czy WordPress ma tworzyć stronę archiwum (/projects/).
            // Możesz też podać string – stanie się on slugiem archiwum.
            'has_archive'        => false,

            // true  = wpisy CPT są WYKLUCZONE z wyników wyszukiwania WP.
            // false = wpisy pojawiają się w wynikach wyszukiwania.
            'exclude_from_search'=> false,

            // Czy wpisy CPT można eksportować przez Narzędzia → Eksportuj.
            'can_export'         => true,

            // Czy wpisy CPT mają być usuwane razem z ich autorem.
            'delete_with_user'   => false,

            // Które pola i sekcje mają być dostępne w edytorze.
            // Dostępne wartości:
            //   'title'           – pole tytułu
            //   'editor'          – edytor treści (Gutenberg / TinyMCE)
            //   'thumbnail'       – obrazek wyróżniający
            //   'excerpt'         – pole skrótu / opisu
            //   'author'          – pole autora wpisu
            //   'comments'        – sekcja komentarzy
            //   'custom-fields'   – klasyczna metabox z polami niestandardowymi
            //   'revisions'       – historia wersji
            //   'page-attributes' – pola "Kolejność" i "Rodzic"
            'supports'           => array(
                'title',
                'editor',
                'thumbnail',
                'custom-fields',
                'revisions',
                'page-attributes',
            ),

            // Taksonomie przypisane do CPT.
            // Możesz użyć wbudowanych ('category', 'post_tag')
            // lub własnych zarejestrowanych przez register_taxonomy().
            // WAŻNE: niestandardową taksonomię musisz najpierw zarejestrować
            // w register_taxonomy(), a następnie tu do niej się odwołać.
            'taxonomies'         => array( 'project-category' ),

            // Konfiguracja przepisywania adresów URL (permalinków).
            //
            // Domyślnie WordPress użyje slugu rejestracji jako bazy URL,
            // więc adresy wyglądałyby tak: /projects/nazwa-projektu/
            //
            // Parametr 'rewrite' pozwala to zmienić niezależnie od slugu rejestracji.
            // Dzięki temu slug w kodzie pozostaje angielski, a URL na stronie – polski.
            //
            //   'slug'       => 'projekty'  – baza URL: /projekty/nazwa-projektu/
            //   'with_front' => true        – czy dodawać prefix z ustawień permalinków
            //                                 (np. /blog/ jeśli masz ustawiony taki prefix).
            //                                 Zazwyczaj false dla CPT, żeby nie duplikować prefixu.
            //   'feeds'      => false       – czy generować feed RSS dla tego CPT
            //   'pages'      => true        – czy obsługiwać paginację (/projekty/page/2/)
            //
            // Możesz też ustawić rewrite => false, żeby całkowicie wyłączyć permalinki
            // (np. dla CPT używanego wyłącznie wewnętrznie, bez stron na frontendzie).
            //
            // ⚠️  UWAGA – strony wielojęzyczne (WPML, Polylang i podobne):
            // Jeśli strona działa w więcej niż jednym języku, nie wpisuj tu
            // slugu na sztywno. Każda wtyczka do wielojęzyczności ma własny
            // mechanizm tłumaczenia slugów CPT – ustaw go tam, a w 'rewrite'
            // zostaw domyślny angielski slug (lub całkowicie pomiń ten parametr).
            // Wpisanie polskiego slugu na sztywno przy wielojęzycznej stronie
            // spowoduje, że tylko jedna wersja językowa będzie działać poprawnie.
            'rewrite'            => array(
                'slug'       => 'projekty',
                'with_front' => false,
                'feeds'      => false,
                'pages'      => true,
            ),
        );

        // Pierwszy argument to unikalny slug CPT.
        // Zasady: tylko małe litery, cyfry i myślniki, max 20 znaków.
        // Unikaj nazw zarezerwowanych przez WP: post, page, attachment,
        // revision, nav_menu_item, custom_css i kilku innych.
        register_post_type( 'projects', $args );
    }

    // Rejestrujemy CPT na hooku 'init' z priorytetem 5 (domyślny to 10).
    // Priorytet 5 sprawia, że CPT rejestruje się wcześniej – taksonomie
    // i inne elementy zależne mogą się wtedy do niego bezpiecznie podpiąć.
    add_action( 'init', 'register_post_type_projects', 5 );
}

Szablon do skopiowania

Gdy potrzebujesz kolejnego CPT, skopiuj ten blok, zamień wszystkie wystąpienia SLUG oraz etykiety i gotowe:

if ( ! function_exists( 'register_post_type_SLUG' ) ) {

    function register_post_type_SLUG() {

        $labels = array(
            'name'               => 'NAZWA MNOGA',
            'singular_name'      => 'NAZWA POJEDYNCZA',
            'menu_name'          => 'NAZWA W MENU',
            'name_admin_bar'     => 'NAZWA POJEDYNCZA',
            'add_new'            => 'Dodaj nowy',
            'add_new_item'       => 'Dodaj nowy ELEMENT',
            'new_item'           => 'Nowy ELEMENT',
            'edit_item'          => 'Edytuj ELEMENT',
            'view_item'          => 'Zobacz ELEMENT',
            'all_items'          => 'Wszystkie ELEMENTY',
            'search_items'       => 'Szukaj ELEMENTÓW',
            'parent_item_colon'  => 'Nadrzędny ELEMENT:',
            'not_found'          => 'Nie znaleziono ELEMENTÓW',
            'not_found_in_trash' => 'Nie znaleziono ELEMENTÓW w koszu',
        );

        $args = array(
            'labels'             => $labels,
            'public'             => true,
            'publicly_queryable' => true,
            'show_ui'            => true,
            'show_in_nav_menus'  => true,
            'show_in_menu'       => true,
            'show_in_rest'       => true,
            'menu_icon'          => 'dashicons-IKONA',
            'menu_position'      => null,
            'query_var'          => true,
            'capability_type'    => 'post',
            'hierarchical'       => false,
            'has_archive'        => false,
            'exclude_from_search'=> false,
            'can_export'         => true,
            'delete_with_user'   => false,
            'supports'           => array(
                'title',
                'editor',
                'thumbnail',
            ),
            'taxonomies'         => array(),
            'rewrite'            => array(
                'slug'       => 'SLUG-PO-POLSKU',
                'with_front' => false,
                'feeds'      => false,
                'pages'      => true,
            ),
        );

        register_post_type( 'SLUG', $args );
    }

    add_action( 'init', 'register_post_type_SLUG', 5 );
}

Checklista po dodaniu nowego CPT

Zanim zamkniesz zadanie, przejdź przez tę listę:

  • Slug CPT jest unikalny, ma max 20 znaków i nie koliduje z nazwami zarezerwowanymi przez WP
  • Slug rejestracji jest po angielsku (projects, nie projekty)
  • Nazwa funkcji jest zgodna z konwencją: register_post_type_{slug}
  • Wszystkie etykiety w $labels są przetłumaczone i mają sens w kontekście projektu
  • Ikona menu_icon pasuje do tematu CPT
  • show_in_rest jest ustawione na true (wymagane dla Gutenberga)
  • Slug w rewrite => 'slug’ jest po polsku i zgodny z oczekiwaniami klienta co do URL
  • Taksonomie w 'taxonomies’ zostały już wcześniej zarejestrowane
  • Po zapisaniu pliku: Ustawienia → Bezpośrednie odnośniki → Zapisz zmiany – bez tego kroku nowe adresy URL nie będą działać!