Skip to content

Next-gen: javaabu/post-types v1.0 #14

Open

Description

@n4ff4h

WordPress-style post types defined with a fluent API: meta fields (javaabu/meta), media (javaabu/mediapicker), block-editor content, routing, admin and public views in Blade and Inertia React, optional translatable.

Package: javaabu/post-types · Target: v1.0 · Wave: 5
Depends on: javaabu/meta, javaabu/mediapicker, javaabu/block-editor, javaabu/permissions, javaabu/filters, javaabu/bulk-actions
Optional: javaabu/translatable

Built to the next-gen general rules: CI with test and coverage badges, full docs for people and agents, Laravel Boost guideline and skill, Javaabu codestyle, PHP 8.4 / Laravel 12+ (Laravel packages) or TypeScript (JS packages), Dhivehi and RTL, Blade and Inertia React for views, overridable controllers and policies with javaabu/permissions.

Checklist

Developed by an agent orchestrated from the Javaabu next-gen plan. Cross-package requests go in the dependency's repo with the cross-package label.

Activity

  1. added
    next-genNext-gen Javaabu packages
    epicTracks a whole package
    on Nov 4, 2026
  2. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Implementation plan

    Studied: ../widgets (closest shape: definitions in code, overridable controller, Blade + Inertia pages through manifest, permissions, optional translatable; I'm copying its tooling, CI and test harness), and the contracts of meta (Javaabu/meta#11), block-editor (Javaabu/block-editor#14), mediapicker (n4ff4h/mediapicker#13, docs), filters (Javaabu/filters#12), bulk-actions (Javaabu/bulk-actions#12), permissions (Javaabu/permissions#14), stencil (Javaabu/stencil#14, statePath() from #16), translatable (n4ff4h/translatable#10) and manifest (Javaabu/manifest#13). dynamic-menubuilder's MenuLinkable draft is on its next-gen branch.

    Architecture

    Storage. One posts table for every post type (WordPress-style), with type, title, slug (unique per type), excerpt, content (block-editor JSON), status, published_at, an author morph (any user model, any guard), soft deletes, and nullable translations/lang for translatable. Taxonomy terms live in post_terms (taxonomy, name, slug, description, parent_id, order, translations, lang) with a post_term pivot. Models come from config.

    Definitions (#2).

    PostTypes::register(
        PostType::make('news')
            ->labels(singular: 'News article', plural: 'News')        // translated when shown
            ->icon('newspaper')
            ->supports(['content', 'excerpt', 'media', 'meta', 'author'])
            ->taxonomies(['categories', 'tags'])
            ->metaFields([TextInput::make('subtitle')->translatable(), Checkbox::make('featured')])
            ->media([MediaPickerField::make('gallery')->mediaType('image')->multiple()])   // more than the featured image
            ->blockProfile('default')
            ->statuses(['draft', 'pending', 'published'])                // + Status::make('archived')->public(false)
            ->permalink('{year}/{month}/{slug}')                          // under ->slug('news'); configurable in config too
            ->archive(perPage: 12)                                        // or ->archive(false)
    );
    
    PostTypes::taxonomy(Taxonomy::make('categories')->labels('Category', 'Categories')->hierarchical());

    Also from config('post-types.types') (array or class names) and PostType subclasses. Scheduling: a published post with a future published_at is "scheduled"; Post::published() / visible() scopes check both, so nothing needs a cron.

    Meta, media, content (#4). metaFields() registers MetaFields::for(Post::class, [...], group: $type) and the model's metaGroup() returns the type; the post form nests them with Group::make()->statePath('meta'). media adds a featured_image attachment collection (javaabu/mediapicker MediaPickerField), plus any collections from ->media([...]). content is cast with AsBlockDocument and edited with block-editor's stencil BlockEditorField (profile per type).

    Routing and permalinks (#3). Two macros, so apps choose prefix, middleware and guard:

    • Route::postTypesAdmin(['guard' => 'web_admin', 'prefix' => 'admin', 'name' => 'admin.']) → per type admin/{slug} index (with status tabs and trash), create, store, edit, update, destroy (trash), restore, force delete, bulk; and per taxonomy admin/taxonomies/{taxonomy} terms CRUD. Names admin.news.index, admin.taxonomies.categories.index.
    • Route::postTypes(['prefix' => '', 'name' => '']) → per type archive (/news), single (from the permalink structure: {slug}, {id}, {year}, {month}, {day}, {category}) and term archives (/news/categories/{term}). Only published, non-future posts; previews of drafts for users who can edit them. $post->url() builds permalinks.

    Admin (#5). Index: javaabu/filters (search, status, taxonomy terms, author, date range; sortable title/date) and javaabu/bulk-actions (trash, restore, delete for good, publish, unpublish) with per-type permissions. Create/edit: one stencil form class (title, slug, content, excerpt, featured image, terms, meta group, status, publish date). Blade with javaabu/forms v2 and React (post-types::Posts/Index, Posts/Edit, Terms/Index, Terms/Edit) with @javaabu/forms, Onu and stencil's renderer, shipped through manifest. config('post-types.views'): blade | inertia.

    Public views (#6). Archive, single and term archive. Blade templates are looked up WordPress-style (post-types.{type}.single → post-types.single → package default) and are publishable; Inertia pages post-types::Public/Archive, Public/Single, overridable through manifest or per type with ->views([...]). Rendered content uses <x-block-editor::content> / React BlockContent.

    RBAC (#11). Per post type, registered through Permissions::register(): view_{type}, edit_{type}, delete_{type}, force_delete_{type}, publish_{type} (+ edit_others_{type}/delete_others_{type} with ->ownable()); per taxonomy view_/edit_/delete_{taxonomy}. PostPolicy / TermPolicy (abilities resolved through the post type), overridable from config, config('permissions.policies') or Gate::policy(). Controllers from config or per route macro; nothing assumes web or App\Models\User (morph author, guard option, tests with two guards).

    Translatable (#12). Optional, through Support\Translations. Models\Translatable\Post / Term (JSON storage: title, slug, excerpt, content; term name, slug, description) are chosen when translatable is enabled; meta fields use meta's own translatable support; tested with and without it (CI leg).

    Order of work

    #1 foundations → #2 definitions, models, migrations → #4 meta/media/content → #3 routing and permalinks → #11 policy and permissions → #5 admin (Blade, then React) → #6 public views → #12 → #9 Dhivehi/RTL → #7 docs, llms.txt, AGENTS.md → #8 Boost. The post type definition API goes on this epic once #2 is pushed.

    Decisions / open questions

    • One table for all types, not a table per type: one model, one set of migrations, menus and search across types; apps that need their own table can point a type at a subclass with ->model().
    • Hierarchical post types (parent pages with nested permalinks) are out of scope for v1; hierarchical taxonomies are in.
    • Bulk actions on post types run synchronously. bulk-actions' queued runs rebuild the set with $class::make(), so a set parameterized by post type can't be queued yet. I'll open a non-blocking cross-package issue there.
    • dynamic-menubuilder: once its contract is posted, posts register one linkable source per post type, feature-detected (suggest, not require).
  3. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Contract: post type definition API (on main, for javaabu/cms)

    Require it with "javaabu/post-types": "^1.0@dev" and a VCS repository https://github.com/Javaabu/post-types (plus the VCS repositories of its unreleased dependencies, see composer.json). Migrations run on their own (post-types.run_migrations); apps also need spatie/laravel-medialibrary's media table and mediapicker's and meta's migrations.

    Defining

    use Javaabu\PostTypes\Facades\PostTypes;
    use Javaabu\PostTypes\{PostType, Status, Taxonomy};
    
    // A service provider's boot() (or config('post-types.types') / ('post-types.taxonomies'), instances or class names)
    PostTypes::taxonomy(Taxonomy::make('categories')->labels('Category', 'Categories')->hierarchical());
    PostTypes::taxonomy(Taxonomy::make('tags'));
    PostTypes::status(Status::make('archived')->label('Archived')->color('info'));   // draft, pending, published are built in
    
    PostTypes::register(
        PostType::make('news')
            ->labels('News article', 'News')            // translated when shown
            ->icon('newspaper')
            ->supports(['content', 'excerpt', 'media', 'meta', 'author', 'scheduling'])   // ->with(...), ->without(...)
            ->taxonomies(['categories', 'tags'])
            ->statuses(['draft', 'pending', 'published', 'archived'])   // first is the default; ->defaultStatus()
            ->metaFields([TextInput::make('subtitle')->translatable(), Checkbox::make('featured')])   // javaabu/meta, group = 'news'
            ->media([MediaPickerField::make('gallery')->mediaType('image')->multiple()])       // besides featured_image
            ->blockProfile('default')                    // javaabu/block-editor profile
            ->slug('news')->permalink('{year}/{month}/{slug}')   // {slug} {id} {year} {month} {day} {<taxonomy>}
            ->archive(per_page: 12)                      // ->archive(false), ->public(false)
            ->ownable()                                  // edit_others_news / delete_others_news
            ->permission('news')                          // permission resource, default: key, snake, plural
            ->model(NewsPost::class)                     // a Post subclass
            ->views(['single' => 'news.show'])->pages(['single' => 'News/Show'])
            ->order(1)
    );

    Subclasses work too: extend PostType/Taxonomy, set $key and the properties, override setUp(). config('post-types.permalinks.news') overrides base and structure without touching the definition.

    Registry (PostTypes facade, PostTypesManager)

    types(), find($key), get($key), has(), findBySlug(), registered(fn (PostType $type) => …) (called for every type, now and later), forget(), taxonomies(), getTaxonomy(), typesWithTaxonomy(), statuses(), getStatus(), postModel(), termModel().

    Models

    • Post (posts table, one for every type; type column): postType(), author() (morph, any guard), terms(), termsOf('categories'), syncTerms('categories', [...]), taxonomies attribute (['categories' => [1, 2]], fillable), meta (javaabu/meta, metaGroup() = type), attachments (javaabu/mediapicker, featured_image + ->media() collections), content (AsBlockDocument), url(), permalinkPath(), summary(), featuredImageUrl(), isPublished(), isScheduled(), displayStatus().
      Scopes: ofType(), published() (public status, publish date passed), scheduled(), unpublished(), withStatus(), withTerms($taxonomy, $terms), byAuthor($user), latestPublished(), search().
    • Term (post_terms): taxonomy, name, slug, description, parent_id, order; posts(), parent(), children(), url($post_type), path(), Term::tree('categories').
    • With javaabu/translatable enabled: Models\Translatable\Post (title, excerpt, content) and Models\Translatable\Term (name, description), JSON storage.

    Routes

    Route::postTypesAdmin(['guard' => 'web_admin', 'prefix' => 'admin', 'name' => 'admin.', 'only' => [...], 'controllers' => [...]]);
    // admin/news (admin.news.index|create|store|edit|update|destroy|restore|force-delete|bulk)
    // admin/taxonomies/categories (admin.taxonomies.categories.*)
    
    Route::postTypes(['prefix' => '', 'name' => '', 'guard' => null]);
    // news (news.index), news/categories/{term} (news.term), news/{permalink} (news.show)

    Authorization

    Per type through javaabu/permissions: view_x, edit_x, delete_x, force_delete_x, publish_x (+ edit_others_x, delete_others_x with ownable()); per taxonomy view_/edit_/delete_{taxonomy}. PostPolicy (abilities on PostType and posts: viewAny, create, update, delete, restore, forceDelete, viewTrash, publish) and TermPolicy (on Taxonomy and terms), overridable from config('post-types.policies'), config('permissions.policies') or Gate::policy().

    Views

    config('post-types.views'): blade | inertia. Admin: post-types::admin.posts.index|edit, admin.terms.index|edit / post-types::Posts/Index|Edit, Terms/Index|Edit. Public: post-types.{type}.{archive|term|single} → post-types.{page} → post-types::public.{page} / post-types::Public/Archive|Term|Single. React exports from @javaabu/post-types (pages, PostCard, postFormComponents, types).

  4. n4ff4h commented on Nov 5, 2026

    @n4ff4h
    CollaboratorAuthor

    Final summary: javaabu/post-types v1.0 is done on main

    Every next-gen issue is closed except #13 (docs.javaabu.com registration, left for you). CI is green on every job (latest run on c616d37):

    • PHP 8.4/8.5 × Laravel 12/13 × lowest/stable, with coverage from 8.4/13/stable
    • a leg without Inertia, javaabu/translatable and javaabu/dynamic-menubuilder
    • Pint, and the guard against javaabu/helpers and settings v1 in production dependencies
    • the JS job: lint, format, typecheck, Vitest with coverage, and the committed dist check

    The suite has 82 PHPUnit tests and 17 Vitest tests.

    What was built

    • Definitions (Fluent post type definitions #2). PostType, Taxonomy and Status, defined fluently, as classes or in config, through the PostTypes registry. Features: content, excerpt, media, meta, author and scheduling. Statuses are draft, pending and published, plus your own. A public status with a future publish date is scheduled, and no cron is needed. The API is in the contract comment above.
    • Storage. One posts table for every type, plus post_terms and post_term.
      • Post and Term models with scopes: published, scheduled, unpublished, withTerms, byAuthor, search.
      • Unique slugs keep Thaana: Str::slug() drops fili, so slugs come from Support\Slug.
      • Authors are a morph, so any user model and guard works.
    • Meta, media and content (Meta, media and content #4).
      • Meta: javaabu/meta fields in a group per post type, nested in the post form with MetaFields::nested() (meta[subtitle]).
      • Media: a featured_image collection plus ->media([...]) mediapicker fields, validated with LibraryMedia.
      • Content: block-editor content with a profile per type.
    • Routing and permalinks (Routing and permalinks #3).
      • Route::postTypesAdmin() and Route::postTypes() take guard, prefix, name, middleware, only/except and controllers.
      • Permalink structures accept {slug}, {id}, {year}, {month}, {day} and taxonomy keys, and config('post-types.permalinks') can override them.
      • Other paths to a post get a 301 to its permalink.
    • Admin (Admin views and controllers #5).
      • The index has status tabs, javaabu/filters (search, status/scheduled, each taxonomy, date, trash, sorts) and javaabu/bulk-actions (publish, move to drafts, trash, restore, delete for good, each checked by the policy).
      • Create and edit use the stencil PostForm. Without publish_x, users can only save drafts or pending posts.
      • Taxonomies get term pages with a tree view and parents that can't loop.
      • Every route also answers JSON.
    • Public (Public views #6). An archive, term archives (hierarchical archives include child terms) and single posts. Drafts and scheduled posts can be previewed by users who can edit them. Templates are found WordPress-style: ->views(), then post-types.{type}.{page}, then post-types.{page}, then the package default.
    • Both stacks (Blade (javaabu/forms) and Inertia React (@javaabu/forms) views #10).
      • Blade uses javaabu/forms v2.
      • Inertia React pages (post-types::Posts/*, Terms/*, Public/*) and their parts ship as @javaabu/post-types through manifest.
      • Vitest renders the pages from props that PHP writes.
    • RBAC (Overridable controllers, policies and default permissions (RBAC, multi-auth) #11).
      • Each post type gets view_/edit_/delete_/force_delete_/publish_{type}, and edit_others/delete_others with ->ownable().
      • Each taxonomy gets view_/edit_/delete_{taxonomy}.
      • PostPolicy and TermPolicy can be overridden from config, permissions.policies or Gate::policy().
      • Controllers, forms, filters and bulk actions can all be extended.
      • Tested with two guards.
    • Translatable (Optional javaabu/translatable support #12). Optional. Translatable Post (title, excerpt, content; translated content is sanitized) and Term (name, description), and per-language meta inputs. Tested with and without the package.
    • Menus (dynamic-menubuilder). Optional. Each public post type registers as a menu source, with its archive as the index page. Menus only show published posts, and items are removed when a post is deleted for good. Post implements Contracts\MenuLinkable, which aliases the menubuilder contract when it's installed and is an empty interface otherwise.
    • Dhivehi and RTL (Dhivehi localization and RTL #9). lang/en and lang/dv (LangTest), logical CSS only (RtlTest), Thaana inputs and slugs, and Thaana in the tests.
    • Docs and AI (Documentation for people and agents #7, Laravel Boost guideline and skill #8). 22 Docusaurus pages, llms.txt (DocsTest), AGENTS.md/CLAUDE.md, and the Boost guideline and javaabu-post-types skill with an API reference and common mistakes.

    Decisions and out of scope

    • One table for every post type. A post type can still use a Post subclass through ->model().
    • Hierarchical post types. Parent pages with nested permalinks aren't in v1. Hierarchical taxonomies are.
    • Post and term URLs. The post URL is Post::permalink() and a term archive is Term::archiveUrl(), because javaabu/translatable's trait owns url(). mediapicker hit the same clash.
    • Bulk actions run during the request. Queued runs rebuild a set with make(), which can't know the post type. Queued runs for sets configured at runtime (context on BulkActionRun) bulk-actions#13 asks for run context. It's non-blocking.
    • JS tests use stand-ins. The React tests and typecheck use small stand-ins for block-editor's and mediapicker's React modules (tests/js/shims), because those modules need TipTap and Onu media. Apps get the real ones through manifest.
    • Not checked in a browser. The pages haven't been opened in a real browser, only in PHP and Vitest tests.

    For the user

    • Codecov: uploads answer "Token required". Make CODECOV_TOKEN available to this repo; the badge stays blank until then.
    • Docs: register on docs.javaabu.com (Register with Javaabu Docs (docs.javaabu.com) #13).
    • Dhivehi review: check the terms in docs/advanced-usage/dhivehi-and-rtl.md#for-review.
    • Release: tag v1.0 once its unreleased dependencies are released, then drop the VCS repositories, including the n4ff4h forks of mediapicker, translatable and dynamic-menubuilder.
  5. n4ff4h commented on Nov 6, 2026

    @n4ff4h
    CollaboratorAuthor

    Follow-up in 2b6acde (CI green):

    • npm scopes (manifest:check asks Blade apps to install the React/Inertia dependencies of package pages manifest#17): React, @javaabu/forms and @javaabu/onu (and the @inertiajs/react peer) are now declared with npm(..., for: 'pages'). The registered styles import nothing, so post-types adds nothing to the assets scope. php artisan manifest:check in a Blade-only app no longer asks for React packages because of post-types. tests/Feature/AssetsTest.php covers both a Blade-only app and an Inertia app.
    • Styles on Inertia pages (Inertia: theme CSS isn't loaded; starter menu label stays English in Dhivehi n4ff4h/cms#12): PostCard (with PostMeta and PostTerms), Public/Archive (and so Public/Term) and Public/Single import resources/css/post-types.css, so Vite bundles it with any page that renders them. Blade is unchanged. A Vitest check makes sure every component using a post-types-* class imports the stylesheet. The admin pages have no styles of their own.
      • For cms: the documented @import "../../vendor/javaabu/post-types/resources/css/post-types.css" in docs/advanced-usage/inertia-react.md#styles is no longer needed and can be dropped whenever convenient. Keeping it does no harm beyond loading the stylesheet twice.
  6. n4ff4h commented on Nov 6, 2026

    @n4ff4h
    CollaboratorAuthor

    From javaabu/bulk-actions: the request in Javaabu/bulk-actions#13 is done (bulk-actions main, a95e330). Queued runs now rebuild a set with fromContext($run->context), so PostBulkActions can override context() (['post_type' => $this->post_type?->getKey()]) and static fromContext() (static::for(PostTypes::get($context['post_type']))), then drop ->queueAfter(false). The adoption steps, code and test outline are in Javaabu/bulk-actions#13 (comment). Apps need php artisan migrate for the new context column.

  7. n4ff4h commented on Nov 6, 2026

    @n4ff4h
    CollaboratorAuthor

    Follow-up in e6274d5 (CI green): post bulk actions now queue large selections like other packages'. PostBulkActions stores the post type's key with withContext(['post_type' => …]) and overrides fromContext() to rebuild with PostBulkActions::for(PostTypes::get(…)). The queueAfter(false) workaround is gone (Javaabu/bulk-actions#13, a95e330). PostsControllerTest::it_queues_large_bulk_actions_for_the_post_type checks the stored context and runs the job: only that type's posts change. The docs, common mistakes and CHANGELOG are updated, including a note to run php artisan migrate for bulk-actions' new context column.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicTracks a whole packagenext-genNext-gen Javaabu packages

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions