Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ You might have to refresh by hand after each change.
If you can't see your changes, and you're sure that `npm run dev` is still going, you'll likely need to disable your browser's caching.
Go to the Network tab in your dev tools and toggle "Disable cache".

### Architecture decisions

Significant decisions about how the theme is built are documented in [`docs/adr/`](docs/adr/). Before making a non-trivial change — adding a dependency, changing how a feature works, picking between approaches — please write an ADR. Use [`docs/adr/template.md`](docs/adr/template.md) as a starting point. The goal isn't process for its own sake; it's making sure future contributors understand *why* things are the way they are, not just what they are.

### Deployment

The theme will automatically check, zip, and upload to Ghost via GitHub Actions. If you want to try it yourself locally:
Expand Down
4 changes: 4 additions & 0 deletions default.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<script src="//code.jquery.com/jquery-3.6.4.min.js" defer></script>
<script src="{{asset "dist/app.js"}}" defer></script>

{{!-- Prism syntax highlighting --}}
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js" defer></script>

{{!-- contentFor/block scripts - use with defer --}}
{{{block "scripts"}}}

Expand Down
32 changes: 32 additions & 0 deletions docs/adr/0001-code-block-syntax-highlighting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ADR 0001: Code Block Syntax Highlighting

**Date:** 2026-03-29
**Status:** Done

## Context

The theme needed support for displaying code blocks in posts and pages. Ghost's Koenig editor outputs `<pre><code class="language-*">` markup but the theme had no styles or syntax highlighting for it.

## Decisions

### Prism.js via CDN

Prism.js is loaded from jsDelivr with the autoloader plugin rather than bundled into `app.js`. This keeps the theme build lean and means any language is supported on demand without maintaining a list of grammar files.

### Token colors from the design system

Syntax token colors are mapped to existing CSS variables (`--color-blue`, `--color-green`, `--color-red`, `--color-yellow`) rather than introducing a third-party theme. This keeps the code blocks feeling native to the site.

### Language label via JS + CSS

Ghost's Markdown card puts `language-*` on the `<code>` element, not the `<pre>`. A script in `post.js` reads the class, extracts the language name, and sets it as `data-language` on the `<pre>`. A CSS `::before` pseudo-element renders the label. This avoids a dependency on Ghost-specific markup that could change.

### Line wrap toggle via `<!--wrap-->` HTML card

Ghost provides no mechanism to add classes to code block elements from the editor. Authors who want line wrapping instead of horizontal scroll can insert an HTML card containing `<!--wrap-->` immediately before a code block. A script in `post.js` detects the comment node and adds a `.wrap` class to the `<pre>`.

## Alternatives Considered

- **Bundling Prism grammars** — would avoid the CDN dependency but add build complexity and require updating the bundle when new languages are needed.
- **A Prism theme (e.g. Tomorrow Night)** — rejected in favor of token colors derived from the design system.
- **Default line wrapping** — rejected because wrapping breaks visual indentation and makes code harder to scan. Opt-in per block is more appropriate.
24 changes: 24 additions & 0 deletions docs/adr/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ADR XXXX: Title

**Date:** YYYY-MM-DD
**Status:** Proposed

<!-- Status options:
Proposed — under discussion, not yet adopted
Accepted — deliberate choice made after weighing options
Done — written after the fact; documents what was built and why
Deprecated — no longer relevant
Superseded by ADR-XXXX — replaced by a later decision
-->

## Context

What problem were you solving? What constraints or forces shaped the decision?

## Decisions

What was decided, and why?

## Alternatives Considered

What else was on the table and why was it ruled out?
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rubycentral-theme",
"version": "1.1.2",
"version": "1.1.3",
"description": "A Ghost theme for Ruby Central",
"engines": {
"ghost": ">=4.0.0"
Expand Down
1 change: 1 addition & 0 deletions src/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

/* Components */
@import './components/button.css';
@import './components/code.css';
@import './components/get-involved.css';
@import './components/sponsors.css';
@import './components/profile-image.css';
Expand Down
119 changes: 119 additions & 0 deletions src/css/components/code.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* Inline code */
:not(pre) > code {
background-color: var(--color-lightblue);
border: 1px solid color-mix(in srgb, var(--color-blue) 25%, transparent);
border-radius: 4px;
color: var(--color-mutednavy);
font-family: 'Menlo', 'Consolas', 'Monaco', monospace;
font-size: 0.875em;
padding: 0.15em 0.4em;
}

/* Code block wrapper — Ghost wraps <pre><code> in a .kg-code-card */
.kg-code-card {
margin: 1.75rem 0;
}

pre {
background-color: var(--color-navy);
border-radius: 6px;
color: #cdd9e5;
font-family: 'Menlo', 'Consolas', 'Monaco', monospace;
font-size: 0.875rem;
line-height: 1.6;
margin: 1.75rem 0;
overflow-x: auto;
padding: 1.25rem 1.5rem;
position: relative;
}

pre code {
background: none;
border: none;
color: inherit;
font-size: inherit;
padding: 0;
}

pre.wrap {
white-space: pre-wrap;
overflow-x: visible;
}

/* Language label — set via JS from Prism's language-* class */
pre[data-language]::before {
background-color: var(--color-yellow);
border-radius: 0 0 4px 4px;
color: var(--color-navy);
content: attr(data-language);
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.05em;
padding: 2px 8px;
position: absolute;
right: 1rem;
text-transform: uppercase;
top: 0;
}

/* Syntax token colors — tuned for the navy background */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #768390;
font-style: italic;
}

.token.punctuation {
color: #8dbbe8;
}

.token.namespace {
opacity: 0.7;
}

.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: var(--color-red);
}

.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: var(--color-green);
}

.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #8dbbe8;
}

.token.atrule,
.token.attr-value,
.token.keyword {
color: var(--color-blue);
}

.token.function,
.token.class-name {
color: var(--color-yellow);
}

.token.regex,
.token.important,
.token.variable {
color: #f69d50;
}
21 changes: 21 additions & 0 deletions src/js/post/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
// Ship JS only active on post pages for better performance
import tocbot from 'tocbot';

// Set data-language on <pre> elements so CSS can display a clean label.
// Runs on load to ensure Prism's autoloader has finished highlighting.
window.addEventListener('load', () => {
document.querySelectorAll('pre > code[class*="language-"]').forEach((code) => {
const match = code.className.match(/\blanguage-(\w+)\b/);
if (match) code.closest('pre').dataset.language = match[1];
});
});

// Apply .wrap class to any <pre> immediately following a <!--wrap--> HTML card
document.querySelectorAll('pre').forEach((pre) => {
let node = pre.closest('.kg-code-card') ?? pre;
let prev = node.previousSibling;
while (prev && prev.nodeType === Node.TEXT_NODE) {
prev = prev.previousSibling;
}
if (prev && prev.nodeType === Node.COMMENT_NODE && prev.nodeValue.trim() === 'wrap') {
pre.classList.add('wrap');
}
});

tocbot.init({
// Where to render the table of contents.
tocSelector: '.gh-toc',
Expand Down
Loading