diff --git a/.agents/skills/refine-write-article-skill/SKILL.md b/.agents/skills/refine-write-article-skill/SKILL.md new file mode 100644 index 0000000..a25fa00 --- /dev/null +++ b/.agents/skills/refine-write-article-skill/SKILL.md @@ -0,0 +1,165 @@ +--- +name: refine-write-article-skill +description: "Analyze manual edits made to an AI-generated blog article and update the write-article skill with the learnings. Use after manually correcting an AI-drafted article to improve future article generation. Triggers: refine skill, learn from edits, update skill from article, improve article skill, analyze my edits." +argument-hint: " - optional path to the article index.mdx (defaults to current file)" +--- + +# Refine Write Article Skill + +Compares what the AI generated against what you actually kept, analyzes the pattern of changes, and updates the `write-article` skill so future articles need fewer corrections. + +--- + +## File Locations + +``` +Original snapshots: skill-out/YYYY-MM/slug/original.mdx ← gitignored, read directly +Live articles: src/pages/YYYY-MM/slug/index.mdx + +Skill files to update: + .agents/skills/write-article/SKILL.md + .agents/skills/write-article/references/components.md + +Copies to keep in sync: + .github/skills/write-article/SKILL.md + .github/skills/write-article/references/components.md + .claude/skills/write-article/SKILL.md + .claude/skills/write-article/references/components.md +``` + +--- + +## Workflow + +### Step 1 — Identify the article + +Determine which article to analyze: + +1. If an article path was passed as an argument, use it. +2. Otherwise, use the currently open file (should be an `index.mdx`). +3. Confirm the file is inside `src/pages/` before proceeding. + +### Step 2 — Load the original and current versions + +Derive the snapshot path from the article path. Given an article at: + +``` +src/pages/YYYY-MM/article-slug/index.mdx +``` + +The original AI-generated snapshot lives at: + +``` +skill-out/YYYY-MM/article-slug/original.mdx +``` + +> **Important:** The `skill-out/` directory is listed in `.gitignore` and will not appear in any git-aware file listing. Use the `read_file` tool directly with the absolute path — do not use git commands, git diff, or any tool that filters gitignored files. The file is present on disk even though git ignores it. + +Read both files in full. If `original.mdx` does not exist, tell the user it was not saved when the article was generated and stop — there is nothing to compare against. Do not fall back to git. + +### Step 3 — Categorize every change + +Read the diff line by line and bucket each change into one or more of these categories. A single edit can belong to multiple categories. + +| Category | What to look for | +| ------------------------ | -------------------------------------------------------------------------------------------------------- | +| **Voice** | Rewrites to sentence structure, word swaps, changes to how "I/you/we" is used, removal of AI-ish phrases | +| **Tone** | Changes to enthusiasm level, hedging removed or added, more/less direct | +| **Structure** | Sections reordered, headings renamed/removed/added, intro rewritten | +| **Anti-pattern removal** | Em dashes replaced, emojis deleted, banned phrases removed ("delve", "leverage", etc.) | +| **Code examples** | Code added, removed, simplified, or corrected | +| **Component decisions** | Inline HTML replaced with a component, or vice versa; component props changed | +| **Frontmatter** | Title, description, tags, or freebie changed | +| **Factual/technical** | Corrections to accuracy, not style | + +Ignore pure factual/technical corrections (category 8) — those are article-specific and do not teach the skill anything reusable. + +### Step 4 — Extract reusable rules + +For each non-technical change, ask: **"If the AI had known this rule upfront, would it have written this correctly the first time?"** + +If yes, it is a candidate for a new or updated rule. + +Group candidates by category and write them as actionable directives, not observations. Examples: + +- Observation (bad): "Kyle removed 'It is worth noting that' three times." +- Rule (good): Add `"It is worth noting that..."` to the banned phrases list with the correction → just say the thing directly. + +- Observation (bad): "Kyle restructured the intro." +- Rule (good): "The intro should state the problem and its cost in the first sentence. Do not lead with historical context or background." + +### Step 5 — Evaluate impact + +Before writing anything, score each candidate rule: + +- **High impact**: Appeared more than once in this diff, OR is a pattern that matches other known AI tendencies +- **Low impact**: One-off phrasing preference that may not generalize + +Only add **high impact** rules to the skill. Low impact observations can be noted as a comment to the user but should not clutter the skill. + +### Step 6 — Update the skill files + +Edit the appropriate files based on what was learned: + +**For voice, tone, structure, and anti-pattern rules → `SKILL.md`** + +Add new banned phrases to the "Banned words and phrases" list. Add new structural rules to the relevant section. If a pattern contradicts an existing rule, update the existing rule rather than adding a duplicate. + +**For component rules → `references/components.md`** + +If changes reveal a preference for when to use inline HTML vs a component, or a specific structural pattern, add it to the relevant section. + +**For frontmatter rules → `SKILL.md` frontmatter section** + +If the title, description, or tags were systematically wrong, refine those guidelines. + +### Step 7 — Sync skill copies + +After updating the canonical skill files, copy them to the `.github` and `.claude` locations: + +```bash +cp .agents/skills/write-article/SKILL.md .github/skills/write-article/SKILL.md +cp .agents/skills/write-article/SKILL.md .claude/skills/write-article/SKILL.md +cp .agents/skills/write-article/references/components.md .github/skills/write-article/references/components.md +cp .agents/skills/write-article/references/components.md .claude/skills/write-article/references/components.md +``` + +### Step 8 — Report back + +Summarize what was learned in a short report using this format: + +``` +## Skill Update Report + +**Article analyzed:** src/pages/YYYY-MM/slug/index.mdx +**Original snapshot:** skill-out/YYYY-MM/slug/original.mdx +**Total changes analyzed:** N + +### Rules Added +- [Section updated]: Description of the new rule + +### Rules Updated +- [Section updated]: What changed and why + +### Patterns Noted (not added — too specific) +- Description of one-off changes that were not generalized +``` + +--- + +## Decision Guide: Add to Skill vs. Skip + +Use this when deciding whether an edit is worth encoding: + +| Edit type | Add to skill? | +| -------------------------------------------------- | --------------------------------------------------------------------- | +| Replaced "leverage" with "use" | Yes — add to banned words | +| Removed an em dash | Yes — reinforce the no-em-dash rule with the specific context | +| Rewrote the intro to lead with the problem | Yes — add as a structural rule if not already present | +| Changed a specific CSS value in a code example | No — factual fix, not reusable | +| Renamed a heading | Maybe — only if the AI's naming pattern is systematically off | +| Removed a "Conclusion" section | Yes if the current rule says to include one; update the rule | +| Simplified a React component to an Astro component | Yes — add to the Astro vs React decision table | +| Added a `` around a paragraph | Maybe — only if there is a clear pattern for what belongs in tangents | +| Changed "Let's" to "Let us" | No — too minor, does not affect reading experience | +| Deleted the closing sentence entirely | Yes if it was a platitude; add it to the banned phrases list | diff --git a/.agents/skills/write-article/SKILL.md b/.agents/skills/write-article/SKILL.md new file mode 100644 index 0000000..3326269 --- /dev/null +++ b/.agents/skills/write-article/SKILL.md @@ -0,0 +1,307 @@ +--- +name: write-article +description: "Write new Web Dev Simplified blog articles in Kyle's voice and style. Use when creating a new article, drafting blog post content, or generating an article stub for the blog. Triggers: write article, new blog post, draft post, create article, write a blog post." +argument-hint: " - the topic or title idea for the new article" +--- + +# Write Article + +Generates new Web Dev Simplified blog articles that match Kyle's established writing voice, structure, and formatting conventions. The article must read as if Kyle wrote it himself. + +--- + +## File Location + +Every article lives at: + +``` +src/pages/YYYY-MM/article-slug/index.mdx +``` + +Create the `YYYY-MM` and `article-slug` folders as needed. The slug should be all lowercase, hyphen-separated, and descriptive of the topic. + +--- + +## Frontmatter + +```yaml +--- +layout: "@layouts/BlogPost.astro" +title: "Article Title Here" +date: "YYYY-MM-DD" +description: "One to two sentence description of what the reader will learn." +tags: ["CSS"] +freebie: "css-selector-cheat-sheet" +--- +``` + +### Required Fields + +| Field | Rules | +| ------------- | --------------------------------------------------------------------------- | +| `layout` | Always `"@layouts/BlogPost.astro"`. Never change this. | +| `title` | Quoted string. Titles are specific and direct, not vague or generic. | +| `date` | ISO date string. **Must always be a Monday.** Calculate the correct Monday. | +| `description` | One or two sentences in plain language. Not marketing copy. | +| `tags` | Array with one or more valid tags (see below). | + +### Optional Fields + +| Field | Rules | +| --------- | ------------------------------------------------------------------------- | +| `freebie` | Only include if the topic has a relevant freebie. See valid values below. | + +### Valid Tags + +``` +"CSS" "TypeScript" "JavaScript" "React" "Next.js" +"Node.js" "Express" "Technical Discussion" "Non-Technical Discussion" +"HTML" "Database" +``` + +### Valid Freebie Values + +``` +"web-dev-roadmap" — Web Dev Roadmap (260+ videos, projects) +"ts-util-cheat-sheet" — TypeScript Utility Types Cheat Sheet +"accessibility-checklist" — Accessibility Checklist (80+ items) +"css-selector-cheat-sheet"— CSS Selector Cheat Sheet +``` + +Match the freebie to the article topic. CSS articles often use `css-selector-cheat-sheet`, TypeScript articles often use `ts-util-cheat-sheet`. Omit the field entirely if none fit well. + +--- + +## Article Structure + +### 1. Opening Paragraph (no heading) + +Start immediately with a paragraph. Do not open with a `## Introduction` heading. The modern style for this blog is to jump straight into content. The opening paragraph should: + +- Establish the problem, context, or why this topic exists +- Explain why it matters or why it used to be painful +- Signal what the article will cover +- Be 2-5 sentences + +### 2. Video Link (if a YouTube video exists) + +If a YouTube video accompanies this article, add this block right after the intro paragraph: + +```mdx +_If you prefer to learn visually, check out the video version of this article._ +`youtube: VIDEO_ID` +``` + +In roundup or overview articles that cover multiple sub-topics, a YouTube embed can also appear at the end of an individual section if there is a dedicated video for that specific sub-topic: + +```mdx +_If you want to go deeper on X, check out my full crash course video._ +`youtube: VIDEO_ID` +``` + +### 3. Body Sections + +Use `##` for top-level sections and `###` for subsections. + +- Explain WHY something exists or matters before explaining HOW it works +- Include code examples for nearly every concept discussed +- Use prose to connect ideas. Bullet lists are for actual enumerable items, not paragraphs in disguise. + +**Roundup / overview articles:** If the article covers multiple independent features or topics, add a short orientation sentence after the intro paragraph that tells the reader how the article is ordered. For example: "I am going to start with the features available in all browsers and end with the newest ones that are still rolling out." + +### 4. Conclusion Section + +End with a short "Conclusion" section. This should recap the main points of the article and reinforce why the topic matters. Do not introduce any new concepts here. The conclusion should be 1-4 sentences max. + +This can be skipped if the article already ends cleanly on the last concept without a need for a recap. + +--- + +## Common Imports and Components + +Import only what the article actually uses. Imports go after the frontmatter, before the article body. + +```mdx +import Tangent from "@blogComponents/lib/Tangent.astro" +import MyComponent from "@blogComponents/myTopic/MyComponent.astro + +" +``` + +The `@blogComponents` alias maps to `src/blogComponents/`. Use it for all blog component imports. + +### `` + +The most commonly used component. Wraps an aside or sidenote that is interesting but not essential to the main flow: + +```mdx + + This is extra context the reader might find interesting, but the article works + fine without it. + +``` + +**When to use ``:** + +- Cross-links to other blog articles that cover a sub-topic in more depth (e.g., "I wrote a full guide on X — that is the article to read if you want to go deeper") +- Links to supplementary resources like cheat sheets or external docs that are optional further reading +- Historical context or background that helps but is not required to understand the concept + +Do NOT leave resource cross-links as plain inline prose when they belong in a ``. If an existing article is the place to go for depth on a topic being summarized, wrap the reference in ``. + +Always include the import at the top of the file if `` is used anywhere in the article. + +### Custom Interactive Components + +Many articles include custom demo components that live in `src/blogComponents/`. These are either Astro (static/simple) or React (interactive/stateful). For full guidance on when to use each, how to structure them, and all available CSS theme variables, see [./references/components.md](./references/components.md). + +**Quick rules:** + +- Custom components go in `src/blogComponents//ComponentName.astro` (or `.jsx`/`.tsx`) +- Prefer `.astro` — only reach for React when the component needs `useState` or complex event-driven re-renders +- React components need `client:load` (or `client:visible`) in MDX to be interactive +- **Never use hardcoded colors.** Always use CSS variables like `var(--theme-purple)` so components work in both light and dark mode + +### Images + +Images reference this path pattern: + +``` +/articleAssets/YYYY-MM/article-slug/image-name.ext +``` + +Use `.webp` for photos, `.svg` for diagrams, `.png` as a fallback. Use `
` + `
` when the image needs a caption. + +--- + +## Voice and Style + +### How Kyle Writes + +Kyle writes in first person and speaks directly to the reader. The tone is knowledgeable but conversational, not formal or academic. He is genuinely enthusiastic about the topics he covers without being hyperbolic. + +- **First person "I"**: "In this article I will walk through...", "I really love this property because...", "I know this sounds too simple to be true, but..." +- **Direct "you"**: "You probably know...", "If you have ever tried to...", "I bet you didn't know..." +- **Collective "we" for shared code work**: "Let's take a look at...", "Now we can see that...", "We can now animate..." +- **Explains reasoning first**: Always explains _why_ before _how_. Never just drops code with no context. +- **Cross-links to other articles**: References other blog posts using relative links like `[semantic versioning article](/2020-01/semantic-versioning)`. + +### Sentence Patterns Kyle Uses + +These patterns appear frequently in Kyle's writing. Use them naturally, not mechanically: + +- "This is why..." / "This is where X comes in." +- "As you can see..." +- "I know this sounds [adjective], but..." +- "The only thing you need to be aware of is..." +- "By far the most [superlative]..." +- "Even in this very simple example..." +- "Before we can dive into X, we need to understand Y." +- "This may seem like [minor thing], but it actually..." +- "If you have [done X] then you have probably [encountered Y]." +- "Tired of [frustration]? [Solution statement]." +- "This is not ideal since..." +- "This can make it difficult to..." + +### Formatting + +- Inline code: backticks around every property, function, variable, or keyword +- Code blocks: always include the language (`css`, `ts`, `js`, `jsx`, `tsx`, `html`, `json`) +- Images with captions: use `
` + `
` tags +- Bold (`**text**`): only for genuinely important terms being introduced, not random emphasis +- Bullet lists: only for actual lists of items, options, or steps — not as a replacement for prose + +--- + +## What NOT to Do + +### Anti-Patterns That Sound Like AI + +These are the most important rules. Violating them is what makes AI-written content obvious. + +**Punctuation and symbols:** + +- No em dashes (`—`). Never. Use a comma, a period, or restructure the sentence instead. +- No emojis. Not in headings, not in text, not anywhere in the article. + +**Banned words and phrases:** + +- "It's worth noting that..." → just say it +- "It's important to note that..." → just say it +- "In the realm of..." → delete this +- "Unlock [potential/power/capability]" → say what it actually does +- "Game-changing" / "Revolutionary" (as casual hyperbole) → be specific about why it matters +- "Furthermore" / "Moreover" / "Additionally" as sentence starters → restructure +- "Absolutely!" / "Great!" / "Certainly!" / "Of course!" → never as affirmations +- "I hope this article was helpful!" / "Happy coding!" / "Feel free to reach out!" → never close with platitudes +- "Without further ado" / "Let's get started!" → delete +- "In this comprehensive guide..." / "In today's digital landscape..." → never open this way +- "Are you looking to..." as an opener → never + +**Structure anti-patterns:** + +- Do not write 5+ bullet points where prose would be more natural +- Do not bold random phrases mid-sentence just for visual interest +- Do not echo the article title as the first line of the body +- Do not write empty transition sentences like "Now let's move on to the next section." +- Do not add code comments that substitute for prose explanation (e.g., `// This is the important part`) +- Do not over-hedge: avoid "might potentially", "could possibly", "may or may not" +- Do not use passive voice when active voice is clearer + +--- + +## Example: Good vs. Bad Opening + +**Good:** + +> For years, `border-radius` has been the only tool we have for shaping corners in CSS. It is genuinely powerful, but it only ever gives us one type of corner: a circular or elliptical arc. That changes with the new `corner-shape` CSS property, which lets you control the actual shape of a corner, not just its size. + +**Good:** + +> If you are reading this then chances are your head is full of amazing project ideas, but when you sit down to build them you always get stuck, give up, or just never finish. This is the most common problem developers face, but this problem can actually be solved quite easily with just a little bit of planning. + +**Bad (AI-style):** + +> In today's rapidly evolving web development landscape, it's important to note that staying ahead of the curve is more important than ever. In this comprehensive guide, we will delve into the fascinating realm of CSS and unlock the full potential of modern styling techniques. Let's dive in! + +--- + +## Browser Support + +For any feature that does not yet have baseline support across all modern browsers: + +- Link the actual caniuse percentage inline rather than saying "check caniuse for the current status". Write it as: `[84% of browsers](https://caniuse.com/feature-slug) supporting this feature as of [month year]`. +- If the feature is fully supported in all major browsers (Chrome, Firefox, Safari) but the global % is dragged down by users on old versions, add: "it is fully supported in all major browsers as of [date] so once users update their browser this will quickly climb to 90+% support." +- If a specific major browser has zero support (most commonly Safari), call it out by name: "This is because Safari has no support for this feature at all yet." +- If the feature is not yet baseline but a reliable polyfill exists, mention it at the end of the section: "Luckily, there are [polyfills](npm-link) available, so you can start using it today." + +--- + +## Pre-Publish Checklist + +- [ ] `date` is a Monday +- [ ] All required frontmatter fields are present +- [ ] `freebie` is only included if a relevant one exists +- [ ] No em dashes (`—`) anywhere in the article +- [ ] No emojis anywhere in the article +- [ ] No banned AI phrases (check the anti-patterns list above) +- [ ] Every code block has a language specifier +- [ ] Cross-links to related blog articles where appropriate +- [ ] Resource cross-links and optional further reading are wrapped in ``, not left as inline prose +- [ ] `import Tangent` is present if `` is used anywhere +- [ ] Browser support for non-baseline features uses inline caniuse percentage links, not generic "check caniuse" prompts +- [ ] Images follow the `/articleAssets/YYYY-MM/slug/image.ext` path pattern + +--- + +## Save Original Copy + +After writing the article, **always** save an exact copy of the AI-generated output to: + +``` +skill-out/YYYY-MM/article-slug/original.mdx +``` + +This snapshot is used by the `refine-article-skill` to compare against the user's manual edits and extract learnings. Without it, the refinement workflow cannot function. + +The `YYYY-MM` and `article-slug` must match the article's actual location in `src/pages/`. Copy the full file contents — do not summarize or truncate. diff --git a/.agents/skills/write-article/references/components.md b/.agents/skills/write-article/references/components.md new file mode 100644 index 0000000..f90c657 --- /dev/null +++ b/.agents/skills/write-article/references/components.md @@ -0,0 +1,322 @@ +# Blog Component Development Guide + +Custom components live in `src/blogComponents//ComponentName.ext`. They are used inside article MDX files as interactive examples or visual demos of the concept being explained. + +--- + +## When to Use Components + +Not every article needs a component. Use them when: + +- A live demo would illustrate the concept better than a screenshot or code block +- The reader needs to interact with something to understand how it behaves +- A visual comparison between two states (before/after, enabled/disabled) adds real value + +Simple one-time HTML layouts can be inlined directly in the MDX with a `
`. Components are for anything reusable within the article or complex enough to warrant its own file. + +--- + +## Folder Structure + +``` +src/blogComponents/ + / + ComponentName.astro # Astro component (preferred) + ComponentName.jsx # React component (when interactivity requires state) + ComponentName.tsx # React component with TypeScript + componentName.module.css # CSS module for React components (if the styles cannot be encoded in inline styles) +``` + +Name the folder after the article's topic slug (e.g., `cssCalcSize`, `cornerShape`, `htmlDialog`). Use PascalCase for component file names. + +--- + +## Astro vs React: Which to Use + +**Default: Astro.** Use `.astro` files unless the component requires client-side state, event-driven re-renders, or complex interactivity that would be painful to do with vanilla JS. + +| Situation | Use | +| ------------------------------------------------------- | ----- | +| Static demo, visual example, CSS showcase | Astro | +| Simple toggle or expand/collapse (one ` +``` + +When there are multiple instances of the component on the same page, always use `querySelectorAll` and scope the interaction to the nearest ancestor rather than using `getElementById`. + +--- + +## React Component Anatomy + +Use `.tsx` for React components. + +### Styles: CSS Modules vs Inline + +- **CSS modules** (`.module.css`): for components with many classes or pseudo-selectors that are hard to express inline +- **Inline styles**: fine for layout/structural styles, but always use CSS variable strings for colors + +```jsx +// ✅ Correct — color variable as string in inline style +
+ +// ❌ Wrong — hardcoded color breaks dark mode +
+``` + +When using a CSS module: + +```tsx +// componentName.module.css +.btn { + background-color: var(--theme-purple); + border: none; + border-radius: 0.25em; + padding: 0.5em 0.75em; + cursor: pointer; +} + +.btn:hover { + background-color: var(--theme-purple-hover); +} +``` + +```tsx +import styles from "./componentName.module.css" + +function Component() { + return +} +``` + +--- + +## Using Components in MDX + +### Importing + +Add import statements at the top of the `.mdx` file, after the frontmatter: + +```mdx +import MyComponent from "@blogComponents/myTopic/MyComponent.astro" +import AnotherComponent from "@blogComponents/myTopic/AnotherComponent + +" +``` + +The `@blogComponents` alias maps to `src/blogComponents/`. Omit the extension for `.jsx`/`.tsx` files (TypeScript resolves them). Include `.astro` explicitly for Astro files. + +### Astro Components + +Render like normal JSX elements. No directive needed — Astro renders them at build time: + +```mdx + +``` + +### React Components + +Interactive React components need a `client:*` hydration directive. Use `client:load` for components that must be interactive immediately. Use `client:visible` for components lower on the page that can hydrate when scrolled into view: + +```mdx +{/* Hydrates as soon as the page loads */} + + + +{/* Hydrates when the component scrolls into view — better for perf */} + + +``` + +Astro components **cannot** use `client:*` directives. React components **must** use one to be interactive. + +--- + +## CSS Theme Variables + +Always use these variables instead of hardcoded colors. They automatically adapt to light and dark mode. + +### Color Variables (adapt to dark mode) + +| Variable | Light mode | Dark mode | Use for | +| ---------------------- | --------------------- | ------------------- | ----------------------------------- | +| `--theme-red` | `hsl(350, 100%, 54%)` | darker red | errors, "bad" examples, warnings | +| `--theme-blue` | `hsl(200, 100%, 50%)` | darker blue | info, primary accent, neutral demos | +| `--theme-green` | `hsl(158, 78%, 42%)` | darker green | success, "good" examples | +| `--theme-orange` | `hsl(21, 100%, 60%)` | darker orange | highlights, secondary accent | +| `--theme-purple` | `hsl(269, 79%, 74%)` | darker purple | buttons, interactive elements | +| `--theme-yellow` | `hsl(41, 100%, 58%)` | darker yellow | callouts, attention | +| `--theme-purple-hover` | lighter purple | lighter dark purple | hover state for purple buttons | + +### UI Variables + +| Variable | Use for | +| ------------------------ | ---------------------------------------------------------- | +| `--theme-text` | Body text, borders that should match text | +| `--theme-text-light` | Secondary / subdued text | +| `--theme-text-lighter` | Placeholder text, very subtle borders | +| `--theme-bg` | Page background — use when you need the "transparent" feel | +| `--theme-divider` | Subtle divider lines and borders | +| `--theme-code-inline-bg` | Inline code backgrounds (outside tangents) | + +--- + +## Common Patterns + +### Passing CSS to a component via inline CSS variables + +When a component needs to be configured visually, pass values as CSS custom properties via `style`: + +```astro +
+``` + +```css +.demo { + border-radius: var(--br); + corner-shape: var(--cs); +} +``` + +This pattern lets you drive CSS entirely from props without needing JavaScript. + +### Wrapper with a dashed border + +A common pattern for component demo areas: + +```css +.demo-wrapper { + border: 1px dashed var(--theme-text-lighter); + border-radius: 0.5rem; + padding: 1.5rem; + margin: 1.5rem 0; +} +``` + +### Buttons + +Standard button style used throughout blog components: + +```css +.btn { + border: none; + border-radius: 0.25em; + padding: 0.5em 0.75em; + font-size: inherit; + background: var(--theme-purple); + cursor: pointer; +} + +.btn:hover { + background: var(--theme-purple-hover); +} +``` + +### Feature detection / browser support warning + +When demonstrating a cutting-edge CSS/JS feature, try to fake what it should look if possible. Otherwise, show a warning message to users of unsupported browsers using `@supports not`: + +```css +.browser-warning { + display: none; +} + +@supports not (corner-shape: round) { + .browser-warning { + display: flex; + } +} +``` + +--- + +## Inline HTML in MDX (no component needed) + +For one-off layout adjustments, use inline HTML directly in the MDX. Keep inline styles to layout only — no hardcoded colors: + +```mdx +
+ ![Image description](/articleAssets/YYYY-MM/slug/image.webp) +
+ +
+ + +
+``` + +Use `
` + `
` when an image needs a caption: + +```mdx +
+ Description +
Caption text here
+
+``` diff --git a/.gitignore b/.gitignore index 49ef202..8db7eca 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ yarn-error.log* .vscode # Local Netlify folder .netlify + +/skill-out \ No newline at end of file diff --git a/src/pages/2026-06/new-javascript-features-es2026/index.mdx b/src/pages/2026-06/new-javascript-features-es2026/index.mdx new file mode 100644 index 0000000..365ddf9 --- /dev/null +++ b/src/pages/2026-06/new-javascript-features-es2026/index.mdx @@ -0,0 +1,290 @@ +--- +layout: "@layouts/BlogPost.astro" +title: "Every New JavaScript Feature Worth Knowing Right Now" +date: "2026-06-01" +description: "ES2025 has fully landed in browsers, ES2026 is wrapping up, and a few ES2027 proposals just crossed the finish line. Here is every new feature that is actually worth your time." +tags: ["JavaScript"] +--- + +import Tangent from "@blogComponents/lib/Tangent.astro" + +JavaScript has been adding features at a pace we have not seen in years. Between ES2025, ES2026, and a few ES2027 proposals that just crossed the finish line recently, there are a lot of genuinely useful things landing in browsers right now. Some of these fill gaps that have been around for years, like doing set math without converting everything to arrays first or handling binary data without reaching for a library. Others, like `using` for resource cleanup and the Temporal date API, are the kinds of features that will reshape how you think about whole categories of problems and are tools I have been waiting years to use. + +I am going to start with the features available in all browsers and end with the latest features that I am most excited about that are still in the process of rolling out. + +## New Set Methods (ES2025) + +If you have ever needed the common elements between two arrays, you have probably written something like `arr1.filter(x => arr2.includes(x))`. It works, but it is not great. It also does not generalize to any of the other set operations you might need. + +`Set` now has all seven set theory methods built in: + +```js +const a = new Set([1, 2, 3, 4]) +const b = new Set([3, 4, 5, 6]) + +a.union(b) // Set {1, 2, 3, 4, 5, 6} +a.intersection(b) // Set {3, 4} +a.difference(b) // Set {1, 2} +a.symmetricDifference(b) // Set {1, 2, 5, 6} + +a.isSubsetOf(b) // false +a.isSupersetOf(b) // false +a.isDisjointFrom(b) // false +``` + +None of these mutate the original set. `union`, `intersection`, `difference`, and `symmetricDifference` all return a new `Set`. The last three return a boolean. + +The thing I find most useful is that these methods accept any iterable as their argument, not just another `Set`. So you can pass an array directly: + +```js +const evens = new Set([2, 4, 6, 8]) +evens.intersection([3, 4, 5, 6]) // Set {4, 6} +``` + +These are now baseline and available in all modern browsers. If you want to brush up on how `Set` itself works, check out my [JavaScript Sets article](/2021-01/javascript-sets). + +## Iterator Helpers (ES2025) + +This is a feature that is underrated, but super exciting. Arrays have had helper methods like `.map()`, `.filter()`, and `.reduce()` forever, which makes chaining transformations easy. Iterators and generators have never had this, which meant working with them was far more awkward than it needed to be. + +Iterator helpers add a full suite of lazy, chainable methods directly to the iterator prototype: + +```js +function* numbers() { + let i = 0 + while (true) yield i++ +} + +const result = numbers() + .filter(n => n % 2 === 0) + .map(n => n * n) + .take(5) + .toArray() + +// [0, 4, 16, 36, 64] +``` + +The key word there is **lazy**. Unlike array methods, these do not process every element upfront. The `take(5)` call stops the entire chain after 5 values, which makes this completely safe to use with infinite generators like the one above. With array methods you could never do this since `.filter()` would try to process an infinite number of elements. + +The full set of available methods includes `.map()`, `.filter()`, `.take()`, `.drop()`, `.flatMap()`, `.reduce()`, `.forEach()`, `.some()`, `.every()`, `.find()`, and `.toArray()`. They work the same way as their array equivalents. + +You can also use these on a regular array by calling `.values()` first: + +```js +const result = [1, 2, 3, 4, 5, 6, 7, 8] + .values() + .filter(n => n % 2 === 0) + .map(n => n * n) + .toArray() + +// [4, 16, 36, 64] +``` + +These are now baseline and available in all modern browsers. For a deeper look at generators and iterators, see my [generators article](/2021-01/javascript-generators). + + + If you are unfamiliar with the base array methods in JavaScript you should + checkout my free [Array Methods Cheat + Sheet](https://webdevsimplified.com/js-array-methods-cheat-sheet.html). + + +## Promise.try (ES2025) + +This one is small but solves a real annoyance. If you have a function that might throw an error or return a rejected promise, you would normally need to wrap it in a `try/catch` block since `.catch()` only handles rejected promises, not synchronous throws: + +```js +// This does NOT catch synchronous throws +somePromise.then(() => functionThatMightThrowSync()) +``` + +`Promise.try` fixes this by converting any synchronous errors into rejections, so you can handle both cases with a single `.catch()`: + +```js +Promise.try(() => { + if (!userId) throw new Error("No user ID") // sync throw + return fetch(`/api/users/${userId}`).then(r => r.json()) // async rejection +}) + .then(user => console.log(user)) + .catch(err => console.error(err)) // handles both cases +``` + +Any function you pass to `Promise.try` will have its sync throws converted to rejections, so your `.catch()` always fires regardless of where the error came from. This is now baseline in all modern browsers. + +## Import Attributes (ES2025) + +You may not know this, but importing JSON files in JavaScript was not supported by JavaScript and required a bundler like Vite to work. This changes with Import Attributes, which let you import non-JS resources directly: + +```js +import config from "./config.json" with { type: "json" } + +console.log(config.version) +``` + +The `with { type: "json" }` part tells the browser what kind of module this is. This is not just syntax sugar. It is a security measure: without it, a server could theoretically serve a JavaScript file at a JSON URL, which would get executed. The explicit `type` prevents that by failing if the MIME type does not match. + +Dynamic imports support it too: + +```js +const data = await import("./data.json", { with: { type: "json" } }) +``` + +This is now baseline and available in all modern browsers. Unfortunately, there is no way to import files other than JSON currently, but that is on the roadmap for a future version. + +## RegExp.escape (ES2025) + +If you have ever built a regular expression dynamically from user input, you know the problem. Characters like `.`, `*`, `+`, `?`, `(`, `[`, and several others have special meaning in regex patterns, so passing raw user input into `new RegExp(userInput)` will not work like the user expects. + +`RegExp.escape` handles this properly: + +```js +const query = "gmail.com" +const pattern = new RegExp(RegExp.escape(query), "i") +``` + +Without escaping, a user searching for "gmail.com" would accidentally match "gmailXcom" or any string where any character appears in the position of the `.`. It is one of those things you almost never think about until it causes a real bug. This is now available in all modern browsers. + +_If you want to master Regular Expressions checkout my full crash course video._ +`youtube: rhzKDrUiJVk` + +## Array.fromAsync (ES2026) + +`Array.from` is a great way to convert any iterable into an array. `Array.fromAsync` does the same thing for async iterables, which come up a lot when dealing with streams, paginated APIs, and async generators: + +```js +async function* fetchPages(url) { + let page = 1 + while (true) { + const data = await fetch(`${url}?page=${page}`).then(r => r.json()) + if (data.items.length === 0) break + yield data.items + page++ + } +} + +const allItems = await Array.fromAsync(fetchPages("/api/items")) +``` + +This has been in all major browsers for a while now and is baseline. + +## Error.isError (ES2026) + +Checking whether something is an `Error` sounds trivial until you run into the case where `instanceof Error` returns `false`. This happens when code crosses iframe or realm boundaries, because the `Error` constructor in one realm is a different object from the `Error` in another. The `instanceof` check relies on object identity, so it fails. + +`Error.isError` is a proper realm-agnostic check: + +```js +Error.isError(new Error("oops")) // true +Error.isError(new TypeError("bad")) // true +Error.isError({ message: "fake" }) // false +Error.isError("just a string") // false +``` + +This is a nice quality of life feature for easily checking if any value is an error without worrying about edge cases. Browser support is nearly complete with [84% of browsers](https://caniuse.com/mdn-javascript_builtins_error_iserror) supporting this feature as of May 2026. + +## Map.getOrInsert (ES2026) + +There is a pattern that comes up constantly when building up a `Map` incrementally: check if a key exists, and if it does not, insert a default value before continuing. Before this feature, you either wrote the check yourself or used a helper: + +```js +// The old pattern +if (!map.has(key)) { + map.set(key, []) +} +const arr = map.get(key) +``` + +`Map.prototype.getOrInsert` collapses this into one call: + +```js +const arr = map.getOrInsert(key, []) +``` + +It returns the existing value if the key is present, or inserts and returns the default value if it is not. Note that the default value is always evaluated, so if it is expensive to compute, use `getOrInsertComputed` instead, which takes a function: + +```js +const set = map.getOrInsertComputed(key, () => new Set()) +``` + +This function has only [77% support](https://caniuse.com/mdn-javascript_builtins_map_getorinsert), but it is fully supported in all major browsers as of May 2026 so once users update their browser this will quickly climb to 90+% support. + +## Math.sumPrecise (ES2026) + +Floating point arithmetic in JavaScript is a bit of a meme at this point, but it is a real problem that all programming languages have to deal with. + +```js +const arr = [0.1, 0.2, 0.3] +arr.reduce((a, b) => a + b) // 0.6000000000000001 +``` + +Computers have a hard time representing decimal number accurately (especially when doing arithmetic), which is why we get weird rounding errors like this. `Math.sumPrecise` was created to solve this issue by using a compensated summation algorithm that avoids this: + +```js +Math.sumPrecise([0.1, 0.2, 0.3]) // 0.6 +``` + +This function has only [67% support](https://caniuse.com/mdn-javascript_builtins_math_sumprecise), but just like `Map.getOrInsert`, it is fully supported in all major browsers as of May 2026 so once users update their browser this will quickly climb to 90+% support. + +## Explicit Resource Management (ES2027) + +This feature is one I have been waiting years for. + +The core idea is that many objects require cleanup when you are done with them: database connections, file handles, event listeners, timers. The typical way to guarantee cleanup is a `try/finally` block, which gets verbose fast, especially when you have multiple resources to manage. + +The `using` keyword handles this automatically for you. When you declare a variable with the `using` keyword, JavaScript will automatically clean up the connections for that variable when it goes out of scope (through garbage collection). This is done by calling a special `[Symbol.dispose]()` function on the variable automatically: + +```js +class DatabaseConnection { + connect() { /* ... */ } + query(sql) { /* ... */ } + + // This is called automatically when the variable goes out of scope + [Symbol.dispose]() { + this.close() + console.log("Connection closed") + } +} + +function runQuery() { + // The `using` keyword is what makes all this happen automatically + using conn = new DatabaseConnection() + conn.connect() + return conn.query("SELECT * FROM users") + // conn[Symbol.dispose]() is called here automatically +} +``` + +The async equivalent uses `await using` and calls `[Symbol.asyncDispose]()`: + +```js +async function processFile() { + await using file = await openFile("data.txt") + const contents = await file.read() + return contents + // file[Symbol.asyncDispose]() is awaited here automatically +} +``` + +This is a genuinely big deal for any code that deals with resources. It brings to JavaScript what `using` does in C# and `with` does in Python, and it composes much more cleanly than nested `try/finally` blocks. TypeScript users can start using it today. + +As of May 2026, this feature only has [70% browser support](https://caniuse.com/mdn-javascript_statements_using). This is because Safari has no support for this feature at all yet. + +## Temporal (ES2027) + +I have been wanting to use the Temporal API for so long and it is finally within grasp. Temporal is essentially a full replacement of the existing `Date` API that is long overdue. It has a much more intuitive and powerful API for working with dates, times, time zones, durations, and more. With Temporal you never need to download a library like `date-fns` or `moment` ever again. + + + Over 4 years ago, I wrote a [full Temporal API + guide](/2022-02/temporal-date-api) back when it was still a proposal, but + everything in that article is still 100% accurate to how Temporal works. If + you want to learn how to use Temporal in depth, that is the article to read. + + +```js +const birthday = Temporal.PlainDate.from("1990-05-15") +const today = Temporal.Now.plainDateISO() +const age = birthday.until(today, { largestUnit: "years" }) + +console.log(`${age.years} years old`) +``` + +As of May 2026, this feature only has [67% browser support](https://caniuse.com/temporal). This is because Safari has no support for this feature at all yet. Luckily, there are [polyfills](https://www.npmjs.com/package/@js-temporal/polyfill) of this API available, so you can start using it today.