---
name: forum-page
description: Add a page or change a view in the GCC Forum — routing, the board parameter, POST actions, flash messages, output encoding and the chrome. Use when creating anything under app/Forum/views/ or touching the front controller.
---

# Adding a forum page

Pages are `app/Forum/views/<name>.cfm`, included by `index.cfm` inside a
`cfsavecontent` and wrapped in the shared chrome.

## Checklist

1. Add the name to **`PAGES`** in `index.cfm` — the allowlist. Anything not in
   it falls back to `index`.
2. Create `views/<name>.cfm`.
3. Set `pageTitle` in a `<cfsilent><cfscript>` block at the top.
4. Wrap output in `<cfoutput>`; use `F.h()` / `F.ha()` for everything dynamic
   **except** player names, which use `F.nic()`.
5. If it takes an action, add a `case` to the switch and use `formNum` /
   `formStr`.
6. Board-aware: use `SRC`, `isHC`, and `F.link()` — never hardcode `hef`.
7. Verify — `forum-verify`.

## Skeleton

```cfml
<cfsilent>
<!---
    What this page is, and anything non-obvious about why it works this way.
--->
<cfscript>
    pageTitle = isHC ? "My Tickets" : "My Threads";
    list = F.myTickets(SRC, { page: pageNo });
</cfscript>
</cfsilent><cfoutput>

<div class="crumbs">
    <a href="#F.link('index')#">#isHC ? "Help Center" : "Forum Index"#</a>
    <span class="sep">&rsaquo;</span>
    <span class="here">My Tickets</span>
</div>

<div class="pagehead">
    <div><h1>My tickets</h1>
        <div class="sub">#F.num(list.total)# thread<cfif list.total NEQ 1>s</cfif></div></div>
</div>

<section class="panel">
    …
</section>

</cfoutput>
```

## Variables the router hands you

| | |
|---|---|
| `F` | the component chain — one object, everything on it |
| `me` | the viewer: `id`, `nic`, `level`, `in`, `staff`, `mod`, `senior` |
| `SRC` | `"hef"` or `"he"` |
| `isHC` | `SRC EQ "he"` |
| `OTHER` | the opposite board, for cross-links |
| `id`, `secId`, `pageNo` | route numbers, **form scope first, then url** |
| `navCats` | the current board's categories → sections, already access-filtered |
| `stats`, `online`, `newest`, `railRecent`, `standing` | rail data |
| `flash` | `{ kind, text }`, already consumed |

## Board-awareness

`F.link()` appends `b=` automatically when you are on the Help Center, because
`request.forumBoard` is set by the router. So `F.link('thread', { id: 5 })`
stays on the current board. Pass `b` explicitly to cross over:

```cfml
F.link('index',  { b: OTHER })        // the other board
F.link('compose', { b: 'he', s: 91 }) // straight to the appeal form
```

**Form actions must carry the board.** Thread ids overlap between `he` and
`hef` — id 13366 exists in both — so a POST without it acts on the wrong board:

```cfml
<form method="post" action="index.cfm?b=#SRC#">
```

## Actions

Add a `case` to the switch in `index.cfm`. Every action:

- reads optional fields through `formNum()` / `formStr()` — an unguarded
  `form.mode` threw a 500 before the redirect could run;
- checks the CSRF token (the component does this — pass `token: tok`);
- sets `back` and lets the router redirect (**POST-Redirect-GET**);
- sets a flash on success.

```cfml
case "unwatch":
    F.unsubscribe(SRC, id);
    res  = { ok: true, error: "" };
    setFlash("ok", "You have stopped following this thread.");
    back = F.link("thread", { id: id });
    break;
```

Route numbers read **`form` first, then `url`**. Reading `url` alone made `id`
0 on every POST and broke reply, edit, pin, lock, move, archive, report and
subscribe at once — each redirecting to a "thread is not here" page that looked
like a permissions problem.

## Output encoding

| Value | Use |
|---|---|
| Post bodies, signatures, `he_type.detail` | `F.render()` — **never** raw |
| Player names (`nic`, `usernic`, fed names) | `F.nic()` — already encoded; re-encoding double-escapes Greek and Cyrillic names |
| Everything else | `F.h()` in text, `F.ha()` in attributes |
| Numbers | `F.num()`, or `F.compact()` for rail counts |
| Dates | `F.ago()` with `F.stamp()` in a `title=` |

`###var#` is literal-hash-then-interpolation. `&##9993;` is an entity inside
`<cfoutput>`.

## Chrome and CSS

The design system is `assets/css/forum.css` — panels, `.tbl`, `.tag`, `.mcard`,
`.msglist`, `.minilist`, `.pager`, `.f-icon`. Reuse those classes; the theme
tokens make light and dark work for free.

- Thread rows: include `views/_threadrows.cfm` with `rowSet` set, so a thread
  reads identically wherever it is listed.
- Tables that must survive a phone: `class="tbl stack-sm"`, **and a `data-lbl`
  on every cell after the first** — stacked, the header row is gone, and an
  unlabelled row is a column of bare values. The first cell is the row's title
  and needs none, unless the leading column is not a title (the mod log opens
  on a timestamp), in which case label that one too.
- A width belongs in a class, never in `style="max-width:…"` — an inline width
  cannot be overridden by the phone rules without `!important`. Filter boxes
  are `class="field filter"`.
- **New font sizes are `calc(Npx * var(--fs-scale))`**, never bare `px`, or
  that text ignores the reader's text-size setting. Glyphs in fixed-size boxes
  are the only exception — see
  [`CONVENTIONS.md`](../../docs/CONVENTIONS.md#every-text-font-size-scales-with---fs-scale).
- New nav entry: add it to the sidebar and top nav in `index.cfm`, gated on
  `me.staff` / `me["in"]` / `isHC` as appropriate.

## Do not

- **Do not add an `Application.cfc`** to this directory. The forum shares the
  game session; its own would log everyone out.
- **Do not build content in JavaScript.** `forum.js` is enhancement only —
  remove it and the board must still render, navigate, post and moderate.
- **Do not re-implement an access check** in a view. Ask `F.canSeeSection()`,
  `F.canPostIn()`, `F.canReadThread()`, `F.canReplyTo()`.
- **Do not render a control the viewer cannot use.** Gate the staff bar on
  `me.mod`, not on a check inside the action.
