feature: First working version with dummy content
Some checks failed
build-site / build (push) Failing after 6m4s

This commit is contained in:
Keith Solomon
2025-11-29 12:27:20 -06:00
parent db4058539a
commit e6f6ea5846
19 changed files with 1193 additions and 1 deletions

26
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: build-site
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build static site
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: dev-notes-dist
path: dist

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
dist/
node_modules/
.vscode/
.DS_Store
.env

View File

@@ -1,3 +1,20 @@
# Dev Notes # Dev Notes
My Collection of notes, ponderings, and documentation for various dev projects. Lightweight static notes site built from Markdown with front matter. A small Node-based generator produces HTML ready for Cloudflare Pages.
## Quick start
- Install Node 20+: `npm install`
- Build the site: `npm run build`
- Open `dist/index.html` in a browser or serve via `npx serve dist`
## Content structure
- Write notes in `content/` using kebab-case filenames and front matter (`title`, `section`, `summary`, `tags`, optional `nav`).
- Sections map to directories in the output (`/section-slug/`), and each note becomes `/section-slug/note-slug/`.
- Templates live in `templates/`; shared styles and scripts live in `assets/`.
## Deployment
- The GitHub action `.github/workflows/build.yml` installs dependencies and runs the build.
- Cloudflare Pages can publish the `dist/` directory produced by `npm run build`.

124
assets/css/components.css Normal file
View File

@@ -0,0 +1,124 @@
.sidebar-head h2 { margin: 0 0 .25rem; font-size: 1.125rem; }
.sidebar-links {
list-style: none;
padding: 0;
margin: 0;
li {
margin: 0;
padding: 0;
}
}
.sidebar-links a {
border-radius: 0.625rem;
border: 0.0625rem solid transparent;
color: var(--muted);
display: block;
font-size: clamp(1rem, 1vw, 1.125rem);
line-height: 1.15;
padding: 0.5rem 0.625rem;
text-decoration: none;
transition: background 0.2s ease, color 0.2s ease;
}
.sidebar-links a:hover,
.sidebar-links a.is-active {
color: var(--text);
background: rgba(56, 189, 248, 0.1);
border-color: rgba(56, 189, 248, 0.25);
}
.note-meta {
display: flex;
align-items: center;
gap: 0.625rem;
color: var(--muted);
font-size: 0.875rem;
}
.note-list {
display: flex;
flex-direction: column;
gap: 0.875rem;
padding: 0;
list-style: none;
}
.note-list a {
text-decoration: none;
color: var(--text);
}
.search {
position: relative;
min-width: 13.75rem;
}
#search-input {
width: 100%;
padding: 0.5625rem 0.75rem;
border-radius: 0.625rem;
border: 0.0625rem solid var(--border);
background: rgba(22, 25, 33, 0.9);
color: var(--text);
}
#search-input:focus { outline: 0.125rem solid rgba(56, 189, 248, 0.4); }
.search-results {
position: absolute;
top: 2.75rem;
right: 0;
width: 20rem;
max-height: 26.25rem;
overflow: auto;
background: var(--panel);
border: 0.0625rem solid var(--border);
border-radius: 0.75rem;
padding: 0.625rem;
box-shadow: var(--shadow);
display: none;
z-index: 30;
}
.search-results.active { display: block; }
.search-results ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.search-results li a { color: var(--text); text-decoration: none; }
.search-results .result-section { color: var(--muted); font-size: 0.75rem; }
.hero {
display: flex;
flex-direction: column;
gap: 0.625rem;
margin-bottom: 1rem;
}
.hero h1 { margin: 0; }
.hero p { margin: 0; color: var(--muted); }
.section-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: var(--gap);
}
.section-card h3 { margin: 0; }
.section-card .meta { color: var(--muted); font-size: 0.875rem; }
.tag { font-size: 0.8125rem; color: var(--muted); }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: 0.625rem; border-bottom: 0.0625rem solid var(--border); text-align: left; }

174
assets/css/layout.css Normal file
View File

@@ -0,0 +1,174 @@
:root {
--bg: #0f1115;
--bg-elevated: #161921;
--panel: #1c202a;
--border: #262a33;
--accent: #7dd3fc;
--accent-strong: #38bdf8;
--text: #e5e7eb;
--muted: #9ca3af;
--shadow: 0 0.75rem 2.5rem rgba(0, 0, 0, 0.35);
--radius: 0.75rem;
--gap: clamp(1rem, 2vw, 2rem);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at 20% 20%, rgba(56, 189, 248, 0.08), transparent 25%),
radial-gradient(circle at 80% 0%, rgba(186, 230, 253, 0.06), transparent 30%),
var(--bg);
color: var(--text);
font-family: 'General Sans', system-ui, -apple-system, sans-serif;
}
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.site-header {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--gap);
padding: 1.125rem var(--gap);
background: rgba(15, 17, 21, 0.9);
backdrop-filter: blur(0.75rem);
border-bottom: 0.0625rem solid var(--border);
box-shadow: var(--shadow);
}
.brand a {
color: var(--text);
font-family: 'Raleway', 'General Sans', sans-serif;
font-weight: 700;
letter-spacing: 0.025rem;
text-decoration: none;
}
.nav {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.nav a {
padding: 0.5rem 0.75rem;
border-radius: 0.625rem;
color: var(--muted);
text-decoration: none;
border: 0.0625rem solid transparent;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.nav a:hover,
.nav a.is-active {
color: var(--text);
border-color: var(--border);
background: rgba(56, 189, 248, 0.12);
}
.page-body {
display: grid;
grid-template-columns: minmax(16.25rem, 17.5rem) 1fr;
gap: var(--gap);
width: calc(100vw - 20%);
margin: var(--gap) auto var(--gap);
}
.sidebar {
padding: 1.25rem;
background: var(--panel);
border: 0.0625rem solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
height: fit-content;
position: sticky;
top: 5.375rem;
}
.content {
background: var(--bg-elevated);
border: 0.0625rem solid var(--border);
border-radius: var(--radius);
padding: clamp(1.25rem, 3vw, 2rem);
box-shadow: var(--shadow);
}
.site-footer {
margin-top: auto;
padding: 1rem var(--gap) 1.75rem;
display: flex;
align-items: center;
justify-content: space-between;
color: var(--muted);
border-top: 0.0625rem solid var(--border);
background: rgba(15, 17, 21, 0.9);
}
.site-footer a {
color: var(--accent);
text-decoration: none;
&:hover {
border: none;
color: var(--accent-strong);
text-decoration: none;
}
}
.footer-links {
display: flex;
gap: 0.75rem;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16.25rem, 1fr));
gap: var(--gap);
}
.card {
background: var(--panel);
border: 0.0625rem solid var(--border);
border-radius: var(--radius);
padding: 1.125rem;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 0.625rem;
text-decoration: none;
color: var(--text);
transition: transform 0.2s ease, border-color 0.2s ease;
}
.card:hover {
border-color: rgba(125, 211, 252, 0.7);
text-decoration: none;
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.625rem;
background: rgba(56, 189, 248, 0.12);
color: var(--accent);
border-radius: 62.4375rem;
border: 0.0625rem solid rgba(56, 189, 248, 0.24);
font-size: 0.8125rem;
}
.divider {
height: 0.0625rem;
width: 100%;
background: var(--border);
margin: 1.125rem 0;
}

31
assets/css/responsive.css Normal file
View File

@@ -0,0 +1,31 @@
@media (max-width: 60rem) {
.page-body {
grid-template-columns: 1fr;
}
.sidebar {
position: relative;
top: 0;
order: 2;
}
}
@media (max-width: 45rem) {
.site-header {
grid-template-columns: 1fr;
gap: 0.625rem;
position: sticky;
}
.nav { order: 3; }
.search { order: 2; }
.site-footer {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
#search-input { width: 100%; }
.search-results { width: 100%; right: auto; }
}

87
assets/css/typography.css Normal file
View File

@@ -0,0 +1,87 @@
:root {
--font-body: 'General Sans', system-ui, -apple-system, sans-serif;
--font-heading: 'Raleway', var(--font-body);
--text-size: clamp(1.125rem, 1.2vw, 1.25rem);
--line-height: 1.25;
}
body {
font-size: var(--text-size);
line-height: var(--line-height);
color: var(--text);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
line-height: 1.25;
margin: 0 0 0.75rem;
color: #f9fafb;
}
h1 { font-size: clamp(2rem, 3vw, 2.5rem); }
h2 { font-size: clamp(1.625rem, 2.4vw, 2rem); }
h3 { font-size: clamp(1.375rem, 2vw, 1.625rem); }
h4 { font-size: clamp(1.25rem, 1.8vw, 1.375rem); }
p { margin: 0 0 0.875rem; color: var(--text); }
ul, ol { margin: 0 0 1rem 1.25rem; padding: 0; }
code {
font-family: 'SFMono-Regular', Consolas, Monaco, monospace;
background: rgba(38, 42, 51, 0.7);
padding: 0.125rem 0.375rem;
border-radius: 0.375rem;
border: 0.0625rem solid var(--border);
}
pre {
margin: 0 0 1.125rem;
padding: 0.875rem;
background: #0b0d11;
border-radius: var(--radius);
border: 0.0625rem solid var(--border);
overflow-x: auto;
}
pre code { background: transparent; border: none; padding: 0; }
a { color: var(--accent-strong); text-decoration: none; }
a:hover { text-decoration: underline; }
blockquote {
margin: 0 0 1rem;
padding: 0.75rem 1rem;
border-left: 0.1875rem solid var(--accent-strong);
background: rgba(56, 189, 248, 0.08);
border-radius: 0.625rem;
color: var(--text);
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.muted { color: var(--muted); }
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.0625rem;
font-size: 0.75rem;
color: var(--muted);
margin: 0 0 0.375rem;
}
.sr-only {
position: absolute;
width: 0.0625rem;
height: 0.0625rem;
padding: 0;
margin: -0.0625rem;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

72
assets/js/search.js Normal file
View File

@@ -0,0 +1,72 @@
(() => {
const input = document.getElementById('search-input');
const resultsBox = document.getElementById('search-results');
if (!input || !resultsBox) return;
let index = [];
let loaded = false;
const render = (items) => {
if (!items.length) {
resultsBox.innerHTML = '<p class="muted">No matches.</p>';
resultsBox.classList.add('active');
return;
}
const list = items
.slice(0, 12)
.map(item => `
<li>
<a href="${item.url}">
<div class="result-title">${item.title}</div>
<div class="result-section">${item.section}</div>
<div class="muted">${item.summary}</div>
</a>
</li>
`).join('');
resultsBox.innerHTML = `<ul>${list}</ul>`;
resultsBox.classList.add('active');
};
const filter = (query) => {
if (!query.trim()) {
resultsBox.classList.remove('active');
return;
}
const q = query.toLowerCase();
const items = index.filter(item => {
return (
item.title.toLowerCase().includes(q) ||
item.section.toLowerCase().includes(q) ||
item.summary.toLowerCase().includes(q) ||
item.tags.some(tag => tag.toLowerCase().includes(q))
);
});
render(items);
};
const loadIndex = async () => {
if (loaded) return index;
try {
const res = await fetch('/search-index.json');
index = await res.json();
loaded = true;
} catch (err) {
console.error('Search index failed to load', err);
}
return index;
};
input.addEventListener('input', async (e) => {
await loadIndex();
filter(e.target.value);
});
document.addEventListener('click', (e) => {
if (!resultsBox.contains(e.target) && e.target !== input) {
resultsBox.classList.remove('active');
}
});
})();

268
bin/build.js Normal file
View File

@@ -0,0 +1,268 @@
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const MarkdownIt = require('markdown-it');
const hljs = require('highlight.js');
const ROOT = path.resolve(__dirname, '..');
const CONTENT_DIR = path.join(ROOT, 'content');
const DIST_DIR = path.join(ROOT, 'dist');
const TEMPLATE_DIR = path.join(ROOT, 'templates');
const ASSETS_DIR = path.join(ROOT, 'assets');
const md = new MarkdownIt({
html: true,
linkify: true,
highlight: (str, lang) => {
if (lang && hljs.getLanguage(lang)) {
return `<pre><code class="hljs ${lang}">${hljs.highlight(str, { language: lang }).value}</code></pre>`;
}
return `<pre><code class="hljs">${md.utils.escapeHtml(str)}</code></pre>`;
},
});
const readTemplate = (name) => fs.readFileSync(path.join(TEMPLATE_DIR, name), 'utf8');
const templates = {
base: readTemplate('base.html'),
header: readTemplate('header.html'),
sidebar: readTemplate('sidebar.html'),
footer: readTemplate('footer.html'),
};
const render = (template, data) => template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => data[key] ?? '');
const slugify = (value) =>
value
.toString()
.trim()
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
const walkMarkdownFiles = (dir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
return entries.flatMap((entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) return walkMarkdownFiles(fullPath);
if (entry.isFile() && entry.name.endsWith('.md')) return [fullPath];
return [];
});
};
const cleanDir = (dir) => {
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
};
const stripMarkdown = (text) => text.replace(/[`*_>#\-]/g, '').replace(/\s+/g, ' ').trim();
const loadNotes = () => {
const files = walkMarkdownFiles(CONTENT_DIR);
return files.map((filePath) => {
const raw = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(raw);
const relative = path.relative(CONTENT_DIR, filePath);
const fileName = path.basename(filePath, '.md');
const section = data.section || path.dirname(relative) || 'notes';
const sectionSlug = slugify(section);
const slug = slugify(data.slug || fileName);
const title = data.title || fileName.replace(/-/g, ' ');
const html = md.render(content);
const summary = (data.summary || stripMarkdown(content).slice(0, 180)) + '...';
return {
title,
section,
sectionSlug,
slug,
summary,
tags: data.tags || [],
nav: data.nav ?? Number.MAX_SAFE_INTEGER,
html,
sourcePath: relative,
};
});
};
const buildNavLinks = (sections, activeSection) => {
return sections
.map(({ name, slug }) => {
const isActive = activeSection === slug ? 'is-active' : '';
return `<a class="${isActive}" href="/${slug}/">${name}</a>`;
})
.join('');
};
const buildSidebarLinks = (notes, currentSlug) =>
notes
.map((note) => {
const isActive = currentSlug === note.slug ? 'is-active' : '';
return `<li><a class="${isActive}" href="/${note.sectionSlug}/${note.slug}/">${note.title}</a></li>`;
})
.join('');
const assemblePage = ({ pageTitle, header, sidebar, content, extraHead = '' }) =>
render(templates.base, { pageTitle, header, sidebar, content, footer: templates.footer, extraHead });
const writePage = (targetPath, html) => {
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, html, 'utf8');
};
const copyAssets = () => {
const target = path.join(DIST_DIR, 'assets');
fs.cpSync(ASSETS_DIR, target, { recursive: true });
};
const buildSearchIndex = (notes) => {
const index = notes.map((note) => ({
title: note.title,
section: note.section,
summary: note.summary,
tags: note.tags,
url: `/${note.sectionSlug}/${note.slug}/`,
}));
fs.writeFileSync(path.join(DIST_DIR, 'search-index.json'), JSON.stringify(index, null, 2));
};
const buildPages = () => {
cleanDir(DIST_DIR);
const notes = loadNotes();
const sectionsMap = new Map();
notes.forEach((note) => {
if (!sectionsMap.has(note.sectionSlug)) {
sectionsMap.set(note.sectionSlug, {
name: note.section,
slug: note.sectionSlug,
notes: [],
});
}
sectionsMap.get(note.sectionSlug).notes.push(note);
});
const sections = Array.from(sectionsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
sections.forEach((section) => section.notes.sort((a, b) => (a.nav - b.nav) || a.title.localeCompare(b.title)));
// Build header once per page.
const renderHeader = (activeSectionSlug) => render(templates.header, {
navLinks: buildNavLinks(sections, activeSectionSlug),
});
// Section pages and notes
sections.forEach((section) => {
const sidebar = render(templates.sidebar, {
sectionTitle: section.name,
sidebarLinks: buildSidebarLinks(section.notes, null),
});
// Section index
const sectionList = section.notes
.map((note) => `
<a class="card" href="/${note.sectionSlug}/${note.slug}/">
<div class="badge">${section.name}</div>
<h3>${note.title}</h3>
<p class="muted">${note.summary}</p>
<div class="tag-list">${note.tags.map((tag) => `<span class="tag">#${tag}</span>`).join('')}</div>
</a>
`)
.join('');
const sectionContent = `
<div class="hero">
<p class="eyebrow">Section</p>
<h1>${section.name}</h1>
<p class="muted">Notes grouped by ${section.name}. Use the sidebar or search to jump in.</p>
</div>
<div class="card-grid">${sectionList}</div>
`;
const sectionPage = assemblePage({
pageTitle: section.name,
header: renderHeader(section.slug),
sidebar,
content: sectionContent,
});
writePage(path.join(DIST_DIR, section.slug, 'index.html'), sectionPage);
// Individual notes
section.notes.forEach((note) => {
const sidebarWithActive = render(templates.sidebar, {
sectionTitle: section.name,
sidebarLinks: buildSidebarLinks(section.notes, note.slug),
});
const tags = note.tags.map((tag) => `<span class="tag">#${tag}</span>`).join('');
const noteContent = `
<article>
<p class="eyebrow">${section.name}</p>
<h1>${note.title}</h1>
<div class="note-meta">
<span>${note.summary}</span>
${tags ? `<span class="tag-list">${tags}</span>` : ''}
</div>
<div class="divider"></div>
${note.html}
</article>
`;
const page = assemblePage({
pageTitle: note.title,
header: renderHeader(section.slug),
sidebar: sidebarWithActive,
content: noteContent,
});
writePage(path.join(DIST_DIR, section.slug, note.slug, 'index.html'), page);
});
});
// Home page
const summaryCards = sections
.map((section) => `
<a class="card" href="/${section.slug}/">
<div class="badge">${section.notes.length} note${section.notes.length === 1 ? '' : 's'}</div>
<h3>${section.name}</h3>
<p class="muted">${section.notes[0]?.summary || 'Section overview'}
</p>
</a>
`)
.join('');
const homeContent = `
<div class="hero">
<p class="eyebrow">Workspace</p>
<h1>Developer Notes Hub</h1>
<p class="muted">Markdown-first notes rendered into a fast static site. Use search or browse by section.</p>
</div>
<div class="card-grid">${summaryCards}</div>
`;
const homeSidebar = render(templates.sidebar, {
sectionTitle: 'Sections',
sidebarLinks: sections
.map((section) => `<li><a href="/${section.slug}/">${section.name}</a></li>`)
.join(''),
});
const homePage = assemblePage({
pageTitle: 'Home',
header: renderHeader(null),
sidebar: homeSidebar,
content: homeContent,
});
writePage(path.join(DIST_DIR, 'index.html'), homePage);
// Assets and search index
copyAssets();
buildSearchIndex(notes);
console.log(`Built ${notes.length} notes across ${sections.length} sections → ${DIST_DIR}`);
};
buildPages();

View File

@@ -0,0 +1,37 @@
---
title: Markdown Authoring Guide
section: docs
summary: Conventions for writing notes, front matter fields, and embedding code or media in the site.
tags: [markdown, style, notes]
nav: 1
---
# Markdown Authoring Guide
## Front matter
Provide metadata at the top of every note:
```
---
title: Example Title
section: docs
summary: One-sentence overview for listings.
tags: [topic, keyword]
nav: 1
---
```
- `section`: logical bucket (docs, infra, backend, frontend, etc.).
- `nav`: optional integer to influence ordering within a section; lower numbers show first.
## Writing tips
- Start with an `#` heading matching the title.
- Keep paragraphs short; use bullet lists for tasks.
- Use fenced code blocks with language hints (` ```bash `, ` ```js `) for highlighting.
- Link to related notes with absolute paths, e.g., `/docs/markdown-authoring-guide/`.
## Media
Place images next to the note or in an `/assets/media` folder and reference relatively.
## Testing locally
Run `npm run build` to regenerate HTML. Open `dist/index.html` in a browser to review layout and syntax highlighting.

26
content/infra-workflow.md Normal file
View File

@@ -0,0 +1,26 @@
---
title: Cloudflare Pages Workflow
section: infra
summary: Steps to publish the static notes site to Cloudflare Pages using the provided workflow and build output.
tags: [cloudflare, ci, deploy]
nav: 1
---
# Cloudflare Pages Workflow
## Overview
This note captures the build and deploy flow for the static notes site. The site compiles Markdown into static HTML under the `dist/` directory.
## Build steps
1. Install dependencies: `npm install`.
2. Generate the site: `npm run build`.
3. Upload the `dist/` output to Cloudflare Pages.
## Environment
- Node 20+ is recommended.
- CI uses `npm ci` and `npm run build` from the default branch.
## Tips
- Keep content in `content/` with front matter to expose metadata.
- The build bundles a `search-index.json`; avoid storing secrets in front matter.
- Regenerate locally before pushing to preview changes.

24
content/ops-onboarding.md Normal file
View File

@@ -0,0 +1,24 @@
---
title: Developer Onboarding
section: ops
summary: Quick start steps to clone, install dependencies, and generate the site locally.
tags: [onboarding, setup]
nav: 1
---
# Developer Onboarding
1. Clone the repository and install Node 20+.
2. Run `npm install` to pull dependencies.
3. Build with `npm run build`; output lands in `dist/`.
4. Serve `dist/` via any static server (e.g., `npx serve dist`).
## Directory overview
- `content/`: Markdown notes with front matter.
- `templates/`: HTML shells for header, footer, and layout.
- `assets/`: CSS and JavaScript shared across pages.
## Conventions
- Use kebab-case filenames.
- Keep summaries short; they populate listings and the search index.
- Add tags to improve search results.

200
package-lock.json generated Normal file
View File

@@ -0,0 +1,200 @@
{
"name": "dev-notes-ssg",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dev-notes-ssg",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"gray-matter": "^4.0.3",
"highlight.js": "^11.9.0",
"markdown-it": "^14.1.0"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/markdown-it": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
}
}
}

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "dev-notes-ssg",
"version": "0.1.0",
"description": "Lightweight static site generator for personal dev notes",
"license": "MIT",
"scripts": {
"build": "node bin/build.js"
},
"dependencies": {
"gray-matter": "^4.0.3",
"highlight.js": "^11.9.0",
"markdown-it": "^14.1.0"
}
}

9
prompt.txt Normal file
View File

@@ -0,0 +1,9 @@
This repo will hold my personal collection of notes, etc for various dev projects. It should have a simple SSG that will render markdown (with appropriate front matter) into a sensible set of files that can be hosted via Cloudflare pages. I don't need something heavy like a standard SSG framework, even a simple php script that will render the content would be fine as long as it can do what I need. The goal is to have a simple, easy to maintain set of notes that I can quickly add to and update as needed. The content will be primarily markdown files with some front matter for metadata. The SSG should be able to parse the front matter and generate appropriate HTML files for hosting.
Each page will have a standard structure with a header, footer, sidebar, and main content area, ideally separated into separate files. The header will include navigation links to each section of the notes (set in the front matter), while the footer will have links to my personal site and other content to be determined. The sidebar will contain links to each note in a section. The main content area will render the markdown content into HTML. Each section should have its own folder with an index page that lists all notes within that section, along with a brief description or excerpt from each note. The main index page should provide an overview of all sections and link to their respective index pages.
For styling, I want a clean, minimalistic design that is easy to read and navigate. A dark theme with good contrast and legible fonts is preferred. Fonts used should be Raleway for headings and General Sans for body text (both from Google). Use a proper type scale utilizing fluid typography techniques based on a 20px main content text size. The layout should be responsive to ensure usability across different devices. Styling should follow a "utility-first" approach, similar to Tailwind CSS, to allow for easy customization and maintenance, but note I *DO NOT* want to use Tailwind itself. Colors should be customizable via CSS variables to allow for easy theming. Styling should be modular, with separate CSS files for layout, typography, components, and responsive adjustments.
In the rendered HTML, code blocks should have syntax highlighting for better readability. I would also like to include support for images and other media within the notes, ensuring they are properly displayed in the rendered HTML. I also want to include a search functionality to quickly find notes based on keywords or tags. This can be a simple client-side search using JavaScript to filter through the notes.
Finally, the SSG should be easy to run locally for testing and generating the static files before deployment to Cloudflare pages. A repo action to generate the static pages should be included.

46
templates/base.html Normal file
View File

@@ -0,0 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{pageTitle}} | Dev Notes</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Raleway:wght@500;700&family=General+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/layout.css" />
<link rel="stylesheet" href="/assets/css/typography.css" />
<link rel="stylesheet" href="/assets/css/components.css" />
<link rel="stylesheet" href="/assets/css/responsive.css" />
<link href="https://cdn.lineicons.com/5.0/lineicons.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" integrity="sha512-Jk4AqjWsdSzSWCSuQTfYRIF84Rq/eV0G2+tu07byYwHcbTGfdmLrHjUSwvzp5HvbiqK4ibmNwdcG49Y5RGYPTg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{{extraHead}}
</head>
<body>
<div class="page">
{{header}}
<div class="page-body">
{{sidebar}}
<main class="content" id="main-content">
{{content}}
</main>
</div>
{{footer}}
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity="sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/assets/js/search.js"></script>
<script>hljs.highlightAll();</script>
</body>
</html>

7
templates/footer.html Normal file
View File

@@ -0,0 +1,7 @@
<footer class="site-footer">
<div>Built for Cloudflare Pages deployment.</div>
<div class="footer-links">
<a class="lni lni-wordpress" href="https://keithsolomon.net" target="_blank" rel="noreferrer"><span class="sr-only">Personal Site</span></a>
<a class="lni lni-github" href="https://github.com/ksolomon/" target="_blank" rel="noreferrer"><span class="sr-only">GitHub</span></a>
</div>
</footer>

15
templates/header.html Normal file
View File

@@ -0,0 +1,15 @@
<header class="site-header">
<div class="brand">
<a href="/">Dev Notes</a>
</div>
<nav class="nav">
{{navLinks}}
</nav>
<div class="search">
<label class="sr-only" for="search-input">Search notes</label>
<input id="search-input" type="search" placeholder="Search notes..." autocomplete="off" />
<div id="search-results" class="search-results" aria-live="polite"></div>
</div>
</header>

10
templates/sidebar.html Normal file
View File

@@ -0,0 +1,10 @@
<aside class="sidebar">
<div class="sidebar-head">
<p class="eyebrow">Section</p>
<h2>{{sectionTitle}}</h2>
</div>
<ul class="sidebar-links">
{{sidebarLinks}}
</ul>
</aside>