diff --git a/packages/model-viewer-effects/README.md b/packages/model-viewer-effects/README.md
index 86e6b5aefd..f98e7bbc8c 100644
--- a/packages/model-viewer-effects/README.md
+++ b/packages/model-viewer-effects/README.md
@@ -46,13 +46,13 @@ npm install three @google/model-viewer @google/model-viewer-effects
```html
-
+
diff --git a/packages/model-viewer/README.md b/packages/model-viewer/README.md
index c283cc9c16..e3523ec95b 100644
--- a/packages/model-viewer/README.md
+++ b/packages/model-viewer/README.md
@@ -43,7 +43,7 @@ import '@google/model-viewer';
It can also be used directly from various free CDNs such as [jsDelivr](https://www.jsdelivr.com/package/npm/@google/model-viewer) and Google's own [hosted libraries](https://developers.google.com/speed/libraries#model-viewer):
```html
-
+
```
For more detailed usage documentation and live examples, please visit our docs
diff --git a/packages/modelviewer.dev/data/docs.json b/packages/modelviewer.dev/data/docs.json
index 2648e19ca0..accd066064 100644
--- a/packages/modelviewer.dev/data/docs.json
+++ b/packages/modelviewer.dev/data/docs.json
@@ -136,7 +136,7 @@
{
"name": "lottieLoaderLocation",
"htmlName": "lottieLoaderLocation",
- "description": "This static, writable property sets <model-viewer>'s LottieLoader location URL. The default URL is https://cdn.jsdelivr.net/npm/three@0.174.0/examples/jsm/loaders/LottieLoader.js. It will also require the server to provide the lottie canvas module at ../libs/lottie_canvas.module.js."
+ "description": "This static, writable property sets <model-viewer>'s LottieLoader location URL. The default URL is https://cdn.jsdelivr.net/npm/three@{{THREEJS_VERSION}}/examples/jsm/loaders/LottieLoader.js. It will also require the server to provide the lottie canvas module at ../libs/lottie_canvas.module.js."
},
{
"name": "minimumRenderScale",
diff --git a/packages/modelviewer.dev/data/faq.json b/packages/modelviewer.dev/data/faq.json
index 084d461fb2..75bf9478fb 100644
--- a/packages/modelviewer.dev/data/faq.json
+++ b/packages/modelviewer.dev/data/faq.json
@@ -86,7 +86,7 @@
{
"name": "How should I access <model-viewer>?",
"htmlName": "cdn",
- "description": "If you control your own hosting, the safest option is always to host model-viewer.min.js yourself on the same server as your site. For smaller sites and blogs, it is often more convenient to use one of various free CDNs - Google provides <model-viewer> as one of its hosted libraries, which we recommend as it is a fast and reliable CDN. Simply specify your desired version in the URL: https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js. We used to recommend unpkg, but it has had several serious outages recently. It can automatically pick the most recent version, but this is not a good practice as it slows loading (two requests) and ideally you should test when updating to ensure no bugs have been introduced. Another good option is jsDelivr.",
+ "description": "If you control your own hosting, the safest option is always to host model-viewer.min.js yourself on the same server as your site. For smaller sites and blogs, it is often more convenient to use one of various free CDNs - Google provides <model-viewer> as one of its hosted libraries, which we recommend as it is a fast and reliable CDN. Simply specify your desired version in the URL: https://ajax.googleapis.com/ajax/libs/model-viewer/{{MODELVIEWER_VERSION}}/model-viewer.min.js. We used to recommend unpkg, but it has had several serious outages recently. It can automatically pick the most recent version, but this is not a good practice as it slows loading (two requests) and ideally you should test when updating to ensure no bugs have been introduced. Another good option is jsDelivr.",
"links": [
"Google Hosted Libraries",
"jsDelivr"
diff --git a/packages/modelviewer.dev/examples/postprocessing/index.html b/packages/modelviewer.dev/examples/postprocessing/index.html
index 444c5ffbfb..54a94e67d0 100644
--- a/packages/modelviewer.dev/examples/postprocessing/index.html
+++ b/packages/modelviewer.dev/examples/postprocessing/index.html
@@ -33,7 +33,7 @@
@@ -114,7 +114,7 @@
Setup Post Processing
@@ -459,7 +459,7 @@ Custom Effects
+
+
diff --git a/packages/modelviewer.dev/scripts/ci-before-deploy.sh b/packages/modelviewer.dev/scripts/ci-before-deploy.sh
index 2cc39e433e..a6715f422d 100755
--- a/packages/modelviewer.dev/scripts/ci-before-deploy.sh
+++ b/packages/modelviewer.dev/scripts/ci-before-deploy.sh
@@ -135,6 +135,8 @@ done
# Add a "VERSION" file containing the last git commit message
git log -n 1 > $DEPLOY_ROOT/VERSION
+node scripts/update-versions.js
+
git status --ignored
popd
diff --git a/packages/modelviewer.dev/scripts/update-versions.js b/packages/modelviewer.dev/scripts/update-versions.js
new file mode 100644
index 0000000000..1d477329b1
--- /dev/null
+++ b/packages/modelviewer.dev/scripts/update-versions.js
@@ -0,0 +1,208 @@
+#!/usr/bin/env node
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import chalk from 'chalk';
+
+// ESM module resolution
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// ============================================================
+// CONFIGURATION
+// ============================================================
+
+const CONFIG = {
+ packagePaths: {
+ modelViewer: path.resolve(__dirname, '../../model-viewer/package.json'),
+ effects: path.resolve(__dirname, '../../model-viewer-effects/package.json'),
+ },
+ targetFiles: [
+ '../dist/index.html',
+ '../dist/data/faq.json',
+ '../dist/data/docs.json',
+ '../dist/examples/postprocessing/index.html',
+ '../dist/examples/twitter/player.html',
+ ].map(file => path.resolve(__dirname, file)),
+ placeholders: {
+ modelViewer: '{{MODELVIEWER_VERSION}}',
+ three: '{{THREEJS_VERSION}}',
+ postprocessing: '{{POSTPROCESSING_VERSION}}',
+ },
+};
+
+// ============================================================
+// LOGGER UTILITIES
+// ============================================================
+
+const logger = {
+ success: (msg) => console.log(chalk.green(`✓ ${msg}`)),
+ error: (msg) => console.log(chalk.red(`✗ ${msg}`)),
+ warning: (msg) => console.log(chalk.yellow(`⚠ ${msg}`)),
+ info: (msg) => console.log(chalk.blue(`ℹ ${msg}`)),
+ separator: () => console.log(''),
+};
+
+// ============================================================
+// FILE OPERATIONS
+// ============================================================
+
+/**
+ * Reads and parses a JSON file safely
+ * @param {string} filePath - Path to JSON file
+ * @returns {Object|null} Parsed JSON or null on error
+ */
+const readJsonFile = (filePath) => {
+ try {
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`File not found: ${filePath}`);
+ }
+ const content = fs.readFileSync(filePath, 'utf8');
+ return JSON.parse(content);
+ } catch (error) {
+ logger.error(`Error reading ${filePath}: ${error.message}`);
+ return null;
+ }
+};
+
+/**
+ * Replaces placeholders in a file with actual values
+ * @param {string} filePath - Target file path
+ * @param {Object} replacements - Key-value pairs for replacement
+ * @returns {boolean} Success status
+ */
+const replacePlaceholdersInFile = (filePath, replacements) => {
+ if (!fs.existsSync(filePath)) {
+ logger.warning(`File ${filePath} does not exist - skipped`);
+ return false;
+ }
+
+ try {
+ let content = fs.readFileSync(filePath, 'utf8');
+ let hasChanges = false;
+
+ for (const [placeholder, value] of Object.entries(replacements)) {
+ if (content.includes(placeholder)) {
+ content = content.replaceAll(placeholder, value);
+ hasChanges = true;
+ } else {
+ logger.warning(`Placeholder ${placeholder} not found in ${path.basename(filePath)}`);
+ }
+ }
+
+ if (hasChanges) {
+ fs.writeFileSync(filePath, content, 'utf8');
+ logger.success(`Updated: ${path.basename(filePath)}`);
+ return true;
+ }
+
+ return false;
+ } catch (error) {
+ logger.error(`Error processing ${filePath}: ${error.message}`);
+ return false;
+ }
+};
+
+// ============================================================
+// VERSION EXTRACTION
+// ============================================================
+
+/**
+ * Extracts package version from package.json
+ * @param {Object} packageJson - Parsed package.json
+ * @param {string} packageName - Name of the package
+ * @returns {string} Cleaned version string
+ */
+const extractPackageVersion = (packageJson, packageName) => {
+ const version =
+ packageJson.dependencies?.[packageName] ||
+ packageJson.devDependencies?.[packageName] ||
+ '';
+
+ return version.replace(/^[^\d]*/, ''); // Remove leading symbols (^, ~, etc.)
+};
+
+/**
+ * Collects all required versions from package.json files
+ * @returns {Object|null} Version object or null on error
+ */
+const collectVersions = () => {
+ const modelViewerPkg = readJsonFile(CONFIG.packagePaths.modelViewer);
+ const effectsPkg = readJsonFile(CONFIG.packagePaths.effects);
+
+ if (!modelViewerPkg || !effectsPkg) {
+ return null;
+ }
+
+ const versions = {
+ three: extractPackageVersion(modelViewerPkg, 'three'),
+ modelViewer: modelViewerPkg.version || '',
+ postprocessing: extractPackageVersion(effectsPkg, 'postprocessing'),
+ };
+
+ // Validate all versions are present
+ const missingVersions = Object.entries(versions)
+ .filter(([_, version]) => !version)
+ .map(([key]) => key);
+
+ if (missingVersions.length > 0) {
+ logger.error(`Missing versions: ${missingVersions.join(', ')}`);
+ return null;
+ }
+
+ return versions;
+};
+
+// ============================================================
+// MAIN EXECUTION
+// ============================================================
+
+/**
+ * Main execution function
+ */
+const main = () => {
+ logger.info('Starting version replacement...');
+ logger.separator();
+
+ // Collect versions
+ const versions = collectVersions();
+ if (!versions) {
+ process.exit(1);
+ }
+
+ // Display versions
+ logger.info(`three.js: ${versions.three}`);
+ logger.info(`model-viewer: ${versions.modelViewer}`);
+ logger.info(`postprocessing: ${versions.postprocessing}`);
+ logger.separator();
+
+ // Prepare replacements
+ const replacements = {
+ [CONFIG.placeholders.three]: versions.three,
+ [CONFIG.placeholders.modelViewer]: versions.modelViewer,
+ [CONFIG.placeholders.postprocessing]: versions.postprocessing,
+ };
+
+ // Process all target files
+ const results = CONFIG.targetFiles.map(file =>
+ replacePlaceholdersInFile(file, replacements)
+ );
+
+ const successCount = results.filter(Boolean).length;
+ const totalCount = CONFIG.targetFiles.length;
+
+ // Final summary
+ logger.separator();
+ if (successCount === totalCount) {
+ logger.success(`All ${totalCount} files updated successfully!`);
+ } else if (successCount > 0) {
+ logger.warning(`${successCount} out of ${totalCount} files updated`);
+ } else {
+ logger.error('No files were updated!');
+ process.exit(1);
+ }
+};
+
+// Execute
+main();
\ No newline at end of file
diff --git a/packages/space-opera/src/components/best_practices/constants.ts b/packages/space-opera/src/components/best_practices/constants.ts
index bd140fd1a6..0dad1cc791 100644
--- a/packages/space-opera/src/components/best_practices/constants.ts
+++ b/packages/space-opera/src/components/best_practices/constants.ts
@@ -28,7 +28,7 @@ export const modelViewerTemplate = `
REPLACEME
-
+