diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml
new file mode 100644
index 0000000..4cad826
--- /dev/null
+++ b/.github/workflows/deploy-site.yml
@@ -0,0 +1,52 @@
+name: Deploy Microsite
+
+on:
+ push:
+ branches: ["main"]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+ env:
+ BASE_HREF: /${{ github.event.repository.name }}/
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./_site
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d0e4835
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+_site/
\ No newline at end of file
diff --git a/README.md b/README.md
index 56fab93..14f8c0f 100644
--- a/README.md
+++ b/README.md
@@ -8,5 +8,63 @@ npx skills add rodydavis/skills
| Skill | Description |
|---|---|
+| [how-to-run-astro-ssr-and-pocketbase-on-the-same-server](./skills/astro-ssr-pocketbase-single-server/SKILL.md) | Learn how to host PocketBase and an Astro SSR application on the same server, using PocketBase's Go integration and a reverse proxy to delegate requests to Astro for dynamic web content. |
+| [async-preact-signals](./skills/async-preact-signal/SKILL.md) | Explore how to effectively manage asynchronous data with Preact Signals by creating a custom `asyncSignal` that handles loading, error, and data states without breaking the synchronous nature of signals. |
+| [automate-flutter-app-releases](./skills/automate-flutter-apps/SKILL.md) | Automate your Flutter app releases to beta or production with this handy shell script that handles version bumping, formatting, cleaning, rebuilding, and deployment via Fastlane. |
+| [dynamic-themes-with-codemirror](./skills/codemirror-dynamic-theme/SKILL.md) | Learn how to create a Lit web component with CodeMirror, dynamically themed using Material Design's color utilities, for a customizable code editing experience. |
+| [how-to-create-html-web-components-with-dart](./skills/dart-html-web-components/SKILL.md) | Discover the power of Web Components and how to build them with both JavaScript and Dart for reusable, framework-agnostic UI elements. |
+| [how-to-do-bitwise-operations-in-dart](./skills/dart_bitwise/SKILL.md) | Explore Dart's bitwise operations for both integers and booleans, including AND, OR (inclusive & exclusive), NAND, NOR, and XNOR, with practical code examples. |
+| [various-ways-to-invoke-functions-in-dart](./skills/dart_function-invoking/SKILL.md) | Discover the surprising flexibility of calling Dart functions, including mixed positional and named arguments, the `.call` operator, and dynamic invocation with `Function.apply`. |
+| [how-to-print-multiple-objects-to-the-console-with-print-in-dart](./skills/dart_print-multiple-objects/SKILL.md) | Learn how to print multiple objects to the console in Dart using Records, offering a similar experience to JavaScript's `console.log()` functionality. |
+| [check-if-an-object-is-truthy-in-dart](./skills/dart_truthy/SKILL.md) | Learn how to extend Dart's functionality to implement JavaScript-style "truthy" checks for easier conditional logic and value evaluations. |
+| [deep-linking-for-flutter-web](./skills/deep-linking-flutter-web/SKILL.md) | Learn how to implement proper URL navigation in your Flutter application, including deep linking to specific pages, handling protected routes, and creating custom transitions for a seamless user experience. |
+| [displaying-html-in-flutter](./skills/display-html-in-flutter/SKILL.md) | Easily display and interact with HTML content in your Flutter app using the `easy_web_view` package, which supports both web and mobile platforms. |
+| [how-to-export-sqlite-tables-to-create-statements](./skills/export-sqlite-dart/SKILL.md) | Learn how to export your entire SQLite database schema, including tables and indexes, into runnable CREATE statements at runtime using Flutter and the `sqlite3` package. |
+| [using-fastlane-in-flutter-and-ci](./skills/fastlane-and-flutter/SKILL.md) | Automate Flutter app builds and deployments to both the App Store and Google Play using Fastlane with this step-by-step guide. |
+| [lit-and-figma](./skills/figma-and-lit/SKILL.md) | Learn how to create a Figma plugin using Lit web components, including project setup, component creation, WASM integration, and building the final plugin for use in Figma. |
+| [creating-your-first-flutter-project](./skills/first-flutter-project/SKILL.md) | Dive into the world of Flutter, Google's UI toolkit, and learn how to build cross-platform apps with ease using this introductory guide and accompanying "Flutter Take 5" video series. |
+| [lit-and-flutter](./skills/flutter-and-lit/SKILL.md) | Learn how to embed a Lit web component directly within your Flutter app to leverage web-based UIs and features while accessing native device APIs for a powerful hybrid development approach. |
+| [how-to-build-a-flutter-app-on-xcode-cloud](./skills/flutter-and-xcode-cloud/SKILL.md) | Learn how to set up Xcode Cloud to build and deploy your Flutter application to TestFlight and the App Store with this step-by-step guide. |
+| [flutter-terminal-cheat-sheet](./skills/flutter-cheat-sheet/SKILL.md) | This post provides a handy collection of Flutter commands and scripts for web development, package creation, troubleshooting, testing, and more, streamlining your Flutter workflow. |
+| [how-to-build-a-graph-database-with-flutter](./skills/flutter-graph-database/SKILL.md) | Learn how to build and utilize a graph database within your Flutter applications using SQLite and the Drift package to model relationships between data. |
+| [multi-touch-canvas-with-flutter](./skills/flutter-multi-touch-canvas/SKILL.md) | Learn how to create a Flutter canvas with multi-touch support for panning, zooming, and object interaction, overcoming common gesture recognition conflicts. |
+| [flutter-fastlane-one-click-beta](./skills/flutter-one-click-release/SKILL.md) | Deploy your Flutter app to the App Store and Google Play with ease using this step-by-step guide covering installation, project setup, Fastlane integration, and automated deployments with Automator. |
+| [server-side-rendering-flutter-apps-with-rfw](./skills/flutter-ssr-rfw/SKILL.md) | Learn how to build a dynamic Flutter app using Server Side Rendering (SSR) with the rfw package, enabling UI updates driven by server logic and binary data exchange via HTTP. |
| [flutter-control-and-screenshot](./skills/flutter_driver_control/SKILL.md) | Guide on how to control a Flutter app using flutter_driver via MCP and capture screenshots. |
+| [how-to-manage-multiple-flutter-versions-with-git-worktrees-and-z](./skills/flutter_git-worktree-channels/SKILL.md) | Manage multiple Flutter versions efficiently using Git worktrees, eliminating the need for external version managers like FVM. |
+| [host-your-flutter-project-as-a-rest-api](./skills/host-flutter-rest-api/SKILL.md) | Learn how to structure a Flutter project to reuse models and business logic across iOS, Android, Web, desktop platforms, and a REST API deployable to Google Cloud Run, enabling a single codebase for both client and server. |
+| [building-a-html-element-sandbox-with-lit](./skills/html-code-sandbox/SKILL.md) | Learn how to build a Lit web component to create a dynamic HTML element sandbox with live updates, perfect for experimenting with and showcasing web components. |
| [install-flutter-from-git](./skills/install-flutter-from-git/SKILL.md) | Install Flutter SDK via git clone and configure for all platforms |
+| [draggable-dom-with-lit](./skills/lit-draggable-dom/SKILL.md) | Learn how to create an interactive, draggable DOM using a Lit web component with CSS transforms and slots, enabling you to manipulate HTML and SVG elements within a canvas-like environment. |
+| [2d-or-3d-force-graph-with-lit](./skills/lit-force-graph/SKILL.md) | Learn how to create interactive 2D and 3D force graphs using Lit, a lightweight web component library, with this step-by-step tutorial. |
+| [json-to-html-table-with-lit](./skills/lit-html-table/SKILL.md) | Learn how to create a dynamic HTML table from JSON data using a Lit web component, with examples for fetching data from a URL or using inline JSON, and the ability to make the table editable. |
+| [lit-and-monaco-editor](./skills/lit-monaco-editor/SKILL.md) | Learn how to create a Lit web component that wraps the Monaco Editor (powering VSCode) to add a fully functional code editor to your web applications. |
+| [building-a-rich-text-editor-with-lit](./skills/lit-rich-text-editor/SKILL.md) | Learn how to build a rich text editor using a Lit web component, complete with a toolbar for formatting text, links, and styles. |
+| [lit-sheet-music](./skills/lit-sheet-music/SKILL.md) | Learn how to create a Lit web component that renders MusicXML using OpenSheetMusicDisplay, allowing you to display sheet music dynamically from a source attribute or inline XML. |
+| [lit-and-vscode-extensions](./skills/lit-vscode-extension/SKILL.md) | Learn how to build a VSCode extension using a Lit web component, covering setup, template creation, component implementation, and extension activation. |
+| [building-a-piano-with-flutter](./skills/making-a-piano/SKILL.md) | Build a Tiny Piano in Flutter: Learn how to create a fully functional, Flutter Create contest-winning piano app using just 5032 bytes of Dart code, complete with MIDI support and customizable features. |
+| [migrating-my-blog-to-pocketbase](./skills/migrating-my-blog-to-pocketbase/SKILL.md) | This blog post chronicles a developer's fun and ongoing journey of migrating their personal website through various tech stacks, ultimately landing on a PocketBase and Coolify setup for simplified deployment and dynamic features. |
+| [how-to-build-a-native-cross-platform-project-with-flutter](./skills/native-cross-platform-flutter/SKILL.md) | Learn how to import `dart:html` and `dart:io` in the same Flutter project to create cross-platform plugins that work seamlessly on mobile and web. |
+| [how-to-do-offline-recommendations-with-sqlite-and-gemini](./skills/offline-vector-recommendations/SKILL.md) | Learn how to enhance your CMS like PocketBase with AI-powered content recommendations using text embeddings, SQLite, and k-nearest neighbor search for efficient and scalable related content suggestions. |
+| [calling-the-palm-2-api-with-dart-and-flutter](./skills/palm-2-api-dart/SKILL.md) | Learn how to integrate the PaLM 2 API into your Flutter apps using Dart, including setting up API keys, creating prompt templates, and securely making API calls. |
+| [how-to-deploy-pocketbase-to-cloud-run](./skills/pocketbase-cloudrun/SKILL.md) | Learn how to deploy PocketBase on Google Cloud Run using the new volume mounting feature, enabling scale-to-zero, infinite storage, and easy backups. |
+| [how-to-build-a-webrtc-signal-server-with-pocketbase](./skills/pocketbase-webrtc-signal-server-js/SKILL.md) | Learn how to build a simple WebRTC video call application using PocketBase as a signaling server, enabling peer-to-peer communication with SQLite on the server and realtime updates via Server Sent Events. |
+| [how-to-host-your-podcast-for-free-on-github-pages](./skills/podcast-github-pages/SKILL.md) | Launch your podcast for free by leveraging GitHub Pages, GitHub Actions, and a customizable podcast player template (fork this repo!) to share your stories and save on hosting costs. |
+| [how-to-send-push-notifications-on-flutter-web-fcm](./skills/push-notifications-flutter-web/SKILL.md) | Learn how to implement Firebase Cloud Messaging (FCM) in your Flutter web app with this guide, covering service worker setup, helper methods, and testing to enable push notifications. |
+| [the-perfect-brisket](./skills/recipes_the-perfect-brisket/SKILL.md) | Follow my evolving journey to the perfect smoked brisket with this recipe, meticulously updated after each low-and-slow, all-out cook. |
+| [signals-and-flutter-hooks](./skills/signals-and-flutter-hooks/SKILL.md) | Explore state management in Flutter, from the basics of `setState` to advanced techniques using ValueNotifier, Signals, Flutter Hooks, and the new signals_hooks package for a reactive and efficient approach. |
+| [flutter-infinite-canvas](./skills/snippets_flutter-infinite-canvas/SKILL.md) | Learn how to build an infinite, multi-touch canvas in Flutter using InteractiveViewer and CustomMultiChildLayout for a flexible and interactive user experience. |
+| [flutter-input-output-preview](./skills/snippets_flutter-input-output-preview/SKILL.md) | Build responsive Flutter apps with a reusable `TwoPane` widget and an `InputOutputPreview` component for side-by-side code and preview display on both mobile and desktop. |
+| [flutter-markdown-view-with-material-3](./skills/snippets_flutter-markdown-view-material-3/SKILL.md) | Learn how to customize the Flutter Markdown widget using Material 3 text and color styles for a visually appealing and consistent design. |
+| [flutter-master-detail-view](./skills/snippets_flutter-master-detail-view/SKILL.md) | Learn how to implement a responsive Master-Detail interface in Flutter that adapts to different screen sizes, leveraging multi-column layouts on larger screens and pushing to detail screens on mobile. |
+| [flutter-native-http-client](./skills/snippets_flutter-native-http-client/SKILL.md) | This blog post explores how to optimize HTTP client selection in Flutter applications based on the platform, using Cronet on Android and Cupertino's native client on iOS for improved performance and caching. |
+| [flutter-stream-widget](./skills/snippets_flutter-stream-widget/SKILL.md) | Learn how to build dynamic Flutter UIs by directly using streams within your widget's build method, enabling reactive screen updates and more efficient data handling. |
+| [lightweight-flutter-animations](./skills/snippets_lightweight-flutter-animations/SKILL.md) | Learn how to create a streamlined animation widget in Flutter that eliminates the need for `setState` by leveraging an abstract class and `SingleTickerProviderStateMixin` for efficient UI updates. |
+| [material-3-to-material-2-theme-adapter](./skills/snippets_m3-to-m2-css-adapter/SKILL.md) | Learn how to seamlessly integrate Material Design 3's styling into your Material Design 2 components using CSS variable overrides. |
+| [color-utilities-in-javascript](./skills/snippets_typescript-color-utilities/SKILL.md) | Explore helpful color utility functions, like RGB to HSL, HEX to RGB, and HSL to HEX, generated with the assistance of GitHub Copilot. |
+| [ios-or-macos-lock-screen-nasa-image-of-the-day](./skills/snippets_workflow-nasa-image-of-day/SKILL.md) | Automate your daily dose of cosmic beauty by setting your lock or home screen to NASA's image of the day using Shortcuts and their public API. |
+| [how-to-do-full-text-search-with-sqlite](./skills/sqlite_fts5/SKILL.md) | Learn how to supercharge your SQLite databases with full-text search capabilities using the built-in fts5 extension, enabling efficient and powerful querying with the `MATCH` keyword. |
+| [using-sqlite-as-a-key-value-store](./skills/sqlite_key-value/SKILL.md) | Learn how to use SQLite as a simple and efficient key/value store for your applications, offering benefits like single-file data containment, attachment capabilities, and easy integration with tools like Drift. |
+| [how-to-store-sqlite-as-nosql-store](./skills/sqlite_no-sql/SKILL.md) | Discover how to leverage SQLite's JSON support to build a NoSQL-like document store, complete with TTL-based expiration, within this powerful embedded database. |
+| [sqlite-on-the-ui-thread](./skills/sqlite_ui-thread/SKILL.md) | Unlock the surprising speed of SQLite in Flutter for building responsive UIs, showcasing its ability to handle large datasets with synchronous queries and optimized configurations. |
+| [file-based-routing-for-static-sites](./skills/static-site-file-based-routing/SKILL.md) | Learn how to create a multi-page static site with file-based routing using TypeScript, allowing for quick updates and easy content management. |
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..f929cb7
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,80 @@
+{
+ "name": "skills-microsite",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "skills-microsite",
+ "version": "1.0.0",
+ "dependencies": {
+ "markdown-it": "^14.0.0"
+ }
+ },
+ "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/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/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/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/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..a61c093
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "skills-microsite",
+ "version": "1.0.0",
+ "description": "Static site generator for skills microsite",
+ "main": "scripts/build_site.js",
+ "scripts": {
+ "build": "node scripts/build_site.js"
+ },
+ "dependencies": {
+ "markdown-it": "^14.0.0"
+ }
+}
diff --git a/scripts/build_site.js b/scripts/build_site.js
new file mode 100644
index 0000000..8aabbd2
--- /dev/null
+++ b/scripts/build_site.js
@@ -0,0 +1,322 @@
+const fs = require('fs');
+const path = require('path');
+const MarkdownIt = require('markdown-it');
+
+const md = new MarkdownIt({
+ html: true,
+ linkify: true,
+ typographer: true
+});
+
+const skillsDir = path.join(__dirname, '../skills');
+const outputDir = path.join(__dirname, '../_site');
+const baseHref = process.env.BASE_HREF || '/';
+
+// Ensure output directory exists
+if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+}
+
+// Copy assets if any (none for now, but good practice)
+
+// Theme Colors & Styles
+const css = `
+:root {
+ --bg-color: oklch(0.15 0.02 260);
+ --card-bg: oklch(0.2 0.03 260);
+ --text-primary: oklch(0.95 0.01 260); /* Pearl White */
+ --text-secondary: oklch(0.7 0.02 260); /* Silver-ish */
+ --accent-blue: oklch(0.6 0.2 250); /* Electric Blue */
+ --accent-glow: oklch(0.6 0.2 250 / 0.5);
+ --border-color: oklch(0.3 0.05 260);
+ --shining-silver: oklch(0.85 0.01 260);
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'Outfit', sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-primary);
+ line-height: 1.6;
+ overflow-x: hidden;
+}
+
+a {
+ color: var(--accent-blue);
+ text-decoration: none;
+ transition: all 0.3s ease;
+}
+
+a:hover {
+ text-shadow: 0 0 10px var(--accent-glow);
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+header {
+ text-align: center;
+ margin-bottom: 3rem;
+ padding: 2rem 0;
+ border-bottom: 1px solid var(--border-color);
+ background: linear-gradient(180deg, rgba(0,0,0,0) 0%, var(--bg-color) 100%);
+}
+
+h1 {
+ font-size: 3rem;
+ font-weight: 800;
+ background: linear-gradient(to right, var(--text-primary), var(--accent-blue));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ margin-bottom: 0.5rem;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+}
+
+h2, h3, h4, h5, h6 {
+ color: var(--shining-silver);
+ margin-top: 2rem;
+ margin-bottom: 1rem;
+}
+
+/* Grid Layout */
+.skills-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 2rem;
+}
+
+.skill-card {
+ background-color: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ padding: 1.5rem;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ position: relative;
+ overflow: hidden;
+}
+
+.skill-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 4px;
+ background: linear-gradient(90deg, var(--accent-blue), var(--shining-silver));
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.skill-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 10px 30px -10px var(--accent-glow);
+ border-color: var(--accent-blue);
+}
+
+.skill-card:hover::before {
+ opacity: 1;
+}
+
+.skill-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ margin-bottom: 0.5rem;
+ color: var(--text-primary);
+}
+
+.skill-desc {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ margin-bottom: 1.5rem;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.btn {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ background: transparent;
+ border: 1px solid var(--accent-blue);
+ color: var(--accent-blue);
+ border-radius: 6px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-size: 0.8rem;
+ transition: all 0.3s ease;
+}
+
+.btn:hover {
+ background: var(--accent-blue);
+ color: #fff;
+ box-shadow: 0 0 15px var(--accent-glow);
+}
+
+/* Detail Page */
+.detail-content {
+ background-color: var(--card-bg);
+ padding: 3rem;
+ border-radius: 16px;
+ border: 1px solid var(--border-color);
+ box-shadow: 0 0 50px -20px #000;
+}
+
+.detail-content img {
+ max-width: 100%;
+ border-radius: 8px;
+ margin: 1rem 0;
+ border: 1px solid var(--border-color);
+}
+
+.detail-content pre {
+ background-color: #0d0d0d;
+ padding: 1rem;
+ border-radius: 8px;
+ overflow-x: auto;
+ border: 1px solid var(--border-color);
+ margin: 1.5rem 0;
+}
+
+.back-link {
+ display: inline-block;
+ margin-bottom: 2rem;
+ color: var(--text-secondary);
+ font-weight: 600;
+}
+
+.back-link:hover {
+ color: var(--accent-blue);
+ transform: translateX(-5px);
+}
+`;
+
+function parseFrontmatter(content) {
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
+ if (!match) return { frontmatter: {}, body: content };
+
+ const frontmatter = {};
+ const lines = match[1].split('\n');
+ lines.forEach(line => {
+ const [key, ...valueParts] = line.split(':');
+ if (key && valueParts.length > 0) {
+ frontmatter[key.trim()] = valueParts.join(':').trim();
+ }
+ });
+
+ const body = content.replace(match[0], '').trim();
+ return { frontmatter, body };
+}
+
+function renderPage(title, content, isIndex = false) {
+ return `
+
+
+ View Skill
+
+ `).join('');
+
+ const indexHtml = renderPage('Agent Skills', cardsHtml, true);
+ fs.writeFileSync(path.join(outputDir, 'index.html'), indexHtml);
+
+ console.log(`Build complete! Generated ${skills.length} skill pages.`);
+}
+
+build();
diff --git a/scripts/prefix_image_paths.js b/scripts/prefix_image_paths.js
new file mode 100644
index 0000000..7a93d52
--- /dev/null
+++ b/scripts/prefix_image_paths.js
@@ -0,0 +1,63 @@
+const fs = require('fs');
+const path = require('path');
+
+const skillsDir = path.join(__dirname, '../skills');
+const dryRun = false; // Set to false to actually modify files
+const prefix = 'https://rodydavis.com';
+
+function walk(dir, fileList = []) {
+ if (!fs.existsSync(dir)) return fileList;
+ const files = fs.readdirSync(dir);
+ files.forEach(file => {
+ const filePath = path.join(dir, file);
+ const stat = fs.statSync(filePath);
+ if (stat.isDirectory()) {
+ walk(filePath, fileList);
+ } else {
+ if (file === 'SKILL.md') {
+ fileList.push(filePath);
+ }
+ }
+ });
+ return fileList;
+}
+
+console.log(`Scanning ${skillsDir} for relative image paths...`);
+if (dryRun) console.log('--- DRY RUN MODE: No changes will be made ---');
+
+const skillFiles = walk(skillsDir);
+let changesCount = 0;
+
+skillFiles.forEach(file => {
+ const content = fs.readFileSync(file, 'utf8');
+
+ // Regex to find markdown images with relative paths starting with /
+ // Pattern: 
+ // We want to capture the whole link to be safe, or just the URL part.
+ // Let's replace `](/` with `](${prefix}/`
+
+ // This regex looks for:
+ // 1. `
+ // 4. `/` (path starting with slash)
+ // 5. `...` (rest of url)
+ // 6. `)` (end of url)
+
+ // To safely replace, we can target the `](/` sequence that follows `![...]`
+ const regex = /(!\[.*?\])\((\/[^)]+)\)/g;
+
+ if (regex.test(content)) {
+ console.log(`[${dryRun ? 'DRY RUN' : 'FIX'}] Found relative image path in ${path.relative(skillsDir, file)}`);
+
+ if (!dryRun) {
+ const newContent = content.replace(regex, (match, alt, url) => {
+ return `${alt}(${prefix}${url})`;
+ });
+ fs.writeFileSync(file, newContent);
+ }
+ changesCount++;
+ }
+});
+
+console.log(`\n${dryRun ? 'Found' : 'Fixed'} relative image paths in ${changesCount} files.`);
diff --git a/scripts/remove_double_headings.js b/scripts/remove_double_headings.js
new file mode 100644
index 0000000..622b46b
--- /dev/null
+++ b/scripts/remove_double_headings.js
@@ -0,0 +1,54 @@
+const fs = require('fs');
+const path = require('path');
+
+const skillsDir = path.join(__dirname, '../skills');
+const dryRun = false; // Set to false to actually modify files
+
+function walk(dir, fileList = []) {
+ if (!fs.existsSync(dir)) return fileList;
+ const files = fs.readdirSync(dir);
+ files.forEach(file => {
+ const filePath = path.join(dir, file);
+ const stat = fs.statSync(filePath);
+ if (stat.isDirectory()) {
+ walk(filePath, fileList);
+ } else {
+ if (file === 'SKILL.md') {
+ fileList.push(filePath);
+ }
+ }
+ });
+ return fileList;
+}
+
+console.log(`Scanning ${skillsDir} for double headings...`);
+if (dryRun) console.log('--- DRY RUN MODE: No changes will be made ---');
+
+const skillFiles = walk(skillsDir);
+let changesCount = 0;
+
+skillFiles.forEach(file => {
+ const content = fs.readFileSync(file, 'utf8');
+
+ // Regex to find two identical H1 headers separated by newlines/whitespace
+ // Captures:
+ // Group 1: Start of string or previous newline
+ // Group 2: The Heading line (e.g. "# Title")
+ // Group 3: The separation (newlines)
+ // Group 4: The repeated Heading line (backreference to \2)
+ const regex = /(^|\n)(# [^\r\n]+)(\r?\n\s*)+(\2)/g;
+
+ if (regex.test(content)) {
+ console.log(`[${dryRun ? 'DRY RUN' : 'FIX'}] Found double heading in ${path.relative(skillsDir, file)}`);
+
+ if (!dryRun) {
+ // Replace with just the first heading (plus preceding newline)
+ // We drop the intermediate newlines and the second heading.
+ const newContent = content.replace(regex, '$1$2');
+ fs.writeFileSync(file, newContent);
+ }
+ changesCount++;
+ }
+});
+
+console.log(`\n${dryRun ? 'Found' : 'Fixed'} double headings in ${changesCount} files.`);
diff --git a/scripts/remove_markdown_links.js b/scripts/remove_markdown_links.js
new file mode 100644
index 0000000..b431c12
--- /dev/null
+++ b/scripts/remove_markdown_links.js
@@ -0,0 +1,51 @@
+const fs = require('fs');
+const path = require('path');
+
+const skillsDir = path.join(__dirname, '../skills');
+const dryRun = false; // Set to false to actually modify files
+
+function walk(dir, fileList = []) {
+ if (!fs.existsSync(dir)) return fileList;
+ const files = fs.readdirSync(dir);
+ files.forEach(file => {
+ const filePath = path.join(dir, file);
+ const stat = fs.statSync(filePath);
+ if (stat.isDirectory()) {
+ walk(filePath, fileList);
+ } else {
+ if (file === 'SKILL.md') {
+ fileList.push(filePath);
+ }
+ }
+ });
+ return fileList;
+}
+
+console.log(`Scanning ${skillsDir} for markdown links...`);
+if (dryRun) console.log('--- DRY RUN MODE: No changes will be made ---');
+
+const skillFiles = walk(skillsDir);
+let changesCount = 0;
+
+skillFiles.forEach(file => {
+ const content = fs.readFileSync(file, 'utf8');
+
+ // Regex to find the specific link format: [](/api/posts/... "View as markdown")
+ // It seems to be on its own line usually, but we should target the exact string pattern.
+ // The pattern is roughly: \[\]\(/api/posts/[a-zA-Z0-9]+/markdown "View as markdown"\)
+ // We also want to handle potential surrounding whitespace/newlines if it's on a standalone line to leave it clean.
+
+ const regex = /(\r?\n|^)\[\]\(\/api\/posts\/[a-zA-Z0-9]+\/markdown "View as markdown"\)(\r?\n|$)/g;
+
+ if (regex.test(content)) {
+ console.log(`[${dryRun ? 'DRY RUN' : 'FIX'}] Found markdown link in ${path.relative(skillsDir, file)}`);
+
+ if (!dryRun) {
+ const newContent = content.replace(regex, '$2'); // Keep one newline if it was surrounded by them, or just empty if at start/end
+ fs.writeFileSync(file, newContent);
+ }
+ changesCount++;
+ }
+});
+
+console.log(`\n${dryRun ? 'Found' : 'Fixed'} markdown links in ${changesCount} files.`);
diff --git a/scripts/rename_skills.js b/scripts/rename_skills.js
new file mode 100644
index 0000000..1e469c7
--- /dev/null
+++ b/scripts/rename_skills.js
@@ -0,0 +1,40 @@
+const fs = require('fs');
+const path = require('path');
+
+const skillsDir = path.join(__dirname, '../skills');
+const prefixToRemove = 'rodydavis_com_posts_';
+
+console.log(`Scanning ${skillsDir} for folders starting with "${prefixToRemove}"...`);
+
+if (!fs.existsSync(skillsDir)) {
+ console.error(`Skills directory not found at ${skillsDir}`);
+ process.exit(1);
+}
+
+const files = fs.readdirSync(skillsDir);
+let count = 0;
+
+files.forEach(file => {
+ const oldPath = path.join(skillsDir, file);
+ const stat = fs.statSync(oldPath);
+
+ if (stat.isDirectory() && file.startsWith(prefixToRemove)) {
+ const newName = file.substring(prefixToRemove.length);
+ const newPath = path.join(skillsDir, newName);
+
+ console.log(`Renaming: ${file} -> ${newName}`);
+
+ try {
+ if (fs.existsSync(newPath)) {
+ console.warn(`WARNING: Destination ${newName} already exists. Skipping.`);
+ } else {
+ fs.renameSync(oldPath, newPath);
+ count++;
+ }
+ } catch (e) {
+ console.error(`ERROR renaming ${file}: ${e.message}`);
+ }
+ }
+});
+
+console.log(`\nRenamed ${count} directories.`);
diff --git a/skills/astro-ssr-pocketbase-single-server/SKILL.md b/skills/astro-ssr-pocketbase-single-server/SKILL.md
new file mode 100644
index 0000000..7559aec
--- /dev/null
+++ b/skills/astro-ssr-pocketbase-single-server/SKILL.md
@@ -0,0 +1,305 @@
+---
+name: how-to-run-astro-ssr-and-pocketbase-on-the-same-server
+description: Learn how to host PocketBase and an Astro SSR application on the same server, using PocketBase's Go integration and a reverse proxy to delegate requests to Astro for dynamic web content.
+metadata:
+ url: https://rodydavis.com/posts/astro-ssr-pocketbase-single-server
+ last_modified: Tue, 03 Feb 2026 20:04:35 GMT
+---
+
+# How to Run Astro SSR and PocketBase on the Same Server
+
+
+In this article I will show you how to host [PocketBase](https://pocketbase.io/) and [Astro in SSR](https://docs.astro.build/en/guides/server-side-rendering/) mode on the same server. PocketBase does let you [render templates](https://pocketbase.io/docs/go-rendering-templates/) on the server but requires [Go Templates](https://pkg.go.dev/text/template) or pre-building with Static Site Generation (SSG).
+
+> This could also be modified to use your web server or framework of choice ([Next.js](https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering), [SvelteKit](https://kit.svelte.dev/docs/page-options), [Qwik](https://qwik.builder.io/), [Angular](https://angular.io/guide/ssr)).
+
+Before getting started make sure you have the latest version of [Node](https://nodejs.org/en/blog/announcements/v19-release-announce) and [Go](https://go.dev/doc/install) installed locally.
+
+## Getting started
+
+In a terminal run the following to create the base project:
+
+```
+mkdir pocketbase_astro_ssr
+cd pocketbase_astro_ssr
+mkdir server
+mkdir www
+```
+
+This will create the `server` and `www` folders in our project needed for both Astro and PocketBase.
+
+## Setting up the server
+
+Create a file at `server/main.go` and update it with the following:
+
+```
+package main
+
+import (
+ "log"
+ "net/http/httputil"
+ "net/url"
+
+ "github.com/labstack/echo/v5"
+ "github.com/pocketbase/pocketbase"
+ "github.com/pocketbase/pocketbase/core"
+)
+
+func main() {
+ app := pocketbase.New()
+
+ app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
+ proxy := httputil.NewSingleHostReverseProxy(&url.URL{
+ Scheme: "http",
+ Host: "localhost:4321",
+ })
+ e.Router.Any("/*", echo.WrapHandler(proxy))
+ e.Router.Any("/", echo.WrapHandler(proxy))
+ return nil
+ })
+
+ if err := app.Start(); err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+Here we are extending [PocketBase with Go](https://pocketbase.io/docs/go-overview/) and taking advantage of the [Echo router](https://echo.labstack.com/docs/routing) integration and using a [reverse proxy](https://www.nginx.com/resources/glossary/reverse-proxy-server/#:~:text=A%20reverse%20proxy%20server%20is,traffic%20between%20clients%20and%20servers.) to handle all requests not defined by PocketBase already and delegating them to Astro.
+
+Next run the following in a terminal to install the dependencies:
+
+```
+go mod init server
+go mod tidy
+```
+
+Now we can start the server and move on to the client:
+
+```
+go run main.go serve
+```
+
+You should see the following and note that this will run in debug mode so all the SQL statements will start to show:
+
+```
+2023/11/09 10:28:52 Server started at http://127.0.0.1:8090
+├─ REST API: http://127.0.0.1:8090/api/
+└─ Admin UI: http://127.0.0.1:8090/_/
+```
+
+### Collections
+
+Open up the Admin UI url and after creating a new admin user, create a new collection `items` and add the following metadata:
+
+Column Name
+
+Column Type
+
+Column Settings
+
+title
+
+Plain Text
+
+
+
+
+
+Then update the API Rules to allow read access for list and view.
+
+
+
+> This is just for example purposes and on a production app you will rely on auth for ACLs
+
+Create 3 new records with placeholder data.
+
+
+
+## Creating the client
+
+Now we can create the client that will be used to connect to PocketBase and serve all of the web traffic.
+
+Navigate to the `www` directory and run the following in a terminal:
+
+```
+npm create astro@latest
+```
+
+Follow the prompts and enter the following:
+
+Question
+
+Answer
+
+Where should we create your new project?
+
+.
+
+How would you like to start your new project?
+
+Empty
+
+Install dependencies?
+
+Yes
+
+Do you plan to write TypeScript?
+
+Yes
+
+How strict should TypeScript be?
+
+Strict
+
+Initialize a new git repository?
+
+No
+
+You can of course customize this as you need, but next we can install the dependencies needed by running the following in a terminal:
+
+```
+npm i -D @astrojs/node
+npm i pocketbase
+```
+
+Next update `www/astro.config.mjs` and update it with the following:
+
+```
+import { defineConfig } from "astro/config";
+import nodejs from "@astrojs/node";
+
+// https://astro.build/config
+export default defineConfig({
+ adapter: nodejs({
+ mode: "standalone",
+ }),
+ output: "server",
+});
+```
+
+This will use Server Side Rendering (SSR) instead of Static Site Generation (SSG) when we run the web server.
+
+### UI
+
+#### Layouts
+
+We can start by creating a shared layout for all the routes. Create a file at `www/src/layouts/Root.astro` and update it with the following:
+
+```
+---
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+```
+
+#### Routes
+
+Now we can update the index `/` route by updating the following file `www/src/pages/index.astro`:
+
+```
+---
+import Root from "../layouts/Root.astro";
+
+import PocketBase from "pocketbase";
+
+const pb = new PocketBase("http://127.0.0.1:8090");
+const items = pb.collection("items");
+const records = await items.getFullList();
+---
+
+
+
+
+```
+
+This will call the `items` collection on the server and render it with 0 JS on the client.
+
+Next create a file `www/src/pages/[...slug].astro` and update it with the following:
+
+```
+---
+import Root from "../layouts/Root.astro";
+
+import PocketBase from "pocketbase";
+
+const slug = Astro.params.slug!;
+const id = slug.split("/").pop()!;
+
+const pb = new PocketBase("http://127.0.0.1:8090");
+const items = pb.collection("items");
+
+const records = await items.getList(1, 1, {
+ filter: `id = '${id}'`,
+});
+
+if (records.items.length === 0) {
+ return new Response("Not found", { status: 404 });
+}
+
+const {title} = records.items[0];
+---
+
+
+ Back
+
{title}
+
+```
+
+This is almost like before but now we can return a proper `404` response if not found for an item.
+
+#### Running
+
+Now we can run the web server with the following command:
+
+```
+npm run dev
+```
+
+You should see the following:
+
+```
+> dev
+> astro dev
+
+ 🚀 astro v3.4.4 started in 67ms
+
+ ┃ Local http://localhost:4321/
+ ┃ Network use --host to expose
+```
+
+Then if we open up the PocketBase url `http://127.0.0.1:8090` and you should see the following for the index route and detail routes:
+
+
+
+
+
+## Conclusion
+
+Now you can build a new binary for both the server and client and deploy them both on the same server instance. 🎉
+
+You can find the final code [here](https://github.com/rodydavis/pocketbase_astro_ssr).
\ No newline at end of file
diff --git a/skills/async-preact-signal/SKILL.md b/skills/async-preact-signal/SKILL.md
new file mode 100644
index 0000000..89cfa54
--- /dev/null
+++ b/skills/async-preact-signal/SKILL.md
@@ -0,0 +1,339 @@
+---
+name: async-preact-signals
+description: Explore how to effectively manage asynchronous data with Preact Signals by creating a custom `asyncSignal` that handles loading, error, and data states without breaking the synchronous nature of signals.
+metadata:
+ url: https://rodydavis.com/posts/async-preact-signal
+ last_modified: Tue, 03 Feb 2026 20:04:16 GMT
+---
+
+# Async Preact Signals
+
+
+When working with [signals](https://github.com/preactjs/signals) in Javascript, it is very common to work with async data from [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
+
+## Async vs Sync
+
+But unlike other state management libraries, signals do not have an _asynchronous_ state graph and all values must be computed _synchronously_.
+
+When people first start using signals they want to simply add **async** to the function callback but this breaks how they work under the hood and leads to **undefined** behavior. ☹️
+
+Async functions are a leaky abstraction and force you to handle them all the way up the graph. Async is also not always better and can have a [performance impact](https://madelinemiller.dev/blog/javascript-promise-overhead/). 😬
+
+## Working with Promises
+
+We can still do so much with sync operations, and make it eaiser to work with common async patterns.
+
+For example when you make a **http** request using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch), you want to return the data in the **Promise** and update some UI.
+
+```
+const el = document.querySelector('#output');
+let postId = '123';
+fetch(`/posts/${postId}`).then(res => res.json()).then(post => {
+ el.innerText = post.title;
+})
+```
+
+Now when we add signals we can rerun the fetch everytime the post id changes.
+
+```
+import { effect, signal } from "@preact/signals-core";
+
+const el = document.querySelector('#output');
+const postId = signal( '123');
+
+effect(() => {
+ fetch(`/posts/${postId.value}`).then(res => res.json()).then(post => {
+ el.innerText = post.title;
+ });
+});
+```
+
+This is better, but now we need to handle stopping the previous request if the post id changes before the previous fetch completes.
+
+```
+import { effect, signal } from "@preact/signals-core";
+
+const el = document.querySelector('#output');
+const postId = signal( '123');
+let controller;
+
+effect(() => {
+ if (controller) {
+ controller.abort();
+ }
+ controller = new AbortController();
+ const signal = controller.signal;
+ try {
+ fetch(`/posts/${postId.value}`, { signal }).then(res => res.json()).then(post => {
+ el.innerText = post.title;
+ });
+ } catch (err) {
+ // todo: show error message
+ }
+});
+```
+
+But this still skips a lot of things we normally want to show like loading states and error states.
+
+```
+import { effect, signal, batch } from "@preact/signals-core";
+
+const el = document.querySelector('#output');
+const postId = signal( '123');
+const postData = signal({});
+const errorMessage = signal('');
+const loading = signal(false);
+let controller;
+
+effect(() => {
+ if (controller) {
+ controller.abort();
+ }
+ controller = new AbortController();
+ const signal = controller.signal;
+ batch(() => {
+ loading.value = true;
+ errorMessage.value = '';
+ postData.value = {};
+ });
+ try {
+ fetch(`/posts/${postId.value}`, { signal }).then(res => res.json()).then(post => {
+ batch(() => {
+ postData.value = post;
+ loading.value = false;
+ });
+ });
+ } catch (err) {
+ errorMessage.value = err.message;
+ }
+});
+effect(() => {
+ if (loading.value) {
+ el.innerText = 'Loading...';
+ } else if (errorMessage.value) {
+ el.innerText = `Error: ${errorMessage.value}`;
+ } else {
+ el.innerText = postData.value.title;
+ }
+});
+```
+
+Now we can show the proper states, but this is only for one request...
+
+We could wrap this up in a class to reuse or create a new type of signal that can work with asynchronous data.
+
+## AsyncState
+
+We want to have a base class that we can make our loading states easily extend from:
+
+```
+export class AsyncState {
+ constructor() {}
+
+ get value(): T | null {
+ return null;
+ }
+
+ get requireValue(): T {
+ throw new Error("Value not set");
+ }
+
+ get error(): any {
+ return null;
+ }
+
+ get isLoading(): boolean {
+ return false;
+ }
+
+ get hasValue(): boolean {
+ return false;
+ }
+
+ get hasError(): boolean {
+ return false;
+ }
+
+ map(builders: {
+ onLoading: () => R;
+ onError: (error: any) => R;
+ onData: (data: T) => R;
+ }): R {
+ if (this.hasError) {
+ return builders.onError(this.error);
+ }
+ if (this.hasValue) {
+ return builders.onData(this.requireValue);
+ }
+ return builders.onLoading();
+ }
+}
+```
+
+> [This class](https://dartsignals.dev/async/state/) actually comes from a [Dart port of preact signals](https://github.com/rodydavis/signals.dart) I created.
+
+This allows us to easily check if there is an actual value, error or if it is loading. It also provides an easy builder method to map the state to another value. 🤩
+
+### AsyncData
+
+The loading state extends **AsyncState** and passes the value in the constructor to the overriden methods.
+
+```
+export class AsyncData extends AsyncState {
+ private _value: T;
+
+ constructor(value: T) {
+ super();
+ this._value = value;
+ }
+
+ get requireValue(): T {
+ return this._value;
+ }
+
+ get hasValue(): boolean {
+ return true;
+ }
+
+ toString() {
+ return `AsyncData{${this._value}}`;
+ }
+}
+```
+
+### AsyncLoading
+
+For the loading state we override the methods like **AsyncData**.
+
+```
+export class AsyncLoading extends AsyncState {
+ get value(): T | null {
+ return null;
+ }
+
+ get isLoading(): boolean {
+ return true;
+ }
+
+ toString() {
+ return `AsyncLoading{}`;
+ }
+}
+```
+
+### AsyncError
+
+For the error state we can pass an object of any type to return the error as value instead of throwing an exception (like Go).
+
+```
+export class AsyncError extends AsyncState {
+ private _error: any;
+
+ constructor(error: any) {
+ super();
+ this._error = error;
+ }
+
+ get error(): any {
+ return this._error;
+ }
+
+ get hasError(): boolean {
+ return true;
+ }
+
+ toString() {
+ return `AsyncError{${this._error}}`;
+ }
+}
+```
+
+## asyncSignal
+
+Now we the state classes created, we can create a function to create an asynchronous signal with all the logic we talked about earlier.
+
+We need to show the sync value at any time and have a way to abort previous requests.
+
+```
+export function asyncSignal(
+ cb: () => Promise
+): ReadonlySignal> {
+ const loading = new AsyncLoading();
+ const reset = Symbol("reset");
+ const s = signal>(loading);
+ const c = computed>(cb);
+ let controller: AbortController | null;
+ let abortSignal: AbortSignal | null;
+
+ function execute(cb: Promise, cancel: AbortSignal) {
+ (async () => {
+ s.value = loading;
+ try {
+ const result = await new Promise(async (resolve, reject) => {
+ if (cancel.aborted) {
+ reject(cancel.reason);
+ }
+ cancel.addEventListener("abort", () => {
+ reject(cancel.reason);
+ });
+ try {
+ const result = await cb;
+ if (cancel.aborted) {
+ reject(cancel.reason);
+ return;
+ }
+ resolve(result);
+ } catch (error) {
+ reject(error);
+ }
+ });
+ s.value = new AsyncData(result);
+ } catch (error) {
+ if (error === reset) {
+ s.value = loading;
+ } else {
+ s.value = new AsyncError(error);
+ }
+ }
+ })();
+ }
+
+ effect(() => {
+ if (controller != null) {
+ controller.abort(reset);
+ }
+ controller = new AbortController();
+ abortSignal = controller.signal;
+ execute(c.value, abortSignal);
+ });
+
+ return s;
+}
+```
+
+This makes it very easy to create multiple asynchronous signals and also use it anywhere else you have signals in the application like effects and computeds.
+
+```
+const el = document.querySelector('#output');
+const postId = signal('123');
+
+const result = asyncSignal(() => fetch(`/posts/${postId.value}`).then(res => res.json()));
+
+effect(() => {
+ el.innerText = result.value.map({
+ onLoading: () => 'Loading...',
+ onError: (err) => `Error: ${err}`,
+ onData: (post) => post.title,
+ });
+});
+
+postId.value = '456';
+```
+
+## Conclusion
+
+I have started a Preact Signals GitHub discussion [here](https://github.com/preactjs/signals/discussions/648) and you can find a gist with the [final source code here](https://gist.github.com/rodydavis/3b5266da2cc07f6574d425f5ce6e1e31). 🎉
+
+This has made working with asynchronous data a lot eaiser to work with and would love to hear your thoughts about ways to improve it 👀
+
+Also if you are curious about how Angular does asynchronous signals you can check out the [resource signal](https://angular.dev/guide/signals/resource) and the [computedFrom/Async signal](https://justangular.com/blog/building-computed-async-for-signals-in-angular).
\ No newline at end of file
diff --git a/skills/automate-flutter-apps/SKILL.md b/skills/automate-flutter-apps/SKILL.md
new file mode 100644
index 0000000..57b7d75
--- /dev/null
+++ b/skills/automate-flutter-apps/SKILL.md
@@ -0,0 +1,172 @@
+---
+name: automate-flutter-app-releases
+description: Automate your Flutter app releases to beta or production with this handy shell script that handles version bumping, formatting, cleaning, rebuilding, and deployment via Fastlane.
+metadata:
+ url: https://rodydavis.com/posts/automate-flutter-apps
+ last_modified: Tue, 03 Feb 2026 20:04:27 GMT
+---
+
+# Automate Flutter App Releases
+
+
+> **TLDR** You can find the script [here](https://gist.github.com/rodydavis/774b36e32d7efa882cca8dd16da6e74c).
+
+```
+#!/bin/bash
+
+echo "App Release Automator by @rodydavis"
+
+action="$1"
+red=`tput setaf 1`
+green=`tput setaf 2`
+reset=`tput sgr0`
+
+if [ ${action} = "build" ]; then
+
+ echo "${green}Generating built files.. ${reset}"
+ flutter packages pub run build_runner clean
+ flutter packages pub run build_runner build --delete-conflicting-outputs
+
+ pub global activate pubspec_version
+ git commit -a -m "Build $(pubver bump patch)"
+
+ echo "${green}Building Project...${reset}"
+ find . -name "*-e" -type f -delete
+ flutter format .
+ flutter clean
+
+ echo "${green}Project Size: $(find . -name "*.dart" | xargs cat | wc -c)${reset}"
+
+ echo "${green}Building APK...${reset}"
+ flutter build apk
+
+ echo "${green}Builing IPA..${reset}"
+ cd ./ios && pod install && pod repo update && cd ..
+ flutter build ios
+
+ git commit -a -m "Project Rebuilt"
+
+
+elif [ ${action} = "beta" ]; then
+
+ echo "${green}Generating built files..${reset}"
+ flutter packages pub run build_runner clean
+ flutter packages pub run build_runner build --delete-conflicting-outputs
+
+ pub global activate pubspec_version
+ git commit -a -m "Beta $(pubver bump patch)"
+
+ echo "${green}Building Project...${reset}"
+ find . -name "*-e" -type f -delete
+ flutter format .
+ flutter clean
+
+ echo "${green}Project Size: $(find . -name "*.dart" | xargs cat | wc -c)${reset}"
+
+ echo "${green}Building APK...${reset}"
+ flutter build apk
+
+ echo "${green}Sending Android to Beta...${reset}"
+ cd ./android && fastlane beta && cd ..
+
+ echo "${green}Builing IPA..${reset}"
+ flutter build ios
+
+ echo "${green}Sending iOS to Beta..${reset}"
+ cd ./ios && fastlane beta && cd ..
+
+ git commit -a -m "Sent to Beta"
+
+
+elif [ ${action} = "release" ]; then
+
+ echo "${green}Generating built files..${reset}"
+ flutter packages pub run build_runner clean
+ flutter packages pub run build_runner build --delete-conflicting-outputs
+
+ pub global activate pubspec_version
+ git commit -a -m "Production $(pubver bump minor)"
+
+ echo "${green}Building Project...${reset}"
+ find . -name "*-e" -type f -delete
+ flutter format .
+ flutter clean
+
+ echo "${green}Project Size: $(find . -name "*.dart" | xargs cat | wc -c)${reset}"
+
+ echo "${green}Building APK...${reset}"
+ flutter build apk
+
+ echo "${green}Sending Android to Production...${reset}"
+ cd ./android && fastlane release && cd ..
+
+ echo "${green}Builing IPA..${reset}"
+ flutter build ios
+
+ echo "${green}Sending iOS to Production...${reset}"
+ cd ./ios && fastlane release && cd ..
+
+ git commit -a -m "Sent to Production"
+
+fi
+
+echo "${green}Successfully completed${reset}"
+```
+
+Needed:
+
+* Fastlane setup in each directory
+* build\_runner as a dependency
+* Git Project in VCS
+
+Steps to Run:
+
+1. Download this file and put it at the root level of your flutter project
+2. Open the terminal and navigate to your project location
+3. Enter this command: `chmod +x release.sh`
+
+Usage
+
+* For beta: `./release.sh beta`
+* For production: `./release.sh release`
+
+It will do the following:
+
+* Bump the version numbers if you are using the version in the `pubspec.yaml`
+* Release the apps with Fastlane
+* Format all Dart Files
+* Clean Project
+* Rebuild classes
+* Add commit message
+
+Make your life easier and automate your builds to beta and production!
+
+## What you need
+
+* [Fastlane](https://fastlane.tools/) setup in each directory
+* [build\_runner](https://pub.dartlang.org/packages/build_runner) as a dependency
+* Git Project in VCS
+
+## Initial Setup
+
+* Download [this file](https://gist.github.com/rodydavis/774b36e32d7efa882cca8dd16da6e74c)
+* Put it at the root level of your flutter project
+* Open the terminal and navigate to your project location
+* Enter this command: chmod +x release.sh
+
+## Usage
+
+Now you can call this script!
+
+* For beta: `./release.sh beta`
+* For production: `./release.sh release`
+
+## Overview
+
+* Bump the version numbers if you are using the version in the pubspec.yaml
+* Release the apps with Fastlane
+* Format all Dart Files
+* Clean Project
+* Rebuild classes
+* Add commit messages
+* Updates Cocoa Pods
\ No newline at end of file
diff --git a/skills/codemirror-dynamic-theme/SKILL.md b/skills/codemirror-dynamic-theme/SKILL.md
new file mode 100644
index 0000000..865918d
--- /dev/null
+++ b/skills/codemirror-dynamic-theme/SKILL.md
@@ -0,0 +1,533 @@
+---
+name: dynamic-themes-with-codemirror
+description: Learn how to create a Lit web component with CodeMirror, dynamically themed using Material Design's color utilities, for a customizable code editing experience.
+metadata:
+ url: https://rodydavis.com/posts/codemirror-dynamic-theme
+ last_modified: Tue, 03 Feb 2026 20:04:24 GMT
+---
+
+# Dynamic Themes with CodeMirror
+
+
+In this article I will go over how to set up a [Lit](https://lit.dev/) web component and use it to create a a code window that uses [CodeMirror](https://codemirror.net/) and apply a dynamic theme with [Material Design](https://material.io/).
+
+> **TLDR** The final source [here](https://github.com/rodydavis/codemirror-dynamic-theme) and an online [demo](https://rodydavis.github.io/codemirror-dynamic-theme/).
+
+## Prerequisites
+
+* Vscode
+* Node >= 16
+* Typescript
+
+## Getting Started
+
+We can start off by navigating in terminal to the location of the project and run the following:
+
+```
+npm init @vitejs/app --template lit-ts
+```
+
+Then enter a project name `codemirror-dynamic-theme` and now open the project in vscode and install the dependencies:
+
+```
+cd codemirror-dynamic-theme
+npm i lit codemirror @material/material-color-utilities
+npm i -D @types/node @types/codemirror
+code .
+```
+
+Update the `vite.config.ts` with the following:
+
+```
+import { defineConfig } from "vite";
+import { resolve } from "path";
+
+export default defineConfig({
+ base: "/codemirror-dynamic-theme/",
+ build: {
+ rollupOptions: {
+ input: {
+ main: resolve(__dirname, "index.html"),
+ },
+ },
+ },
+});
+```
+
+## Template
+
+Open up the `index.html` and update it with the following:
+
+```
+
+
+
+
+
+
+ CodeMirror Dynamic Theme
+
+
+
+
+
+
+
+```
+
+## Web Component
+
+Before we update our component we need to rename `my-element.ts` to `code-window.ts`
+
+Open up `code-window.ts` and update it with the following:
+
+```
+import { html, css, LitElement, unsafeCSS } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import {
+ applyTheme,
+ argbFromHex,
+ hexFromArgb,
+ themeFromSourceColor,
+} from "@material/material-color-utilities";
+import CodeMirror from "codemirror";
+import codemirrorStyles from "codemirror/lib/codemirror.css";
+
+@customElement("code-window")
+export class CodeWindow extends LitElement {
+ static styles = css`
+ ${unsafeCSS(codemirrorStyles)}
+
+ main {
+ width: 100vw;
+ height: 100vh;
+ background-color: var(--md-sys-color-background);
+ color: var(--md-sys-color-on-background);
+ --header-height: 48px;
+ --input-size: 32px;
+ }
+
+ .toolbar {
+ height: var(--header-height);
+ background-color: var(--md-sys-color-primary-container);
+ color: var(--md-sys-color-on-primary-container);
+ display: flex;
+ align-items: center;
+ }
+
+ .actions > * {
+ margin-left: 4px;
+ margin-right: 4px;
+ }
+
+ .toolbar .title {
+ font-family: sans-serif;
+ font-size: 18px;
+ padding-left: 4px;
+ }
+
+ .toolbar .actions {
+ display: flex;
+ align-items: center;
+ }
+
+ .toolbar a {
+ padding: 0;
+ margin: 0;
+ padding-left: 8px;
+ padding-right: 8px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ input[type="color"] {
+ width: calc(var(--input-size) * 2);
+ height: var(--input-size);
+ outline: none;
+ border: none;
+ border-radius: 50%;
+ background-color: var(--md-sys-color-primary-container);
+ }
+ input[type="color"]::-webkit-color-swatch-wrapper {
+ padding: 0;
+ }
+ input[type="color"]::-webkit-color-swatch {
+ border: none;
+ border-radius: var(--input-size);
+ border: var(--md-sys-color-outline) solid 1px;
+ }
+
+ button {
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ }
+
+ .tertiary {
+ background-color: var(--md-sys-color-tertiary);
+ color: var(--md-sys-color-on-tertiary);
+ }
+
+ .secondary {
+ background-color: var(--md-sys-color-secondary);
+ color: var(--md-sys-color-on-secondary);
+ }
+
+ .spacer {
+ flex: 1;
+ }
+
+ .editor {
+ height: calc(100% - var(--header-height));
+ width: 100%;
+ }
+ `;
+
+ @property() value = [
+ `import {html, css, LitElement} from 'lit';`,
+ `import {customElement, property} from 'lit/decorators.js';`,
+ ``,
+ `@customElement('simple-greeting')`,
+ `export class SimpleGreeting extends LitElement {`,
+ ` static styles = css\`p { color: blue }\`;`,
+ ``,
+ ` @property()`,
+ ` name = 'Somebody';`,
+ ``,
+ ` render() {`,
+ ` return html\`
+
+
+ `;
+ }
+
+ firstUpdated() {
+ const root = this.shadowRoot!.querySelector(".editor") as HTMLElement;
+ const editor = CodeMirror(root, {
+ value: this.value,
+ mode: "javascript",
+ lineNumbers: true,
+ lineWrapping: true,
+ indentUnit: 4,
+ tabSize: 4,
+ indentWithTabs: true,
+ autofocus: true,
+ });
+ console.debug(editor);
+ editor.setSize("100%", `100%`);
+ this.updateTheme();
+ window
+ .matchMedia("(prefers-color-scheme: dark)")
+ .addEventListener("change", (e) => {
+ this.dark = e.matches;
+ this.updateTheme();
+ });
+ }
+
+ private updateTheme() {
+ // TODO: Generate Theme
+ }
+
+ private setColor(val: string) {
+ this.color = val;
+ this.updateTheme();
+ }
+
+ private onColor(e: Event) {
+ const target = e.target as HTMLInputElement;
+ this.setColor(target.value);
+ }
+
+ private randomColor() {
+ const letters = "0123456789ABCDEF";
+ let color = "#";
+ for (let i = 0; i < 6; i++) {
+ color += letters[Math.floor(Math.random() * 16)];
+ }
+ this.setColor(color);
+ }
+
+ private toggleDark() {
+ this.dark = !this.dark;
+ this.updateTheme();
+ }
+
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "code-window": CodeWindow;
+ }
+}
+```
+
+Here we are setting up some of the editor basics to load in the styles needed for the basic layout.
+
+There are also few methods that handle updating of properties on the element such as `toggleDark` and `setColor`. When you run the application you should see the following:
+
+
+
+It doesn't look great yet, but now we can add a CodeMirror theme to import. Create a file `src/theme.ts` and update it with the following:
+
+```
+import { css } from "lit";
+
+export const codeMirrorTheme = css`
+ .CodeMirror {
+ background-color: var(--md-sys-color-background);
+ color: var(--md-sys-color-on-background);
+ }
+
+ .CodeMirror-gutters {
+ background: var(--md-sys-color-surface-variant);
+ color: var(--md-sys-color-on-surface-variant);
+ border: none;
+ }
+
+ .CodeMirror-guttermarker,
+ .CodeMirror-guttermarker-subtle,
+ .CodeMirror-linenumber {
+ color: var(--md-sys-color-on-background);
+ }
+
+ .CodeMirror-cursor {
+ border-left: 1px solid var(--md-sys-color-primary);
+ }
+ .cm-fat-cursor .CodeMirror-cursor {
+ background-color: var(--md-sys-color-background);
+ }
+ .cm-animate-fat-cursor {
+ background-color: var(--md-sys-color-background);
+ }
+
+ div.CodeMirror-selected {
+ background: var(--md-sys-color-surface-variant);
+ }
+
+ .CodeMirror-focused div.CodeMirror-selected {
+ background: var(--md-sys-color-surface-variant);
+ }
+
+ .CodeMirror-line::selection,
+ .CodeMirror-line > span::selection,
+ .CodeMirror-line > span > span::selection {
+ background: var(--md-sys-color-surface-variant);
+ }
+
+ .CodeMirror-line::-moz-selection,
+ .CodeMirror-line > span::-moz-selection,
+ .CodeMirror-line > span > span::-moz-selection {
+ background: var(--md-sys-color-surface-variant);
+ }
+
+ .CodeMirror-activeline-background {
+ background: var(--md-sys-color-surface);
+ }
+
+ .cm-keyword {
+ color: var(--md-custom-color-keyword) !important;
+ }
+
+ .cm-operator {
+ color: var(--md-custom-color-operator) !important;
+ }
+
+ .cm-variable-2 {
+ color: var(--md-custom-color-variable-2) !important;
+ }
+
+ .cm-variable-3,
+ .cm-type {
+ color: var(--md-custom-color-variable-3) !important;
+ }
+
+ .cm-builtin {
+ color: var(--md-custom-color-builtin) !important;
+ }
+
+ .cm-atom {
+ color: var(--md-custom-color-atom) !important;
+ }
+
+ .cm-number {
+ color: var(--md-custom-color-number) !important;
+ }
+
+ .cm-def {
+ color: var(--md-custom-color-def) !important;
+ }
+
+ .cm-string {
+ color: var(--md-custom-color-string) !important;
+ }
+
+ .cm-string-2 {
+ color: var(--md-custom-color-string-2) !important;
+ }
+
+ .cm-comment {
+ color: var(--md-custom-color-comment) !important;
+ }
+
+ .cm-variable {
+ color: var(--md-custom-color-variable) !important;
+ }
+
+ .cm-tag {
+ color: var(--md-custom-color-tag) !important;
+ }
+
+ .cm-meta {
+ color: var(--md-custom-color-meta) !important;
+ }
+
+ .cm-attribute {
+ color: var(--md-custom-color-attribute) !important;
+ }
+
+ .cm-property {
+ color: var(--md-custom-color-property) !important;
+ }
+
+ .cm-qualifier {
+ color: var(--md-custom-color-qualifier) !important;
+ }
+
+ .cm-variable-3,
+ .cm-type {
+ color: var(--md-custom-color-variable-3) !important;
+ }
+
+ .cm-error {
+ color: var(--md-sys-color-on-error);
+ background-color: var(--md-sys-color-error);
+ }
+
+ .CodeMirror-matchingbracket {
+ text-decoration: underline;
+ color: var(--md-sys-color-on-surface);
+ }
+`;
+```
+
+Here we are defining all the styles as [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) so we can easily update them.
+
+Now import the theme and apply the styles in the component:
+
+```
+import { codeMirrorTheme } from "./theme";
+
+@customElement("code-window")
+export class CodeWindow extends LitElement {
+ static styles = css`
+ ${unsafeCSS(codemirrorStyles)}
+ ${codeMirrorTheme}
+ ...
+```
+
+Now we need to implement the `updateTheme` method in our element:
+
+```
+updateTheme() {
+ const source = this.color;
+ const dark = this.dark;
+ const target = this.shadowRoot!.querySelector("main") as HTMLElement;
+ const properties = [
+ `--md-custom-color-keyword: #c75779;`,
+ `--md-custom-color-operator: #008800;`,
+ `--md-custom-color-variable: #90ccff;`,
+ `--md-custom-color-variable-2: #dd7700;`,
+ `--md-custom-color-variable-3: #3333bb;`,
+ `--md-custom-color-variable-3: #decb6b;`,
+ `--md-custom-color-builtin: #003388;`,
+ `--md-custom-color-atom: #bb4646;`,
+ `--md-custom-color-number: #b4c5ff;`,
+ `--md-custom-color-def: #82aaff;`,
+ `--md-custom-color-string: #ffb4a9;`,
+ `--md-custom-color-string-2: #ffb4a9;`,
+ `--md-custom-color-comment: #888888;`,
+ `--md-custom-color-tag: #000080;`,
+ `--md-custom-color-meta: #a9c7ff;`,
+ `--md-custom-color-attribute: #008080;`,
+ `--md-custom-color-property: #336699;`,
+ `--md-custom-color-qualifier: #690;`,
+ ];
+ const customColors = properties.map((property) => {
+ const [key, value] = property.split(":");
+ const name = key.trim().replace(/^--md-custom-color-/, "");
+ const color = argbFromHex(value.trim().replace(";", ""));
+ return {
+ name,
+ value: color,
+ blend: true,
+ };
+ });
+ const theme = themeFromSourceColor(argbFromHex(source), customColors);
+ applyTheme(theme, { target, dark });
+ for (const custom of theme.customColors) {
+ const name = custom.color.name;
+ const section = dark ? custom.dark : custom.light;
+ target.style.setProperty(
+ `--md-custom-color-${name}`,
+ hexFromArgb(section.color)
+ );
+ }
+}
+```
+
+Here we are using the [Material Color Utilities](https://github.com/material-foundation/material-color-utilities) package and generating a theme from a source color. We can take advantage of Custom Colors to blend the values to the theme.
+
+After the theme is generated we can apply them to the root element and have the custom properties update the editor.
+
+
+
+Changing the source color can update the theme:
+
+
+
+Changing the brightness can set the colors as well:
+
+
+
+
+
+## Conclusion
+
+If you want to learn more about building with Lit you can read the docs [here](https://lit.dev/).
+
+The source for this example can be found [here](https://github.com/rodydavis/codemirror-dynamic-theme).
+
+If you want to check out dynamic themes for VSCode I created an extension [here](https://github.com/rodydavis/vscode-dynamic-theme).
\ No newline at end of file
diff --git a/skills/dart-html-web-components/SKILL.md b/skills/dart-html-web-components/SKILL.md
new file mode 100644
index 0000000..f4b0a54
--- /dev/null
+++ b/skills/dart-html-web-components/SKILL.md
@@ -0,0 +1,234 @@
+---
+name: how-to-create-html-web-components-with-dart
+description: Discover the power of Web Components and how to build them with both JavaScript and Dart for reusable, framework-agnostic UI elements.
+metadata:
+ url: https://rodydavis.com/posts/dart-html-web-components
+ last_modified: Tue, 03 Feb 2026 20:04:14 GMT
+---
+
+# How to create HTML Web Components with Dart
+
+
+I am a long time [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) fan (since helping DevRel [lit.dev](https://lit.dev/) and [Material Web Components](https://github.com/material-components/material-web)) and have also loved writing [Dart](https://dart.dev/) in both [Flutter](https://flutter.dev/) applications and full stack apps.
+
+Despite being [used at so many companies](https://arewebcomponentsathingyet.com/), Web Components have faced a lot of pushback from JavaScript developers that use frameworks to target the web. ☹️
+
+What you may not realize is that the web has a way to create new HTML tags that can be used in **ANY** JS framework or place that returns HTML and you can progressively enchance applications. 🤩
+
+Since they are custom HTML tags, if you swap implementations, you do not need to update where it is used and you can ship components a separate files [instead of one big bundle](https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755).
+
+Dart [used to support Web Components](https://github.com/dart-archive/web-components) at one point and was even used by a precursor to Lit in a product call [Polymer](https://github.com/polymer-dart).
+
+## Creating a Web Component in Javascript
+
+To create a web component in Javascript you just need to extend HTML element and provide callbacks for when the component is mounted.
+
+```
+class HelloWorld extends HTMLElement {
+ static observedAttributes = ["name"];
+
+ constructor() {
+ super();
+ }
+
+ update() {
+ this.innerHTML = `Hello: ${this.getAttribute('name')}`;
+ }
+
+ connectedCallback() {
+ console.log("Custom element added to page.");
+ this. update();
+ }
+
+ disconnectedCallback() {
+ console.log("Custom element removed from page.");
+ }
+
+ adoptedCallback() {
+ console.log("Custom element moved to new page.");
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ console.log(`Attribute ${name} has changed.`);
+ if (name === 'name') {
+ this. update();
+ }
+ }
+}
+
+customElements.define("hello-world", HelloWorld);
+```
+
+We can then use it in HTML like the following:
+
+```
+
+
+
+
+
+
+```
+
+This works really well, and we don't even need a build step to create them!
+
+## Creating Web Components with Dart
+
+To create them on the Dart side we need to use the [js\_interop package](https://dart.dev/interop/js-interop/usage) and the new [web package](https://pub.dev/packages/web).
+
+We need to create a factory on the dart side that can create these JS classes without actually being able to create a class in the normal way (since JS and Dart classes are different).
+
+There is a great API [`Reflect.construct()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct) which allows us to take a normal function and invoke it class a class constructor. JavaScript did not always support native classes and was only added with [ES6](https://www.w3schools.com/js/js_es6.asp).
+
+By using this built in API, we can create the classes with just pure Dart:
+
+```
+import 'dart:js_interop';
+import 'dart:js_interop_unsafe';
+
+import 'package:web/web.dart';
+
+class WebComponent {
+ late T element;
+ final String extendsType = 'HTMLElement';
+
+ void connectedCallback() {}
+
+ void disconnectedCallback() {}
+
+ void adoptedCallback() {}
+
+ void attributeChangedCallback(
+ String name,
+ String? oldValue,
+ String? newValue,
+ ) {}
+
+ Iterable get observedAttributes => [];
+
+ bool get formAssociated => false;
+
+ ElementInternals? get internals => element['_internals'] as ElementInternals?;
+ set internals(ElementInternals? value) {
+ element['_internals'] = value;
+ }
+
+ R getRoot() {
+ final hasShadow = element.shadowRoot != null;
+ return (hasShadow ? element.shadowRoot! : element) as R;
+ }
+
+ static void define(String tag, WebComponent Function() create) {
+ final obj = _factory(create);
+ window.customElements.define(tag, obj);
+ }
+}
+
+@JS('Reflect.construct')
+external JSAny _reflectConstruct(
+ JSObject target,
+ JSAny args,
+ JSFunction constructor,
+);
+
+final _instances = {};
+
+JSFunction _factory(WebComponent Function() create) {
+ final base = create();
+ final elemProto = globalContext[base.extendsType] as JSObject;
+ late JSAny obj;
+
+ JSAny constructor() {
+ final args = [].jsify()!;
+ final self = _reflectConstruct(elemProto, args, obj as JSFunction);
+ final el = self as HTMLElement;
+ _instances.putIfAbsent(el, () => create()..element = el);
+ return self;
+ }
+
+ obj = constructor.toJS;
+ obj = obj as JSObject;
+
+ final observedAttributes = base.observedAttributes;
+ final formAssociated = base.formAssociated;
+
+ obj['prototype'] = elemProto['prototype'];
+ obj['observedAttributes'] = observedAttributes.toList().jsify()!;
+ obj['formAssociated'] = formAssociated.jsify()!;
+
+ final prototype = obj['prototype'] as JSObject;
+ prototype['connectedCallback'] = (HTMLElement instance) {
+ _instances[instance]?.connectedCallback();
+ }.toJSCaptureThis;
+ prototype['disconnectedCallback'] = (HTMLElement instance) {
+ _instances[instance]?.disconnectedCallback();
+ _instances.remove(instance);
+ }.toJSCaptureThis;
+ prototype['adoptedCallback'] = (HTMLElement instance) {
+ _instances[instance]?.adoptedCallback();
+ }.toJSCaptureThis;
+ prototype['attributeChangedCallback'] = (
+ HTMLElement instance,
+ String name,
+ String? oldName,
+ String? newName,
+ ) {
+ _instances[instance]?.attributeChangedCallback(name, oldName, newName);
+ }.toJSCaptureThis;
+
+ return obj as JSFunction;
+}
+```
+
+This may seem like a lot to digest, but that is ok. It simply does some JS magic to upgrade functions to classes and provide the correct callbacks to create the web components.
+
+> If you want a package that does this for you, [html\_web\_components](https://pub.dev/packages/html_web_components) is on pub.dev.
+
+To create a Web Component like we did before, we can just extend the class and define the component.
+
+```
+import 'package:html_web_components/html_web_components.dart';
+
+class HelloWorld extends WebComponent {
+ @override
+ List observedAttributes = ['name'];
+
+ void update() {
+ element.innerText = "Hello: ${element.getAttribute('name')}!";
+ }
+
+ @override
+ void connectedCallback() {
+ super.connectedCallback();
+ update();
+ }
+
+ @override
+ void attributeChangedCallback(
+ String name,
+ String? oldValue,
+ String? newValue,
+ ) {
+ super.attributeChangedCallback(name, oldValue, newValue);
+ if (observedAttributes.contains(name)) {
+ update();
+ }
+ }
+}
+
+void main() {
+ WebComponent.define('hello-world', HelloWorld.new);
+}
+```
+
+This should look very similar (that is the goal) and makes it so easy to publish the compoents or build a full web application with it.
+
+## Conclusion
+
+Web Components allow you to upgrade your client side interactivity while having the freedom to use server rendering to create the template files or just use a SPA on the frontend. You can take these components and use them in **ANY** JS frameworks! 🤯
+
+I would highly suggest that you try it out for yourself before you write off Web Components. This is especially true for Flutter developers wanting an alternative to Flutter web (and even use with [Jaspr](https://pub.dev/packages/jaspr)).
+
+You can take advantage of Dart's great ecosystem of packages on [pub.dev](https://pub.dev/) and the ability to compile to WASM and JS. If you use a builder like [peanut](https://pub.dev/packages/peanut) it will even create the script that tries to load WASM and can fallback to JS for you 🔥
+
+If you want to see the code, you can [find it on GitHub](https://github.com/rodydavis/dart-web-components). Reach out if you have any questions or want to show off something cool you built with them!
\ No newline at end of file
diff --git a/skills/dart_bitwise/SKILL.md b/skills/dart_bitwise/SKILL.md
new file mode 100644
index 0000000..91e95c1
--- /dev/null
+++ b/skills/dart_bitwise/SKILL.md
@@ -0,0 +1,122 @@
+---
+name: how-to-do-bitwise-operations-in-dart
+description: Explore Dart's bitwise operations for both integers and booleans, including AND, OR (inclusive & exclusive), NAND, NOR, and XNOR, with practical code examples.
+metadata:
+ url: https://rodydavis.com/posts/dart/bitwise
+ last_modified: Tue, 03 Feb 2026 20:04:33 GMT
+---
+
+# How to do Bitwise operations in Dart
+
+
+In Dart it is possible to do [Bitwise Operations](https://en.wikipedia.org/wiki/Bitwise_operation#:~:text=In%20computer%20programming%2C%20a%20bitwise,directly%20supported%20by%20the%20processor.) with **int** and **bool** types.
+
+## AND
+
+Checks if the left and right side are both true. [Learn more](https://www.ibm.com/docs/en/xl-c-and-cpp-aix/16.1?topic=expressions-bitwise-operator).
+
+```
+// int
+print(0 & 1); // 0
+print(1 & 0); // 0
+print(1 & 1); // 1
+print(0 & 0); // 0
+
+// bool
+print(false & true); // false
+print(true & false); // false
+print(true & true); // true
+print(false & false); // false
+```
+
+## OR
+
+### Inclusive
+
+Checks if either the left or right side are true. [Learn more](https://www.ibm.com/docs/en/xl-c-and-cpp-aix/16.1?topic=be-logical-operator).
+
+```
+// int
+print(0 | 1); // 1
+print(1 | 0); // 1
+print(1 | 1); // 1
+print(0 | 0); // 0
+
+// bool
+print(false | true); // true
+print(true | false); // true
+print(true | true); // true
+print(false | false); // false
+```
+
+### Exclusive
+
+Checks if both the left or right side are true but not both. [Learn more](https://www.ibm.com/docs/en/xl-c-and-cpp-aix/16.1?topic=expressions-bitwise-exclusive-operator).
+
+```
+// int
+print(0 ^ 1); // 1
+print(1 ^ 0); // 1
+print(1 ^ 1); // 0
+print(0 ^ 0); // 0
+
+// bool
+print(false ^ true); // true
+print(true ^ false); // true
+print(true ^ true); // false
+print(false ^ false); // false
+```
+
+## NAND
+
+Negated AND operation.
+
+```
+// int
+print(~(0 & 1) & 1); // 1
+print(~(1 & 0) & 1); // 1
+print(~(1 & 1) & 1); // 0
+print(~(0 & 0) & 1); // 1
+
+// bool
+print(!(false & true)); // true
+print(!(true & false)); // true
+print(!(true & true)); // false
+print(!(false & false)); // true
+```
+
+## NOR
+
+Negated inclusive OR operation.
+
+```
+// int
+print(~(0 | 1) & 1); // 0
+print(~(1 | 0) & 1); // 0
+print(~(1 | 1) & 1); // 0
+print(~(0 | 0) & 1); // 1
+
+// bool
+print(!(false | true)); // false
+print(!(true | false)); // false
+print(!(true | true)); // false
+print(!(false | false)); // true
+```
+
+## XNOR
+
+Negated exclusive OR operation.
+
+```
+// int
+print(~(0 ^ 1) & 1); // 0
+print(~(1 ^ 0) & 1); // 0
+print(~(1 ^ 1) & 1); // 1
+print(~(0 ^ 0) & 1); // 1
+
+// bool
+print(!(false ^ true)); // false
+print(!(true ^ false)); // false
+print(!(true ^ true)); // true
+print(!(false ^ false)); // true
+```
\ No newline at end of file
diff --git a/skills/dart_function-invoking/SKILL.md b/skills/dart_function-invoking/SKILL.md
new file mode 100644
index 0000000..ccea904
--- /dev/null
+++ b/skills/dart_function-invoking/SKILL.md
@@ -0,0 +1,49 @@
+---
+name: various-ways-to-invoke-functions-in-dart
+description: Discover the surprising flexibility of calling Dart functions, including mixed positional and named arguments, the `.call` operator, and dynamic invocation with `Function.apply`.
+metadata:
+ url: https://rodydavis.com/posts/dart/function-invoking
+ last_modified: Tue, 03 Feb 2026 20:04:32 GMT
+---
+
+# Various Ways to Invoke Functions in Dart
+
+
+There are multiple ways to call a [Function](https://dart.dev/language/functions) in Dart.
+
+The examples below will assume the following function:
+
+```
+void myFunction(int a, int b, {int? c, int? d}) {
+ print((a, b, c, d));
+}
+```
+
+But recently I learned that you can call a functions positional arguments in any order mixed with the named arguments. 🤯
+
+```
+myFunction(1, 2, c: 3, d: 4);
+myFunction(1, c: 3, d: 4, 2);
+myFunction(c: 3, d: 4, 1, 2);
+myFunction(c: 3, 1, 2, d: 4);
+```
+
+In addition you can use the [`.call`](https://dart.dev/language/callable-objects) operator to invoke the function if you have a reference to it:
+
+```
+myFunction.call(1, 2, c: 3, d: 4);
+```
+
+You can also use [`Function.apply`](https://api.flutter.dev/flutter/dart-core/Function/apply.html) to dynamically invoke a function with a reference but it should be noted that it will effect js dart complication size and performance:
+
+```
+Function.apply(myFunction, [1, 2], {#c: 3, #d: 4});
+```
+
+All of these methods print the following:
+
+```
+(1, 2, 3, 4)
+```
+
+## Demo
\ No newline at end of file
diff --git a/skills/dart_print-multiple-objects/SKILL.md b/skills/dart_print-multiple-objects/SKILL.md
new file mode 100644
index 0000000..e504522
--- /dev/null
+++ b/skills/dart_print-multiple-objects/SKILL.md
@@ -0,0 +1,67 @@
+---
+name: how-to-print-multiple-objects-to-the-console-with-print-in-dart
+description: Learn how to print multiple objects to the console in Dart using Records, offering a similar experience to JavaScript's `console.log()` functionality.
+metadata:
+ url: https://rodydavis.com/posts/dart/print-multiple-objects
+ last_modified: Tue, 03 Feb 2026 20:04:33 GMT
+---
+
+# How to Print Multiple Objects to the Console with print() in Dart
+
+
+If you are coming from JavaScript you may be used to printing multiple objects to the console with `console.log()`:
+
+```
+console.log('a', 1, 'b', 2); // a 1 b 2
+```
+
+In Dart we can only print `Object?` to the console with [`print()`](https://api.dart.dev/stable/3.3.1/dart-core/print.html):
+
+```
+print(1); // 1
+print(null); // null
+print({'a': 1, 'b': 2}); // {a: 1, b: 1}
+```
+
+But it is totally possible to print multiple objects too, we need to use [Records](https://dart.dev/language/records):
+
+```
+final number = 1;
+final str = 'Hello World';
+
+print((number, str));
+
+print((DateTime.now(), str));
+
+print((DateTime.now(), count: number, description: str));
+
+print((DateTime.now(), StackTrace.current));
+```
+
+Print the following:
+
+```
+(1, Hello World)
+(2024-03-06 15:48:26.514, Hello World)
+(2024-03-06 15:48:26.514, count: 1, description: Hello World)
+(2024-03-06 15:48:26.514, Error
+ at get current [as current] (https://storage.googleapis.com/nnbd_artifacts/3.3.0/dart_sdk.js:139991:30)
+ at Object.main$0 [as main] (:52:94)
+ at Object.main$ [as main] (:44:10)
+ at :89:26
+ at Object.execCb (https://dartpad.dev/require.js:5:16727)
+ at e.check (https://dartpad.dev/require.js:5:10499)
+ at e. (https://dartpad.dev/require.js:5:12915)
+ at https://dartpad.dev/require.js:5:1542
+ at https://dartpad.dev/require.js:5:13376
+ at each (https://dartpad.dev/require.js:5:1020)
+ at e.emit (https://dartpad.dev/require.js:5:13344)
+ at e.check (https://dartpad.dev/require.js:5:11058)
+ at e.enable (https://dartpad.dev/require.js:5:13242)
+ at e.init (https://dartpad.dev/require.js:5:9605)
+ at a (https://dartpad.dev/require.js:5:8305)
+ at Object.completeLoad (https://dartpad.dev/require.js:5:15962)
+ at HTMLScriptElement.onScriptLoad (https://dartpad.dev/require.js:5:16882))
+```
+
+## Demo
\ No newline at end of file
diff --git a/skills/dart_truthy/SKILL.md b/skills/dart_truthy/SKILL.md
new file mode 100644
index 0000000..ef27a91
--- /dev/null
+++ b/skills/dart_truthy/SKILL.md
@@ -0,0 +1,70 @@
+---
+name: check-if-an-object-is-truthy-in-dart
+description: Learn how to extend Dart's functionality to implement JavaScript-style "truthy" checks for easier conditional logic and value evaluations.
+metadata:
+ url: https://rodydavis.com/posts/dart/truthy
+ last_modified: Tue, 03 Feb 2026 20:04:34 GMT
+---
+
+# Check if an Object is Truthy in Dart
+
+
+If you are coming from language like JavaScript you may be used to checking if an object is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy).
+
+```
+if (true)
+if ({})
+if ([])
+if (42)
+if ("0")
+if ("false")
+if (new Date())
+if (-42)
+if (12n)
+if (3.14)
+if (-3.14)
+if (Infinity)
+if (-Infinity)
+```
+
+In Dart you need to explicitly check if an object is not null, true/false or determine if the value is true based on the type.
+
+It is possible however to use Dart extensions to add the truthy capability.
+
+```
+extension on Object? {
+ bool get isTruthy => truthy(this);
+}
+
+bool truthy(Object? val) {
+ if (val == null) return false;
+ if (val is bool) return val;
+ if (val is num && val == 0) return false;
+ if (val is String && (val == 'false' || val == '')) return false;
+ if (val is Iterable && val.isEmpty) return false;
+ if (val is Map && val.isEmpty) return false;
+ return true;
+}
+```
+
+This will now make it possible for any object to be evaluated as a truthy value in if statements or value assignments.
+
+Prints the following:
+
+```
+(null, false)
+(, false)
+(false, false)
+(true, true)
+(0, false)
+(1, true)
+(false, false)
+(true, true)
+([], false)
+([1, 2, 3], true)
+({}, false)
+({1, 2, 3}, true)
+({a: 1, b: 2}, true)
+```
+
+## Demo
\ No newline at end of file
diff --git a/skills/deep-linking-flutter-web/SKILL.md b/skills/deep-linking-flutter-web/SKILL.md
new file mode 100644
index 0000000..1397017
--- /dev/null
+++ b/skills/deep-linking-flutter-web/SKILL.md
@@ -0,0 +1,136 @@
+---
+name: deep-linking-for-flutter-web
+description: Learn how to implement proper URL navigation in your Flutter application, including deep linking to specific pages, handling protected routes, and creating custom transitions for a seamless user experience.
+metadata:
+ url: https://rodydavis.com/posts/deep-linking-flutter-web
+ last_modified: Tue, 03 Feb 2026 20:04:25 GMT
+---
+
+# Deep Linking for Flutter Web
+
+
+In this article I will show you how to have proper URL navigation for your application. Open links to specific pages, protected routes and custom transitions.
+
+> **TLDR** The final source [here](https://github.com/rodydavis/flutter_deep_linking) and an online [demo](https://rodydavis.github.io/flutter_deep_linking/).
+
+## Setup
+
+Create a new flutter project called “flutter\_deep\_linking”
+
+Open that folder up in VSCode.
+
+Update your “pubspec.yaml” with the following:
+
+
+
+## Step 1
+
+Create a file at “lib/ui/home/screen.dart” and add the following:
+
+
+
+Update your “lib/main.dart” with the following:
+
+
+
+Run your application and you should see the following:
+
+
+
+## Step 2
+
+Now we need to grab the url the user enters into the address bar.
+
+Create a folder at this location “lib/plugins/navigator”
+
+Create a file inside named: “web.dart” with the following:
+
+
+
+Create a file inside named: “unsupported.dart” with the following:
+
+
+
+Create a file inside named: “navigator.dart” with the following:
+
+
+
+Now go back to your “lib/main.dart” file and add the navigator:
+
+
+
+> It’s important to import the navigator as shown as this will have the conditional import for web compiling.
+
+If you run the app now nothing should change.
+
+## Step 3
+
+Now let’s add the proper routing.
+
+Create a new file “lib/ui/router.dart” and add the following:
+
+
+
+Also update “lib/main.dart” with the following:
+
+
+
+> Notice how we removed the “home” field for MaterialApp. This is because the router will handle everything. By default we will go home on “/”
+
+## Step 4
+
+Now let’s add multiple screens to put this to the test! Add the following folders and files.
+
+Create a file “lib/ui/account/screen.dart” and add the following:
+
+
+
+Create a file “lib/ui/settings/screen.dart” and add the following:
+
+
+
+Create a file “lib/ui/about/screen.dart” and add the following:
+
+
+
+Add the following to “lib/ui/router.dart”:
+
+
+
+Now when you navigate to /about, /account and /settings you will go to the new pages!
+
+
+
+## Step 5
+
+Now let’s tie into the browser navigation buttons! Update “lib/ui/home/screen.dart” with the following:
+
+
+
+Now when you run the application and click on the settings icon it will launch the new screen as expected. But if you click your browsers back button it will go back to the home screen!
+
+
+
+
+
+## Step 6
+
+These urls are great but what if you want to pass data such as an ID that is not known ahead of time? No worries!
+
+Update “lib/ui/account/screen.dart” with the following:
+
+
+
+Let’s update our “lib/ui/router.dart” with the following:
+
+
+
+Now when you run your application and navigate to “/account/40” you will see the following:
+
+
+
+## Conclusion
+
+Dynamic routes work great for Flutter web, you just need to know what to tweak! This package uses a forked version of fluro for some fixes I added but once the PRs is merged you can just use the regular package. Let me know what you think below and if there is a better way I am not seeing!
+
+Here is the final code: [https://github.com/rodydavis/flutter\_deep\_linking](https://github.com/rodydavis/flutter_deep_linking)
\ No newline at end of file
diff --git a/skills/display-html-in-flutter/SKILL.md b/skills/display-html-in-flutter/SKILL.md
new file mode 100644
index 0000000..097677e
--- /dev/null
+++ b/skills/display-html-in-flutter/SKILL.md
@@ -0,0 +1,89 @@
+---
+name: displaying-html-in-flutter
+description: Easily display and interact with HTML content in your Flutter app using the `easy_web_view` package, which supports both web and mobile platforms.
+metadata:
+ url: https://rodydavis.com/posts/display-html-in-flutter
+ last_modified: Tue, 03 Feb 2026 20:04:25 GMT
+---
+
+# Displaying HTML in Flutter
+
+
+Sometimes you have content in HTML that needs to be displayed and interacted with in Flutter.
+
+Online Demo:[https://rodydavis.github.io/easy\_web\_view/#/](https://rodydavis.github.io/easy_web_view/#/)
+
+
+
+For those impatient I created a package for you to get all the following functionally and more here: [https://pub.dev/packages/easy\_web\_view](https://pub.dev/packages/easy_web_view)
+
+## Getting Started
+
+Create a new flutter project named whatever you want.
+
+> If you plan on showing HTML content on iOS/Android you will need to add the following to your pubspec.yaml
+
+```
+dependencies:
+ webview_flutter: ^0.3.15+1
+```
+
+## Web
+
+Reference: [/lib/src/web.dart](https://github.com/rodydavis/easy_web_view/blob/master/lib/src/web.dart)
+
+To show html on Flutter web we need to use an HTMLElementView. This is a platform view that allows us to display native content.
+
+We first need to register the Element and add all the options we need. Here we are creating an iFrame element and setting the source based on if it is markdown, html or a url.
+
+
+
+To display valid HTML you can set the src field to the following:
+
+```
+_src = "data:text/html;charset=utf-8," + Uri.encodeComponent("HTML_CONTENT_HERE");
+```
+
+> For the package you can also pass markdown to the src and it will convert it for you.
+
+After you call the setup method it is now time to display your new platform view:
+
+
+
+You need to use the same viewType string as you registered for “registerViewFactory” method earlier.
+
+Finally you need to wrap it in a container or sized box with an explicit width and height!
+
+## Mobile
+
+Reference: [https://github.com/rodydavis/easy\_web\_view/blob/master/lib/src/mobile.dart](https://github.com/rodydavis/easy_web_view/blob/master/lib/src/mobile.dart)
+
+Mobile setup should be easier. Let’s add a method for updating the url that we will pass to the web view.
+
+
+
+Create the controller:
+
+```
+WebViewController _controller;
+```
+
+And when ever the src changes call this method:
+
+```
+_controller.loadUrl(_updateUrl(widget.src), headers: widget.headers);
+```
+
+Finally lets show the html in the widget tree:
+
+
+
+## Conclusion
+
+If you want to see a complete example and advanced use case view the source here: [https://github.com/rodydavis/easy\_web\_view](https://github.com/rodydavis/easy_web_view)
+
+And if you just want to have it all done for you use this package: [https://pub.dev/packages/easy\_web\_view](https://pub.dev/packages/easy_web_view)
+
+Feel free to make PRs if you have anything that could help make it better too (Or if you find bugs).
+
+When you show HTML this way you will find that you can interact, select text and work with it just like you would it it were a regular web page. If you are using the package you can also just pass embedded content or html elements too without needing a full html valid file (YouTube video for example).
\ No newline at end of file
diff --git a/skills/export-sqlite-dart/SKILL.md b/skills/export-sqlite-dart/SKILL.md
new file mode 100644
index 0000000..eaa9026
--- /dev/null
+++ b/skills/export-sqlite-dart/SKILL.md
@@ -0,0 +1,144 @@
+---
+name: how-to-export-sqlite-tables-to-create-statements
+description: Learn how to export your entire SQLite database schema, including tables and indexes, into runnable CREATE statements at runtime using Flutter and the `sqlite3` package.
+metadata:
+ url: https://rodydavis.com/posts/export-sqlite-dart
+ last_modified: Tue, 03 Feb 2026 20:04:37 GMT
+---
+
+# How to Export SQLite Tables to CREATE Statements
+
+
+In this article I will show you how to export all the tables and indexes in a [SQLite](https://www.sqlite.org/index.html) database to CREATE statements at runtime.
+
+## Getting started
+
+Start by creating a new directory and [Flutter](https://flutter.dev/) project:
+
+```
+mkdir sqlite_introspect
+cd sqlite_introspect
+flutter create .
+flutter pub add sqlite3 mustache_template
+```
+
+This will add the `sqlite3` package which uses FFI to call the native executable and mustache that we will use for templates later.
+
+## Creating the database
+
+Creating the database can be done either in memory or based on a local file. For this example we will use in memory:
+
+```
+final Database db = sqlite3.openInMemory();
+```
+
+Don't forget to dispose of the database after use:
+
+```
+db.dispose();
+```
+
+## Defining the template
+
+Since we will be using [Mustache](https://mustache.github.io/) we can define the variables that we will pass to the template as JSON.
+
+Create a `TableInfo` class that will store the fields and indexes:
+
+```
+class TableInfo {
+ final String name;
+ final List