Portfolio platform · open source
mobinshaterian.com
This portfolio, blog, and publishing system.
A fast, searchable personal publishing platform that brings together a professional profile, 200+ imported articles, video channels, structured data, and static-first SEO.
What it includes
- More than 200 imported engineering articles
- Full-text search, tags, and content discovery
- Server rendering and route prerendering
- Structured data, RSS, sitemap, and social metadata
- Repeatable import pipelines for external publishing channels
- GitHub Pages-compatible static delivery
Languages
Quick start
$ bun install
$ bun run dev
$ bun run buildComplete README
mobinshaterian.com
Personal portfolio and technical blog for Mobin Shaterian. The site presents a resume-style home page and a searchable archive of 200+ articles imported from Medium.
The application is built with React and TanStack Start, then pre rendered to static HTML for deployment to GitHub Pages. Blog content is stored as JSON in the repository, so the site does not need a database, CMS, or production API.
What the site contains
/— portfolio page with profile, experience, skills, education, recent writing, and contact details/blogs— full blog archive with tag filters and client-side full-text search/blog/:slug— an individual article rendered from local JSON/data/posts-index.json— public, machine-readable post metadata/data/posts/:slug.json— public JSON representation of an article- Custom application and article 404 pages
- SEO metadata, Open Graph fields, canonical Medium links, and article JSON-LD
Technology stack
Application framework
| Technology | Purpose in this project |
|---|---|
| React 19 | Builds the user interface from components. |
| TypeScript | Adds static types to the React and server code. Strict mode is enabled. |
| TanStack Start | Full-stack React framework responsible for server rendering, application startup, and prerendering. |
| TanStack Router | Type-safe, file-based routing, route loaders, URL search parameters, metadata, 404 handling, and scroll restoration. |
| TanStack Query | Provides the application-level QueryClient. It is ready for cached asynchronous data, although the current pages mainly use local JSON. |
| Vite | Development server, hot module replacement, and production build pipeline. |
| Nitro | Server/build engine used by TanStack Start. The configured node-server preset produces the .output build, including .output/public for static hosting. |
Styling and UI
| Technology | Purpose in this project |
|---|---|
| Tailwind CSS 4 | Utility-first styling, loaded through the Vite plugin and src/styles.css. |
| shadcn/ui | Project convention and configuration for reusable UI components. Components are copied into src/components/ui, rather than installed as a single runtime package. |
| Radix UI | Accessible primitives underlying the shadcn components. |
| Lucide React | Icons used throughout the portfolio and blog pages. |
class-variance-authority, clsx, tailwind-merge | Class composition, component variants, and conflict-safe Tailwind class merging. |
| Inter and JetBrains Mono | Web fonts used for the main text and terminal-inspired interface. |
tw-animate-css | Reusable Tailwind animation utilities. |
The repository also includes shadcn components backed by libraries such as Sonner, Vaul, Recharts, Embla Carousel, React Hook Form, Zod, and React Day Picker. Many are available for future UI work and are not currently used by the three main pages.
Content and search
| Technology | Purpose in this project |
|---|---|
| JSON files | Store profile details, post metadata, full post content, and the search corpus. |
| MiniSearch | Runs full-text blog search entirely in the browser. The search index is fetched lazily when search is used. |
Zod and @tanstack/zod-adapter | Validate the /blogs URL parameters (q and tag). |
| Cheerio | Parses exported Medium HTML in the import script. It is a development dependency, not browser code. |
slugify | Installed slug-generation utility; it is not currently referenced by the application or import scripts. |
Tooling and delivery
| Technology | Purpose in this project |
|---|---|
| Bun | Package manager and command runner. bun.lock makes installs reproducible. |
| ESLint 9 | Lints TypeScript, React hooks, and refresh-compatible exports. |
| Prettier | Formats source and configuration files; formatting violations are surfaced through ESLint too. |
| GitHub Actions | Builds the site on every push to main and deploys .output/public to GitHub Pages. |
| GitHub Pages | Static production host. CNAME and public/CNAME configure the custom domain. |
| Lovable | The project is connected to Lovable and uses its TanStack/Vite configuration package and browser error-reporting bridge. |
How the application works
Routing and rendering
Routes live in src/routes and follow TanStack Router's file-based conventions:
| Source file | Route |
|---|---|
src/routes/__root.tsx | Root document, global metadata, providers, error boundary, and 404 UI |
src/routes/index.tsx | / |
src/routes/blogs.tsx | /blogs |
src/routes/blog.$slug.tsx | /blog/:slug |
src/routeTree.gen.ts is generated from these files. Do not edit it manually.
At build time, vite.config.ts reads every slug from src/data/posts-index.json and adds the home page, archive, and all article URLs to the prerender list. TanStack Start and Nitro render those routes ahead of time. The resulting static site is written to .output/public and can be hosted without a running Node server.
The custom server entry in src/server.ts is still used by the build/server-rendering pipeline. Together with src/start.ts and the error helpers in src/lib, it converts catastrophic SSR failures into a user-friendly HTML error page and reports client-side route errors to Lovable when that bridge is available.
Blog data flow
The content pipeline intentionally writes two copies of some data:
Medium HTML export
|
v
scripts/import-medium.mjs
|
+-- src/data/posts/*.json --------> bundled by article route loaders
+-- src/data/posts-index.json ----> home page, archive, prerender route list
+-- src/data/search-index.json ---> source copy of the search corpus
|
+-- public/data/posts/*.json ------> public /data/posts/:slug.json files
+-- public/data/posts-index.json --> public metadata endpoint
+-- public/data/search-index.json -> browser full-text search
The copies under src/data are application source inputs. The copies under public/data are copied unchanged into the public build and can be fetched directly by browsers or other tools.
An article JSON file contains metadata plus an ordered content array. PostContent.tsx renders supported block types: headings, paragraphs, code, images, quotes, lists, embeds, and horizontal rules. Code blocks include a copy button; YouTube embeds render inline, while other embeds become external links.
On /blogs, titles and post cards come from the small metadata index. The larger search index is downloaded only after a visitor focuses the search field or supplies a query. MiniSearch gives titles and tags more weight than article body text and supports prefixes and fuzzy matches. The selected query and tag stay in the URL, so filtered views are shareable.
GitHub Pages routing
Static hosts cannot normally resolve an unknown nested client-side URL. public/404.html stores the requested location in sessionStorage and redirects to /; the root React component then restores that path through TanStack Router. Pre rendered known routes load directly, while unknown routes reach the application's 404 UI.
Project structure
.
├── .github/workflows/deploy.yml # GitHub Pages build and deployment
├── public/
│ ├── 404.html # GitHub Pages SPA fallback
│ ├── data/ # Public post, metadata, and search JSON
│ ├── CNAME # Custom domain copied into the build
│ └── robots.txt, favicons...
├── scripts/
│ ├── import-medium.mjs # Convert a Medium export to site JSON
│ ├── fix-images.mjs # Replace broken local image paths with Medium CDN URLs
│ └── retag-ml.mjs # Recalculate the Machine Learning tag
├── src/
│ ├── components/
│ │ ├── PostContent.tsx # Article block renderer
│ │ └── ui/ # shadcn/Radix UI components
│ ├── data/
│ │ ├── profile.json # Portfolio content and contact links
│ │ ├── posts/ # Full article JSON used by route loaders
│ │ ├── posts-index.json # Compact post metadata
│ │ └── search-index.json # Full-text search source data
│ ├── lib/ # Utilities and error handling
│ ├── routes/ # File-based pages
│ ├── router.tsx # Router and QueryClient context
│ ├── server.ts # Custom TanStack Start server entry
│ ├── start.ts # Request middleware
│ └── styles.css # Tailwind theme and global styles
├── components.json # shadcn/ui configuration
├── eslint.config.js # Lint and formatting rules
├── package.json # Dependencies and commands
├── tsconfig.json # TypeScript configuration and @/* alias
└── vite.config.ts # TanStack Start, Nitro, and prerender config
Local development
Requirements
- Bun (the repository uses a Bun lockfile)
- A current Node-compatible environment; CI installs the latest Bun release
Install and start the development server:
curl -fsSL https://bun.sh/install | bash # skip if Bun is installed
bun install
bun run dev
Vite prints the local URL, normally http://localhost:3000.
No environment variables, external database, or API credentials are required for the current site.
Available commands
| Command | Description |
|---|---|
bun run dev | Start the Vite development server with hot reload. |
bun run build | Create the production/pre rendered build in .output. |
bun run build:dev | Build using Vite's development mode. |
bun run preview | Preview the production build locally. |
bun run lint | Run ESLint and Prettier checks. |
bun run format | Rewrite supported files with Prettier. |
There is currently no automated test command.
Editing site content
Portfolio content is data-driven. Update src/data/profile.json to change the name, summary, contact details, skills, work history, or education. The home page reads that file directly.
Global colors, fonts, terminal effects, and reusable CSS utilities are in src/styles.css. The theme uses CSS variables with OKLCH colors and Tailwind's @theme mapping.
To add a route, create a file under src/routes using the conventions documented in src/routes/README.md. The route tree will be regenerated by the TanStack Router tooling.
Importing articles from Medium
Medium articles are generated from the posts directory inside an unzipped Medium export:
bun scripts/import-medium.mjs /absolute/path/to/medium-export/posts
By default, the importer attempts to download article images into public/blog-assets. To retain the remote Medium image URLs instead:
DOWNLOAD_IMAGES=0 bun scripts/import-medium.mjs /absolute/path/to/medium-export/posts
The importer:
- Ignores draft HTML files.
- Extracts article metadata and canonical URLs.
- Converts supported HTML elements into typed content blocks.
- Estimates reading time at roughly 220 words per minute.
- Preserves existing tags from
src/data/posts.jsonwhen slugs match. - Writes matching source and public post collections and indexes.
After importing, run the normal validation commands and inspect the changed JSON before committing:
bun run lint
bun run build
Two one-off maintenance scripts are also included:
bun scripts/fix-images.mjsmaps missing/blog-assets/...references back to original Medium CDN images. It currently expects exported HTML at/tmp/medium-ext/posts.bun scripts/retag-ml.mjsscans article text and consistently adds or removes theMachine Learningtag across both source and public indexes.
Both maintenance scripts rewrite many generated JSON files, so review their diff before committing.
Production deployment
Pushing to main triggers .github/workflows/deploy.yml. The workflow:
- Checks out the repository.
- Installs Bun.
- Runs
bun install --frozen-lockfile. - Runs
bun run build. - Uploads
.output/publicas the GitHub Pages artifact. - Deploys that artifact to the
github-pagesenvironment.
For a manual deployment, run the workflow from the GitHub Actions interface. GitHub Pages must be configured to use GitHub Actions as its source.
Important maintenance notes
- Keep
src/dataandpublic/datasynchronized; the import and maintenance scripts already update both. - Add new article slugs to
src/data/posts-index.jsonthrough the importer sovite.config.tspre renders them. - Do not edit
src/routeTree.gen.tsby hand. - Do not commit
.output,dist,.vinxi, ornode_modules. - This repository is connected to Lovable. Do not force-push, rebase, amend, or squash commits that have already been pushed, because rewriting published history can break Lovable's project history.
Technology
- React – the actual UI library, same as what you know.
- TanStack Start – a full-stack React framework (like Next.js, but from the TanStack team — makers of React Query, TanStack Router). It adds file-based routing, server-side rendering, and API routes on top of plain React.
- Vite – the build tool/dev server. Fast hot-reload, bundles everything for production.
- Nitro – the server engine TanStack Start uses under the hood to actually run/deploy the server (handles the
node-serverpreset in yourvite.config.ts, and produces the static output in.output/public). - TypeScript – your code is
.ts/.tsx, typed React. - shadcn/ui (implied by
components.json) – a component library built on Radix UI + Tailwind CSS, pretty common in Lovable.dev-generated projects. - Pre rendering – your
vite.config.tshasprerender.enabled: truewith a list of static pages (home, blogs, each blog post) — so at build time it crawls your routes and outputs static HTML, which is why this can deploy to GitHub Pages (a static host) instead of needing a live Node server. - bun – package manager/runtime, replacing npm.
- Lovable.dev plugins (
@lovable.dev/vite-plugin-*) – dev-time tooling from the platform this was originally built in (hot-reload bridges, etc.) — not something you'll need to touch for normal editing.