ForumSpark Support

support.forumspark.net

Official support and community forum for ForumSpark. Access announcements, troubleshooting, customization help, extension development, theme resources, and platform updates.

205 members 714 topics 3K replies
Cedric ·ForumSpark Headlines ·3 months ago
Translate your community

Translate your community

One thing ForumSpark was still missing was the ability to translate your community or simply change certain wording to suit your needs. Until now, changing interface text usually meant working around it with CSS. That is no longer necessary.

This update brings full language management to every ForumSpark board.

Translate directly from your forum

The easiest way to translate your board is through Live Translate.

Open Languages under Appearance in your admin panel and press Live Translate. You will return to your forum with every interface string outlined.

Long-press any text to open the editor, enter your translation and save it. Every matching occurrence on the page updates immediately.

Amber outlines show strings that still need translating, while green means they are done. Normal clicks continue to work, so you can browse through your community and translate each page as you go.

The counter in the translation bar shows how many untranslated strings remain on the current page. Click it to open the full list and work through them one by one without having to find each string on the page.

Tooltips and other text that is not directly visible are listed separately under hidden strings, so those can be translated too.

Or use the string editor

Prefer having everything in one place? The string editor shows the original English string next to your translation.

You can filter untranslated strings or browse them by area, including emails and notifications.

Placeholders such as :name are validated automatically, preventing you from accidentally removing information that needs to be inserted later.

Custom menu items and widget titles are included as well.

Bring your own translators

Each language can be exported as a single JSON file. You can export the entire language or only the strings that are still missing.

Send the file to a translator, edit it yourself or work on it however you prefer, then import it back into ForumSpark. Everything is validated before it is saved, and you receive a full report of anything that needs your attention.

New languages begin as drafts and can only be previewed by administrators. Nothing becomes visible to your members until you are ready.

What your members see

You can set one default language for the entire board or allow members to choose their own through a selector next to the theme selector.

Dates and times adjust automatically. A Dutch community, for example, will show 13 seconden geleden without you having to translate it yourself.

Member-created content is never translated or changed. This only applies to the interface around it.

Languages are available today on every board and every plan.

Open Appearance, choose Languages, add your language and press Live Translate to get started. You can also view the full guide here.

Should you find any strings that cannot be translated, please let us know through Support.

Also in this update

Flagging a member as a spammer now works as expected.

Their content is immediately hidden from the public, and their profile clearly states:

This member is flagged as a spammer. Their content is hidden from the forum.

Staff can still view everything through a red overlay, allowing you to review what was posted. You can delete the content immediately or leave it to the automatic cleanup, which removes content from flagged spammers after 48 hours.

The Activity page also received some attention. Topic titles and labels now sit together more neatly, the page behaves properly on mobile, and previews show more content before being truncated.

And finally, ForumSpark is officially out of beta. Yay! 🤗

Originally published on the ForumSpark blog

4 replies 470 Read more →
A AlChEmY ·Feedback ·3 months ago
Not to bad Cedric! You done a superb job!

Overall I like what you have done and I might just buy the the second or third tier package.

It matches what I want for my MMORPG browser game, that's in early beta v15.0.0. It is live right now for a trail period and feedback.

What am I actually getting if I was to pick this forum platform?

Will I get support like actual help creating my forum because if my game goes big and people ask about my forum software, I will point them your way if not is their like a affiliate or discount promotion or some way I earn if not I wont mention your forum platform.

Thank you, with kind humble regards, AlChEmY/Ricardo

20 replies 587 Read more →
Cedric ·ForumSpark Headlines ·3 months ago
Events is live

Events is live

A few board owners asked us for events. They were running meetups out of pinned topics, keeping the guest list in the first post and bumping it by hand. That deserved better, so we built it properly. Events is the headline, but this release also brings email for private messages, a new moderation menu on topics, and a reorganised admin panel navigation.

What Events does

You publish events on your board. Members RSVP going, maybe or declined. Set a capacity and we hold the seats safely, so nothing oversells, and everyone attending gets a reminder before it starts.

Outside guests are handled. Attendees can bring named guests, and you can invite people by email with a confirm or decline link, no account needed. Every event has calendar buttons for Google, Outlook and Apple, so it lands in everyone's own calendar too.

Run the day from the attendee list and export it to CSV. Group events into categories with their own icon and colour, give events their own discussion topics, and let the Upcoming Events widget surface the next few anywhere on your board.

  • A list and a month view, so members always see what is coming.

  • Capacity that holds. Places left are counted live and nothing oversells.

  • Guests without accounts, invited by email with one click to confirm or decline.

  • Reminders included. Attendees hear about it before it starts, automatically.

Included from Launch

Events ships with the Launch plan alongside Blog, Gallery, Marketplace and Page Studio. You will find it in your admin panel under Applications, ready to switch on. Go and schedule something your next big event.

Private messages now reach the inbox

Members get an email when someone sends them a private message: who wrote, the conversation title and a short preview. It is on by default and every member can switch it off in their notification settings. A burst of messages becomes one email, and if the conversation has already been read on the board, no email goes out at all.

Moderate without leaving the topic

Moderators get a Moderate menu on the topic itself: close or open, pin or unpin, move, merge and delete, all without bouncing back to the topic list.

An redesigned admin panel navigation

Due to it being quite long, the admin panel navigation has been reorganised into clear sections, from Settings and Community through to Applications and System. Hover a section to peek inside, click to jump in, and whatever page you open, the menu highlights where you are.

Better on phones

Various fixes have been done mobile, the New Topic button is back on the mobile topic list, the forum header takes half the space it used to, bulk moderation works from a phone, etc. We also clicked through every page on desktop and mobile this week and repaired what we found along the way, including private message editing, hard to read gray text in light mode, and a stubborn redirect on the activity page. As always, feel free to leave feedback and recommend the next big thing!

Not running on ForumSpark yet? Start a board today and host your first event this week.

Create your forum

Originally published on the ForumSpark blog

2 replies 418 Read more →
Cedric ·Customization Help ·3 months ago
Custom Templates: Topic list template

Here are some examples for the topic list view:

Template:

<div class="pl">
{{#if pinned}}
{{#each pinned}}
<div class="pl-card pl-card--pinned{{#unless this.is_read}} pl-card--unread{{/unless}}">
    <span class="pl-avatar">
        <a href="{{ this.author.profile_url }}"><img src="{{ this.author.avatar_url }}" alt="{{ this.author.username }}" /></a>
    </span>
    <div class="pl-main">
        <div class="pl-chips">
            <span class="pl-pin"><span class="fa-solid fa-thumbtack"></span> Pinned</span>
            {{#each this.labels}}
                <span class="pl-chip">{{ this.title }}</span>
            {{/each}}
            {{#if this.is_locked}}<span class="pl-state"><span class="fa-solid fa-lock"></span> Locked</span>{{/if}}
            {{#if this.is_solved}}<span class="pl-state pl-state--solved"><span class="fa-solid fa-check"></span> Solved</span>{{/if}}
        </div>
        <div class="pl-title"><a href="{{ this.url }}">{{ this.title }}</a></div>
        <div class="pl-meta">{{ this.author.username }} <span class="pl-sep">·</span> {{ this.created_at }}</div>
        <div class="pl-foot">
            <span class="pl-stat"><span class="fa-regular fa-comment"></span> {{ this.replies_count }}</span>
            <span class="pl-stat"><span class="fa-regular fa-eye"></span> {{ this.views_short }}</span>
            {{#if this.last_post.author}}
            <span class="pl-activity">
                <img src="{{ this.last_post.avatar_url }}" alt="{{ this.last_post.author }}" />
                <a href="{{ this.last_post.url }}">{{ this.last_post.author }} <span class="pl-sep">·</span> {{ this.last_post.date }}</a>
            </span>
            {{/if}}
        </div>
    </div>
    {{{ this.checkbox }}}
</div>
{{/each}}
{{/if}}
{{#each topics}}
<div class="pl-card{{#unless this.is_read}} pl-card--unread{{/unless}}">
    <span class="pl-avatar">
        <a href="{{ this.author.profile_url }}"><img src="{{ this.author.avatar_url }}" alt="{{ this.author.username }}" /></a>
    </span>
    <div class="pl-main">
        <div class="pl-chips">
            {{#each this.labels}}
                <span class="pl-chip">{{ this.title }}</span>
            {{/each}}
            {{#if this.is_hot}}<span class="pl-state pl-state--hot"><span class="fa-solid fa-fire-flame-curved"></span> Hot</span>{{/if}}
            {{#if this.is_locked}}<span class="pl-state"><span class="fa-solid fa-lock"></span> Locked</span>{{/if}}
            {{#if this.is_solved}}<span class="pl-state pl-state--solved"><span class="fa-solid fa-check"></span> Solved</span>{{/if}}
        </div>
        <div class="pl-title"><a href="{{ this.url }}">{{ this.title }}</a></div>
        <div class="pl-meta">{{ this.author.username }} <span class="pl-sep">·</span> {{ this.created_at }}</div>
        <div class="pl-foot">
            <span class="pl-stat"><span class="fa-regular fa-comment"></span> {{ this.replies_count }}</span>
            <span class="pl-stat"><span class="fa-regular fa-eye"></span> {{ this.views_short }}</span>
            {{#if this.participants}}
            <span class="pl-people">
                {{#each this.participants}}
                    <img src="{{ this.avatar_url }}" alt="{{ this.username }}" />
                {{/each}}
            </span>
            {{/if}}
            {{#if this.last_post.author}}
            <span class="pl-activity">
                <img src="{{ this.last_post.avatar_url }}" alt="{{ this.last_post.author }}" />
                <a href="{{ this.last_post.url }}">{{ this.last_post.author }} <span class="pl-sep">·</span> {{ this.last_post.date }}</a>
            </span>
            {{/if}}
        </div>
    </div>
    {{{ this.checkbox }}}
</div>
{{/each}}
{{#unless topics}}{{#unless pinned}}
<div class="pl-empty">No topics here yet.</div>
{{/unless}}{{/unless}}
{{#if pagination.has_pages}}
<div class="pl-pages">
    Page {{ pagination.current_page }} of {{ pagination.last_page }}
    {{#if pagination.prev_url}}<a href="{{ pagination.prev_url }}">Previous</a>{{/if}}
    {{#if pagination.next_url}}<a href="{{ pagination.next_url }}">Next</a>{{/if}}
</div>
{{/if}}
</div>

CSS:

.pl { display: flex; flex-direction: column; gap: 0.65rem; padding: 0.75rem; }
.pl-card { display: flex; gap: 0.8rem; padding: 0.9rem 1rem; background: var(--fs-topic-bg, rgba(128,128,128,0.04)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.18)); border-radius: 0.75rem; transition: border-color 0.14s ease, box-shadow 0.14s ease; }
.pl-card:hover { border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.4)); box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); }
.pl-card--unread { box-shadow: inset 3px 0 0 var(--fs-panel-footer-accent, currentColor); }
.pl-card--pinned { background: var(--fs-panel-section-bg, rgba(128,128,128,0.07)); }
.pl-avatar { flex-shrink: 0; }
.pl-avatar img { width: 2.6rem; height: 2.6rem; border-radius: 50%; object-fit: cover; display: block; }
.pl-main { flex: 1; min-width: 0; }
.pl-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-bottom: 0.25rem; }
.pl-pin, .pl-chip, .pl-state { display: inline-flex; align-items: center; gap: 0.28rem; font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; padding: 0.14rem 0.5rem; border-radius: 999px; }
.pl-pin { color: #fff; background: var(--fs-panel-footer-accent, #35a377); }
.pl-chip { background: var(--fs-panel-section-bg, rgba(128,128,128,0.12)); opacity: 0.85; }
.pl-state { border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.25)); opacity: 0.8; }
.pl-state--solved { color: var(--fs-panel-footer-accent, inherit); border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.4)); opacity: 1; }
.pl-state--hot { opacity: 1; }
.pl-title a { font-size: 1rem; font-weight: 700; letter-spacing: -0.01em; color: var(--fs-forum-link-color, inherit); text-decoration: none; }
.pl-title a:hover { color: var(--fs-panel-footer-accent, inherit); }
.pl-card--unread .pl-title a { color: var(--fs-panel-footer-accent, inherit); }
.pl-meta { font-size: 0.73rem; opacity: 0.6; margin-top: 0.1rem; }
.pl-sep { opacity: 0.5; margin: 0 0.1rem; }
.pl-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; margin-top: 0.55rem; font-size: 0.74rem; }
.pl-stat { display: inline-flex; align-items: center; gap: 0.32rem; opacity: 0.75; font-variant-numeric: tabular-nums; }
.pl-people { display: inline-flex; }
.pl-people img { width: 1.4rem; height: 1.4rem; border-radius: 50%; object-fit: cover; border: 2px solid var(--fs-topic-bg, #fff); margin-left: -0.4rem; }
.pl-people img:first-child { margin-left: 0; }
.pl-activity { display: inline-flex; align-items: center; gap: 0.4rem; margin-left: auto; min-width: 0; }
.pl-activity img { width: 1.35rem; height: 1.35rem; border-radius: 50%; object-fit: cover; }
.pl-activity a { font-size: 0.72rem; color: inherit; opacity: 0.7; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-activity a:hover { opacity: 1; }
.pl-card .topic-row__bulk-select { align-self: center; }
.pl-empty { padding: 1.6rem 1rem; text-align: center; opacity: 0.7; }
.pl-pages { padding: 0.4rem 0.3rem; font-size: 0.78rem; opacity: 0.8; display: flex; gap: 0.8rem; }
.pl-pages a { color: var(--fs-link-color, inherit); font-weight: 600; text-decoration: none; }
.pl-pages a:hover { text-decoration: underline; }
@media (max-width: 640px) {
    .pl-activity { display: none; }
    .pl-people { display: none; }
}

Template:

<div class="tk">
<div class="tk-cols">
    <span></span>
    <span>Topic</span>
    <span class="tk-r">Replies</span>
    <span class="tk-r">Views</span>
    <span>Activity</span>
</div>
{{#if pinned}}
{{#each pinned}}
<div class="tk-row tk-row--pinned{{#unless this.is_read}} tk-row--unread{{/unless}}">
    <span class="tk-glyph"><span class="fa-solid fa-thumbtack"></span></span>
    <div class="tk-main">
        <div class="tk-title">
            <a href="{{ this.url }}">{{ this.title }}</a>
            {{#each this.labels}}
                <span class="tk-label">{{ this.title }}</span>
            {{/each}}
            {{#if this.is_locked}}<span class="tk-mini fa-solid fa-lock"></span>{{/if}}
            {{#if this.is_solved}}<span class="tk-mini tk-mini--solved fa-solid fa-check"></span>{{/if}}
        </div>
        <div class="tk-meta">{{ this.author.username }} · {{ this.created_at }}</div>
    </div>
    <span class="tk-num">{{ this.replies_count }}</span>
    <span class="tk-num">{{ this.views_short }}</span>
    <div class="tk-act">
        {{#if this.last_post.author}}
        <a href="{{ this.last_post.url }}">{{ this.last_post.author }}</a>
        <span>{{ this.last_post.date }}</span>
        {{/if}}
    </div>
    {{{ this.checkbox }}}
</div>
{{/each}}
{{/if}}
{{#each topics}}
<div class="tk-row{{#unless this.is_read}} tk-row--unread{{/unless}}">
    <span class="tk-glyph">
        {{#if this.is_locked}}
            <span class="fa-solid fa-lock"></span>
        {{else}}
            {{#if this.is_hot}}
                <span class="tk-hot fa-solid fa-fire-flame-curved"></span>
            {{else}}
                {{#if this.is_solved}}
                    <span class="tk-ok fa-solid fa-check"></span>
                {{else}}
                    <span class="tk-dot"></span>
                {{/if}}
            {{/if}}
        {{/if}}
    </span>
    <div class="tk-main">
        <div class="tk-title">
            <a href="{{ this.url }}">{{ this.title }}</a>
            {{#each this.labels}}
                <span class="tk-label">{{ this.title }}</span>
            {{/each}}
        </div>
        <div class="tk-meta">{{ this.author.username }} · {{ this.created_at }}</div>
    </div>
    <span class="tk-num">{{ this.replies_count }}</span>
    <span class="tk-num">{{ this.views_short }}</span>
    <div class="tk-act">
        {{#if this.last_post.author}}
        <a href="{{ this.last_post.url }}">{{ this.last_post.author }}</a>
        <span>{{ this.last_post.date }}</span>
        {{/if}}
    </div>
    {{{ this.checkbox }}}
</div>
{{/each}}
{{#unless topics}}{{#unless pinned}}
<div class="tk-empty">No topics here yet.</div>
{{/unless}}{{/unless}}
{{#if pagination.has_pages}}
<div class="tk-pages">
    Page {{ pagination.current_page }} of {{ pagination.last_page }}
    {{#if pagination.prev_url}}<a href="{{ pagination.prev_url }}">Previous</a>{{/if}}
    {{#if pagination.next_url}}<a href="{{ pagination.next_url }}">Next</a>{{/if}}
</div>
{{/if}}
</div>

CSS:

.tk-cols, .tk-row { display: grid; grid-template-columns: 1.7rem minmax(0, 1fr) 4.5rem 4.5rem 11rem; gap: 0.9rem; align-items: center; padding: 0 0.9rem; }
.tk:has(.topic-row__bulk-select) .tk-cols,
.tk:has(.topic-row__bulk-select) .tk-row { grid-template-columns: 1.7rem minmax(0, 1fr) 4.5rem 4.5rem 11rem 1.6rem; }
.topic-row__bulk-select { display: flex; justify-content: center; }
.tk-cols { padding-top: 0.55rem; padding-bottom: 0.45rem; font-size: 0.58rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.11em; opacity: 0.45; border-bottom: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); }
.tk-r { text-align: right; }
.tk-row { padding-top: 0.55rem; padding-bottom: 0.55rem; }
.tk-row + .tk-row { border-top: 1px solid var(--fs-forum-divider, rgba(128,128,128,0.1)); }
.tk-row:hover { background: var(--fs-panel-section-bg, rgba(128,128,128,0.05)); }
.tk-row--pinned { background: var(--fs-panel-section-bg, rgba(128,128,128,0.06)); }
.tk-glyph { display: flex; align-items: center; justify-content: center; font-size: 0.72rem; opacity: 0.65; }
.tk-row--pinned .tk-glyph { color: var(--fs-panel-footer-accent, inherit); opacity: 1; }
.tk-hot { color: var(--fs-panel-footer-accent, inherit); }
.tk-ok { color: var(--fs-panel-footer-accent, inherit); }
.tk-dot { width: 0.42rem; height: 0.42rem; border-radius: 50%; background: currentColor; opacity: 0.28; }
.tk-row--unread .tk-dot { background: var(--fs-panel-footer-accent, currentColor); opacity: 1; }
.tk-main { min-width: 0; }
.tk-title { display: flex; align-items: center; gap: 0.45rem; min-width: 0; flex-wrap: wrap; }
.tk-title a { font-size: 0.86rem; font-weight: 500; color: var(--fs-forum-link-color, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tk-title a:hover { text-decoration: underline; }
.tk-row--unread .tk-title a { font-weight: 750; }
.tk-label { font-size: 0.58rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.1rem 0.42rem; border-radius: 0.28rem; background: var(--fs-panel-section-bg, rgba(128,128,128,0.12)); opacity: 0.85; }
.tk-mini { font-size: 0.62rem; opacity: 0.6; }
.tk-mini--solved { color: var(--fs-panel-footer-accent, inherit); opacity: 1; }
.tk-meta { font-size: 0.67rem; opacity: 0.52; margin-top: 0.05rem; }
.tk-num { text-align: right; font-size: 0.8rem; font-weight: 600; font-variant-numeric: tabular-nums; opacity: 0.8; }
.tk-act { display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
.tk-act a { font-size: 0.72rem; font-weight: 600; color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tk-act a:hover { text-decoration: underline; }
.tk-act span { font-size: 0.64rem; opacity: 0.5; }
.tk-empty { padding: 1.6rem 1rem; text-align: center; opacity: 0.7; }
.tk-pages { padding: 0.6rem 0.9rem; font-size: 0.76rem; opacity: 0.8; display: flex; gap: 0.8rem; border-top: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); }
.tk-pages a { color: var(--fs-link-color, inherit); font-weight: 600; text-decoration: none; }
.tk-pages a:hover { text-decoration: underline; }
@media (max-width: 760px) {
    .tk-cols { display: none; }
    .tk-row { grid-template-columns: 1.4rem minmax(0, 1fr) 3rem; gap: 0.6rem; }
    .tk:has(.topic-row__bulk-select) .tk-row { grid-template-columns: 1.4rem minmax(0, 1fr) 3rem 1.6rem; }
    .tk-num + .tk-num { display: none; }
    .tk-act { display: none; }
}

Template:

<div class="hd">
{{#if pinned}}
<div class="hd-eyebrow">Featured</div>
<div class="hd-features">
    {{#each pinned}}
    <div class="hd-hero{{#unless this.is_read}} hd-hero--unread{{/unless}}">
        <div class="hd-hero-chips">
            <span class="hd-pin"><span class="fa-solid fa-thumbtack"></span></span>
            {{#each this.labels}}
                <span class="hd-chip">{{ this.title }}</span>
            {{/each}}
            {{#if this.is_locked}}<span class="hd-chip"><span class="fa-solid fa-lock"></span> Locked</span>{{/if}}
        </div>
        <div class="hd-hero-title"><a href="{{ this.url }}">{{ this.title }}</a></div>
        <div class="hd-hero-meta">
            <img src="{{ this.author.avatar_url }}" alt="{{ this.author.username }}" />
            <span>{{ this.author.username }} <span class="hd-sep">·</span> {{ this.created_at }}</span>
        </div>
        <div class="hd-hero-stats">
            <b>{{ this.replies_count }}</b> replies <span class="hd-sep">·</span> <b>{{ this.views_short }}</b> views
            {{#if this.last_post.author}}
            <span class="hd-sep">·</span> last {{ this.last_post.author }}, {{ this.last_post.date }}
            {{/if}}
        </div>
    </div>
    {{/each}}
</div>
{{/if}}
<div class="hd-eyebrow">Latest</div>
<div class="hd-list">
    {{#each topics}}
    <div class="hd-row{{#unless this.is_read}} hd-row--unread{{/unless}}">
        <div class="hd-main">
            <div class="hd-title">
                <a href="{{ this.url }}">{{ this.title }}</a>
                {{#if this.is_hot}}<span class="hd-flame fa-solid fa-fire-flame-curved"></span>{{/if}}
                {{#if this.is_solved}}<span class="hd-check fa-solid fa-check"></span>{{/if}}
                {{#if this.is_locked}}<span class="hd-lock fa-solid fa-lock"></span>{{/if}}
            </div>
            <div class="hd-meta">
                {{#each this.labels}}
                    <span class="hd-tag">{{ this.title }}</span>
                {{/each}}
                {{ this.author.username }} <span class="hd-sep">·</span> {{ this.created_at }}
            </div>
        </div>
        <div class="hd-side">
            <span class="hd-count"><b>{{ this.replies_count }}</b> replies</span>
            <span class="hd-count hd-count--dim"><b>{{ this.views_short }}</b> views</span>
            {{#if this.last_post.author}}
            <a href="{{ this.last_post.url }}" class="hd-when">{{ this.last_post.date }}</a>
            {{/if}}
        </div>
        {{{ this.checkbox }}}
    </div>
    {{/each}}
</div>
{{#unless topics}}{{#unless pinned}}
<div class="hd-empty">No topics here yet.</div>
{{/unless}}{{/unless}}
{{#if pagination.has_pages}}
<div class="hd-pages">
    Page {{ pagination.current_page }} of {{ pagination.last_page }}
    {{#if pagination.prev_url}}<a href="{{ pagination.prev_url }}">Previous</a>{{/if}}
    {{#if pagination.next_url}}<a href="{{ pagination.next_url }}">Next</a>{{/if}}
</div>
{{/if}}
</div>

CSS:

.hd { padding: 0.9rem; }
.hd-eyebrow { font-size: 0.62rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.18em; opacity: 0.5; margin: 0 0 0.6rem; }
.hd-list + .hd-eyebrow, .hd-features + .hd-eyebrow { margin-top: 1.4rem; }
.hd-features { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.8rem; margin-bottom: 1.4rem; }
.hd-hero { padding: 1rem 1.1rem; background: var(--fs-panel-section-bg, rgba(128,128,128,0.06)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.18)); border-top: 3px solid var(--fs-panel-footer-accent, rgba(128,128,128,0.4)); border-radius: 0.6rem; }
.hd-hero--unread { background: var(--fs-topic-bg, rgba(128,128,128,0.08)); }
.hd-hero-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-bottom: 0.4rem; }
.hd-pin { display: inline-flex; align-items: center; justify-content: center; width: 1.4rem; height: 1.4rem; border-radius: 50%; color: #fff; background: var(--fs-panel-footer-accent, #35a377); font-size: 0.6rem; }
.hd-chip { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; padding: 0.13rem 0.5rem; border-radius: 999px; background: var(--fs-panel-bg, rgba(255,255,255,0.6)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); opacity: 0.85; }
.hd-hero-title a { font-size: 1.22rem; font-weight: 800; letter-spacing: -0.02em; line-height: 1.25; color: var(--fs-forum-link-color, inherit); text-decoration: none; }
.hd-hero-title a:hover { color: var(--fs-panel-footer-accent, inherit); }
.hd-hero--unread .hd-hero-title a { color: var(--fs-panel-footer-accent, inherit); }
.hd-hero-meta { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.5rem; font-size: 0.73rem; opacity: 0.7; }
.hd-hero-meta img { width: 1.5rem; height: 1.5rem; border-radius: 50%; object-fit: cover; }
.hd-hero-stats { margin-top: 0.55rem; font-size: 0.73rem; opacity: 0.75; font-variant-numeric: tabular-nums; }
.hd-hero-stats b { font-weight: 800; }
.hd-sep { opacity: 0.45; margin: 0 0.12rem; }
.hd-row { display: flex; align-items: center; gap: 1.2rem; padding: 0.75rem 0.2rem; }
.hd-row + .hd-row { border-top: 1px solid var(--fs-forum-divider, rgba(128,128,128,0.12)); }
.hd-row:hover { background: var(--fs-panel-section-bg, rgba(128,128,128,0.04)); }
.hd-main { flex: 1; min-width: 0; }
.hd-title { display: flex; align-items: center; gap: 0.45rem; min-width: 0; }
.hd-title a { font-size: 0.97rem; font-weight: 650; letter-spacing: -0.01em; color: var(--fs-forum-link-color, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hd-title a:hover { color: var(--fs-panel-footer-accent, inherit); }
.hd-row--unread .hd-title a { font-weight: 850; color: var(--fs-panel-footer-accent, inherit); }
.hd-flame, .hd-check { font-size: 0.7rem; color: var(--fs-panel-footer-accent, inherit); }
.hd-lock { font-size: 0.68rem; opacity: 0.55; }
.hd-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem; font-size: 0.7rem; opacity: 0.6; margin-top: 0.12rem; }
.hd-tag { font-size: 0.58rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.08rem 0.4rem; border-radius: 0.26rem; background: var(--fs-panel-section-bg, rgba(128,128,128,0.12)); }
.hd-side { flex-shrink: 0; display: flex; align-items: baseline; gap: 1.1rem; }
.hd-count { font-size: 0.72rem; opacity: 0.8; font-variant-numeric: tabular-nums; }
.hd-count b { font-size: 0.88rem; font-weight: 800; }
.hd-count--dim { opacity: 0.55; }
.hd-when { font-size: 0.7rem; opacity: 0.6; color: inherit; text-decoration: none; }
.hd-when:hover { opacity: 1; text-decoration: underline; }
.hd-empty { padding: 1.6rem 1rem; text-align: center; opacity: 0.7; }
.hd-pages { margin-top: 0.8rem; font-size: 0.76rem; opacity: 0.8; display: flex; gap: 0.8rem; }
.hd-pages a { color: var(--fs-link-color, inherit); font-weight: 600; text-decoration: none; }
.hd-pages a:hover { text-decoration: underline; }
@media (max-width: 760px) {
    .hd-features { grid-template-columns: 1fr; }
    .hd-side { flex-direction: column; align-items: flex-end; gap: 0.15rem; }
    .hd-count--dim { display: none; }
}

4 replies 30 Read more →
NeoMetallix ·Bug Report ·3 months ago
Tagging bug

@Cedric On the quick reply, I've noticed if you do @ and then the person's name, their name still shows up to click on and tag, but it appears at the top of the first page not in the text editor.

2 replies 338 Read more →
Cedric ·ForumSpark Headlines ·3 months ago
Custom domain issue resolved

Hi everyone,

Over the past days, some boards using a custom domain were temporarily unreachable. Visitors saw a browser security warning or an offline page instead of the forum.

The cause was an internal check on our proxy that silently blocked SSL certificate renewals for custom domains. As certificates expired, those domains stopped loading. Board data and visitor security were never at risk: no content was exposed or lost.

I have deployed a fix and certificate renewals are running normally again. Affected domains are automatically recovered, so no action is needed.

Apologies for the disruption,

Cedric

Cedric ·Themes & Extensions ·3 months ago
Aphelion

Aphelion

Aphelion is now available in the marketplace.

A tribute to the glossy dark forum skins of the late 2000s, inspired by the ZetaBoards classic Alphacide. A chrome-white header band carries your logo, menu and user tools, wrapped around deep space plates with angular cut corners, alternating blue and red glass category buttons, glowing unread markers and an ice-blue shine on every link.

View the product, screenshots and reviews

Use this topic for questions and discussion.

2 replies 45 Read more →
Cedric ·Customization Help ·3 months ago
Custom Templates: Topic view template

Here's a few examples for the topic view:

Template:

<div class="dk">
{{#each replies}}
<div class="reply">
    <div class="dk-post{{#if this.reply.is_best}} dk-post--best{{/if}}{{#if this.reply.is_op}} dk-post--op{{/if}}">
        <div class="dk-head">
            <a href="{{ this.user.profile_url }}" class="dk-avatar">
                <img src="{{ this.user.avatar_url }}" alt="{{ this.user.username }}" />
            </a>
            <div class="dk-id">
                <span class="dk-name">
                    <a href="{{ this.user.profile_url }}">{{ this.user.username }}</a>
                    {{#if this.reply.is_op}}<span class="dk-tag dk-tag--op">Author</span>{{/if}}
                    {{#if this.user.is_staff}}<span class="dk-tag dk-tag--staff">{{ this.user.group_name }}</span>{{/if}}
                    {{#if this.reply.is_best}}<span class="dk-tag dk-tag--best"><span class="fa-solid fa-circle-check"></span> Best answer</span>{{/if}}
                </span>
                <span class="dk-sub">{{ this.user.num_posts }} posts <span class="dk-sep">·</span> joined {{ this.user.joined_long }}</span>
            </div>
            <a href="{{ this.reply.permalink }}" class="dk-time">{{ this.reply.created_at }}</a>
            {{{ this.reply.meta }}}
        </div>
        <div class="dk-body">
            {{{ this.reply.body }}}
        </div>
        <div class="dk-actions">
            {{{ this.reply.actions }}}
        </div>
    </div>
</div>
{{/each}}
{{#if pagination.has_pages}}
<div class="dk-pages">
    Page {{ pagination.current_page }} of {{ pagination.last_page }}
    {{#if pagination.prev_url}}<a href="{{ pagination.prev_url }}">Previous</a>{{/if}}
    {{#if pagination.next_url}}<a href="{{ pagination.next_url }}">Next</a>{{/if}}
</div>
{{/if}}
</div>

CSS:

.dk { display: flex; flex-direction: column; gap: 0.9rem; padding: 0.9rem; }
.dk-post { background: var(--fs-reply-bg, rgba(128,128,128,0.03)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); border-radius: 0.7rem; overflow: hidden; }
.dk-post--best { border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.45)); box-shadow: 0 0 0 1px var(--fs-panel-footer-accent, transparent); }
.dk-head { display: flex; align-items: center; gap: 0.7rem; padding: 0.75rem 1rem; border-bottom: 1px solid var(--fs-forum-divider, rgba(128,128,128,0.13)); background: var(--fs-reply-details-bg, rgba(128,128,128,0.04)); }
.dk-avatar { flex-shrink: 0; line-height: 0; }
.dk-avatar img { width: 2.4rem; height: 2.4rem; border-radius: 50%; object-fit: cover; }
.dk-id { flex: 1; min-width: 0; }
.dk-name { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; }
.dk-name a { font-size: 0.95rem; font-weight: 750; color: inherit; text-decoration: none; }
.dk-name a:hover { color: var(--fs-panel-footer-accent, inherit); }
.dk-tag { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.58rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.13rem 0.5rem; border-radius: 999px; }
.dk-tag--op { background: var(--fs-panel-section-bg, rgba(128,128,128,0.12)); }
.dk-tag--staff { color: #fff; background: var(--fs-panel-footer-accent, #35a377); }
.dk-tag--best { color: var(--fs-panel-footer-accent, inherit); border: 1px solid var(--fs-panel-footer-accent, rgba(128,128,128,0.4)); }
.dk-sub { display: block; font-size: 0.68rem; opacity: 0.55; margin-top: 0.1rem; }
.dk-sep { opacity: 0.6; margin: 0 0.1rem; }
.dk-time { flex-shrink: 0; font-size: 0.72rem; opacity: 0.6; color: inherit; text-decoration: none; }
.dk-time:hover { opacity: 1; text-decoration: underline; }
.dk-head .post-layout-meta { flex-shrink: 0; display: inline-flex; align-items: center; gap: 0.5rem; }
.dk-head .post-layout-meta__number { font-size: 0.72rem; font-weight: 700; opacity: 0.55; color: inherit; text-decoration: none; }
.dk-body { padding: 1rem 1.1rem 0.5rem; }
.dk-actions { display: flex; padding: 0.5rem 1rem 0.7rem; }
.dk-actions .reply-action-group { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.dk-actions .reply-action-group--reply { margin-left: auto; }
.dk-pages { padding: 0.2rem 0.3rem; font-size: 0.78rem; opacity: 0.8; display: flex; gap: 0.8rem; }
.dk-pages a { color: var(--fs-link-color, inherit); font-weight: 600; text-decoration: none; }
.dk-pages a:hover { text-decoration: underline; }
@media (max-width: 640px) {
    .dk-sub { display: none; }
}

Template:

<div class="td">
{{#each replies}}
<div class="reply">
    <div class="td-msg{{#if this.reply.is_op}} td-msg--op{{/if}}{{#if this.reply.is_best}} td-msg--best{{/if}}">
        <a href="{{ this.user.profile_url }}" class="td-avatar">
            <img src="{{ this.user.avatar_url }}" alt="{{ this.user.username }}" />
        </a>
        <div class="td-col">
            <div class="td-top">
                <a href="{{ this.user.profile_url }}" class="td-name">{{ this.user.username }}</a>
                {{#if this.user.is_staff}}<span class="td-staff">{{ this.user.group_name }}</span>{{/if}}
                {{#if this.reply.is_op}}<span class="td-op">OP</span>{{/if}}
                <a href="{{ this.reply.permalink }}" class="td-time">{{ this.reply.created_at }}</a>
                {{{ this.reply.meta }}}
            </div>
            <div class="td-bubble">
                {{#if this.reply.is_best}}<div class="td-best"><span class="fa-solid fa-circle-check"></span> Marked as best answer</div>{{/if}}
                <div class="td-body">
                    {{{ this.reply.body }}}
                </div>
            </div>
            <div class="td-actions">
                {{{ this.reply.actions }}}
            </div>
        </div>
    </div>
</div>
{{/each}}
{{#if pagination.has_pages}}
<div class="td-pages">
    Page {{ pagination.current_page }} of {{ pagination.last_page }}
    {{#if pagination.prev_url}}<a href="{{ pagination.prev_url }}">Previous</a>{{/if}}
    {{#if pagination.next_url}}<a href="{{ pagination.next_url }}">Next</a>{{/if}}
</div>
{{/if}}
</div>

CSS:

.td { display: flex; flex-direction: column; gap: 1.1rem; padding: 1rem; }
.td-msg { display: flex; gap: 0.75rem; }
.td-avatar { flex-shrink: 0; line-height: 0; margin-top: 0.15rem; }
.td-avatar img { width: 2.3rem; height: 2.3rem; border-radius: 50%; object-fit: cover; }
.td-col { flex: 1; min-width: 0; }
.td-top { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.3rem; }
.td-name { font-size: 0.9rem; font-weight: 750; color: inherit; text-decoration: none; }
.td-name:hover { color: var(--fs-panel-footer-accent, inherit); }
.td-staff { font-size: 0.56rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: #fff; background: var(--fs-panel-footer-accent, #35a377); padding: 0.12rem 0.45rem; border-radius: 999px; }
.td-op { font-size: 0.56rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.12rem 0.45rem; border-radius: 999px; background: var(--fs-panel-section-bg, rgba(128,128,128,0.12)); }
.td-time { font-size: 0.68rem; opacity: 0.55; color: inherit; text-decoration: none; }
.td-time:hover { opacity: 1; text-decoration: underline; }
.td-top .post-layout-meta { margin-left: auto; display: inline-flex; align-items: center; gap: 0.5rem; }
.td-top .post-layout-meta__number { font-size: 0.7rem; font-weight: 700; opacity: 0.5; color: inherit; text-decoration: none; }
.td-bubble { background: var(--fs-reply-bg, rgba(128,128,128,0.05)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.16)); border-radius: 0.25rem 0.9rem 0.9rem 0.9rem; padding: 0.9rem 1rem; }
.td-msg--op .td-bubble { border-left: 3px solid var(--fs-panel-footer-accent, rgba(128,128,128,0.45)); }
.td-msg--best .td-bubble { border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.45)); }
.td-best { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.68rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: var(--fs-panel-footer-accent, inherit); margin-bottom: 0.5rem; }
.td-actions { display: flex; margin-top: 0.35rem; opacity: 0.45; transition: opacity 0.15s ease; }
.td-msg:hover .td-actions, .td-actions:focus-within { opacity: 1; }
.td-actions .reply-action-group { display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center; }
.td-actions .reply-action-group--reply { margin-left: auto; }
.td-actions .btn { font-size: 0.68rem; padding: 0.22rem 0.55rem; }
.td-pages { padding: 0.2rem 0.3rem; font-size: 0.78rem; opacity: 0.8; display: flex; gap: 0.8rem; }
.td-pages a { color: var(--fs-link-color, inherit); font-weight: 600; text-decoration: none; }
.td-pages a:hover { text-decoration: underline; }
@media (prefers-reduced-motion: reduce) { .td-actions { transition: none; } }

Template:

<div class="tx">
{{#each replies}}
<div class="reply">
    <div class="tx-post{{#if this.reply.is_best}} tx-post--best{{/if}}">
        <span class="tx-beam"></span>
        <div class="tx-rail">
            <span class="tx-hex">
                <img src="{{ this.user.avatar_url }}" alt="{{ this.user.username }}" />
            </span>
            <a href="{{ this.user.profile_url }}" class="tx-name">{{ this.user.username }}</a>
            {{#if this.user.is_staff}}<span class="tx-staff">{{ this.user.group_name }}</span>{{/if}}
            {{#if this.reply.is_op}}<span class="tx-origin">Origin</span>{{/if}}
            <span class="tx-data">{{ this.user.num_posts }} posts</span>
            <span class="tx-data">Est. {{ this.user.joined_long }}</span>
        </div>
        <div class="tx-main">
            <div class="tx-bar">
                {{#if this.reply.is_best}}<span class="tx-best"><span class="fa-solid fa-circle-check"></span> Verified answer</span>{{/if}}
                <a href="{{ this.reply.permalink }}" class="tx-time">{{ this.reply.created_at }}</a>
                {{{ this.reply.meta }}}
            </div>
            <div class="tx-body">
                {{{ this.reply.body }}}
            </div>
            <div class="tx-actions">
                {{{ this.reply.actions }}}
            </div>
        </div>
    </div>
</div>
{{/each}}
{{#if pagination.has_pages}}
<div class="tx-pages">
    Page {{ pagination.current_page }} of {{ pagination.last_page }}
    {{#if pagination.prev_url}}<a href="{{ pagination.prev_url }}">Previous</a>{{/if}}
    {{#if pagination.next_url}}<a href="{{ pagination.next_url }}">Next</a>{{/if}}
</div>
{{/if}}
</div>

CSS:

.tx { display: flex; flex-direction: column; gap: 1.1rem; padding: 0.9rem; counter-reset: txsig; }
.tx-post { counter-increment: txsig; position: relative; display: flex; background: #0a0d18; border: 1px solid rgba(122, 92, 255, 0.3); border-radius: 0.8rem; color: rgba(255, 255, 255, 0.88); overflow: hidden; }
.tx-post::before { content: ""; position: absolute; inset: 0; background: repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.025) 0 1px, transparent 1px 3px); pointer-events: none; z-index: 0; }
.tx-post::after { content: "SIG " counter(txsig, decimal-leading-zero); position: absolute; top: 0.5rem; right: 0.9rem; font-size: 2.6rem; font-weight: 900; letter-spacing: -0.04em; color: rgba(255, 255, 255, 0.05); pointer-events: none; font-variant-numeric: tabular-nums; }
.tx-beam { position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #59f3ff, #7a5cff, #ff5ee7, #59f3ff); background-size: 300% 100%; animation: txFlow 5s linear infinite; z-index: 1; }
.tx-post--best { border-color: #59f3ff; box-shadow: 0 0 26px rgba(89, 243, 255, 0.18); }
.tx-rail { position: relative; z-index: 1; flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 0.3rem; width: 9.5rem; padding: 1.3rem 0.8rem; border-right: 1px solid rgba(122, 92, 255, 0.25); text-align: center; }
.tx-hex { display: block; width: 3.6rem; height: 3.6rem; padding: 2px; background: linear-gradient(160deg, #59f3ff, #7a5cff); clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%); }
.tx-hex img { width: 100%; height: 100%; object-fit: cover; clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%); }
.tx-name { margin-top: 0.35rem; font-size: 0.92rem; font-weight: 800; color: #fff; text-decoration: none; }
.tx-name:hover { color: #59f3ff; text-shadow: 0 0 14px rgba(89, 243, 255, 0.8); }
.tx-staff { font-size: 0.54rem; font-weight: 800; letter-spacing: 0.2em; text-transform: uppercase; color: #fff; text-shadow: 0.08em 0 0 rgba(255, 94, 231, 0.85), -0.08em 0 0 rgba(89, 243, 255, 0.85); }
.tx-origin { font-size: 0.52rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; color: #0a0d18; background: linear-gradient(90deg, #59f3ff, #ff5ee7); padding: 0.15rem 0.55rem; border-radius: 999px; }
.tx-data { font-size: 0.58rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(255, 255, 255, 0.4); }
.tx-main { position: relative; z-index: 1; flex: 1; min-width: 0; display: flex; flex-direction: column; }
.tx-bar { display: flex; align-items: center; gap: 0.7rem; padding: 0.65rem 1rem; border-bottom: 1px solid rgba(122, 92, 255, 0.22); font-size: 0.74rem; }
.tx-best { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.62rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #59f3ff; }
.tx-time { color: rgba(255, 255, 255, 0.55); text-decoration: none; }
.tx-time:hover { color: #59f3ff; }
.tx-bar .post-layout-meta { margin-left: auto; display: inline-flex; align-items: center; gap: 0.5rem; }
.tx-bar .post-layout-meta__number { font-size: 0.72rem; font-weight: 800; color: rgba(255, 255, 255, 0.45); text-decoration: none; }
.tx-bar .post-layout-meta__number:hover { color: #59f3ff; }
.tx-body { padding: 1rem 1.1rem 0.5rem; }
.tx-body .content { color: rgba(255, 255, 255, 0.88); }
.tx-body .content a { color: #59f3ff; }
.tx-body .content blockquote { background: rgba(122, 92, 255, 0.12); border-left: 3px solid #7a5cff; }
.tx-body .editby { color: rgba(255, 255, 255, 0.45); }
.tx-body .signature { border-top: 1px solid rgba(122, 92, 255, 0.2); color: rgba(255, 255, 255, 0.55); }
.tx-actions { display: flex; padding: 0.5rem 1rem 0.75rem; margin-top: auto; }
.tx-actions .reply-action-group { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.tx-actions .reply-action-group--reply { margin-left: auto; }
.tx-actions .btn { background: transparent; border: 1px solid rgba(122, 92, 255, 0.4); color: rgba(255, 255, 255, 0.75); }
.tx-actions .btn:hover { border-color: #59f3ff; color: #59f3ff; box-shadow: 0 0 12px rgba(89, 243, 255, 0.25); }
.tx-pages { padding: 0.2rem 0.3rem; font-size: 0.78rem; opacity: 0.85; display: flex; gap: 0.8rem; }
.tx-pages a { color: var(--fs-link-color, inherit); font-weight: 600; text-decoration: none; }
.tx-pages a:hover { text-decoration: underline; }
@keyframes txFlow { to { background-position: 300% 0; } }
@media (prefers-reduced-motion: reduce) { .tx-beam { animation: none; } }
@media (max-width: 760px) {
    .tx-post { flex-direction: column; }
    .tx-rail { flex-direction: row; width: 100%; padding: 0.8rem 1rem; border-right: 0; border-bottom: 1px solid rgba(122, 92, 255, 0.25); text-align: left; align-items: center; gap: 0.6rem; }
    .tx-hex { width: 2.4rem; height: 2.4rem; }
    .tx-name { margin-top: 0; }
    .tx-data { display: none; }
    .tx-post::after { font-size: 1.7rem; }
}

5 replies 30 Read more →
Cedric ·Themes & Extensions ·3 months ago
Robo Redux

Robo Redux

Robo Redux is now available in the marketplace.

Robo Redux. A machined dark theme. Brushed charcoal plates with hairline edge lighting, near-monochrome at rest, and an acid lime charge that ignites everything you touch: glowing tabs, power-cell unread markers, and buttons that light up under your cursor.

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·Customization Help ·3 months ago
Custom Templates: Board index template

Customizing the board index template allows you to further style your forum to your preference. Below a few examples:

Template:

<div class="bx">
{{#each categories}}
<div class="bx-cat">
    <div class="bx-cat-head">
        <a href="{{ this.url }}" class="bx-cat-title">{{ this.name }}</a>
        <span class="bx-cat-line"></span>
    </div>
    <div class="bx-grid">
        {{#each this.forums}}
        <div class="bx-card{{#unless this.is_read}} bx-card--unread{{/unless}}">
            <div class="bx-top">
                <span class="bx-ico">
                    {{#if this.marker_image_url}}<img src="{{ this.marker_image_url }}" alt="{{ this.name }}" />{{else}}<span class="{{ this.marker_icon | default:fa-regular fa-comments }}"></span>{{/if}}
                </span>
                {{#unless this.is_read}}<span class="bx-new">New</span>{{/unless}}
            </div>
            <div class="bx-name"><a href="{{ this.url }}">{{ this.name }}</a></div>
            <div class="bx-desc">{{{ this.description }}}</div>
            {{#if this.subforums}}
            <div class="bx-subs">
                {{#each this.subforums}}<a class="bx-sub{{#unless this.is_read}} bx-sub--unread{{/unless}}" href="{{ this.url }}">{{ this.name }}</a>{{/each}}
            </div>
            {{/if}}
            <div class="bx-meta">
                <span class="bx-chip"><span class="fa-regular fa-comments"></span> {{ this.topics_count }}</span>
                <span class="bx-chip"><span class="fa-regular fa-message"></span> {{ this.replies_count }}</span>
            </div>
            {{#if this.last_post.author}}
            <div class="bx-last">
                <span class="bx-last-av"><img src="{{ this.last_post.avatar_url }}" alt="{{ this.last_post.author }}" /></span>
                <span class="bx-last-txt">
                    <a href="{{ this.last_post.url }}">{{ this.last_post.title | limit:34 }}</a>
                    <span class="bx-last-meta">{{ this.last_post.author }} · {{ this.last_post.date }}</span>
                </span>
            </div>
            {{else}}
            <div class="bx-last bx-last--empty">No posts yet</div>
            {{/if}}
        </div>
        {{/each}}
    </div>
</div>
{{/each}}
<div class="bx-stats">
    <span class="bx-stat"><span class="fa-regular fa-comments"></span> <b>{{ stats.topics }}</b> topics</span>
    <span class="bx-stat"><span class="fa-regular fa-message"></span> <b>{{ stats.replies }}</b> posts</span>
    <span class="bx-stat"><span class="fa-regular fa-user"></span> <b>{{ stats.members }}</b> members</span>
    <span class="bx-stat"><span class="bx-pulse"></span> <b>{{ stats.online }}</b> online</span>
    <span class="bx-stat bx-stat--right">Newest: <a href="{{ stats.latest_member_url }}">{{ stats.latest_member }}</a></span>
</div>
</div>

CSS:

.bx-cat + .bx-cat { margin-top: 1.6rem; }
.bx-cat-head { display: flex; align-items: center; gap: 0.8rem; margin-bottom: 0.7rem; }
.bx-cat-title { font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.16em; color: inherit; text-decoration: none; opacity: 0.85; }
.bx-cat-title:hover { color: var(--fs-panel-footer-accent, inherit); opacity: 1; }
.bx-cat-line { flex: 1; height: 1px; background: linear-gradient(90deg, var(--fs-panel-footer-accent, rgba(128,128,128,0.4)), transparent); }
.bx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 0.8rem; }
.bx-card { display: flex; flex-direction: column; padding: 0.9rem 0.95rem 0; background: var(--fs-panel-bg, rgba(128,128,128,0.05)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); border-radius: 0.8rem; transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; }
.bx-card:hover { transform: translateY(-3px); border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.4)); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.12); }
.bx-card--unread { border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.45)); box-shadow: inset 3px 0 0 var(--fs-panel-footer-accent, transparent); }
.bx-top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 0.6rem; }
.bx-ico { display: flex; align-items: center; justify-content: center; width: 2.6rem; height: 2.6rem; border-radius: 0.6rem; background: var(--fs-panel-section-bg, rgba(128,128,128,0.12)); color: var(--fs-panel-footer-accent, inherit); font-size: 1.05rem; }
.bx-ico img { width: 2.6rem; height: 2.6rem; object-fit: contain; border-radius: 0.6rem; }
.bx-new { font-size: 0.56rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.1em; color: #fff; background: var(--fs-panel-footer-accent, #35a377); border-radius: 999px; padding: 0.2rem 0.55rem; }
.bx-name a { font-size: 0.95rem; font-weight: 700; color: inherit; text-decoration: none; }
.bx-name a:hover { color: var(--fs-panel-footer-accent, inherit); }
.bx-card--unread .bx-name a { color: var(--fs-panel-footer-accent, inherit); }
.bx-desc { font-size: 0.78rem; opacity: 0.68; line-height: 1.45; margin-top: 0.25rem; }
.bx-desc p { margin: 0; }
.bx-subs { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.55rem; }
.bx-sub { font-size: 0.68rem; padding: 0.16rem 0.5rem; border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.25)); border-radius: 999px; color: inherit; text-decoration: none; opacity: 0.75; }
.bx-sub:hover { border-color: var(--fs-panel-footer-accent, currentColor); opacity: 1; }
.bx-sub--unread { font-weight: 700; opacity: 1; border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.4)); }
.bx-meta { display: flex; gap: 0.4rem; margin-top: 0.7rem; }
.bx-chip { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.7rem; font-weight: 600; padding: 0.22rem 0.55rem; border-radius: 999px; background: var(--fs-panel-section-bg, rgba(128,128,128,0.1)); }
.bx-chip .fa-regular, .bx-chip .fa-solid { font-size: 0.66rem; opacity: 0.7; }
.bx-last { display: flex; align-items: center; gap: 0.5rem; margin: 0.75rem -0.95rem 0; padding: 0.6rem 0.95rem; border-top: 1px solid var(--fs-forum-divider, rgba(128,128,128,0.15)); min-width: 0; }
.bx-last-av { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; overflow: hidden; line-height: 0; }
.bx-last-av img { width: 100%; height: 100%; object-fit: cover; }
.bx-last-txt { display: flex; flex-direction: column; min-width: 0; }
.bx-last-txt a { font-size: 0.75rem; font-weight: 600; color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bx-last-txt a:hover { color: var(--fs-panel-footer-accent, inherit); }
.bx-last-meta { font-size: 0.66rem; opacity: 0.55; }
.bx-last--empty { font-size: 0.72rem; font-style: italic; opacity: 0.5; }
.bx-stats { display: flex; flex-wrap: wrap; align-items: center; gap: 1.4rem; margin-top: 1.6rem; padding: 0.7rem 1rem; background: var(--fs-panel-bg, rgba(128,128,128,0.05)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); border-radius: 0.8rem; font-size: 0.78rem; }
.bx-stat { display: inline-flex; align-items: center; gap: 0.4rem; opacity: 0.85; }
.bx-stat b { font-weight: 800; }
.bx-stat a { color: var(--fs-panel-footer-accent, inherit); text-decoration: none; font-weight: 700; }
.bx-stat--right { margin-left: auto; }
.bx-pulse { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--fs-online-active, #30a661); animation: bxPulse 1.8s ease-in-out infinite; }
@keyframes bxPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
@media (prefers-reduced-motion: reduce) { .bx-pulse, .bx-card { animation: none; transition: none; } }
@media (max-width: 640px) { .bx-grid { grid-template-columns: 1fr; } .bx-stat--right { margin-left: 0; } }

Template:

<div class="at">
{{#each categories}}
<div class="at-cat">
    <div class="at-head">
        <a href="{{ this.url }}" class="at-title">{{ this.name }}</a>
        <span class="at-rule"></span>
    </div>
    <div class="at-grid">
        {{#each this.forums}}
        <div class="at-row{{#unless this.is_read}} at-row--unread{{/unless}}">
            <span class="at-tile">
                {{#if this.marker_image_url}}
                    <img src="{{ this.marker_image_url }}" alt="{{ this.name }}" />
                {{else}}
                    <span class="{{ this.marker_icon | default:fa-regular fa-comments }}"></span>
                {{/if}}
            </span>
            <div class="at-body">
                <div class="at-name">
                    <a href="{{ this.url }}">{{ this.name }}</a>
                    {{#unless this.is_read}}<span class="at-dot"></span>{{/unless}}
                </div>
                <div class="at-desc">{{{ this.description }}}</div>
                <div class="at-meta"><b>{{ this.topics_count }}</b> topics <span class="at-sep">·</span> <b>{{ this.replies_count }}</b> replies</div>
                {{#if this.subforums}}
                <div class="at-subs">
                    {{#each this.subforums}}
                        <a href="{{ this.url }}" class="at-sub{{#unless this.is_read}} at-sub--unread{{/unless}}">{{ this.name }}</a>
                    {{/each}}
                </div>
                {{/if}}
                {{#if this.last_post.author}}
                <div class="at-last">
                    <img src="{{ this.last_post.avatar_url }}" alt="{{ this.last_post.author }}" />
                    <span class="at-last-txt">
                        <a href="{{ this.last_post.url }}">{{ this.last_post.title | limit:38 }}</a>
                        <span class="at-last-meta">{{ this.last_post.author }} <span class="at-sep">·</span> {{ this.last_post.date }}</span>
                    </span>
                </div>
                {{/if}}
            </div>
        </div>
        {{/each}}
    </div>
</div>
{{/each}}
<div class="at-stats">
    <span class="at-cell"><b>{{ stats.topics }}</b><span>Topics</span></span>
    <span class="at-cell"><b>{{ stats.replies }}</b><span>Posts</span></span>
    <span class="at-cell"><b>{{ stats.members }}</b><span>Members</span></span>
    <span class="at-cell"><b>{{ stats.online }}</b><span>Online</span></span>
    <span class="at-cell at-cell--new"><b><a href="{{ stats.latest_member_url }}">{{ stats.latest_member }}</a></b><span>Newest member</span></span>
</div>
</div>

CSS:

.at-cat + .at-cat { margin-top: 1.8rem; }
.at-head { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.85rem; }
.at-title { font-size: 1.18rem; font-weight: 800; letter-spacing: -0.015em; color: inherit; text-decoration: none; }
.at-title:hover { color: var(--fs-panel-footer-accent, inherit); }
.at-rule { flex: 1; height: 3px; border-radius: 999px; background: linear-gradient(90deg, var(--fs-panel-footer-accent, rgba(128,128,128,0.4)), transparent 55%); }
.at-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.75rem; }
.at-row { display: flex; gap: 0.85rem; padding: 0.95rem 1rem; background: var(--fs-panel-bg, rgba(128,128,128,0.04)); border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); border-radius: 0.65rem; transition: border-color 0.14s ease, box-shadow 0.14s ease; }
.at-row:hover { border-color: var(--fs-panel-footer-accent, rgba(128,128,128,0.45)); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.07); }
.at-row--unread { box-shadow: inset 3px 0 0 var(--fs-panel-footer-accent, currentColor); }
.at-tile { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 2.7rem; height: 2.7rem; border-radius: 0.55rem; background: var(--fs-panel-section-bg, rgba(128,128,128,0.1)); color: var(--fs-panel-footer-accent, inherit); font-size: 1.05rem; }
.at-tile img { width: 2.7rem; height: 2.7rem; object-fit: contain; border-radius: 0.55rem; }
.at-body { flex: 1; min-width: 0; }
.at-name { display: flex; align-items: center; gap: 0.45rem; }
.at-name a { font-size: 0.98rem; font-weight: 700; letter-spacing: -0.01em; color: var(--fs-forum-link-color, inherit); text-decoration: none; }
.at-name a:hover { color: var(--fs-panel-footer-accent, inherit); }
.at-row--unread .at-name a { color: var(--fs-panel-footer-accent, inherit); }
.at-dot { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: var(--fs-panel-footer-accent, currentColor); }
.at-desc { font-size: 0.77rem; opacity: 0.62; line-height: 1.45; margin-top: 0.1rem; }
.at-desc p { margin: 0; }
.at-meta { font-size: 0.72rem; opacity: 0.75; margin-top: 0.35rem; font-variant-numeric: tabular-nums; }
.at-meta b { font-weight: 700; }
.at-sep { opacity: 0.45; margin: 0 0.15rem; }
.at-subs { display: flex; flex-wrap: wrap; gap: 0.15rem 0.8rem; margin-top: 0.3rem; font-size: 0.72rem; }
.at-sub { color: inherit; opacity: 0.65; text-decoration: none; }
.at-sub:hover { opacity: 1; text-decoration: underline; }
.at-sub--unread { opacity: 1; font-weight: 700; color: var(--fs-panel-footer-accent, inherit); }
.at-last { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.55rem; padding-top: 0.55rem; border-top: 1px solid var(--fs-forum-divider, rgba(128,128,128,0.13)); min-width: 0; }
.at-last img { flex-shrink: 0; width: 1.45rem; height: 1.45rem; border-radius: 50%; object-fit: cover; }
.at-last-txt { display: flex; flex-direction: column; min-width: 0; }
.at-last-txt a { font-size: 0.75rem; font-weight: 600; color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.at-last-txt a:hover { color: var(--fs-panel-footer-accent, inherit); }
.at-last-meta { font-size: 0.65rem; opacity: 0.55; }
.at-stats { display: flex; flex-wrap: wrap; margin-top: 1.8rem; border: 1px solid var(--fs-panel-border, rgba(128,128,128,0.2)); border-radius: 0.65rem; overflow: hidden; background: var(--fs-panel-bg, rgba(128,128,128,0.04)); }
.at-cell { flex: 1 1 0; min-width: 7rem; display: flex; flex-direction: column; align-items: center; gap: 0.05rem; padding: 0.85rem 0.5rem; }
.at-cell + .at-cell { border-left: 1px solid var(--fs-forum-divider, rgba(128,128,128,0.14)); }
.at-cell b { font-size: 1.15rem; font-weight: 800; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; }
.at-cell b a { color: var(--fs-panel-footer-accent, inherit); text-decoration: none; font-size: 0.92rem; }
.at-cell b a:hover { text-decoration: underline; }
.at-cell span { font-size: 0.58rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; opacity: 0.55; }
.at-cell--new { flex: 1.3 1 0; }
@media (max-width: 900px) {
    .at-grid { grid-template-columns: 1fr; }
    .at-cell { min-width: 33%; }
}

Template:

<div class="ch">
{{#each categories}}
<div class="ch-cat">
    <div class="ch-spine">
        <a href="{{ this.url }}">{{ this.name }}</a>
    </div>
    <div class="ch-list">
        {{#each this.forums}}
        <div class="ch-row{{#unless this.is_read}} ch-row--unread{{/unless}}">
            <div class="ch-main">
                <div class="ch-name">
                    <a href="{{ this.url }}">{{ this.name }}</a>
                    {{#unless this.is_read}}<span class="ch-flag">New</span>{{/unless}}
                </div>
                <div class="ch-desc">{{{ this.description }}}</div>
                {{#if this.subforums}}
                <div class="ch-subs">
                    {{#each this.subforums}}
                        <a href="{{ this.url }}" class="ch-sub{{#unless this.is_read}} ch-sub--unread{{/unless}}">{{ this.name }}</a>
                    {{/each}}
                </div>
                {{/if}}
                {{#if this.last_post.author}}
                <div class="ch-last">
                    <img src="{{ this.last_post.avatar_url }}" alt="{{ this.last_post.author }}" />
                    <a href="{{ this.last_post.url }}">{{ this.last_post.title | limit:44 }}</a>
                    <span class="ch-last-meta">{{ this.last_post.author }} · {{ this.last_post.date }}</span>
                </div>
                {{/if}}
            </div>
            <div class="ch-stats">
                <span class="ch-stat"><b>{{ this.topics_count }}</b><span>topics</span></span>
                <span class="ch-stat"><b>{{ this.replies_count }}</b><span>replies</span></span>
            </div>
        </div>
        {{/each}}
    </div>
</div>
{{/each}}
<div class="ch-foot">
    <span class="ch-fstat"><b>{{ stats.topics }}</b> topics</span>
    <span class="ch-fslash"></span>
    <span class="ch-fstat"><b>{{ stats.replies }}</b> posts</span>
    <span class="ch-fslash"></span>
    <span class="ch-fstat"><b>{{ stats.members }}</b> members</span>
    <span class="ch-fslash"></span>
    <span class="ch-fstat"><b>{{ stats.online }}</b> online</span>
    <span class="ch-fslash"></span>
    <span class="ch-fstat">newest <a href="{{ stats.latest_member_url }}">{{ stats.latest_member }}</a></span>
</div>
</div>

CSS:

.ch-cat { display: flex; gap: 1.2rem; counter-reset: chrow; }
.ch-cat + .ch-cat { margin-top: 2.4rem; }
.ch-spine { flex-shrink: 0; display: flex; justify-content: center; width: 2rem; border-right: 3px solid var(--fs-panel-footer-accent, rgba(128,128,128,0.5)); padding-right: 0.6rem; }
.ch-spine a { writing-mode: vertical-rl; transform: rotate(180deg); font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.28em; color: inherit; opacity: 0.55; text-decoration: none; white-space: nowrap; }
.ch-spine a:hover { opacity: 1; color: var(--fs-panel-footer-accent, inherit); }
.ch-list { flex: 1; min-width: 0; }
.ch-row { counter-increment: chrow; display: flex; align-items: flex-start; gap: 1.4rem; padding: 1.15rem 0.2rem; }
.ch-row::before { content: counter(chrow, decimal-leading-zero); font-size: 3rem; font-weight: 900; line-height: 1; letter-spacing: -0.04em; opacity: 0.14; font-variant-numeric: tabular-nums; flex-shrink: 0; min-width: 4.2rem; transition: opacity 0.15s ease, color 0.15s ease; }
.ch-row:hover::before { opacity: 0.85; color: var(--fs-panel-footer-accent, inherit); }
.ch-row--unread::before { opacity: 1; color: var(--fs-panel-footer-accent, inherit); }
.ch-row + .ch-row { border-top: 2px solid var(--fs-panel-border, rgba(128,128,128,0.2)); }
.ch-main { flex: 1; min-width: 0; }
.ch-name { display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; }
.ch-name a { font-size: 1.18rem; font-weight: 800; letter-spacing: -0.02em; color: inherit; text-decoration: none; }
.ch-name a:hover { color: var(--fs-panel-footer-accent, inherit); }
.ch-row--unread .ch-name a { color: var(--fs-panel-footer-accent, inherit); }
.ch-flag { font-size: 0.56rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.12em; color: #fff; background: var(--fs-panel-footer-accent, #35a377); padding: 0.16rem 0.5rem; }
.ch-desc { font-size: 0.8rem; opacity: 0.62; line-height: 1.5; margin-top: 0.25rem; max-width: 36rem; }
.ch-desc p { margin: 0; }
.ch-subs { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; margin-top: 0.4rem; font-size: 0.73rem; }
.ch-sub { color: inherit; opacity: 0.65; text-decoration: none; }
.ch-sub:hover { opacity: 1; text-decoration: underline; }
.ch-sub--unread { opacity: 1; font-weight: 700; color: var(--fs-panel-footer-accent, inherit); }
.ch-last { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.55rem; min-width: 0; font-size: 0.75rem; }
.ch-last img { flex-shrink: 0; width: 1.4rem; height: 1.4rem; border-radius: 50%; object-fit: cover; }
.ch-last a { font-weight: 600; color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ch-last a:hover { text-decoration: underline; }
.ch-last-meta { flex-shrink: 0; opacity: 0.55; font-size: 0.68rem; }
.ch-stats { flex-shrink: 0; display: flex; gap: 1.4rem; padding-top: 0.35rem; }
.ch-stat { display: flex; flex-direction: column; align-items: flex-end; }
.ch-stat b { font-size: 1.05rem; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.ch-stat span { font-size: 0.58rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.5; }
.ch-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 0.9rem; margin-top: 2.4rem; padding-top: 1rem; border-top: 3px solid var(--fs-panel-footer-accent, rgba(128,128,128,0.5)); font-size: 0.85rem; }
.ch-fstat b { font-size: 1.25rem; font-weight: 900; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.ch-fstat a { color: var(--fs-panel-footer-accent, inherit); font-weight: 800; text-decoration: none; }
.ch-fstat a:hover { text-decoration: underline; }
.ch-fslash { width: 2px; height: 1.3rem; background: var(--fs-panel-border, rgba(128,128,128,0.3)); transform: skewX(-18deg); }
@media (max-width: 768px) {
    .ch-cat { gap: 0.7rem; }
    .ch-spine { width: 1.4rem; padding-right: 0.4rem; }
    .ch-row { gap: 0.8rem; }
    .ch-row::before { font-size: 1.9rem; min-width: 2.6rem; }
    .ch-stats { display: none; }
}

More examples soon.

3 replies 18 Read more →
Cedric ·ForumSpark Headlines ·3 months ago
Add-ons, automation and our Marketplace

Thanks to some great feedback, ForumSpark is in a much better shape than ever. But we're not slowing down. On the contrary...

The Marketplace is now the official place for releases

From today we will start using our official Marketplace on the support board. I will be converting all current resources to the Marketplace. Everything released there gets automatically linked in the Extension Directory and gets its own topic in the forums, so you can follow and discuss new stuff like before. One place to release, visible everywhere.

Template add-ons

The Extension Directory in your admin panel now has template add-ons. These are small packs you install in one click: template snippets, profile fields and styling, no code needed. We made five official ones to get you started: Character Sheets, Gamer Tags, Post Rail Flair, Profile Showcase and Thread Meta. Install one and it just works. Uninstall it and your board is exactly like before. And further improve to your preference.

And if you build something nice yourself in Custom Templates, you can now export it as an add-on and publish it through the developer portal. Your work can end up on every board.

Automation

There is a new Automation page in the admin panel, free on every 2.0 board. You make rules like "when a topic reaches 50 replies, add the Hot label" or "when a report comes in, lock the topic and email me". Pick a trigger, add conditions, add actions, done. Every rule keeps its own run history so you can see exactly what it did. You are free to suggest new rules anytime.

Developer boards

If you build extensions or themes, you can now request a free developer board from the developer portal. It is a real board with every plan feature turned on, just for building and testing. You can install your draft packages on it directly, and templates show detailed errors instead of failing silently.

App data

For developers: installed apps can now push live data to a board through the API, and templates can show it. Think a Discord widget with a real online count, or your game server status in the post rail. Your code runs on your own server, the board just shows the numbers.

Docs

Guides for all of this are up. Admin guides in the normal docs, developer guides (with a few examples) on the developer portal.

Have fun with it, and show me what you build.

Originally published on the ForumSpark blog

1 reply 324 Read more →
Cedric ·Themes & Extensions ·3 months ago
Monokai

Monokai

Monokai is now available in the marketplace.

A legendary editor palette, rebuilt as a full forum theme. The whole board lives inside a floating editor window with the rainbow syntax stripe running across the top, pink to violet, in both modes.Dark mode is the classic: deep carbon surfaces, violet links, and near-black panel caps with syntax colored titles. Categories read in Monokai green, widgets in string yellow, exactly like code. Light m...

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·Themes & Extensions ·3 months ago
Nord

Nord

Nord is now available in the marketplace.

The classic arctic palette. Light mode carries the snowy Nord Light look, dark mode the deep polar night.Flat panels with solid blue caps, teal widget headers, and a clean underline navigation. Under the header runs a thin aurora line, the full Nord spectrum from red to frost in one quiet gradient. Unread markers glow frost blue, and a subtle frost tint lights up rows on hover.Compared to the orig...

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·Themes & Extensions ·3 months ago
Seaglass & Clay

Seaglass & Clay

Seaglass & Clay is now available in the marketplace.

Seaglass & Clay, and it's exactly what it sounds like. Soft seaglass blues and greens with warm clay accents on the buttons and highlights.The header is a floating bar that sits on the page like a card. Scroll down and it smoothly stretches into a full width sticky bar.Unread forums get a warm clay marker, so you spot new stuff right away.Full dark mode included, it feels like the same theme a...

View the product, screenshots and reviews

Use this topic for questions and discussion.

1 reply 43 Read more →
Cedric ·Themes & Extensions ·3 months ago
Porcelain Bloom

Porcelain Bloom

Porcelain Bloom is now available in the marketplace.

A soft, airy theme for communities that like things a bit calmer. Warm porcelain tones, mauve and lavender accents, and a deep navy for buttons and unread markers so nothing gets lost.The whole forum sits on a rounded porcelain card with a floating navigation dock, a decorative intro band up top, and a subtle gradient ribbon running across the frame. Widget headers come in mauve, usermenu icons si...

View the product, screenshots and reviews

Use this topic for questions and discussion.

1 reply 38 Read more →
NeoMetallix ·Feedback ·3 months ago
Premium customization?

Something I think might be beneficial for FS is a premium option that let's you customize features.

For instance maybe you don't want a feature included with the package, so you can deselect it for a small discount.

Or maybe you want features from a higher tier but don't need the extra server storage space?

Maybe mix and match features.

I'm sure you'd have to price features individually and I'm not sure how hard it would be to implement but I see it as something beneficial in the future maybe in 2.5?

1 reply 349 Read more →
Cedric ·Themes & Extensions ·3 months ago
Thread Meta

Thread Meta

Thread Meta is now available in the marketplace.

Two additions the stock topic row does not show: an Active line under the stats telling you how fresh the discussion is at a glance, and the topic's exact start date under the byline for boards where the archive matters.Independent snippets you can toggle and reorder from the Custom Templates page. Works alongside Topic Status Chips if you want the full treatment.

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·Themes & Extensions ·3 months ago
Profile Showcase

Profile Showcase

Profile Showcase is now available in the marketplace.

Two additions to the identity card at the top of member profiles: a standing line that shows the member's trust level and whether they are online right now, and a footer with how long they have been around plus their location when they shared one.Both are outlet snippets, so the profile card design itself stays untouched and themeable. Toggle either one off from the Custom Templates page or duplic...

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·Themes & Extensions ·3 months ago
Post Rail Flair

Post Rail Flair

Post Rail Flair is now available in the marketplace.

Two small identity lines for the post rail: the member's trust level under their group name, and their credit balance below the joined date when the Credits extension is active. They give every post a little more context about who is talking.Each line is a separate snippet: keep the ones you like, toggle the rest off, or duplicate one to build your own flair. Lines tied to features you have disabl...

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·Themes & Extensions ·3 months ago
Gamer Tags

Gamer Tags

Gamer Tags is now available in the marketplace.

Provisions four profile fields (Steam, Discord, PSN and Xbox) and lists whichever ones a member filled in as a compact table under their avatar in the post rail. Perfect for gaming boards where "what is your tag" is the first question in every thread.Fields members leave empty simply do not render. Uninstalling keeps the tags people entered unless you purge them, and the snippet can be duplicated...

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·Themes & Extensions ·3 months ago
Character Sheets

Character Sheets

Character Sheets is now available in the marketplace.

Adds a compact character sheet to the post rail, the member column beside every post. Members fill in three new profile fields (Character name, Class and Alignment) and their answers appear as a tidy label-and-value table under their avatar in every topic they post in.Installing provisions the fields automatically and the sheet only renders for members who filled something in. Uninstalling keeps m...

View the product, screenshots and reviews

Use this topic for questions and discussion.

Cedric ·ForumSpark Headlines ·3 months ago
ForumSpark 2.0.1 BETA Release

Greetings all,

Since the release yesterday I have continued to do some fine tuning and minor fixes to ForumSpark 2. Here's a few fixes in 2.0.1:

  • Section description blank on save

  • Deleting default sections gave a server error

  • Post rail customization ignored in the topic view

  • Reaction, quote, and mention notifications linked to the first post instead of your post

  • Directory counted topics and posts together, now posts only

  • Active Topics page error on busy boards

  • Missing sub-forum options on the section delete page

  • Docs Search guide unreachable at its own address

That's it for today!

Originally published on the ForumSpark blog

7 replies 520 Read more →
Cedric ·Themes & Extensions ·3 months ago
Aurora theme

Aurora blends soft gradients, clean panels, and adaptive dark/light modes for a theme that feels as smooth as it looks. It’s professional without being cold, stylish without losing usability.

Perfect for communities that want a premium, future-ready design that still feels effortless.

✔️ Adaptive Dark & Light Modes
Aurora automatically adapts to the user’s theme choice or OS setting, delivering consistent, balanced visuals in any environment.

✔️ Gradient Category & Widget Headers
Forum categories and widgets feature Aurora’s signature gradient trim - crisp, modern, and seamlessly blending with the rest of the UI.

✔️ Smooth UI
Panels, widgets, and dropdowns keep it clean, assuring focus on your content while adding depth.

✔️ Pixel-Perfect for Any Niche
From tech forums to creative communities, Aurora delivers a clean canvas that’s ready for your brand and content.

Demo

Install through your Extension directory in the ACP.

3 replies 53 Read more →
Cedric ·Customization Help ·3 months ago
Custom Templates: Post rail template

I realize sometimes things can get a little overwhelming. But possibilities within ForumSpark's customization are quite endless. Themes, widgets, posts, custom templates all have a pretty extended way possible in customization. Here are a few examples for the Post rail template:

For this example above, you'd set your post rail template like this:

<div class="cc">
    <div class="cc-band"></div>
    <div class="cc-avwrap">
        <img class="cc-av" src="{{ user.avatar_url }}" alt="{{ user.username }}" />
        {{#if user.is_online}}<span class="cc-online"></span>{{/if}}
    </div>
    {{#if user.is_staff}}<div><span class="cc-staff">{{ user.is_staff }}</span></div>{{/if}}
    <div class="cc-title">{{#if user.custom_title}}{{ user.custom_title }}{{else}}{{ user.rank }}{{/if}}</div>
    {{#if user.group_image_url}}
        <img class="cc-banner" src="{{ user.group_image_url }}" alt="{{ user.group_name }}" />
    {{else}}
        <span class="cc-group">{{ user.group_name }}</span>
    {{/if}}
    <div class="cc-stats">
        <div class="cc-stat"><b>{{ user.num_posts }}</b><span>Posts</span></div>
        <div class="cc-stat"><b>{{ user.reputation }}</b><span>Rep</span></div>
    </div>
    {{#if user.trust_level}}
        <div class="cc-chip"><span class="fa-solid fa-shield-halved"></span> {{ user.trust_level }}</div>
    {{/if}}
    {{#if user.credits}}
        <div class="cc-chip"><span class="fa-solid fa-coins"></span> {{ user.credits }}</div>
    {{/if}}
    <div class="cc-shelf">
        {{#each user.achievements}}
            {{#if this.icon}}
                <img class="cc-ach" src="{{ this.icon }}" alt="{{ this.name }}" />
            {{else}}
                <span class="cc-ach-fallback fa-solid fa-trophy"></span>
            {{/if}}
        {{/each}}
    </div>
    <div class="cc-foot"><span class="fa-regular fa-calendar"></span> {{ user.joined_long }}{{#if user.location}} · <span class="fa-solid fa-location-dot"></span> {{ user.location }}{{/if}}</div>
</div>

And put this in Custom CSS:

.cc { margin-top: 0.2rem; }
.cc-band { height: 2.7rem; margin: 0 -0.3rem; border-radius: 0.6rem; background: linear-gradient(135deg, var(--fs-panel-title-from, #003049), var(--fs-panel-footer-accent, #35a377)); }
.cc-avwrap { position: relative; display: inline-block; margin-top: -2rem; }
.cc-av { width: 4.6rem; height: 4.6rem; object-fit: cover; border-radius: 50%; border: 3px solid var(--fs-panel-section-bg, #f8f9fe); box-shadow: 0 5px 14px rgba(0, 0, 0, 0.22); }
.cc-online { position: absolute; bottom: 0.25rem; right: 0.25rem; width: 0.8rem; height: 0.8rem; border-radius: 50%; background: var(--fs-online-active, #30a661); border: 2px solid var(--fs-panel-section-bg, #fff); }
.cc-staff { display: inline-block; margin-top: 0.3rem; font-size: 0.55rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: #fff; background: var(--fs-panel-footer-accent, #35a377); border-radius: 999px; padding: 0.16rem 0.6rem; }
.cc-title { margin-top: 0.3rem; font-size: 0.68rem; font-style: italic; opacity: 0.75; }
.cc-banner { margin: 0.4rem auto 0; max-width: 100%; }
.cc-group { display: inline-block; margin-top: 0.35rem; font-size: 0.58rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; opacity: 0.6; }
.cc-stats { display: flex; gap: 0.35rem; margin-top: 0.55rem; }
.cc-stat { flex: 1; background: rgba(128, 128, 128, 0.1); border: 1px solid rgba(128, 128, 128, 0.16); border-radius: 0.55rem; padding: 0.4rem 0.15rem; }
.cc-stat b { display: block; font-size: 0.82rem; }
.cc-stat span { font-size: 0.52rem; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.6; }
.cc-chip { margin-top: 0.35rem; font-size: 0.64rem; background: rgba(128, 128, 128, 0.1); border: 1px solid rgba(128, 128, 128, 0.16); border-radius: 999px; padding: 0.25rem 0.5rem; }
.cc-chip .fa-solid { opacity: 0.7; margin-right: 0.2rem; font-size: 0.58rem; }
.cc-shelf { display: flex; justify-content: center; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.55rem; }
.cc-ach { width: 1.15rem; height: 1.15rem; object-fit: contain; }
.cc-ach-fallback { font-size: 0.9rem; color: #d9a520; }
.cc-foot { margin-top: 0.55rem; font-size: 0.58rem; opacity: 0.55; }
.cc-foot .fa-solid, .cc-foot .fa-regular { font-size: 0.52rem; }
@media (max-width: 1024px) {
    .cc-band, .cc-title, .cc-banner, .cc-group, .cc-stats, .cc-chip, .cc-shelf, .cc-foot, .cc-staff { display: none; }
    .cc-avwrap { margin-top: 0; }
    .cc-av { border-width: 2px; box-shadow: none; }
}

Another example:

Post rail code:

<div class="hc">
    <div class="hc-frame">
        <div class="hc-inner">
            <div class="hc-avhex">
                <img class="hc-av" src="{{ user.avatar_url }}" alt="{{ user.username }}" />
            </div>
            {{#if user.is_online}}<div class="hc-live"><span class="hc-live-dot"></span>Online</div>{{/if}}
            {{#if user.is_staff}}<div class="hc-staff">{{ user.is_staff }}</div>{{/if}}
            <div class="hc-title">{{#if user.custom_title}}{{ user.custom_title }}{{else}}{{ user.rank }}{{/if}}</div>
            <div class="hc-stat">
                <div class="hc-stat-top"><span>Posts</span><b>{{ user.num_posts }}</b></div>
                <div class="hc-bar"><span class="hc-fill hc-fill-a"></span></div>
            </div>
            <div class="hc-stat">
                <div class="hc-stat-top"><span>Rep</span><b>{{ user.reputation }}</b></div>
                <div class="hc-bar"><span class="hc-fill hc-fill-b"></span></div>
            </div>
            {{#if user.trust_level}}
            <div class="hc-stat">
                <div class="hc-stat-top"><span>Level</span><b>{{ user.trust_level }}</b></div>
                <div class="hc-bar"><span class="hc-fill hc-fill-c"></span></div>
            </div>
            {{/if}}
            <div class="hc-shelf">
                {{#each user.achievements}}
                    {{#if this.icon}}
                        <img class="hc-ach" src="{{ this.icon }}" alt="{{ this.name }}" />
                    {{else}}
                        <span class="hc-ach-fb fa-solid fa-trophy"></span>
                    {{/if}}
                {{/each}}
            </div>
            <div class="hc-foot">Est. {{ user.joined_long }}</div>
        </div>
    </div>
</div>

Custom CSS:

.hc { margin-top: 0.3rem; }
.hc-frame { position: relative; border-radius: 0.85rem; padding: 2px; overflow: hidden; }
.hc-frame::before { content: ""; position: absolute; inset: -60%; background: conic-gradient(from 0deg, #ff5ee7, #ffd76e, #59f3ff, #7a5cff, #ff5ee7); animation: hcSpin 6s linear infinite; }
.hc-inner { position: relative; border-radius: 0.72rem; background: #0d0f1e; padding: 0.85rem 0.6rem 0.75rem; color: rgba(255, 255, 255, 0.9); overflow: hidden; }
.hc-inner::before { content: ""; position: absolute; inset: 0; background: repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.035) 0 1px, transparent 1px 3px); pointer-events: none; }
.hc-inner::after { content: ""; position: absolute; top: -150%; left: -70%; width: 55%; height: 320%; transform: rotate(25deg); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.14), transparent); animation: hcSheen 5.5s ease-in-out infinite; pointer-events: none; }
.hc-avhex { width: 4.5rem; height: 4.5rem; margin: 0.15rem auto 0; padding: 2px; background: linear-gradient(160deg, #59f3ff, #7a5cff); clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%); }
.hc-av { width: 100%; height: 100%; object-fit: cover; clip-path: polygon(50% 0%, 93% 25%, 93% 75%, 50% 100%, 7% 75%, 7% 25%); }
.hc-live { display: inline-flex; align-items: center; gap: 0.3rem; margin-top: 0.4rem; font-size: 0.5rem; font-weight: 800; letter-spacing: 0.18em; text-transform: uppercase; color: #3ddc84; }
.hc-live-dot { width: 0.42rem; height: 0.42rem; border-radius: 50%; background: #3ddc84; animation: hcPulse 1.6s ease-in-out infinite; }
.hc-staff { margin-top: 0.35rem; font-size: 0.6rem; font-weight: 800; letter-spacing: 0.22em; text-transform: uppercase; color: #fff; text-shadow: 0.08em 0 0 rgba(255, 94, 231, 0.85), -0.08em 0 0 rgba(89, 243, 255, 0.85); }
.hc-title { margin-top: 0.3rem; font-size: 0.64rem; font-style: italic; color: rgba(255, 255, 255, 0.55); }
.hc-stat { margin-top: 0.55rem; text-align: left; }
.hc-stat-top { display: flex; justify-content: space-between; align-items: baseline; }
.hc-stat-top span { font-size: 0.5rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(255, 255, 255, 0.42); }
.hc-stat-top b { font-size: 0.74rem; }
.hc-bar { height: 0.28rem; margin-top: 0.2rem; border-radius: 999px; background: rgba(255, 255, 255, 0.08); overflow: hidden; }
.hc-fill { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, #59f3ff, #7a5cff, #ff5ee7, #59f3ff); background-size: 300% 100%; animation: hcFlow 4s linear infinite; }
.hc-fill-a { width: 78%; }
.hc-fill-b { width: 52%; animation-delay: -1.3s; }
.hc-fill-c { width: 90%; animation-delay: -2.6s; }
.hc-shelf { display: flex; justify-content: center; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.6rem; }
.hc-ach { width: 1.1rem; height: 1.1rem; object-fit: contain; }
.hc-ach-fb { font-size: 0.85rem; color: #ffd76e; }
.hc-foot { margin-top: 0.55rem; font-size: 0.5rem; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; color: rgba(255, 255, 255, 0.35); }
@keyframes hcSpin { to { transform: rotate(360deg); } }
@keyframes hcSheen { 0%, 55% { left: -70%; } 85%, 100% { left: 150%; } }
@keyframes hcPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@keyframes hcFlow { to { background-position: 300% 0; } }
@media (prefers-reduced-motion: reduce) {
    .hc-frame::before, .hc-inner::after, .hc-live-dot, .hc-fill { animation: none; }
}
@media (max-width: 1024px) {
    .hc-live, .hc-staff, .hc-title, .hc-stat, .hc-shelf, .hc-foot { display: none; }
    .hc { margin-top: 0; }
    .hc-frame { border-radius: 50%; padding: 2px; }
    .hc-inner { padding: 0; background: transparent; border-radius: 50%; }
    .hc-inner::before, .hc-inner::after { display: none; }
    .hc-avhex { width: 2.6rem; height: 2.6rem; margin: 0; clip-path: none; border-radius: 50%; padding: 0; background: none; }
    .hc-av { clip-path: none; border-radius: 50%; }
}

AI is pretty good at providing you some code based on what you want. Give it a try and customize to your preference. I'll give some more examples for other custom templates soon.

1 reply 25 Read more →
Daedalus ·Support ·3 months ago
2.0 Issues

So I started playing around with 2.0 today,

1.) I tried to edit the description of a new section and when I clicked save, it reloaded the page with the description blank.

2.) Trying to delete the default sections generated with the creation of the board has created 500 errors.

3.) Customizing the post rail doesn't seem to be reflecting the changes in the actual thread view. When inspecting the post rail element on the forum, it states the avatar is medium but when viewing the custom post rail setting in the ACP, it is set to large. I tried circumventing this by adding custom.css to the theme stylesheet to force the 100x100 avatar dimension and it didn't work.

25 replies 803 Read more →
Load more
ForumSpark © 2023 - 2026 · ForumSpark.net · Terms