Next.js 16 (App Router, static export) + React 19 + Tailwind CSS 3.
Bilingual: every page exists at /en/... and /ar/... (full SSG for both, with hreflang).
npm install
npm run dev # http://localhost:3000/en (root / redirects)
npm run build # static export → out/
npm run typecheck
npm run lintEnvironment variables: copy .env.example → .env.local (form key, GA id, search-console verification tokens). In CI they come from GitHub repository Variables.
-
Create two files in
content/blog/:my-article-slug.en.mdxmy-article-slug.ar.mdx
-
Each file = YAML frontmatter + Markdown body. Required frontmatter:
--- title: "Article title" excerpt: "One-two sentences shown in listings and search results." category: "E-Commerce" readTime: "8 min read" publishedAt: "2026-06-12" icon: ShoppingCart # ShoppingCart | BarChart | Server | Layers ---
Optional:
coverImage,relatedSolutions: [other-slugs],cta: {title, description, buttonText}. -
Body is normal Markdown. Rich blocks are available as components:
<Highlights items={["Point one", "Point two"]} /> <Steps items={[{"title":"Step","description":"..."}]} /> <Features items={[{"name":"Feature","description":"..."}]} /> <Tech items={["Next.js", "PostgreSQL"]} /> <Stats items={[{"value":"40%","label":"Faster"}]} />
-
git push→ GitHub Action builds and deploys. The article appears in the blog listing, gets its own metadata/OG tags, and is added tositemap.xmlautomatically. Nothing else to do.
If the Arabic file is missing the English version is served at /ar/... as a fallback (build does not break). Invalid frontmatter fails the build with a clear error.
Toggle manually in src/lib/constants.tsx: set Active: true on exactly one entry in Greetings, then push (rebuild + deploy). Components and artwork are lazy-loaded — inactive seasons cost visitors zero bytes.
Project data lives in src/data/projects.ts — marketing-first: client-facing features and a measurable result lead the card; tech renders small and muted.
Gallery screenshots are dynamic. Each project points at a folder (imagesFolder: "/Projects/Imtithal"). Drop WebP/PNG files into that folder — named 01-x.webp, 02-y.webp… for display order — and they appear in the gallery:
- In production: upload straight into
out/Projects/<Name>/on the VPS — the site picks them up on the next page load, no rebuild needed (nginx serves the folder listing as JSON; seedeploy/nginx.conf). The deploy workflow protects these server-side uploads fromrsync --delete. - Locally/dev: files under
public/Projects/<Name>/are read from disk on each reload.
Keep images ≤1200px wide WebP (~30-60KB). node scripts/optimize-assets.mjs has the sharp recipes.
- CI: .github/workflows/deploy.yml — on push to
master: build + rsyncout/to the VPS. Requires secretsVPS_HOST,VPS_USER,VPS_SSH_KEY,VPS_PATH. - nginx: deploy/nginx.conf — caching, www→non-www,
/→/en, legacy/about→/en/about301s, real 404s. Apply once on the server (nginx -t && systemctl reload nginx).
- Per-page titles/descriptions (both languages) live in src/lib/seo.ts —
PAGE_SEO. - Canonical + hreflang are generated per page;
sitemap.xmlis generated at build from real content (src/app/sitemap.ts). - Organization JSON-LD is in the layout; Article JSON-LD on every blog post.
- OG image:
public/og/default.png(regenerate withnode scripts/generate-og.mjs).
No file over ~300KB enters public/. Compress first (scripts/optimize-assets.mjs shows the sharp recipes — WebP, quality 80).