diff --git a/README.md b/README.md index 3677dbf..e5ca0ea 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/default.hbs b/default.hbs index 55bacf0..78a304e 100644 --- a/default.hbs +++ b/default.hbs @@ -10,6 +10,10 @@ + {{!-- Prism syntax highlighting --}} + + + {{!-- contentFor/block scripts - use with defer --}} {{{block "scripts"}}} diff --git a/docs/adr/0001-code-block-syntax-highlighting.md b/docs/adr/0001-code-block-syntax-highlighting.md new file mode 100644 index 0000000..d578423 --- /dev/null +++ b/docs/adr/0001-code-block-syntax-highlighting.md @@ -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 `
` 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 `` element, not the ``. A script in `post.js` reads the class, extracts the language name, and sets it as `data-language` on the ``. A CSS `::before` pseudo-element renders the label. This avoids a dependency on Ghost-specific markup that could change.
+
+### Line wrap toggle via `` 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 `` immediately before a code block. A script in `post.js` detects the comment node and adds a `.wrap` class to the ``.
+
+## 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.
diff --git a/docs/adr/template.md b/docs/adr/template.md
new file mode 100644
index 0000000..eb89f3f
--- /dev/null
+++ b/docs/adr/template.md
@@ -0,0 +1,24 @@
+# ADR XXXX: Title
+
+**Date:** YYYY-MM-DD
+**Status:** Proposed
+
+
+
+## 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?
diff --git a/package.json b/package.json
index 1150ea5..f423741 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/src/css/app.css b/src/css/app.css
index 42dcd3c..18d7dfe 100644
--- a/src/css/app.css
+++ b/src/css/app.css
@@ -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';
diff --git a/src/css/components/code.css b/src/css/components/code.css
new file mode 100644
index 0000000..863b8ca
--- /dev/null
+++ b/src/css/components/code.css
@@ -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 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;
+}
diff --git a/src/js/post/index.js b/src/js/post/index.js
index 163b633..c604cac 100644
--- a/src/js/post/index.js
+++ b/src/js/post/index.js
@@ -1,6 +1,27 @@
// Ship JS only active on post pages for better performance
import tocbot from 'tocbot';
+// Set data-language on 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 immediately following a 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',