diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md new file mode 100644 index 0000000..6cd1b04 --- /dev/null +++ b/.claude/commands/plan.md @@ -0,0 +1,36 @@ +Plan how to implement the specified feature. + +This is the second step in the Spec-Driven Development lifecycle. + +Given the implementation details provided as an argument, do this: + +1. Run `scripts/setup-plan.sh --json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. +2. Read and analyze the feature specification to understand: + - The feature requirements and user stories + - Functional and non-functional requirements + - Success criteria and acceptance criteria + - Any technical constraints or dependencies mentioned + +3. Read the constitution at `/memory/constitution.md` to understand constitutional requirements. + +4. Execute the implementation plan template: + - Load `/templates/implementation-plan-template.md` (already copied to IMPL_PLAN path) + - Set Input path to FEATURE_SPEC + - Run the Execution Flow (main) function steps 1-10 + - The template is self-contained and executable + - Follow error handling and gate checks as specified + - Let the template guide artifact generation in $SPECS_DIR: + - Phase 0 generates research.md + - Phase 1 generates data-model.md, contracts/, quickstart.md + - Phase 2 generates tasks.md + - Incorporate user-provided details from arguments into Technical Context: $ARGUMENTS + - Update Progress Tracking as you complete each phase + +5. Verify execution completed: + - Check Progress Tracking shows all phases complete + - Ensure all required artifacts were generated + - Confirm no ERROR states in execution + +6. Report results with branch name, file paths, and generated artifacts. + +Use absolute paths with the repository root for all file operations to avoid path issues. diff --git a/.claude/commands/specify.md b/.claude/commands/specify.md new file mode 100644 index 0000000..5021768 --- /dev/null +++ b/.claude/commands/specify.md @@ -0,0 +1,12 @@ +Start a new feature by creating a specification and feature branch. + +This is the first step in the Spec-Driven Development lifecycle. + +Given the feature description provided as an argument, do this: + +1. Run the script `scripts/create-new-feature.sh --json "$ARGUMENTS"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. +2. Load `templates/spec-template.md` to understand required sections. +3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. +4. Report completion with branch name, spec file path, and readiness for the next phase. + +Note: The script creates and checks out the new branch and initializes the spec file before writing. diff --git a/.claude/commands/tasks.md b/.claude/commands/tasks.md new file mode 100644 index 0000000..edeb151 --- /dev/null +++ b/.claude/commands/tasks.md @@ -0,0 +1,58 @@ +Break down the plan into executable tasks. + +This is the third step in the Spec-Driven Development lifecycle. + +Given the context provided as an argument, do this: + +1. Run `scripts/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. +2. Load and analyze available design documents: + - Always read plan.md for tech stack and libraries + - IF EXISTS: Read data-model.md for entities + - IF EXISTS: Read contracts/ for API endpoints + - IF EXISTS: Read research.md for technical decisions + - IF EXISTS: Read quickstart.md for test scenarios + + Note: Not all projects have all documents. For example: + - CLI tools might not have contracts/ + - Simple libraries might not need data-model.md + - Generate tasks based on what's available + +3. Generate tasks following the template: + - Use `/templates/tasks-template.md` as the base + - Replace example tasks with actual tasks based on: + - **Setup tasks**: Project init, dependencies, linting + - **Test tasks [P]**: One per contract, one per integration scenario + - **Core tasks**: One per entity, service, CLI command, endpoint + - **Integration tasks**: DB connections, middleware, logging + - **Polish tasks [P]**: Unit tests, performance, docs + +4. Task generation rules: + - Each contract file β†’ contract test task marked [P] + - Each entity in data-model β†’ model creation task marked [P] + - Each endpoint β†’ implementation task (not parallel if shared files) + - Each user story β†’ integration test marked [P] + - Different files = can be parallel [P] + - Same file = sequential (no [P]) + +5. Order tasks by dependencies: + - Setup before everything + - Tests before implementation (TDD) + - Models before services + - Services before endpoints + - Core before integration + - Everything before polish + +6. Include parallel execution examples: + - Group [P] tasks that can run together + - Show actual Task agent commands + +7. Create FEATURE_DIR/tasks.md with: + - Correct feature name from implementation plan + - Numbered tasks (T001, T002, etc.) + - Clear file paths for each task + - Dependency notes + - Parallel execution guidance + +Context for task generation: $ARGUMENTS + +The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context. diff --git a/.claude/constitution.md b/.claude/constitution.md new file mode 100644 index 0000000..35e5e0b --- /dev/null +++ b/.claude/constitution.md @@ -0,0 +1,56 @@ +# Node.js Design Patterns Website Constitution + +## Core Principles + +### Minimal Static Site + +This project represents a minimal static site built with Astro. The core principle is to keep the site lean and fast for the users. So everything that can be pre-built should be pre-built. Images should be optimized and served in modern formats (e.g., WebP) to reduce load times. Whenever we implement a new feature we should aim for the smallest possible footprint and, if possible, the simplest and least intrusive solution. + +This means for example favoring Astro components and minimal vanilla JavaScript snippets, rather than React. +React is also available for more advanced use cases, though. But it should be used sparingly. + +### Modern tooling + +This website assumes we use modern tooling such as recent versions of Node.js and pnpm and Tailwind CSS for styling. + +### Data-driven approach + +Where it makes sense we separate data from actual templates, components and other rendering logic. We have data files organized in the `src/content` subdivided in specific types, for example `authors`, `blog`, `chapters`, `quotes`, `faq`, and `reviews`. Each piece of content can be in markdown or JSON format and it should be strongly typed using Astro collections which are defined in `src/content.config.ts`. + +### Responsive mobile-first design + +The website must be designed to follow a responsive mobile-first approach. This is generally achieved using specific Tailwind CSS utilities that prioritize mobile layouts and progressively enhance them for larger screens (e.g., using `sm:`, `md:`, and `lg:` prefixes). + +### Accessibility + +The website must follow accessibility best practices to ensure it is usable by everyone, including users with disabilities. This includes: + +- Using semantic HTML elements (e.g., `
`, `
\ No newline at end of file diff --git a/src/js/faqs.js b/src/js/faqs.js deleted file mode 100644 index b57c39a..0000000 --- a/src/js/faqs.js +++ /dev/null @@ -1,38 +0,0 @@ -export default function faqs () { - const buttons = document.querySelectorAll('dl.faq button') - - function toggle (btn, skipIfAnchor = false) { - const isExpanded = btn.getAttribute('aria-expanded') === 'true' - const faqId = btn.getAttribute('data-faq-id') - const targetId = btn.getAttribute('aria-controls') - const target = document.getElementById(targetId) - - if (skipIfAnchor && window.location.hash.length > 0 && faqId === window.location.hash.substr(1)) { - if (window.dataLayer) { - window.dataLayer.push({ event: `faq_open_${faqId}` }) - } - return - } - - if (isExpanded) { - btn.setAttribute('aria-expanded', 'false') - target.style.maxHeight = 0 - target.style.padding = '0 0 0 3.2rem' - } else { - if (window.dataLayer) { - window.dataLayer.push({ event: `faq_open_${faqId}` }) - } - btn.setAttribute('aria-expanded', 'true') - target.style.maxHeight = '1000px' - target.style.padding = '1.5rem 0 1.5rem 3.2rem' - } - } - - buttons.forEach((button) => { - button.addEventListener('click', (e) => { - e.preventDefault() - toggle(e.currentTarget) - }) - toggle(button, true) // once the page loads collapse all tha faqs - }) -} diff --git a/src/js/index.js b/src/js/index.js deleted file mode 100644 index 1d8049c..0000000 --- a/src/js/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import '../scss/style.scss' -import navbar from './navbar.js' -import faqs from './faqs.js' - -document.addEventListener('DOMContentLoaded', () => { - navbar() // init navbar - faqs() // init faqs -}) diff --git a/src/js/navbar.js b/src/js/navbar.js deleted file mode 100644 index 8053260..0000000 --- a/src/js/navbar.js +++ /dev/null @@ -1,49 +0,0 @@ -export default function navbar () { - const html = document.getElementsByTagName('html')[0] - const navbar = document.getElementById('navbar') - const htmlStickyClasses = ['has-navbar-fixed-top'] - const navbarStickyClasses = ['is-fixed-top', 'has-shadow'] - - let lastScrollY = 0 - let state = { - stickyBarEnabled: true, - stickyBarVisible: true - } - - function updateState (stateChanges) { - const newState = { ...state, ...stateChanges } - if (JSON.stringify(newState) !== JSON.stringify(state)) { - state = newState - update() - } - } - - function update () { - if (state.stickyBarEnabled && state.stickyBarVisible) { - for (const className of htmlStickyClasses) { - html.classList.add(className) - } - for (const className of navbarStickyClasses) { - navbar.classList.add(className) - } - } else { - for (const className of htmlStickyClasses) { - html.classList.remove(className) - } - for (const className of navbarStickyClasses) { - navbar.classList.remove(className) - } - } - } - - const mobileMq = window.matchMedia('screen and (max-width: 768px)') - mobileMq.addEventListener('change', (e) => { - updateState({ stickyBarEnabled: e.matches }) - }) - - window.addEventListener('scroll', (e) => { - const isScrollingUp = window.scrollY < lastScrollY - updateState({ stickyBarVisible: isScrollingUp }) - lastScrollY = window.scrollY - }) -} diff --git a/src/lib/const.ts b/src/lib/const.ts new file mode 100644 index 0000000..40891af --- /dev/null +++ b/src/lib/const.ts @@ -0,0 +1,51 @@ +export const READERS = 30000 +export const CHAPTERS = 13 +export const PAGES = 732 +export const EXAMPLES = 170 +export const EXERCISES = 54 +export const RATING = 4.6 +export const MAX_RATING = 5 +export const REVIEWS = 780 +export const BUY_LINK_PRINT = + 'https://zhinaydjxzpysk4hel3tqbhn3q0kewtv.lambda-url.eu-west-1.on.aws/1803238941' +export const BUY_LINK_EBOOK = + 'https://zhinaydjxzpysk4hel3tqbhn3q0kewtv.lambda-url.eu-west-1.on.aws/B0F4WXGHJX' +export const BUY_LINK_PACKT = + 'https://www.packtpub.com/en-us/product/nodejs-design-patterns-9781803235431' + +// SEO Constants +export const SITE_TITLE = + 'Node.js Design Patterns: Master production-grade Node.js applications' +export const SITE_DESCRIPTION = + 'The definitive guide to Node.js patterns and best practices. Learn proven techniques for building scalable, maintainable applications with 660+ pages, 150+ examples, and expert insights from Mario Casciaro and Luciano Mammino.' +export const SITE_URL = 'https://nodejsdesignpatterns.com' + +// Open Graph +export const OG_TITLE = 'Node.js Design Patterns - Fourth Edition' +export const OG_DESCRIPTION = + 'Level up your Node.js skills and design production-grade applications using proven techniques.' +export const OG_TYPE = 'book' +export const OG_IMAGE = '/images/og-image.jpg' +export const OG_IMAGE_ALT = 'Node.js Design Patterns Fourth Edition book cover' + +// Twitter Card +export const TWITTER_CARD = 'summary_large_image' +export const TWITTER_SITE = '@nodejspatterns' +export const TWITTER_CREATOR = '@loige' + +// Book Information +export const BOOK_ISBN = '9781803238944' +export const BOOK_AUTHORS = 'Luciano Mammino, Mario Casciaro' +export const BOOK_PUBLISHER = 'Packt Publishing' +export const BOOK_EDITION = 'Fourth Edition' +export const BOOK_PUBLICATION_DATE = '2025-10-09' + +// Additional +export const CANONICAL_URL = 'https://nodejsdesignpatterns.com' +export const THEME_COLOR = '#16a34a' // Green color from the design + +// Free chapter +export const FREE_CHAPTER_PAGES = 80 +export const FREE_CHAPTER_FORM_ACTION = + 'https://app.kit.com/forms/8585017/subscriptions' +export const FREE_CHAPTER_FORM_FIELD_NAME = 'email_address' diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 0000000..88907d0 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,102 @@ +export type ThemePreference = 'light' | 'dark' | 'system' +export type ActualTheme = 'light' | 'dark' +export type ThemeSelection = { + preference: ThemePreference + actual: ActualTheme +} + +const codeThemes = { + light: 'min-light', + dark: 'material-theme-ocean', +} + +function getActualThemeFromMedia(): ActualTheme { + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' +} + +export function getTheme(): ThemeSelection { + let savedTheme = (localStorage.getItem('theme') || + 'system') as ThemePreference + if ( + savedTheme !== 'light' && + savedTheme !== 'dark' && + savedTheme !== 'system' + ) { + localStorage.removeItem('theme') // Clear invalid theme + savedTheme = 'system' // Fallback to system if invalid value is found + } + + const preference = savedTheme + const actual = + savedTheme === 'system' ? getActualThemeFromMedia() : savedTheme + return { preference, actual } +} + +export function isValidThemeChangeEvent( + event: Event, +): event is CustomEvent { + return ( + event instanceof CustomEvent && + event.detail && + typeof event.detail === 'object' && + 'preference' in event.detail && + 'actual' in event.detail && + ['light', 'dark', 'system'].includes(event.detail.preference as string) && + ['light', 'dark'].includes(event.detail.actual as string) + ) +} + +export function setTheme(theme: ThemeSelection) { + const { preference, actual } = theme + + const root = window.document.documentElement + root.dataset.theme = actual + root.dataset.codeTheme = codeThemes[actual] + localStorage.setItem('theme', preference) + + root.dispatchEvent( + new CustomEvent('themechange', { + detail: { preference, actual }, + }), + ) +} + +export function toggleTheme(): ThemeSelection { + const { preference } = getTheme() + + let newActual: ThemePreference + let newPreference: ThemePreference + + if (preference === 'light') { + newPreference = 'dark' + newActual = 'dark' + } else if (preference === 'dark') { + newPreference = 'system' + newActual = getActualThemeFromMedia() + } else { + newPreference = 'light' + newActual = 'light' + } + + const newThemeSelection = { preference: newPreference, actual: newActual } + setTheme(newThemeSelection) + return newThemeSelection +} + +export function initTheme() { + const { preference, actual } = getTheme() + setTheme({ preference, actual }) + + // Listen for system theme changes + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', (e) => { + // Only update if using system theme + if (preference === 'system') { + const newActual = e.matches ? 'dark' : 'light' + setTheme({ preference, actual: newActual }) + } + }) +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..91d1de9 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,16 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export const WORDS_PER_MINUTE = 200 + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function formatDate(date: Date) { + return date.toISOString().substring(0, 10) +} + +export function calculateReadingTime(text: string) { + return Math.ceil(text.split(' ').length / WORDS_PER_MINUTE) +} diff --git a/src/node-js-design-patterns.jpg b/src/node-js-design-patterns.jpg deleted file mode 100644 index 367a3bb..0000000 Binary files a/src/node-js-design-patterns.jpg and /dev/null differ diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro new file mode 100644 index 0000000..e102b5c --- /dev/null +++ b/src/pages/blog/[slug].astro @@ -0,0 +1,46 @@ +--- +import { getCollection, getEntries, render } from 'astro:content' +import type { InferGetStaticPropsType } from 'astro' +import BlogLayout from '../../components/blog/BlogLayout.astro' + +export async function getStaticPaths() { + const posts = await getCollection('blog') + // Sort posts by date (newest first) to determine prev/next + const sortedPosts = posts.sort( + (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(), + ) + + return sortedPosts.map((post, index) => { + // Find previous (older) and next (newer) posts + const prevPost = sortedPosts[index + 1] || null + const nextPost = sortedPosts[index - 1] || null + + return { + params: { + slug: post.id, + }, + props: { + post, + prevPost, + nextPost, + }, + } + }) +} + +type Props = InferGetStaticPropsType + +const props = Astro.props +const authors = await getEntries(props.post.data.authors) + +const { Content } = await render(props.post) +--- + + + + diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro new file mode 100644 index 0000000..95c96bc --- /dev/null +++ b/src/pages/blog/index.astro @@ -0,0 +1,79 @@ +--- +import { getCollection } from 'astro:content' +import Layout from '../../Layout.astro' +import BlogCard from '../../components/blog/BlogCard.astro' +import Breadcrumb from '@components/blog/Breadcrumb.astro' +import Footer from '@components/Footer.astro' +import BookPromo from '@components/blog/BookPromo.astro' + +// Get all blog posts, sorted by date (newest first) +const posts = (await getCollection('blog')).sort( + (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(), +) + +const pageTitle = 'Blog - Node.js Design Patterns' +const pageDescription = + 'Learn Node.js patterns, best practices, and advanced techniques through in-depth articles and tutorials from the authors of Node.js Design Patterns.' +--- + + +
+
+
+ + + +

+ Node.js Design Patterns Blog +

+ +

+ Learn Node.js patterns, best practices, and advanced techniques + through in-depth articles and tutorials from the authors of Node.js + Design Patterns. +

+
+
+ +
+

+ Latest articles from the blog +

+
+ +
+ { + posts.length === 0 ? ( +
+

+ No blog posts available yet. Check back soon! +

+
+ ) : ( +
+ {posts.map((post, tabindex) => ( + + ))} +
+ ) + } +
+ +
+
+
+