MyGenify

Using Custom Post Types to Store Names in WordPress

Table of Contents
AI-generated illustration; not taken on location · A conceptual dashboard of a name generator with gender, origin, and style filters on the left and a grid of name cards on the right.
AI-generated illustration; not taken on location · Conceptual illustration of a name generator dashboard with filter controls and result cards.AI-generated illustration

Building a name generator site like mygenify.cloud on WordPress starts with a honest architectural question: where do the names live, and how can the WordPress admin edit them without touching code? The answer that fits the platform is a custom post type (CPT) that treats each name as its own content entry. This gives you the familiar list and edit screens, revision history, and REST API support for free, while keeping the data model clean enough for fast filtering.

This article walks through the full implementation path: registering the CPT in a site-specific plugin, defining labels and supports, attaching taxonomies and metadata for filters, writing efficient generator queries, delivering no‑refresh results with vanilla JavaScript, and adding small retention features like copy-to-clipboard and localStorage favorites. It uses Blue Studio as a functional reference only—you should aim to match its feature set, not its visual design. The approach stays plugin‑owned, so your data layer survives theme changes and stays portable.

Data model and CPT registration

The core of the data model is a custom post type named mg_name, where each post represents one name. Register it in a small site-specific plugin, not in your theme, so that activation and deactivation are explicit. Hook the registration to the init action, keep the post type key under 20 characters, and use a short prefix like mg_ to avoid collisions with other plugins.

Registration parameters

add_action( 'init', function() {
    register_post_type( 'mg_name', [
        'public'       => false,
        'show_ui'      => true,
        'show_in_rest' => true,
        'labels'       => [ /* see next section */ ],
        'supports'     => [ 'title', 'editor', 'excerpt', 'thumbnail' ],
        'menu_icon'    => 'dashicons-id',
    ] );
} );

Setting public to false prevents the CPT from having public single pages, which is fine for a data layer. show_ui => true gives you the admin screens. show_in_rest => true exposes the CPT through the REST API—this is essential for your generator to read names without a full page reload. For an even lighter endpoint, you can register a custom REST route, which we’ll cover later.

One post per name

Keep the rule simple: one post per distinct name variant. If you have localizations or transliterations, store them as meta fields (like mg_variants) rather than creating a separate post. This keeps the query logic predictable and avoids duplicate results.

Taxonomies for filterable attributes

The filters users need—gender, origin, style, theme—are hierarchical or non-hierarchical categories that map perfectly to WordPress taxonomies. Register them in the same plugin, and associate them with mg_name.

add_action( 'init', function() {
    register_taxonomy( 'mg_gender', 'mg_name', [
        'hierarchical' => true,
        'show_in_rest' => true,
    ] );
    // Repeat for mg_origin, mg_style, mg_theme
} );

Taxonomies allow you to reuse the same terms across many names, and they are filterable with tax_query. They also display in the admin as familiar checkbox lists, which content operators will appreciate.

Post meta for scalar and range attributes

Attributes like syllable count, rarity score, and pronunciation are better stored as post meta. Meta values are efficient to query with meta_query, and you can index them when the library grows. Use the mg_ prefix for all keys.

Examples:

  • mg_syllables (integer)
  • mg_rarity (integer or decimal)
  • mg_pronunciation (text)
  • mg_variants (array, stored as JSON)

You can register these meta keys with register_post_meta() to make them visible in the REST API, which is useful if you ever need to expose them to the front end without a custom route.

Why not plain JSON or a custom table?

For a small library (say, a few hundred names), JSON files or a custom table might seem simpler, but you lose the admin UI, revision control, and the ability to delegate editing to non-technical staff. A CPT gives you all that out of the box. If the library grows beyond what WP_Query handles comfortably—typically tens of thousands of posts—you can add a custom table or a transient index, as discussed in the scaling section.

Admin labels and supports

Good labels make the CPT feel like a first-class citizen in the admin. Define them in the labels argument of register_post_type(). Here’s a practical set:

'labels' => [
    'name'               => __( 'Names', 'mygenify' ),
    'singular_name'      => __( 'Name', 'mygenify' ),
    'add_new'            => __( 'Add New Name', 'mygenify' ),
    'edit_item'          => __( 'Edit Name', 'mygenify' ),
    'all_items'          => __( 'All Names', 'mygenify' ),
    'search_items'       => __( 'Search Names', 'mygenify' ),
],

These labels appear in the menu and in the list screen, making the section instantly recognizable.

Choosing post type supports

  • title – always needed; the title is the name itself.
  • custom-fields – necessary if you use the default meta box for quick editing.
  • editor – optional; you might use it for a longer description or the name’s story.
  • excerpt – optional; handy for a short meaning or cultural note.
  • thumbnail – optional; useful if you allow images for themed names, but it can bloat the database.

Weigh the trade‑off: each support adds interface clutter. For a lean data layer, title and custom-fields are often enough.

Structured fields with SCF or ACF

Built‑in custom fields are a blank text field, which is error‑prone. Use a plugin like SCF (Secure Custom Fields) or ACF (Advanced Custom Fields) to define fields for syllable count, rarity, pronunciation, and other attributes. These plugins give you dropdowns, number inputs, and repeaters, and they store values as post meta.

A typical field group for mg_name might include:

  • Syllable count (number)
  • Rarity (select or number)
  • Pronunciation (text)
  • Meaning (textarea)
  • Variants (repeater)

With SCF, you can show these fields only on the mg_name edit screen, keeping the UI clean.

CSV import for bulk loading

For the initial library or ongoing growth, a CSV importer is a must. Plugin choices:

  • WP All Import – likely the most flexible; you can map CSV columns to fields, taxonomies, and meta. It handles thousands of rows without timing out, though you may need the pro version for scheduled imports.
  • Built‑in WordPress importer – not ideal for custom data; it’s designed for posts and pages.

A good practice is to write a small custom importer script that uses wp_insert_post() and wp_set_object_terms() for taxonomies and update_post_meta() for meta. That gives you full control and can be run during a downtime window.

Efficient generator queries

The generator’s core is a query that returns a random batch of names matching the selected filters. The goal is to keep it fast and avoid loading unnecessary data.

Basic WP_Query with tax_query and meta_query

When a user selects a gender and a style, you build a query like:

$args = [
    'post_type'      => 'mg_name',
    'posts_per_page' => 12,
    'no_found_rows'  => true,
    'orderby'        => 'rand',
    'tax_query'      => [
        'relation' => 'AND',
        [
            'taxonomy' => 'mg_gender',
            'field'    => 'slug',
            'terms'    => [ 'female', 'neutral' ],
        ],
        [
            'taxonomy' => 'mg_style',
            'field'    => 'slug',
            'terms'    => 'modern',
        ],
    ],
    'meta_query' => [
        [
            'key'     => 'mg_syllables',
            'value'   => 2,
            'compare' => '<=',
        ],
    ],
];
$query = new WP_Query( $args );

Use no_found_rows => true because you don’t need pagination; that skips SQL_CALC_FOUND_ROWS and speeds up the query. orderby => 'rand' does the random selection in SQL, but it can be slow on large datasets, so consider alternative approaches when the library grows.

Caching results and IDs

The random query can be cached for a short TTL to reduce database load. Use Transients API:

$cache_key = 'mg_results_' . md5( serialize( $filters ) );
$ids = get_transient( $cache_key );
if ( false === $ids ) {
    // Run query, collect IDs
    set_transient( $cache_key, $ids, 60 ); // cache for 1 minute
}

Caching the IDs rather than full post objects saves memory. Then you can fetch only the needed posts with wp_get_post or a second query. Remember to invalidate the cache when a name is added or edited, so stale results don’t linger.

Prebuilt index for large libraries

If you have more than a few thousand names, WP_Query with orderby => 'rand' can become slow. Options:

  • Custom table in the database – store only the fields you need to filter on (syllables, rarity, taxonomy IDs) in a separate table, and do a SELECT with ORDER BY RAND() there. This is fast if you index appropriately.
  • Transient/JSON index – at plugin activation or after each import, generate a JSON file or transient that maps filter combinations to lists of post IDs. Then the generator reads this index instead of hitting the database.

Both approaches require extra development but are worthwhile at scale. A pragmatic middle ground is to keep WP_Query for common filter combinations and add a custom table only for the heaviest queries.

Using REST for the actual request

Your front‑end JavaScript will call a REST route that wraps this query. Instead of using the default /wp-json/wp/v2/mg_name, register a custom route to control the response and avoid exposing unnecessary fields. For example: /wp-json/mygenify/v1/generate. The endpoint receives filter parameters, runs the optimized query, and returns a JSON array of name data (title, pronunciation, meaning, etc.).

No-refresh front-end delivery

The front end should feel like a SPA without the complexity of a framework. A custom page template in your theme renders the filter UI, and vanilla JavaScript talks to a REST endpoint to fetch results and update the page.

Custom page template

Create a template file, e.g., page-generator.php, and assign it to a page. The template includes:

  • A set of filter controls: checkboxes or dropdowns for gender, origin, style, theme, plus sliders or inputs for length, syllables, and rarity.
  • A container div where results will be inserted.
  • A generate button and a regenerate button.
  • A small area for messages (e.g., “Loading…” or “No results found”).

JavaScript fetch() and aborting previous requests

Attach a click listener to the generate button. Inside the handler, collect the current filter values, build a query string, and call fetch():

async function fetchNames() {
    const params = new URLSearchParams();
    params.append('gender', getChecked('gender'));
    // ... add other filters
    const url = `${mgApi.root}generate?${params}`;

    // Abort previous request if any
    if (currentAbort) currentAbort.abort();
    currentAbort = new AbortController();

    try {
        const res = await fetch(url, { signal: currentAbort.signal });
        if (!res.ok) throw new Error('Request failed');
        const data = await res.json();
        renderCards(data.results);
    } catch (e) {
        if (e.name !== 'AbortError') showError();
    }
}

Aborting the previous request prevents out-of-order responses when the user clicks rapidly. Use the AbortController API, which is supported in all modern browsers.

Rendering name cards

For each result, create a card element with the name, pronunciation, meaning, and maybe a few tags. “Regenerate” simply calls fetchNames() again with the same filters; the server does a new random query.

To make the UI feel faster, you can cache the last batch in a JavaScript object. If the user clicks “Regenerate” and the server returns the same batch (because of randomness), you can show a “Try again” message. Alternatively, you can request a slightly different random seed each time by adding a parameter.

Avoiding page reload

The entire interaction stays within the page. No location.reload(), no form submission. This is clean and fast. For accessibility, ensure the results container is announced to screen readers (e.g., with aria-live).

Retention features

Retention features encourage users to return and build their shortlist. Implement them with lightweight client-side storage and a few well-placed buttons.

Copy-to-clipboard

Each name card includes a copy button. Use the modern Clipboard API:

async function copyName(name) {
    try {
        await navigator.clipboard.writeText(name);
        // show a “Copied” tooltip
    } catch (err) {
        // fallback to document.execCommand('copy') for older browsers
    }
}

Include a fallback using a temporary textarea for browsers that don’t support the API.

Favorites and stash list with localStorage

Store the user’s favorite names in localStorage under a key like mg_favorites. It’s a simple array of objects: name, timestamp, maybe a link back to the full post.

function addFavorite(nameData) {
    let favs = JSON.parse(localStorage.getItem('mg_favorites') || '[]');
    favs.push({ ...nameData, savedAt: Date.now() });
    localStorage.setItem('mg_favorites', JSON.stringify(favs));
}

Add a “Save” button on each card. A separate “Stash” (temporary list) can be kept in sessionStorage if you want it to disappear after the session; using localStorage makes it persist.

Optional user meta sync for logged-in users

If users have accounts, you can sync favorites to user meta so they persist across devices. When logged in, call wp.apiFetch to update wc_user_meta—or better, a custom REST endpoint that stores an array of name IDs in update_user_meta(). This is a progressive enhancement: anonymous users use localStorage, logged-in users get cross-device sync.

Export CSV/JSON

Give users the ability to download their favorites as a CSV or JSON file. This is straightforward with client-side generation: create a Blob and trigger a download.

function exportCSV(favs) {
    const csv = favs.map(f => `${f.name},${f.origin || ''}`).join('\n');
    const blob = new Blob([csv], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'favorites.csv'; a.click();
}

Shareable result links

If you want users to share a generated set, you can encode the filter parameters and maybe the result IDs in the URL hash. The page can read these on load and re-generate the same set—or at least the same filters. This adds complexity, so evaluate whether it’s worth it for your audience.

Reference Blue Studio functionality

Blue Studio serves as a functional blueprint for what your generator should do, but you are explicitly not copying its design. Use the following list as a requirements checklist for mygenify.cloud:

  • Generator matrix – a central place where users select criteria and get results.
  • Themes – allow users to browse names by theme (e.g., nature, fantasy, vintage).
  • Instant names – results appear immediately without a page reload.
  • Customization filters – culture, style, fandom, and other attributes.
  • 10–20 results per batch – a comfortable number for scanning.
  • Regenerate – get a new random batch with the same filters.
  • Favorites/Export – save interesting names and download them.
  • Free/no-signup – no account required to use the core generator.

These are the functional targets. The design should be original, matching your brand. During development, keep a table of these requirements and test each one.

Avoiding design copying

When you look at Blue Studio for inspiration, focus on the user flow, not the pixels. Sketch your own layout, color palette, and typography. This keeps you legally safe and gives the site its own identity.

Plugin and scale recommendations

This section gathers concrete plugin choices and scaling strategies.

Admin and field management

  • SCF or ACF – for structured meta fields. SCF is a fork of ACF with a modern interface; both work well.
  • CPT UI – if you prefer a GUI for registering the CPT. Note that CPT UI stores its definitions in PHP, which is fine, but a custom plugin gives you more control.
  • Meta Box – an alternative to ACF with similar functionality.

Delivery and performance

  • Core REST API – use it for data delivery. It’s free and reliable.
  • FacetWP or SearchWP – optional; if you find yourself needing advanced filtering or search, these plugins can integrate with WP_Query. They add overhead, so evaluate carefully.
  • WP All Import – recommended for CSV import of the initial library.

Scaling beyond WP_Query

When the name library grows (e.g., tens of thousands of entries), you might hit performance issues. Options:

  • Custom table – store a subset of fields in a separate table, join with the CPT for post content. This can be done with a custom admin screen or a plugin like WP Data Access.
  • Transient/JSON index – precompute filter combinations and cache the resulting post IDs in transients or as a static JSON file served via a CDN.
  • Elasticsearch – overkill for most projects, but possible with plugins like ElasticPress.

A pragmatic approach is to start with WP_Query and add a custom table only if performance measurements show a problem. Monitor query times in the database logs.

Final thoughts

By building the data layer as a plugin-owned CPT, you get a maintainable system that uses WordPress’s native strengths. The front-end remains snappy with vanilla JavaScript and a custom REST route. Retention features are simple to add. As mygenify.cloud grows, you have a clear path to scale without a full rewrite.