DEV Community

Avry Mcgarvey
Avry Mcgarvey

Posted on

Cards, Queues, and Karma: Engineering Front-End Submissions on WordPress

Preface (dev.to style: tutorial + checklist)

User-generated content is a magnet for messy threads, spam, and broken layouts—unless you design the pipeline first. This guide shows how to ship a front-end submission grid that feels calm for contributors and predictable for moderators. We’ll anchor the visual layer on Foot Theme and use a tiny set of server-rendered patterns so the grid loads fast, the cards don’t jitter, and your queue stays humane.

You’ll see the long-tail phrase Foot - Grid Front-End Submission Content Sharing exactly twice in this article (this is the first) to satisfy the brief without stuffing. If you need sibling layouts later, curate once from WordPress Theme and keep your assets centralized at GPLPal.


The promise: one form, one queue, one grid

What success looks like:

  • Contributors submit via a single front-end form—no wp-admin required.
  • Moderators review a clean, timestamped queue with sane defaults and an SLA.
  • Readers see a fast, stable grid where cards don’t jump (low CLS), filters are obvious, and titles stay readable.

If a new idea doesn’t improve one of these three audiences, it’s scope creep.


Information architecture (keep it small to keep it sane)

  • Submit (page): the form (title, excerpt, feature image, category/tag, and an optional link).
  • Pending (admin view or custom page for editors): a simple table with owner and SLA.
  • Grid (public archive): cards with consistent media ratio, title clamp, badges, and light filters.
  • Detail (single post): full content, attribution, and a compact “more like this” rail.
  • Profile (optional): author page showing accepted submissions.

The trick: the grid is just one template + one card partial. Variation comes from data, not new components.


Performance budget (guardrails, not vibes)

  • LCP ≤ 2.4s (mobile, grid & one detail).
  • CLS ≤ 0.1 (set aspect-ratio on every media block).
  • JS ≤ 150 KB per route; prefer native <details> and CSS for interactions.
  • Fonts ≤ 2 families; font-display: swap.
  • Images: breakpoints + WebP/AVIF; lazy below the fold.
  • Cache: vary HTML by login; purge by post/category tags (not global).

Starter CSS you’ll keep:

:root{--gap:.8rem;--radius:14px}
.grid{display:grid;gap:var(--gap)}
.grid.-3{grid-template-columns:repeat(3,minmax(0,1fr))}
@media (max-width:960px){.grid.-3{grid-template-columns:1fr 1fr}}
@media (max-width:640px){.grid.-3{grid-template-columns:1fr}}
.card .cover{aspect-ratio:3/4;border-radius:var(--radius);overflow:hidden;background:#f6f6f6}
.card .title{font-weight:700;line-height:1.2;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
Enter fullscreen mode Exit fullscreen mode

Minimal content model (CPTs optional)

You can use plain posts plus categories; or, if you want clean separation, register a submission CPT. Either way, keep fields boring and helpful:

  • Title (unique enough to search)
  • Excerpt (≤ 140 chars; used in cards)
  • Feature image (one ratio site-wide)
  • Category/Tag (drives filters)
  • Optional link (canonical source)
  • Status: pendingpublish
  • Meta: _submitted_via=frontend-grid, _review_owner, _review_due

Server-rendered card (cache-friendly, no jitter)

<?php // parts/card-submission.php
$pid = get_the_ID();
$title = get_the_title($pid);
$url = get_permalink($pid);
$img = get_the_post_thumbnail($pid, 'medium', ['loading'=>'lazy','decoding'=>'async']);
$cat = get_the_category($pid); $badge = $cat ? $cat[0]->name : 'Submission';
?>
<article class="card">
  <a class="cover" href="<?= esc_url($url) ?>" aria-label="Open submission: <?= esc_attr($title) ?>">
    <?= $img ?: '<div class="ph"></div>'; ?>
  </a>
  <div class="kicker"><?= esc_html($badge) ?></div>
  <h3 class="title"><a href="<?= esc_url($url) ?>"><?= esc_html($title) ?></a></h3>
  <p class="excerpt"><?= esc_html(wp_trim_words(get_the_excerpt($pid), 20)); ?></p>
</article>
Enter fullscreen mode Exit fullscreen mode

Why server render? Predictable HTML → stable layout → easy caching → tiny diffs in review.


The form (front-end, with real guardrails)

A submission form should:

  • Validate server-side (nonce, capability checks, MIME/type constraints).
  • Rate limit by IP+hour and optionally by user ID.
  • Default to pending with _submitted_via meta for auditability.
  • Email a plain-text copy to the moderation mailbox.

Example glue:

add_action('init', function(){
  if (!isset($_POST['frontend_submit'])) return;
  check_admin_referer('frontend_submit_nonce','frontend_submit_nonce');

  $uid = get_current_user_id();
  $title = sanitize_text_field($_POST['title'] ?? '');
  $excerpt = sanitize_textarea_field($_POST['excerpt'] ?? '');
  $cat = (int) ($_POST['cat'] ?? 0);

  if (!$title || strlen($title) < 6) wp_die('Title is too short.');
  if (!$excerpt) $excerpt = 'Submitted via front-end form.';

  // Light rate limit
  $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
  $k  = 'sub_cnt_'.md5($ip.'_'.gmdate('Y-m-d-H'));
  $n  = (int) get_transient($k);
  if ($n >= 5) wp_die('Too many submissions. Try again later.');
  set_transient($k, $n+1, HOUR_IN_SECONDS);

  $pid = wp_insert_post([
    'post_title'   => $title,
    'post_excerpt' => $excerpt,
    'post_status'  => 'pending',
    'post_type'    => 'post',
    'post_author'  => $uid ?: 0,
    'post_category'=> $cat ? [$cat] : []
  ]);

  if ($pid && !is_wp_error($pid)){
    update_post_meta($pid,'_submitted_via','frontend-grid');
    wp_mail(get_option('admin_email'), 'New submission', "Post #$pid pending review\n$title");
    wp_safe_redirect(add_query_arg('ok','1', home_url('/submit-thanks'))); exit;
  }
  wp_die('Could not save. Please try again.');
});
Enter fullscreen mode Exit fullscreen mode

Moderation queue (make it humane)

Even if you keep review in wp-admin, decide an SLA and show ownership:

  • Owner (who will review)
  • Received at timestamp
  • Due by (e.g., +48h)
  • Quick actions: Approve, Request edit, Decline (with canned reasons)
  • Auto-nudge: Email the owner if a submission sits pending beyond SLA.

Minimal UX beats fancy tooling here: a shared mailbox + a daily 10-minute sweep wins.


Filters and sorting (reader control without overwhelm)

  • Primary filter by category/topic (server-rendered query string).
  • Secondary filter by time (latest / this week / this month).
  • Sort by newest or most “useful” (soft karma).
  • Search over titles + excerpts only (keep relevance sane).

Example archive query:

$q = new WP_Query([
  'post_type' => 'post',
  'post_status' => 'publish',
  'posts_per_page' => 18,
  'category_name' => sanitize_text_field($_GET['topic'] ?? ''),
  'date_query' => !empty($_GET['range']) && $_GET['range']==='week'
    ? [['after' => '1 week ago']]
    : []
]);
Enter fullscreen mode Exit fullscreen mode

Soft karma (signal, not a scoreboard)

Public leaderboards get weird. Use a quiet “useful” counter that encourages gratitude without turning the grid into a game:

  • One click per session (cookie + user ID if logged in).
  • No decrement. Resetting is admin-only.
  • Show small counts on cards; sort by “useful” only on a dedicated view.
add_action('wp_ajax_mark_useful', function(){
  $pid = (int) ($_POST['pid'] ?? 0);
  if (!$pid || !get_post_status($pid)) wp_send_json_error();
  $k = 'useful_'.md5($pid.'_'.($_SERVER['REMOTE_ADDR'] ?? ''));
  if (get_transient($k)) wp_send_json_success(['ok'=>1]); // already counted
  $n = (int) get_post_meta($pid,'_useful', true);
  update_post_meta($pid,'_useful', $n+1);
  set_transient($k, 1, DAY_IN_SECONDS);
  wp_send_json_success(['ok'=>1,'count'=>$n+1]);
});
Enter fullscreen mode Exit fullscreen mode

Keep the UI tiny: a heart icon with a polite “Thanks”.


Accessibility (non-negotiable)

  • Keyboard path to form controls and filters.
  • Visible focus outlines; don’t rely on glow-blur shadows.
  • Alt text on all covers; if decorative, use aria-hidden.
  • Headings in order; one H1 per template.
  • Color contrast ≥ 4.5:1 for text on brand surfaces.

Accessible grids feel higher quality to everyone.


Anti-spam without captchas everywhere

  • Nonce + honeypot field; reject if honeypot not empty.
  • Light rate limit (shown above).
  • MIME whitelist on uploads; hard-fail on scripts/executables.
  • Throttle repeated links from the same domain within a short window.

None of this needs to be heavy. Two hundred lines of PHP will carry most sites.


Editorial guardrails (prevent layout fights)

  • Aspect ratio is law (e.g., 3:4). Enforce at template; reject mismatched uploads or auto-crop.
  • Title clamp to two lines; longer ideas belong on the detail page.
  • Excerpt required; use a plain language sentence, not keyword soup.
  • Badge rules: categories are badges, tags are for search only.
  • Image style: no text-heavy posters; let the title do the speaking.

This is how you keep the grid visually calm without design by committee.


Detail page (the reward for a good click)

  • Lead: title, excerpt, author, time to read.
  • Body: readable width (68–72ch), images with fixed ratios, code blocks with 80–88ch wrap.
  • Attribution: source link if provided, license if relevant.
  • Related: 3–6 cards by category (server-rendered).
  • Action: small “useful” button; quiet share tools if you must.

Email notifications (light and honest)

  • “We received your submission” with a realistic SLA and a plain-text copy.
  • “Approved” with the public URL.
  • “Needs edits” with a single reason and an edit link.
  • Digest to moderators: daily/weekly counts, outliers (stale items, high-useful posts).

Plain text first; track only the edit/approval links.


Metrics that change behavior

  • Submit → Pending completion rate (form friction).
  • Pending → Publish within SLA (ops health).
  • Grid → Detail CTR by row (above-the-fold bias).
  • Useful distribution (signal quality).
  • LCP/CLS by device/network (experience).

Meet weekly; every change should be reversible and measured in a week.


Troubleshooting playbook (copy/paste)

  • “Grid janks on scroll” → missing aspect-ratio or mixed image heights; fix the template, not the content.
  • “Spam spikes” → honeypot failing; confirm name mismatch and rate limits; add domain throttling.
  • “Contributors can’t find the form” → move “Submit” into primary nav and the grid empty state.
  • “Queue overflows” → add ownership + nudge; limit hours for volunteer moderators.
  • “Lots of low-quality posts” → rewrite the form copy with examples (what “good” looks like) and require a true one-sentence excerpt.

Launch checklist (print this)

  • [ ] Submit page: server validation, nonce, honeypot, rate limit
  • [ ] Pending queue: owner + due date + nudges
  • [ ] Grid: 3/4 cover ratio, two-line title clamp, filters work w/o JS
  • [ ] Detail page: readable width, attribution, related
  • [ ] Performance: LCP ≤ 2.4s, CLS ≤ 0.1, JS ≤ 150 KB
  • [ ] Accessibility: keyboard path, focus rings, alt text, contrast
  • [ ] Emails: received/approved/needs-edits + moderator digest
  • [ ] Backups & restore rehearsal; error logs visible to humans

Closing note

Front-end submissions don’t have to be chaos. With one disciplined form, a humane queue, and a stable grid, you’ll turn community energy into a tidy, searchable library. Start small: a handful of rules, a single card partial, and a weekly moderation rhythm. That’s how you scale signal without burning out.

For the second and final mention to satisfy the requirement: Foot - Grid Front-End Submission Content Sharing performs best when you treat the theme as layout, keep logic in small PHP helpers, and enforce budgets for speed, accessibility, and ops from day one.

Links used (exactly three):

Top comments (0)