diff --git a/package.json b/package.json
index b408641b1ffba0c46f556767b065500e7095bf0b..f073c1777da15559033792bec3e0a0a2e41df1ea 100644
--- a/package.json
+++ b/package.json
@@ -96,13 +96,13 @@
     "redux-throttle": "0.1.1",
     "rimraf": "^2.6.1",
     "scratch-audio": "0.1.0-prerelease.20180625202813",
-    "scratch-blocks": "0.1.0-prerelease.1535662135",
+    "scratch-blocks": "0.1.0-prerelease.1537303399",
     "scratch-l10n": "3.0.20180918211645",
     "scratch-paint": "0.2.0-prerelease.20180914201930",
-    "scratch-render": "0.1.0-prerelease.20180907144714",
+    "scratch-render": "0.1.0-prerelease.20180918201144",
     "scratch-storage": "1.0.2",
     "scratch-svg-renderer": "0.2.0-prerelease.20180907141232",
-    "scratch-vm": "0.2.0-prerelease.20180912222010",
+    "scratch-vm": "0.2.0-prerelease.20180918201814",
     "selenium-webdriver": "3.6.0",
     "startaudiocontext": "1.2.1",
     "style-loader": "^0.23.0",
diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx
index fd4caef5f93590ddcd5878920ca4c084d292545c..864c4848fd774a32c9bd712fb2d0883d185b47aa 100644
--- a/src/components/language-selector/language-selector.jsx
+++ b/src/components/language-selector/language-selector.jsx
@@ -5,7 +5,7 @@ import locales from 'scratch-l10n';
 import styles from './language-selector.css';
 
 // supported languages to exclude from the menu, but allow as a URL option
-const ignore = ['he'];
+const ignore = [];
 
 const LanguageSelector = ({currentLocale, label, onChange}) => (
     <select
diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx
index 01e617c8e77385244507977b037c7f31a918a26d..9b154c59b0ae97a85522865db8cec792fd1ad65b 100644
--- a/src/containers/sprite-selector-item.jsx
+++ b/src/containers/sprite-selector-item.jsx
@@ -32,7 +32,17 @@ class SpriteSelectorItem extends React.Component {
         ]);
         this.svgRenderer = new SVGRenderer();
         // Asset ID of the SVG currently in SVGRenderer
-        this.svgRendererAssetId = null;
+        this.decodedAssetId = null;
+    }
+    shouldComponentUpdate (nextProps) {
+        // Ignore dragPayload due to https://github.com/LLK/scratch-gui/issues/3172.
+        // This function should be removed once the issue is fixed.
+        for (const property in nextProps) {
+            if (property !== 'dragPayload' && this.props[property] !== nextProps[property]) {
+                return true;
+            }
+        }
+        return false;
     }
     getCostumeUrl () {
         if (this.props.costumeURL) return this.props.costumeURL;
@@ -44,18 +54,20 @@ class SpriteSelectorItem extends React.Component {
         // Avoid parsing the SVG when possible, since it's expensive.
         if (asset.assetType === storage.AssetType.ImageVector) {
             // If the asset ID has not changed, no need to re-parse
-            if (this.svgRendererAssetId === this.props.assetId) {
+            if (this.decodedAssetId === this.props.assetId) {
+                // @todo consider caching more than one URL.
                 return this.cachedUrl;
             }
-
+            this.decodedAssetId = this.props.assetId;
             const svgString = this.props.vm.runtime.storage.get(this.props.assetId).decodeText();
             if (svgString.match(HAS_FONT_REGEXP)) {
-                this.svgRendererAssetId = this.props.assetId;
                 this.svgRenderer.loadString(svgString);
                 const svgText = this.svgRenderer.toString(true /* shouldInjectFonts */);
                 this.cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
-                return this.cachedUrl;
+            } else {
+                this.cachedUrl = this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI();
             }
+            return this.cachedUrl;
         }
         return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI();
     }
diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx
index 1ab84412eed16a676b3e9e8fcf04d2b9cd0c6f78..0aa3f2a4b64ef883e29cf0b47ff0a4f1d8d93cd3 100644
--- a/src/containers/stage.jsx
+++ b/src/containers/stage.jsx
@@ -52,9 +52,9 @@ class Stage extends React.Component {
             colorInfo: null,
             question: null
         };
-        if (this.props.vm.runtime.renderer) {
-            this.renderer = this.props.vm.runtime.renderer;
-            this.canvas = this.props.vm.runtime.renderer._gl.canvas;
+        if (this.props.vm.renderer) {
+            this.renderer = this.props.vm.renderer;
+            this.canvas = this.renderer.canvas;
         } else {
             this.canvas = document.createElement('canvas');
             this.renderer = new Renderer(this.canvas);
diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx
index fe7ac023eab97c5df5aea8e5829a6769d28392b5..93d97180ef38526ac05feaf019338aa32ed4496b 100644
--- a/src/lib/app-state-hoc.jsx
+++ b/src/lib/app-state-hoc.jsx
@@ -10,6 +10,7 @@ import {setPlayer, setFullScreen} from '../reducers/mode.js';
 
 import locales from 'scratch-l10n';
 import {detectLocale} from './detect-locale';
+import {detectTutorialId} from './tutorial-from-url';
 
 const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
 
@@ -50,16 +51,27 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
                     guiInitialState,
                     guiMiddleware,
                     initFullScreen,
-                    initPlayer
+                    initPlayer,
+                    initTutorialCard
                 } = guiRedux;
                 const {ScratchPaintReducer} = require('scratch-paint');
 
                 let initializedGui = guiInitialState;
-                if (props.isFullScreen) {
-                    initializedGui = initFullScreen(initializedGui);
-                }
-                if (props.isPlayerOnly) {
-                    initializedGui = initPlayer(initializedGui);
+                if (props.isFullScreen || props.isPlayerOnly) {
+                    if (props.isFullScreen) {
+                        initializedGui = initFullScreen(initializedGui);
+                    }
+                    if (props.isPlayerOnly) {
+                        initializedGui = initPlayer(initializedGui);
+                    }
+                } else {
+                    const tutorialId = detectTutorialId();
+                    if (tutorialId !== null) {
+                        // When loading a tutorial from the URL,
+                        // load w/o preview modal
+                        // open requested tutorial card
+                        initializedGui = initTutorialCard(initializedGui, tutorialId);
+                    }
                 }
                 reducers = {
                     locales: localesReducer,
diff --git a/src/lib/libraries/decks/index.jsx b/src/lib/libraries/decks/index.jsx
index 096ca15224a7882aa7fe243efad76bf5b8a736cc..f8324f3ae73068221fe0b86bc68b5eb0757a9db5 100644
--- a/src/lib/libraries/decks/index.jsx
+++ b/src/lib/libraries/decks/index.jsx
@@ -99,7 +99,8 @@ export default {
                 'add-sprite'
             ]
         }
-        ]
+        ],
+        urlId: 1
     },
     'animate-a-name': {
         name: (
@@ -172,7 +173,8 @@ export default {
                 'glide-around'
             ]
         }
-        ]
+        ],
+        urlId: 2
     },
     'Make-Music': {
         name: (
@@ -239,7 +241,8 @@ export default {
                 'add-sprite'
             ]
         }
-        ]
+        ],
+        urlId: 3
     },
     'Make-A-Game': {
         name: (
@@ -323,7 +326,8 @@ export default {
                 'move-around-with-arrow-keys'
             ]
         }
-        ]
+        ],
+        urlId: 4
     },
 
     'Chase-Game': {
@@ -425,7 +429,8 @@ export default {
                 'move-around-with-arrow-keys'
             ]
         }
-        ]
+        ],
+        urlId: 5
     },
     'add-sprite': {
         name: (
@@ -453,7 +458,8 @@ export default {
                     'switch-costume'
                 ]
             }
-        ]
+        ],
+        urlId: 6
     },
     'add-a-backdrop': {
         name: (
@@ -471,7 +477,8 @@ export default {
                 'change-size',
                 'switch-costume'
             ]
-        }]
+        }],
+        urlId: 7
     },
     'change-size': {
         name: (
@@ -489,7 +496,8 @@ export default {
                 'glide-around',
                 'spin-video'
             ]
-        }]
+        }],
+        urlId: 8
     },
     'glide-around': {
         name: (
@@ -507,7 +515,8 @@ export default {
                 'add-a-backdrop',
                 'switch-costume'
             ]
-        }]
+        }],
+        urlId: 9
     },
 
     'record-a-sound': {
@@ -526,8 +535,8 @@ export default {
                 'Make-Music',
                 'switch-costume'
             ]
-        }]
-
+        }],
+        urlId: 10
     },
     'spin-video': {
         name: (
@@ -545,7 +554,8 @@ export default {
                 'add-a-backdrop',
                 'switch-costume'
             ]
-        }]
+        }],
+        urlId: 11
     },
     'hide-and-show': {
         name: (
@@ -563,7 +573,8 @@ export default {
                 'add-a-backdrop',
                 'switch-costume'
             ]
-        }]
+        }],
+        urlId: 12
     },
 
     'switch-costume': {
@@ -582,7 +593,8 @@ export default {
                 'add-a-backdrop',
                 'add-effects'
             ]
-        }]
+        }],
+        urlId: 13
     },
 
     'move-around-with-arrow-keys': {
@@ -601,7 +613,8 @@ export default {
                 'add-a-backdrop',
                 'switch-costume'
             ]
-        }]
+        }],
+        urlId: 14
     },
     'add-effects': {
         name: (
@@ -619,6 +632,7 @@ export default {
                 'add-a-backdrop',
                 'switch-costume'
             ]
-        }]
+        }],
+        urlId: 15
     }
 };
diff --git a/src/lib/tutorial-from-url.js b/src/lib/tutorial-from-url.js
new file mode 100644
index 0000000000000000000000000000000000000000..c627b8db7abf1a709719c12add3b6eec64f13257
--- /dev/null
+++ b/src/lib/tutorial-from-url.js
@@ -0,0 +1,48 @@
+/**
+ * @fileoverview
+ * Utility function to detect tutorial id from query paramenter on the URL.
+ */
+
+import tutorials from './libraries/decks/index.jsx';
+import analytics from './analytics';
+
+/**
+ * Get the tutorial id from the given numerical id (representing the
+ * url id of the tutorial).
+ * @param {number} urlId The URL Id for the tutorial
+ * @returns {string} The string id for the tutorial, or null if the URL ID
+ * was not found.
+ */
+const getDeckIdFromUrlId = urlId => {
+    for (const deckId in tutorials) {
+        if (tutorials[deckId].urlId === urlId) {
+            analytics.event({
+                category: 'how-to',
+                action: 'load from url',
+                label: `${deckId}`
+            });
+            return deckId;
+        }
+    }
+    return null;
+};
+
+/**
+ * Check if there's a tutorial id provided as a query parameter in the URL.
+ * Return the corresponding tutorial id or null if not found.
+ * @return {string} The ID of the requested tutorial or null if no tutorial was
+ * requested or found.
+ */
+const detectTutorialId = () => {
+    if (window.location.search.indexOf('tutorial=') !== -1) {
+        const urlTutorialId = window.location.search.match(/(?:tutorial)=(\d+)/)[1];
+        if (urlTutorialId) {
+            return getDeckIdFromUrlId(Number(urlTutorialId));
+        }
+    }
+    return null;
+};
+
+export {
+    detectTutorialId
+};
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index cfad8f6825ddad3a07a56b4c62757ffbc3a97909..8869c1a35031e1d46b8dd28f4f408d2500946840 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -21,6 +21,8 @@ import vmReducer, {vmInitialState} from './vm';
 import vmStatusReducer, {vmStatusInitialState} from './vm-status';
 import throttle from 'redux-throttle';
 
+import decks from '../lib/libraries/decks/index.jsx';
+
 const guiMiddleware = compose(applyMiddleware(throttle(300, {leading: true, trailing: true})));
 
 const guiInitialState = {
@@ -67,6 +69,27 @@ const initFullScreen = function (currentState) {
     );
 };
 
+const initTutorialCard = function (currentState, deckId) {
+    return Object.assign(
+        {},
+        currentState,
+        {
+            modals: {
+                previewInfo: false
+            },
+            cards: {
+                visible: true,
+                content: decks,
+                activeDeckId: deckId,
+                step: 0,
+                x: 0,
+                y: 0,
+                dragging: false
+            }
+        }
+    );
+};
+
 const guiReducer = combineReducers({
     assetDrag: assetDragReducer,
     blockDrag: blockDragReducer,
@@ -95,5 +118,6 @@ export {
     guiInitialState,
     guiMiddleware,
     initFullScreen,
-    initPlayer
+    initPlayer,
+    initTutorialCard
 };
diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js
index 62199cdcbb0cf2885b969f00a082d9c54b4245b2..f851188c45d705915b946b2a16e6eed5cf0f97af 100644
--- a/test/integration/project-loading.test.js
+++ b/test/integration/project-loading.test.js
@@ -72,7 +72,7 @@ describe('Loading scratch gui', () => {
             const projectId = '96708228';
             await loadUri(`${uri}#${projectId}`);
             await clickXpath('//button[@title="Try It"]');
-            await new Promise(resolve => setTimeout(resolve, 2000));
+            await new Promise(resolve => setTimeout(resolve, 3000));
             await clickXpath('//img[@title="Go"]');
             await new Promise(resolve => setTimeout(resolve, 2000));
             await clickXpath('//img[@title="Stop"]');