Ian Boyle
Creative Generalist
Oakland, California.

Quick reference for adding 3D content to blog posts.


1. Import a Model (No-Frills)

Just want to showcase a .glb model with auto-rotate? Create a wrapper in your post folder:

---
// ModelViewer.astro (create this file beside your index.mdx)
import ModelViewerSceneIsland from "../../../../components/three/ModelViewerSceneIsland.astro";
import myModel from "./your-model.glb?url"; // Replace with your .glb filename

const { height = "500px" } = Astro.props;
---

<ModelViewerSceneIsland height={height} modelPath={myModel} />

Then import it in your MDX:

import ModelViewer from "./ModelViewer.astro";

<ModelViewer height="500px" />

That’s it! Here’s the complete workflow:

  1. Export your 3D model as a .glb file (from Blender, Cinema 4D, etc.)
  2. Drop the .glb file into your post folder (same location as index.mdx)
  3. Create ModelViewer.astro wrapper file (copy the code above)
  4. Update the import path to match your .glb filename: import myModel from "./your-model.glb?url"
  5. Import in your MDX and use: <ModelViewer height="500px" />

Example file structure:

src/content/blog/2025/my-post/
├── index.mdx                 ← Your blog post
├── ModelViewer.astro         ← The wrapper you create
└── robot-animation.glb       ← Your 3D model

The ?url suffix tells Vite to treat the .glb as a static asset and return its public URL.


2. Build a Custom Scene

Want to create custom Three.js animations? Start with this template:

Step 1: Create MyCustomScene.jsx in your post folder

import React, { useRef, Suspense } from &quot;react&quot;;
import { Canvas, useFrame } from &quot;@react-three/fiber&quot;;
import { OrbitControls } from &quot;@react-three/drei&quot;;

// Your custom component
function AnimatedSphere({ position, color }) {
const meshRef = useRef();

useFrame((state, delta) =&gt; {
if (meshRef.current) {
meshRef.current.rotation.x += delta _ 0.3;
meshRef.current.position.y = position[1] + Math.sin(state.clock.elapsedTime) _ 0.5;
}
});

return (

&lt;mesh ref={meshRef} position={position}&gt;
&lt;sphereGeometry args={[1, 32, 32]} /&gt;
&lt;meshStandardMaterial color={color} /&gt;
&lt;/mesh&gt;
); }

// Scene composition
function Scene() {
return (

&lt;&gt;
&lt;color attach=&quot;background&quot; args={[&quot;#111111&quot;]} /&gt;
&lt;ambientLight intensity={0.5} /&gt;
&lt;directionalLight position={[5, 5, 5]} intensity={1} /&gt;

    {/* Add your 3D objects here */}
    &lt;AnimatedSphere position={[-2, 0, 0]} color=&quot;#ff6b6b&quot; /&gt;
    &lt;AnimatedSphere position={[0, 0, 0]} color=&quot;#4ecdc4&quot; /&gt;
    &lt;AnimatedSphere position={[2, 0, 0]} color=&quot;#feca57&quot; /&gt;
  &lt;/&gt;

);
}

export default function MyCustomScene({ height = &quot;400px&quot; }) {
return (
  &lt;div style={{ width: &quot;100%&quot;, height }}&gt;
    &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
      &lt;Canvas camera={{ position: [0, 2, 8], fov: 75 }}&gt;
        &lt;Scene /&gt;
        &lt;OrbitControls /&gt;
      &lt;/Canvas&gt;
    &lt;/Suspense&gt;
  &lt;/div&gt;
);
}

Step 2: Create MyCustomSceneIsland.astro wrapper

---
import MyCustomScene from &quot;./MyCustomScene.jsx&quot;;
const { height = &quot;400px&quot; } = Astro.props;
---
&lt;MyCustomScene height={height} client:visible /&gt;

Step 3: Import in your MDX

import MyCustomSceneIsland from &quot;./MyCustomSceneIsland.astro&quot;;

&lt;MyCustomSceneIsland height=&quot;400px&quot; /&gt;

Loading Models in Custom Scenes

Reference models from the same folder:

import { useGLTF } from &quot;@react-three/drei&quot;;
import modelUrl from &quot;./your-model.glb?url&quot;;

function Model() {
const { scene } = useGLTF(modelUrl);
return &lt;primitive object={scene} scale={1} /&gt;;
}

3. Shared Components (Optional)

Rarely needed, but available. Import from src/components/three/ for reusable scenes across posts.

import ThreeCanvasIsland from &quot;../../../../components/three/ThreeCanvasIsland.astro&quot;;

&lt;ThreeCanvasIsland height=&quot;350px&quot; /&gt;

Note: Changes to shared components affect all posts using them.


Quick Reference

Import Model: Create wrapper → point to .glb?url → import in MDX Custom Scene: Create .jsx + .astro wrapper → import in MDX Models in Scenes: import model from "./model.glb?url"

Always import .astro files in MDX, never .jsx directly.