Book Library
OOP WordPress plugin that registers a Book post type with a Genre taxonomy, a Book Details meta box, and a books archive visitors can filter by genre. One class per concern, a shared render hook so the listing and single view never drift, theme-overridable templates, escaping and capability checks throughout.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/busayor2020/book-library/archive/refs/heads/main.zipReadme
Book Library
A WordPress plugin submitted for the Fat Beehive WordPress Developer test.
It registers a Book post type with a Genre taxonomy and a Book Details meta box, serves a books archive that can be filtered by genre, and lets an administrator restyle the cards in that listing from Books > Card Appearance.
Live demo
The plugin running on a live site, seeded with the test content described in Seeding test content. Pick a genre and press Apply to see the filter work.
| Version | 1.1.0 |
| Requires WordPress | 6.0+ |
| Requires PHP | 7.4+ |
| Licence | GPL-2.0-or-later |
| Text domain | book-library |
Contents
- Live demo
- Installation
- What it adds
- How the requirements are met
- Card appearance
- Structure
- Design notes
- Extending it
- Testing it
- Seeding test content
- Coding standards
- Uninstalling
Installation
- Copy the
book-libraryfolder intowp-content/plugins/. - Activate Book Library in Plugins.
- Rewrite rules are flushed on activation, so
/books/works straight away. If permalinks ever misbehave, re-save Settings > Permalinks.
What it adds
| Post type | book, admin menu Books, icon dashicons-book-alt |
| Supports | title, editor, thumbnail |
| Taxonomy | genre, non-hierarchical, admin column shown |
| Archive | /books/, 12 books per page, paginated |
| Filter | /books/?book_genre=<slug> |
| Meta box | Book Details, side panel, Author Name field |
| Meta key | _book_author_name, registered and exposed over REST |
| Settings | Books > Card Appearance, one option row, book_library_card_appearance |
| Card button | Optional <a> per card, linking to the book or to a custom field |
| Stylesheet | assets/css/book-library.css, loaded only on book pages |
Post type, taxonomy and meta are all registered with show_in_rest, so the block editor and the REST API see them.
How the requirements are met
| Requirement | Where |
|---|---|
| OOP plugin | includes/, one class per concern, each implementing the Hookable interface |
| Custom post type "Book" | includes/class-post-type.php |
| Supports title, editor, thumbnail | Post_Type::register(), supports argument |
Admin menu "Books" with dashicons-book-alt |
Post_Type::register(), menu_icon argument |
| Publicly queryable | public and publicly_queryable set to true, archive at /books/ |
| Custom taxonomy "Genre", non-hierarchical | includes/class-taxonomy.php, hierarchical set to false |
| Meta box titled "Book Details" | includes/class-meta-box.php |
| "Author Name" field, saved on post save | Meta_Box::render_meta_box() and Meta_Box::save() |
| Author shown below post content | Meta_Box::append_author_to_content() on the the_content filter |
| Archive template listing books | templates/archive-book.php, loaded by includes/class-template-loader.php |
| Listing filterable by genre | Filter form in the template, query handled in includes/class-archive-query.php |
| Listing reuses the meta box hook | Template calls do_action( 'book_library_book_meta', get_the_ID() ) |
| Part 1b debugging | debug/part-1b-archive-query-debug.php |
| Part 2 client response | docs/part-2-client-response.md |
Card appearance
Books > Card Appearance styles every card in the listing. Nothing has to be saved for the plugin to work: with no option row the built-in defaults apply, which is the state a fresh install is in.
| Section | Settings |
|---|---|
| Card | Master styling switch, border width, style and colour, corner radius, inner padding, background colour |
| Shadow | Preset (none, subtle, medium, strong, custom), and for custom: horizontal and vertical offset, blur, spread, colour, opacity |
| Button | Show or hide, label, destination, background, text colour, hover background, hover text colour, corner radius, vertical and horizontal padding, full width |
| Concern | Where |
|---|---|
| Field registry, defaults, sanitisation, CSS output | includes/class-card-appearance.php |
| Admin screen, sections, fields, reset control | includes/class-settings-page.php |
| Button markup and link resolver | includes/class-card-button.php |
| Variable consumption and button styles | assets/css/book-library.css |
| Live preview | assets/js/settings-preview.js |
| Tests | tests/test-card-appearance.php |
One registry, everything derived from it. Every setting is declared once, in Card_Appearance::get_registry(). The defaults come from wp_list_pluck() over that registry, sanitisation dispatches on the sanitize key each entry names, the admin fields and the live preview are rendered from it, and the CSS custom properties are built by walking it. Adding a setting means adding one entry.
Output is custom properties, not inline styles. The plugin prints one block scoped to .book-library-grid and attaches it to its own stylesheet with wp_add_inline_style(), so it lands in the right place in the cascade and disappears with the stylesheet if a theme dequeues it. assets/css/book-library.css reads every property with a var() fallback, so the cards still render when styling is switched off.
The shadow is composed in PHP. The colour and the opacity become one rgba() value, the preset is authoritative, and the custom component fields are only read when the preset is custom. A preset of none emits no box-shadow declaration at all rather than box-shadow: none, so a theme's own shadow is left alone.
Offsets can be negative. shadow_offset_x, shadow_offset_y and shadow_spread are sanitised with a signed handler. absint() would turn -4 into 4 and flip the shadow to the wrong side of the card without an error anywhere.
Reset clears the row. The reset control deletes the option rather than writing the defaults into it, which keeps the fresh-install path exercised rather than only tested once.
The button. An <a> styled as a button, because it navigates. It links to the book by default, or to the book_library_button_url custom field on each book, falling back to the book page when that field is empty. Setting the destination to None hides it.
Structure
book-library/
├── book-library.php Bootstrap, constants, autoloader, activation
├── uninstall.php Removes plugin meta and settings, leaves content alone
├── readme.txt WordPress plugin readme and changelog
├── phpcs.xml.dist WordPress coding standards ruleset
├── includes/
│ ├── class-hookable.php Interface every component implements
│ ├── class-plugin.php Builds components, registers their hooks
│ ├── class-post-type.php Book post type
│ ├── class-taxonomy.php Genre taxonomy
│ ├── class-meta-box.php Book Details meta box, save, front-end output
│ ├── class-archive-query.php Genre filtering, the corrected Part 1b logic
│ ├── class-template-loader.php Serves the archive template
│ ├── class-assets.php Conditional stylesheet loading
│ ├── class-card-appearance.php Field registry, sanitisation, CSS custom properties
│ ├── class-card-button.php The per-card button and its link resolver
│ └── class-settings-page.php Books > Card Appearance screen and live preview
├── templates/
│ └── archive-book.php Books listing, overridable by the theme
├── assets/
│ ├── css/book-library.css Layout and card presentation, driven by custom properties
│ ├── css/book-library-admin.css Settings screen furniture only
│ └── js/settings-preview.js Live preview, no AJAX and no save needed
├── tests/
│ ├── run-tests.php Entry point, WP-CLI or standalone
│ ├── class-test-runner.php Assertions and reporting
│ ├── test-card-appearance.php Structural invariants and behavioural assertions
│ └── wp-stubs.php Enough WordPress to run the suite without WordPress
├── debug/
│ └── part-1b-archive-query-debug.php Original snippet, faults found, corrected code
└── docs/
└── part-2-client-response.md Written response to Bob OldClient
Classes autoload from includes/ on a PSR-4 style map, so BookLibrary\Post_Type resolves to includes/class-post-type.php. The underscore-to-hyphen conversion keeps the file naming aligned with the WordPress coding standards.
Design notes
One class, one concern. Each component owns its own hooks through register_hooks(). Plugin never needs to know what a component does, so adding a feature means adding a class to one array and nothing else changes. It also makes each piece testable in isolation.
A shared action for the author output. The meta box exposes book_library_book_meta. The single view reaches it through the the_content filter, and the archive template calls it directly. Both render through Meta_Box::get_author_markup(), so the listing and the book page can never drift apart, and a theme can restyle both by filtering book_library_author_markup in one place.
Theme override. Template_Loader checks locate_template() first, so a theme can take over the listing by dropping archive-book.php into the theme folder. The plugin only steps in when the theme has no opinion. A theme template copied from before 1.1.0 keeps working, it simply has no button until do_action( 'book_library_card_button', get_the_ID() ) is added to it. The card styling still applies, because it is delivered as custom properties on the wrapper rather than as markup.
One place to declare a setting. Card_Appearance::get_registry() is the only place a card setting is described. get_defaults() is plucked from it rather than written out again, so the two cannot drift, and the test suite fails immediately if a hand-written defaults array is ever reintroduced. Sanitisation dispatches on the handler each entry names, so there is no switch listing field names anywhere.
Escaping and capabilities. Output is escaped at the point of output. The meta box save is guarded by a nonce, an autosave and revision check, and current_user_can( 'edit_post' ). A save that carries no nonce, such as a REST or WP-CLI update, leaves the stored value alone rather than wiping it. The genre in the request is passed through sanitize_title() and confirmed to exist before it reaches the query. Settings go through register_setting() with a sanitisation callback, and the reset control checks a nonce and manage_options.
CSS values are reduced, not escaped. Every generated value is stripped down to characters that can only be a CSS value, so anything that could close a declaration, close a block, open a comment, start a string or reach the network is removed rather than encoded. That runs after the book_library_card_css_variables filter as well, so a filter cannot smuggle a value through either.
Tolerant public entry points. render_author() and append_author_to_content() take untyped parameters on purpose. Both sit on hooks that any theme or plugin can call, and under declare( strict_types = 1 ) a badly behaved caller upstream would turn a declared type into a fatal error on the whole page. The values are cast instead.
Note on themes. The archive template uses get_header() and get_footer(), so it assumes a classic theme. Under a block theme, add archive-book.html to the theme or copy the loop into a template part.
Extending it
Everything below is public API and safe to hook.
Actions
| Hook | Parameters | Purpose |
|---|---|---|
book_library_book_meta |
int $post_id |
Renders the author line. Call it from any template to place the author wherever you want. |
book_library_card_button |
int $post_id |
Renders the card button. Call it from any template to place the button wherever you want. |
do_action( 'book_library_book_meta', get_the_ID() );
do_action( 'book_library_card_button', get_the_ID() );