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:
- Export your 3D model as a
.glbfile (from Blender, Cinema 4D, etc.) - Drop the
.glbfile into your post folder (same location asindex.mdx) - Create
ModelViewer.astrowrapper file (copy the code above) - Update the import path to match your
.glbfilename:import myModel from "./your-model.glb?url" - 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 "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
// Your custom component
function AnimatedSphere({ position, color }) {
const meshRef = useRef();
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.x += delta _ 0.3;
meshRef.current.position.y = position[1] + Math.sin(state.clock.elapsedTime) _ 0.5;
}
});
return (
<mesh ref={meshRef} position={position}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color={color} />
</mesh>
); }
// Scene composition
function Scene() {
return (
<>
<color attach="background" args={["#111111"]} />
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} />
{/* Add your 3D objects here */}
<AnimatedSphere position={[-2, 0, 0]} color="#ff6b6b" />
<AnimatedSphere position={[0, 0, 0]} color="#4ecdc4" />
<AnimatedSphere position={[2, 0, 0]} color="#feca57" />
</>
);
}
export default function MyCustomScene({ height = "400px" }) {
return (
<div style={{ width: "100%", height }}>
<Suspense fallback={<div>Loading...</div>}>
<Canvas camera={{ position: [0, 2, 8], fov: 75 }}>
<Scene />
<OrbitControls />
</Canvas>
</Suspense>
</div>
);
}Step 2: Create MyCustomSceneIsland.astro wrapper
---
import MyCustomScene from "./MyCustomScene.jsx";
const { height = "400px" } = Astro.props;
---
<MyCustomScene height={height} client:visible />Step 3: Import in your MDX
import MyCustomSceneIsland from "./MyCustomSceneIsland.astro";
<MyCustomSceneIsland height="400px" />Loading Models in Custom Scenes
Reference models from the same folder:
import { useGLTF } from "@react-three/drei";
import modelUrl from "./your-model.glb?url";
function Model() {
const { scene } = useGLTF(modelUrl);
return <primitive object={scene} scale={1} />;
}3. Shared Components (Optional)
Rarely needed, but available. Import from src/components/three/ for reusable scenes across posts.
import ThreeCanvasIsland from "../../../../components/three/ThreeCanvasIsland.astro";
<ThreeCanvasIsland height="350px" />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.