Developer documentation

OForum v1.0.0

A modern community discussion forum for WordPress — rooms, threads, replies, reactions, @mentions, trust levels, shadow banning, full-text search, and a REST API. No BuddyPress, no bbPress, no jQuery.

WordPress plugin WP 6.0 · PHP 8.0+ GPL-2.0+ Released 2026-04-25
01 · Overview

What OForum does

A full forum system built entirely on WordPress CPTs — no external dependencies, no legacy libraries. Everything from post types to REST routes is first-party code.

🏠
Forum Rooms
Nested sub-rooms, visibility controls, role-gating, emoji icons
📌
Thread Types
Discussion, Question, Announcement, Showcase — plus sticky & closed
Accepted Solutions
Mark a reply as the accepted answer on Question threads
😊
Emoji Reactions
Configurable emoji set, toggle-to-remove, per-reply counts
🔔
Subscriptions
Follow rooms or threads, in-forum bell, @mention notifications
🛡️
Moderation
Report queue, shadow ban, IP ban, audit log
⬆️
Trust Levels
4-level auto-recalculated system with configurable thresholds
🔍
Full-Text Search
Filters by room, type, author, date — keyword highlights in results
🌐
SEO
BreadcrumbList schema, DiscussionForumPosting, Yoast + SEO Framework sitemap
REST API
Full CRUD for rooms, threads, replies, reactions, and search
📐
Shortcodes
[oforum], [oforum_room], [oforum_thread]
🚫
No jQuery
Frontend runs on vanilla JS — no framework dependency
02 · Installation

Getting installed

Manual upload

1
Upload the oforum/ folder to /wp-content/plugins/.
2
Go to Plugins → Installed Plugins and activate OForum. On activation: registers CPTs (of_room, of_thread, of_reply), creates 6 database tables, creates a /forum page with the index template, and flushes rewrite rules.
3
Navigate to OForum in the WordPress admin sidebar.
4
Create your first rooms under Rooms → Add Room.

Via ZIP upload

Go to Plugins → Add New → Upload Plugin, select oforum.zip, click Install Now, then Activate.

03 · Quick Start

Up and running in minutes

1
Create rooms — OForum → Rooms → Add Room. Give each room a name, optional icon/emoji, visibility setting, and colour accent.
2
Add rooms to your menu — Appearance → Menus → link to /forum/room-slug/.
3
Test posting — Log in as a subscriber, visit /forum/your-room/ and click + New Thread.
4
Configure settings — OForum → Settings to set replies-per-page, editor preferences, and trust thresholds.
ℹ️The forum index is at /forum/. If the page was not created on activation, go to OForum → Settings and the plugin will recreate it on the next page load.
04 · Architecture

Plugin architecture

oforum/ ├── oforum.php Main plugin entry point ├── admin/ │ ├── class-of-admin.php Admin panel controller │ └── views/ │ ├── view-dashboard.php Dashboard tab │ ├── view-rooms.php Room management │ ├── view-threads.php Thread management │ ├── view-moderation.php Moderation queue, IP bans, log │ ├── view-analytics.php Analytics charts and tables │ └── view-settings.php Plugin settings ├── includes/ │ ├── class-of-cpt.php CPT registration + DB table creation │ ├── class-of-rooms.php Room CRUD and query helpers │ ├── class-of-query.php Thread and reply queries │ ├── class-of-permissions.php Permission levels, IP ban checks │ ├── class-of-trust.php Trust level recalculation │ ├── class-of-moderation.php Report queue, shadow ban, audit log │ ├── class-of-notify.php Subscriptions, notifications, email │ ├── class-of-reactions.php Emoji reaction toggle and counts │ ├── class-of-search.php Full-text search with filters │ ├── class-of-seo.php Breadcrumbs, schema, sitemap │ ├── class-of-shortcode.php Shortcode handlers │ └── class-of-rest.php REST API routes ├── templates/ │ ├── forum-index.php Forum index page │ ├── room-page.php Individual room page │ ├── thread-page.php Individual thread page │ ├── profile-page.php User profile page │ └── partials/ │ ├── thread-card.php Thread card (room listing) │ ├── reply-block.php Single reply block │ ├── new-thread-form.php Thread submission form │ ├── new-reply-form.php Reply submission form │ └── report-modal.php Content report modal └── assets/ ├── css/ │ ├── of-admin-v2.css Admin panel styles │ └── of-front.css Frontend styles └── js/ ├── of-admin-v2.js Admin panel interactions └── of-front.js Frontend (vanilla JS, no jQuery)

Data model

CPTpost_typeVisibilityNotes
Forum Roomof_roomPrivateSettings stored in post_meta
Forum Threadof_threadPublicSEO-friendly URL: /forum/thread/[slug]
Forum Replyof_replyPrivateManaged entirely via plugin, not WP admin

Thread → room: post_meta key of_thread_room_id. Reply → thread: of_reply_thread_id. Reply hierarchy (quoting): of_reply_parent_id.

05 · Admin Panel

Admin panel

A standalone top-nav interface registered as a single add_menu_page() entry — no sidebar sub-items. Matches the Orravo admin UI pattern.

RowContents
Brand barOForum icon + name + version badge + light/dark theme toggle
Top navDashboard · Rooms · Threads | Moderation | Analytics | Settings

The Moderation tab shows a live badge with the count of pending reports + pending approval posts. Theme preference is saved to localStorage under of_admin_theme; dark mode applies class of-dark to #of-wrap.

06 · Forum Structure

Rooms & structure

Rooms are of_room CPT posts. Settings are stored as post meta. Sub-rooms (one level of nesting) appear indented under their parent on the forum index.

Room properties

PropertyMeta keyDescription
Namepost_titleDisplay name
Slugpost_nameURL slug
Descriptionpost_contentRoom description
Iconof_room_iconEmoji or text icon
Colorof_room_colorHex colour for accent
Visibilityof_room_visibilitypublic, members, or role
Required roleof_room_required_roleWP role slug for role-gated rooms
Sub-room ofpost_parentParent room ID (0 = top-level)
Sort ordermenu_orderDisplay sort order

PHP API

PHP// Create a room
$room_id = OF_Rooms::create([
    'name'       => 'General Discussion',
    'icon'       => '💬',
    'color'      => '#38BDF8',
    'visibility' => 'public',
]);

// Query rooms
$rooms = OF_Rooms::get_all();               // All rooms (flat)
$rooms = OF_Rooms::get_all(false);          // Top-level only
$room  = OF_Rooms::get($room_id);           // By ID
$room  = OF_Rooms::get_by_slug('general-discussion');

// Visibility check
$can   = OF_Rooms::user_can_view($room_id, $user_id);
07 · Thread System

Thread system

Threads are of_thread CPT posts at URL /forum/thread/[slug]/. Thread type controls the display badge and behavior.

Thread meta keys

KeyTypeDescription
of_thread_room_idintRoom the thread belongs to
of_thread_typestringdiscussion, question, announcement, showcase
of_thread_stickyint1 = pinned to top of room
of_thread_closedint1 = no new replies (except mods)
of_thread_solvedint1 = has an accepted solution
of_thread_solution_idintReply ID of the accepted solution
of_thread_reply_countintCached reply count
of_thread_viewsintView count
of_thread_last_replydatetimeLast reply timestamp (for sorting)
of_thread_last_reply_uidintUser ID of the last replier

Querying threads

PHP$result = OF_Query::get_threads([
    'room_id'      => 5,
    'thread_type'  => 'question',
    'sticky_first' => true,
    'page'         => 1,
    'per_page'     => 25,
    'orderby'      => 'last_reply',  // last_reply | date | reply_count
]);

// Returns:
// [
//   'threads'   => [...],  // array of formatted thread arrays
//   'total'     => 42,
//   'max_pages' => 2,
// ]

Admin thread actions (AJAX)

All admin thread actions use the of_thread_action AJAX endpoint with an action_type param:

action_typeEffect
stickyToggle sticky (pinned to top)
closeToggle closed (no new replies)
deleteTrash the thread
approvePublish a pending thread
08 · Reply System

Reply system

Replies are of_reply CPT posts (not public). All interactions go through the plugin's REST API and AJAX handlers — never through the standard WP admin.

Reply meta keys

KeyTypeDescription
of_reply_thread_idintParent thread ID
of_reply_parent_idintQuoted reply ID (0 = top-level)
of_reply_edited_atdatetimeLast edit timestamp
of_reply_is_solutionint1 = accepted as solution
of_shadow_bannedint1 = shadow banned (only author sees it)

Accepted solutions

Thread authors and moderators can mark a reply as the accepted answer on Question threads. This sets of_reply_is_solution = 1 on the reply, and of_thread_solved = 1 + of_thread_solution_id on the thread. The solution reply is pinned above pagination with a green ✓ Accepted Answer badge.

Edit window

Users can edit their own replies within a configurable window (default: 30 minutes). After the window, only moderators can edit. Edited replies show (edited) next to the timestamp. Set to 0 in Settings to disable editing entirely.

Reply quoting

Clicking Quote stores the reply ID in #of-quoted-reply-id and prefills the form with a quote preview. The of_reply_parent_id is set on submission to maintain the reply hierarchy.

09 · Reactions

Emoji reactions

Stored in wp_of_reactions. Toggling the same emoji again removes the reaction. One reaction per emoji per user per reply, enforced by a unique DB constraint.

PHP API

PHP// Toggle a reaction (returns action + updated counts)
$result = OF_Reactions::toggle($user_id, $reply_id, '👍');
// ['action' => 'added'|'removed', 'counts' => ['👍' => 3, '❤️' => 1]]

// Get counts for a reply
$counts = OF_Reactions::get_counts($reply_id);

// Get emojis a user has reacted with
$user_reactions = OF_Reactions::get_user_reactions($user_id, $reply_id);

REST API

HTTPPOST /wp-json/oforum/v1/reactions
{ "reply_id": 42, "emoji": "👍" }

// Response:
{ "action": "added", "counts": { "👍": 3, "❤️": 1 } }

Configure available emojis at Settings → Reactions → Available Emojis as a comma-separated list. Default: 👍,❤️,😂,🔥,💡. Override programmatically with the of_allowed_emojis filter.

10 · Subscriptions & Notifications

Subscriptions & notifications

Subscription types & frequencies

TypeNotified when
roomAny new thread posted in the room
threadAny new reply posted in the thread
FrequencyBehaviour
immediateEmail sent immediately on each new post
dailyQueued for daily digest (planned for v1.1.0)
weeklyQueued for weekly digest (planned for v1.1.0)
noneIn-forum notification only, no email
PHPOF_Notify::subscribe($user_id, 'thread', $thread_id, 'immediate');
OF_Notify::unsubscribe($user_id, 'thread', $thread_id);
$is_subbed = OF_Notify::is_subscribed($user_id, 'thread', $thread_id);

In-forum notification types

TypeTrigger
new_replySomeone replied to a thread you follow
mentionSomeone @mentioned you
moderation_noticeA moderator actioned one of your posts
solution_acceptedYour reply was marked as the solution
PHPOF_Notify::create($user_id, [
    'type'      => 'mention',
    'post_id'   => $post_id,
    'from_user' => $author_id,
    'message'   => 'John Doe mentioned you.',
    'url'       => get_permalink($post_id) . '#reply-' . $reply_id,
]);

@mentions & polling

On reply submit, OF_Notify::parse_mentions() scans for @username patterns, resolves users by user_login and user_slug, and fires immediate in-forum notifications. The notification bell polls for unread counts every 60 seconds via AJAX (of_get_notifications).

11 · Moderation

Moderation tools

Permission levels

ConstantLevelLabelDescription
LEVEL_BANNED-1BannedCannot post; IP ban check runs here
LEVEL_NEW0New MemberFirst posts need moderator approval
LEVEL_MEMBER1MemberFull posting permissions
LEVEL_MOD2ModeratorCan approve, warn, delete, shadow-ban
LEVEL_ADMIN3AdminWordPress admins auto-receive this level
PHPOF_Permissions::set_level($user_id, OF_Permissions::LEVEL_MOD);

Report queue

Users click Report on any reply to file a report. Moderators see the queue at Moderation → Flagged Reports with four actions:

ActionEffect
✓ ApprovePublish the post
⚠ WarnSend a moderation email to the author (no post action)
🗑 DeleteTrash the post
✕ DismissDismiss the report without action

Shadow ban

Shadow-banned users' future posts are only visible to themselves and moderators. Other users see nothing — the banned user has no idea they're banned.

PHPOF_Moderation::shadow_ban_user($user_id, $mod_user_id);
OF_Moderation::unshadow_ban_user($user_id, $mod_user_id);
$is_banned = OF_Moderation::is_shadow_banned($user_id);

IP ban

PHP// Permanent ban
OF_Moderation::ban_ip('192.168.1.1', $mod_id, 'Spam', null);

// Timed ban — expires 2026-12-31
OF_Moderation::ban_ip('192.168.1.1', $mod_id, 'Abuse', '2026-12-31 23:59:59');

OF_Moderation::unban_ip('192.168.1.1', $mod_id);
$is_banned = OF_Permissions::is_ip_banned('192.168.1.1');

IP bans block the post/reply REST API endpoints. The auto-moderation hook also checks IPs on reply submission.

Audit log

Every moderation action is logged to wp_of_mod_log. View at Moderation → Audit Log.

PHP$log = OF_Moderation::get_log(50); // Last 50 entries
12 · Trust Level System

Trust level system

Trust levels are automatically recalculated every time a user posts a thread or reply. Thresholds are configurable at Settings → Trust Level Thresholds.

LevelConstantLabelDefault threshold
0OF_Trust::LEVEL_NEWNew Member0 posts — first post needs approval
1OF_Trust::LEVEL_MEMBERMember5 posts
2OF_Trust::LEVEL_REGULARRegular25 posts
3OF_Trust::LEVEL_LEADERLeader100 posts

Trust gates

ActionMinimum trust required
Post threads and repliesLevel 1 Member
Post links in contentLevel 1 Member
Edit own reply (within window)All levels
First post requires approvalLevel 0 New Member only

PHP API

PHP$level         = OF_Trust::get_level($user_id);        // int 0–3
$label         = OF_Trust::get_label($level);           // "New Member", "Member", …
OF_Trust::recalculate($user_id);                        // Force recalculate
$needs_approval = OF_Trust::requires_approval($user_id); // bool
14 · SEO

SEO integration

Breadcrumbs

PHPOF_SEO::render_breadcrumbs();             // Forum index
OF_SEO::render_breadcrumbs($room);        // Room page
OF_SEO::render_breadcrumbs($room, $thread); // Thread page

Renders BreadcrumbList Schema.org markup on all forum pages.

DiscussionForumPosting schema

On thread pages, OF_SEO::inject_schema() outputs a <script type="application/ld+json"> block with DiscussionForumPosting structured data — headline, author, date, reply count, and first 5 reply comments.

Page titles & sitemap

The plugin filters document_title_parts and wp_title for proper titles: Thread Title — Site Name and Room Name — Forum — Site Name.

OForum integrates with Yoast SEO (wpseo_sitemap_index_links) and The SEO Framework (the_seo_framework_sitemap_urls) to include the 200 most recently modified published thread URLs in the sitemap.

15 · REST API

REST API reference

Base URL: /wp-json/oforum/v1/. All write endpoints require WordPress authentication via X-WP-Nonce header or cookie-based auth.

Rooms

MethodEndpointAuthDescription
GET/roomsNoneList all rooms
GET/rooms/{id}NoneGet single room
POST/roomsAdminCreate room
PUT/rooms/{id}AdminUpdate room
DELETE/rooms/{id}AdminDelete room

Threads

MethodEndpointAuthDescription
GET/threads?room_id=XNoneList threads (paginated)
GET/threads/{id}NoneGet single thread
POST/threadsLogged inCreate thread
PUT/threads/{id}Author or ModUpdate thread
DELETE/threads/{id}ModeratorDelete thread
JSON// Create thread body
{
  "title":   "My thread title",
  "content": "<p>Thread content</p>",
  "type":    "discussion",
  "room_id": 5
}

Replies

MethodEndpointAuthDescription
GET/replies/{id}NoneGet single reply
POST/repliesLogged inPost reply
PUT/replies/{id}Author (within window)Edit reply
DELETE/replies/{id}Author or ModDelete reply
JSON// Create reply body
{
  "thread_id": 42,
  "parent_id": 0,
  "content":   "<p>My reply</p>"
}

Reactions & search

MethodEndpointAuthDescription
POST/reactionsLogged inToggle emoji reaction — body: {"reply_id":42,"emoji":"👍"}
GET/search?q=termNoneFull-text search — params: q, room_id, page, per_page
16 · Shortcodes

Shortcodes

[oforum]
Renders the full forum index — all rooms list.
[oforum]
[oforum_room]
Renders a specific room with its thread list. Use either slug or id.
[oforum_room slug="general-discussion"]
[oforum_room id="5"]
[oforum_thread]
Renders a specific thread with its replies.
[oforum_thread id="42"]
17 · Frontend Templates

Frontend templates

Templates live in the templates/ directory. They load the active theme's header.php and footer.php via get_header() / get_footer().

URL routing

URL patternTemplate
/forum/templates/forum-index.php — via page template
/forum/{room-slug}/templates/room-page.php — via of_room rewrite var
/forum/thread/{thread-slug}/templates/thread-page.php — via is_singular('of_thread')
/forum/profile/{username}/templates/profile-page.php — via of_profile rewrite var

Frontend JS globals

The OF global object is available on all forum pages:

JSwindow.OF = {
  ajaxUrl:   'https://site.com/wp-admin/admin-ajax.php',
  restUrl:   'https://site.com/wp-json/oforum/v1/',
  nonce:     '...',       // WP REST nonce
  ajaxNonce: '...',       // AJAX nonce
  loggedIn:  1,           // 1 or 0
  userId:    42,
  loginUrl:  '...',
  forumUrl:  '/forum/',
  perPage:   25,
};

window.OF_emojis = ['👍', '❤️', '😂', '🔥', '💡']; // from settings
ℹ️Template overrides: Copy templates into your theme at your-theme/oforum/ to override them. (Template override detection is planned for v1.1.0; child-theme CSS overrides are the recommended approach in v1.0.0.)
18 · Settings Reference

Settings reference

All settings stored as WordPress options via get_option / update_option.

Option keyTypeDefaultDescription
of_replies_per_pageint25Replies loaded per page on thread view
of_edit_window_minutesint30Minutes users can edit their own replies (0 = disabled)
of_guest_readbool1Whether non-logged-in users can read public rooms
of_rich_text_enabledbool1Enable rich text toolbar in reply form
of_markdown_enabledbool0Enable Markdown mode toggle per user
of_reaction_emojisstring👍,❤️,😂,🔥,💡Comma-separated allowed emojis
of_akismet_enabledbool0Enable Akismet spam filtering (requires Akismet plugin)
of_trust_threshold_1int5Posts needed to reach Level 1 (Member)
of_trust_threshold_2int25Posts needed to reach Level 2 (Regular)
of_trust_threshold_3int100Posts needed to reach Level 3 (Leader)
19 · Filters & Action Hooks

Filters & actions

Filters

PHP// Modify allowed reaction emojis
add_filter('of_allowed_emojis', function($emojis) {
    return ['👍', '❤️', '🎉', '🤔', '👀'];
});

// Modify room visibility check
add_filter('of_room_can_view', function($can, $room_id, $user_id) {
    // Custom logic — return bool
    return $can;
}, 10, 3);

// Modify reply content before save
add_filter('of_reply_content', function($content, $user_id) {
    // e.g. strip disallowed HTML
    return $content;
}, 10, 2);

Actions

PHP// Fired after a thread is created
add_action('of_thread_created', function($thread_id, $room_id, $author_id) {
    // e.g. post to Slack channel
}, 10, 3);

// Fired after a reply is created and published
add_action('of_reply_created', function($reply_id, $thread_id, $author_id) {
    // e.g. award points in a gamification plugin
}, 10, 3);

// Fired after trust level changes
add_action('of_trust_level_changed', function($user_id, $new_level, $old_level) {
    // e.g. award badge
}, 10, 3);

// Fired after a report is filed
add_action('of_report_filed', function($report_id, $reporter_id, $post_id) {
    // e.g. notify admin via email
}, 10, 3);
⚠️The above hooks are documented for the v1.1.0 planned implementation. In v1.0.0, use the CPT hooks save_post_of_thread and save_post_of_reply directly.
20 · Database Schema

Database schema

6 custom tables in addition to the standard WP tables used for the CPTs.

of_reactions

ColumnTypeDescription
idBIGINT UNSIGNED AI PK
user_idBIGINT UNSIGNEDReacting user
reply_idBIGINT UNSIGNEDReply being reacted to
emojiVARCHAR(10)Emoji character
created_atDATETIMEWhen the reaction was added

Unique constraint on (user_id, reply_id, emoji) — one reaction per emoji per user per reply.

of_subscriptions

ColumnTypeDescription
user_idBIGINT UNSIGNEDSubscriber
sub_typeVARCHAR(20)thread or room
object_idBIGINT UNSIGNEDThread ID or room post ID
frequencyVARCHAR(20)immediate, daily, weekly, none
created_atDATETIME

of_notifications

ColumnTypeDescription
user_idBIGINT UNSIGNEDRecipient
typeVARCHAR(50)new_reply, mention, moderation_notice, solution_accepted
post_idBIGINT UNSIGNEDRelated post ID
from_userBIGINT UNSIGNEDSender user ID
messageTEXTNotification text
urlVARCHAR(500)Click-through URL
is_readTINYINT(1)0 = unread, 1 = read
email_sentTINYINT(1)0 = pending, 1 = sent
created_atDATETIME

of_reports

ColumnTypeDescription
reporter_idBIGINT UNSIGNEDUser who filed the report
post_idBIGINT UNSIGNEDReported post
post_typeVARCHAR(20)of_thread or of_reply
reasonVARCHAR(50)spam, offensive, misinformation, off-topic, other
detailTEXTOptional additional context
statusVARCHAR(20)pending, dismissed, actioned
reviewed_byBIGINT UNSIGNEDModerator who reviewed
reviewed_atDATETIMEWhen reviewed
created_atDATETIMEWhen filed

of_ip_bans & of_mod_log

TableKey columns
of_ip_bansip_address (UNIQUE), reason, banned_by, expires_at (NULL = permanent)
of_mod_logmod_id, action (e.g. shadow_ban, ip_ban, dismiss_report), target_id, target_type, detail
21 · Uninstall

Uninstall & cleanup

When the plugin is deleted (not just deactivated), of_uninstall() permanently removes all forum data.

1
Drops all 6 custom tables.
2
Deletes all of_room, of_thread, of_reply posts and their post meta.
3
Deletes all options with the of_ prefix.
⚠️Warning: This permanently removes all forum data. There is no undo. Export your data before uninstalling. To deactivate without data loss, use Plugins → Deactivate (not Delete).
22 · Changelog

What's shipped

v1.0.0 Initial release · 2026-04-25
  • Full forum room + thread + reply system with of_room, of_thread, of_reply CPTs
  • 4-level trust system with auto-recalculation on every post
  • Emoji reactions — configurable set, toggle-to-remove, per-reply counts
  • Full-text search with filters (room, type, author, date range) + keyword highlights
  • Thread and room subscriptions + immediate email notifications
  • In-forum notification bell with unread count badge + dropdown
  • @mention detection with immediate in-forum notification
  • Reply quoting — prefills form with quoted text + sets parent ID
  • Thread reading position memory (localStorage)
  • Solved / accepted solution marking — pinned above pagination
  • Moderation queue: report, dismiss, warn, delete
  • Shadow ban + IP ban (with optional expiry) + moderation audit log
  • Standalone admin panel with top nav (Orravo UI pattern)
  • Dark / light theme toggle in admin (localStorage persistence)
  • REST API: full CRUD for rooms, threads, replies, reactions, search
  • SEO: BreadcrumbList schema, DiscussionForumPosting schema, Yoast + SEO Framework sitemap integration
  • Analytics dashboard: posts/day, peak hours, top users, room stats
  • Shortcodes: [oforum], [oforum_room], [oforum_thread]
  • Clean uninstall hook — drops tables, CPT posts, and options
  • No jQuery dependency on frontend — vanilla JS throughout
  • PHP 8.0+ required, WordPress 6.0+ required
  • Multisite compatible
✦ Need help?

Got a question about OForum?

Reach out directly — Kenneth replies within 24 hours.