This guide explains how to deploy your Shinmun blog to GitHub Pages, allowing you to host your blog for free on GitHub’s infrastructure. It also covers Shinmun’s powerful TypeScript and React integration features with interactive demos.
Shinmun includes a static site exporter that generates plain HTML files from your blog. These files can be hosted on GitHub Pages, which serves static content directly from your repository.
One of Shinmun’s most powerful features is its ability to embed TypeScript mini apps and React components directly in your Markdown pages. This makes it perfect for creating interactive documentation, tutorials, and technical blogs.
cd your-blog-directory
shinmun export docs
Commit and push the docs folder to GitHub
Enable GitHub Pages in your repository settings, selecting the docs folder as the source
The shinmun export command generates a static HTML version of your entire blog. By default, it exports to a _site directory, but for GitHub Pages, export to a docs folder:
shinmun export docs
This command will:
index.html for your homepageindex.rss)public directoryInitialize a Git repository if you haven’t already:
git init
git add .
git commit -m "Initial commit with Shinmun blog"
Create a GitHub repository and push your code:
git remote add origin https://github.com/yourusername/your-blog.git
git branch -M main
git push -u origin main
Your blog will be available at https://yourusername.github.io/your-blog/ within a few minutes.
For a more automated workflow, you can use GitHub Actions to build and deploy your site whenever you push changes. This approach doesn’t require committing the docs folder—instead, the site is built and deployed automatically on each push.
Create .github/workflows/deploy.yml:
name: Deploy to GitHub Pages
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 Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2.0'
bundler-cache: true
- name: Build site
run: bundle exec shinmun export _site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
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
Now your site will automatically rebuild and deploy whenever you push to the main branch.
To use a custom domain with your Shinmun blog:
CNAME file to your public directory containing your domain:yourdomain.com
yourusername.github.ioIn repository Settings → Pages, enter your custom domain
Shinmun’s most powerful feature is its seamless integration of TypeScript and React components directly into your Markdown pages. This enables you to create interactive tutorials, live code demos, and dynamic content without leaving your blog workflow.
Before using TypeScript features, install esbuild in your blog directory:
npm install esbuild
This allows Shinmun to compile TypeScript code on-the-fly during the build process.
The simplest way to add interactivity is with inline TypeScript blocks. Use the @@typescript syntax to embed code that runs directly in the browser.
@@typescript[container-id]
// Your TypeScript code here
const element = document.getElementById('container-id')!;
element.innerHTML = '<p>Hello from TypeScript!</p>';
The [container-id] creates a <div> with that ID where your app can render.
Here’s a simple TypeScript app that displays a greeting:
const user: User = { name: 'Developer', role: 'admin' };
const container = document.getElementById('greeting-demo')!;
const roleEmoji: Record<User['role'], string> = {
admin: '👑',
user: '👤',
guest: '👋'
};
container.innerHTML = `
<div style="padding: 1rem; background: #f0f9ff; border-radius: 8px; border-left: 4px solid #0284c7;">
<p style="margin: 0; font-size: 1.1rem;">
${roleEmoji[user.role]} Welcome, <strong>${user.name}</strong>!
</p>
<p style="margin: 0.5rem 0 0; color: #64748b; font-size: 0.9rem;">
You're logged in as: <code>${user.role}</code>
</p>
</div>
`;
const state: State = { count: 0, history: [] };
const container = document.getElementById('counter-demo')!;
function render(): void {
container.innerHTML = `
<div style="padding: 1.5rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; color: white; text-align: center;">
<div style="font-size: 3rem; font-weight: bold; margin-bottom: 1rem;">${state.count}</div>
<div style="margin-bottom: 1rem;">
<button onclick="window.decrement()" style="padding: 0.5rem 1.5rem; margin: 0.25rem; font-size: 1.2rem; cursor: pointer; border: none; border-radius: 6px; background: rgba(255,255,255,0.2); color: white;">−</button>
<button onclick="window.reset()" style="padding: 0.5rem 1.5rem; margin: 0.25rem; font-size: 1.2rem; cursor: pointer; border: none; border-radius: 6px; background: rgba(255,255,255,0.2); color: white;">↺</button>
<button onclick="window.increment()" style="padding: 0.5rem 1.5rem; margin: 0.25rem; font-size: 1.2rem; cursor: pointer; border: none; border-radius: 6px; background: rgba(255,255,255,0.2); color: white;">+</button>
</div>
<div style="font-size: 0.8rem; opacity: 0.8;">History: [${state.history.slice(-5).join(', ')}]</div>
</div>
`;
}
(window as any).increment = (): void => { state.history.push(state.count); state.count++; render(); };
(window as any).decrement = (): void => { state.history.push(state.count); state.count--; render(); };
(window as any).reset = (): void => { state.history.push(state.count); state.count = 0; render(); };
render();
function updateClock(): void {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
clockContainer.innerHTML = `
<div style="font-family: 'SF Mono', monospace; background: #1a1a2e; padding: 1.5rem; border-radius: 12px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
<div style="font-size: 3rem; color: #00ff88; text-shadow: 0 0 20px #00ff8844;">
${hours}:${minutes}:<span style="color: #ff6b6b;">${seconds}</span>
</div>
<div style="color: #64748b; font-size: 0.9rem; margin-top: 0.5rem;">
${now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</div>
</div>
`;
}
updateClock();
setInterval(updateClock, 1000);
TypeScript brings powerful type safety to your mini apps. Here are some patterns you can use:
// Define clear interfaces for your data
interface BlogPost {
id: number;
title: string;
author: string;
tags: string[];
published: boolean;
createdAt: Date;
}
// Use union types for controlled values
type Status = 'draft' | 'review' | 'published' | 'archived';
// Use generics for reusable components
type ApiResponse<T> = {
data: T;
error: string | null;
loading: boolean;
};
// Type guards for runtime type checking
function isError(response: unknown): response is { error: string } {
return typeof response === 'object' && response !== null && 'error' in response;
}
// Create types from other types
type ReadonlyPost = Readonly<BlogPost>;
type PartialPost = Partial<BlogPost>;
type PostKeys = keyof BlogPost;
// Pick specific properties
type PostSummary = Pick<BlogPost, 'id' | 'title' | 'author'>;
For more complex interactive components, Shinmun supports embedding React components from external TSX files. This is perfect for reusable UI elements, forms, and data visualizations.
React is automatically loaded from a CDN via import maps. No additional setup is required—just create your TSX files in the public/apps/ directory.
@@typescript-file[container-id](public/apps/component-name.tsx)
This compiles the TSX file with bundling enabled and embeds the result.
This component demonstrates useState, useEffect, CSS-in-JS patterns, and TypeScript interfaces:
Key Features:
'light' | 'dark' | 'ocean' | 'forest')Record<K, V> type for theme configurationsuseState hook for theme state managementuseEffect hook for transition animationsReact.CSSProperties typeInteractive bar chart with auto-update functionality:
Key Features:
useMemo for computed valuesuseEffect for interval-based updates with cleanupA comprehensive form demonstrating TypeScript generics and validation patterns:
Key Features:
Validator<T>)Record<K, V> for field configurationsuseCallback for memoized validationSearch and filter component showcasing useMemo and optimized rendering:
Key Features:
useMemo for optimized filtering/sortingHere’s a quick reference for React hooks commonly used in Shinmun components:
// Basic state
const [count, setCount] = useState(0);
// State with type inference
const [user, setUser] = useState<User | null>(null);
// State with initial function
const [items, setItems] = useState(() => loadFromStorage());
// Run on mount
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);
// Run when dependency changes
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// Interval with cleanup
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
// Expensive computation cached
const sortedItems = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Filtered list
const visibleItems = useMemo(() => {
return items.filter(item => item.category === selectedCategory);
}, [items, selectedCategory]);
// Memoized callback for child components
const handleClick = useCallback((id: number) => {
setSelectedId(id);
}, []);
// Callback with dependencies
const handleSubmit = useCallback(() => {
submitForm(formData);
}, [formData]);
Create a new file in public/apps/:
// public/apps/my-component.tsx
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
interface Props {
initialValue?: number;
}
function MyComponent({ initialValue = 0 }: Props) {
const [value, setValue] = useState(initialValue);
return (
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '8px' }}>
<p>Value: {value}</p>
<button onClick={() => setValue(v => v + 1)}>Increment</button>
</div>
);
}
// Mount to container
const container = document.getElementById('my-component');
if (container) {
const root = createRoot(container);
root.render(<MyComponent />);
}
In your page or post:
@@typescript-file[my-component](public/apps/my-component.tsx)
# Preview locally
rackup
# Export for GitHub Pages
shinmun export docs
Each component file should do one thing well. Split complex UIs into smaller, reusable pieces.
Enable strict mode benefits by defining clear interfaces:
// Good: Clear interface
interface User {
id: number;
name: string;
email: string;
}
// Avoid: Loose typing
const user: any = { ... };
function DataComponent() {
const [data, setData] = useState<Data | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data</div>;
return <div>{/* Render data */}</div>;
}
// Define styles as constants for reusability
const styles = {
container: {
padding: '1rem',
borderRadius: '8px',
} as React.CSSProperties,
button: {
background: '#4a90d9',
color: 'white',
border: 'none',
padding: '0.5rem 1rem',
borderRadius: '4px',
cursor: 'pointer',
} as React.CSSProperties,
};
Always return cleanup functions from effects that create subscriptions or timers:
useEffect(() => {
const subscription = eventSource.subscribe(handler);
return () => subscription.unsubscribe();
}, []);
If you prefer to only export during deployment (using GitHub Actions), add the export directory to .gitignore:
_site/
The typical workflow for updating your blog:
shinmun post "My New Post"
# Edit the created file in posts/YYYY/M/my-new-post.md
rackup
# Visit http://localhost:9292
shinmun export docs
git add .
git commit -m "Add new post"
git push
When hosting on a GitHub Pages project site (not a user/organization site), your blog will be at a subpath like /your-repo/. Make sure your templates use relative paths for assets:
<link rel="stylesheet" href="<%= base_path %>/styles.css">
Ensure your posts have the .html extension when exported. GitHub Pages serves index.html automatically but requires explicit .html for other files.
Check that your CSS paths are correct. If your site is hosted at a subpath, you may need to configure base_path in your config.
Gemfile includes the shinmun gemAfter setup, your repository should look like this:
your-blog/
├── .github/
│ └── workflows/
│ └── deploy.yml
├── config.ru
├── config.yml
├── Gemfile
├── Gemfile.lock
├── pages/
│ └── about.md
├── posts/
│ └── 2024/
│ └── 1/
│ └── my-first-post.md
├── public/
│ ├── styles.css
│ └── CNAME
├── templates/
│ ├── index.rhtml
│ ├── layout.rhtml
│ ├── post.rhtml
│ └── ...
└── docs/ # Generated static site
├── index.html
├── index.rss
└── ...