diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..79174c5 --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e97806b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +dist/ +node_modules/ +.vscode/ +.DS_Store +.env diff --git a/README.md b/README.md index ecba6f5..c64a672 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ # 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`. diff --git a/assets/css/components.css b/assets/css/components.css new file mode 100644 index 0000000..4f2ed13 --- /dev/null +++ b/assets/css/components.css @@ -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; } diff --git a/assets/css/layout.css b/assets/css/layout.css new file mode 100644 index 0000000..0c984c6 --- /dev/null +++ b/assets/css/layout.css @@ -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; +} diff --git a/assets/css/responsive.css b/assets/css/responsive.css new file mode 100644 index 0000000..7d97f91 --- /dev/null +++ b/assets/css/responsive.css @@ -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; } +} diff --git a/assets/css/typography.css b/assets/css/typography.css new file mode 100644 index 0000000..195935f --- /dev/null +++ b/assets/css/typography.css @@ -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; +} diff --git a/assets/js/search.js b/assets/js/search.js new file mode 100644 index 0000000..1a10865 --- /dev/null +++ b/assets/js/search.js @@ -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 = '

No matches.

'; + resultsBox.classList.add('active'); + return; + } + + const list = items + .slice(0, 12) + .map(item => ` +
  • + +
    ${item.title}
    +
    ${item.section}
    +
    ${item.summary}
    +
    +
  • + `).join(''); + + resultsBox.innerHTML = ``; + 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'); + } + }); +})(); diff --git a/bin/build.js b/bin/build.js new file mode 100644 index 0000000..a6b820e --- /dev/null +++ b/bin/build.js @@ -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 `
    ${hljs.highlight(str, { language: lang }).value}
    `; + } + return `
    ${md.utils.escapeHtml(str)}
    `; + }, +}); + +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 `${name}`; + }) + .join(''); +}; + +const buildSidebarLinks = (notes, currentSlug) => + notes + .map((note) => { + const isActive = currentSlug === note.slug ? 'is-active' : ''; + return `
  • ${note.title}
  • `; + }) + .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) => ` + +
    ${section.name}
    +

    ${note.title}

    +

    ${note.summary}

    +
    ${note.tags.map((tag) => `#${tag}`).join('')}
    +
    + `) + .join(''); + + const sectionContent = ` +
    +

    Section

    +

    ${section.name}

    +

    Notes grouped by ${section.name}. Use the sidebar or search to jump in.

    +
    +
    ${sectionList}
    + `; + + 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) => `#${tag}`).join(''); + const noteContent = ` +
    +

    ${section.name}

    +

    ${note.title}

    +
    + ${note.summary} + ${tags ? `${tags}` : ''} +
    +
    + ${note.html} +
    + `; + + 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) => ` + +
    ${section.notes.length} note${section.notes.length === 1 ? '' : 's'}
    +

    ${section.name}

    +

    ${section.notes[0]?.summary || 'Section overview'} +

    +
    + `) + .join(''); + + const homeContent = ` +
    +

    Workspace

    +

    Developer Notes Hub

    +

    Markdown-first notes rendered into a fast static site. Use search or browse by section.

    +
    +
    ${summaryCards}
    + `; + + const homeSidebar = render(templates.sidebar, { + sectionTitle: 'Sections', + sidebarLinks: sections + .map((section) => `
  • ${section.name}
  • `) + .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(); diff --git a/content/docs-markdown-authoring.md b/content/docs-markdown-authoring.md new file mode 100644 index 0000000..42d667c --- /dev/null +++ b/content/docs-markdown-authoring.md @@ -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. diff --git a/content/infra-workflow.md b/content/infra-workflow.md new file mode 100644 index 0000000..3bfbfda --- /dev/null +++ b/content/infra-workflow.md @@ -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. diff --git a/content/ops-onboarding.md b/content/ops-onboarding.md new file mode 100644 index 0000000..414a5bc --- /dev/null +++ b/content/ops-onboarding.md @@ -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. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6a83b01 --- /dev/null +++ b/package-lock.json @@ -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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..eadee2a --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/prompt.txt b/prompt.txt new file mode 100644 index 0000000..3858b61 --- /dev/null +++ b/prompt.txt @@ -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. diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..81b11ba --- /dev/null +++ b/templates/base.html @@ -0,0 +1,46 @@ + + + + + + + {{pageTitle}} | Dev Notes + + + + + + + + + + + + + + + {{extraHead}} + + + +
    + {{header}} + +
    + {{sidebar}} + +
    + {{content}} +
    +
    + + {{footer}} +
    + + + + + + + + diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..fb77402 --- /dev/null +++ b/templates/footer.html @@ -0,0 +1,7 @@ + diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..4b3a9f1 --- /dev/null +++ b/templates/header.html @@ -0,0 +1,15 @@ + diff --git a/templates/sidebar.html b/templates/sidebar.html new file mode 100644 index 0000000..8e5aac6 --- /dev/null +++ b/templates/sidebar.html @@ -0,0 +1,10 @@ +