Skip to content
Y
YAS.SH
Docs

Blog Guide

Add a post in under 5 minutes — file-system MDX, co-located images, i18n EN/AR RTL.

Blog Guide — yas.sh

Add a blog in under 5 minutes. No framework magic, no code edits for basic posts.

1. Where Blogs Live

blog/content/
  <slug>/
    index.mdx        ← required: frontmatter + markdown
    hero.webp        ← required: 1280×720 (used on post + listing)
    cover.webp       ← optional: 800×600 (fallback to hero)
    thumb.webp       ← optional: 400×300 (listing thumb, fallback to hero)
  _stubs.json        ← backlog of 45 stub ideas (not rendered)
  • Adding a post = adding a folder. No registry file. generateStaticParams() reads blog/content directly.
  • Images are co-located; serving via /blog-img/[slug]/[image] is handled by Next (copy to public/blog-img in prod via scripts/copy-blog-images.sh or via next.config rewrites). During dev, public/blog-img symlinked or copied.

2. Frontmatter (Metadata)

Every index.mdx starts with YAML frontmatter between --- lines.

---
title: "URL Shortening Explained: Architecture, Trade-offs, and Best Practices"
slug: understanding-url-shortening
description: "How modern URL shorteners generate codes, handle redirects, and scale to millions of links — with API examples."
excerpt: "A practical guide to short links that actually convert — from 301 vs 302 to branded domains."
author: yas-team
publishedAt: "2026-08-08"
updatedAt: "2026-08-08"
category: "URL Shortening"
tags: ["links", "analytics", "seo", "qr", "api"]
heroImage: "./hero.webp"
coverImage: "./cover.webp"
thumbImage: "./thumb.webp"
readingTime: 9
featured: true
related: ["qr-codes-for-business", "custom-aliases-seo", "link-analytics-explained"]
faq:
  - q: "Do short links hurt SEO?"
    a: "No. A 301 preserves link equity; 302 preserves attribution. Set canonical on destination."
  - q: "What's the difference between 301 and 302?"
    a: "301 is permanent (cached), 302 is temporary (counted each click)."
seo:
  title: "URL Shortening Explained: Architecture, Trade-offs, and Best Practices | yas.sh"
  description: "How shorteners generate codes, handle redirects, aliases, expiry, and analytics — with API examples."
  keywords: ["url shortener", "short link", "custom alias", "link analytics"]
---

Field Reference

Field Required Description
title yes 50–70 chars, frontmatter + <h1> + SEO
slug yes kebab-case, folder name, URL /blog/<slug>
description yes 140–160 chars for SEO + card excerpt
excerpt yes Short card text (fallback to description)
author yes yas-team or author id
publishedAt yes YYYY-MM-DD ISO date
updatedAt yes Bumped on edit
category yes Single category, displayed as Badge
tags yes Array up to 5, lowercase
heroImage yes ./hero.webp relative
coverImage no fallback to hero
thumbImage no fallback to hero
readingTime yes integer minutes (auto-calc ~200 wpm if omitted, defaults 7)
featured no boolean, pins to top on /blog
related no array of slugs → renders links
faq no array of {q,a} → FAQ section + JSON-LD
seo.title no defaults to `title
seo.description no defaults to description
seo.keywords no defaults to tags

Validation: lib/blog not needed — generateStaticParams + generateMetadata read frontmatter directly. Zod validation at build would be added via blog/schema.ts in Phase 2.

3. How to Add a New Article (5 Minutes)

  1. Create folder
    mkdir -p blog/content/my-new-post
    
  2. Add index.mdx
    cp blog/content/understanding-url-shortening/index.mdx blog/content/my-new-post/index.mdx
    # edit frontmatter + body
    
  3. Add images
    cp blog/content/understanding-url-shortening/hero.webp blog/content/my-new-post/hero.webp
    # replace with 1280×720 WebP. Use `npx sharp-cli` to convert.
    # Optional: cover.webp + thumb.webp (or omit — hero used)
    
  4. Verify
    pnpm dev
    # open http://localhost:3000/blog → your post appears, no restart needed (HMR)
    # open http://localhost:3000/blog/my-new-post → renders MDX, SEO, JSON-LD
    

No code edits. No re-build. Auto-listed via filesystem.

4. Images

  • Format: WebP (sharp optimized). Keep hero <150kb, thumb <40kb.
  • Co-location: ./hero.webp keeps assets near content for easy diff/review.
  • Serving: app/blog/[slug]/page.tsx uses <img src="/blog-img/<slug>/hero.webp">. In prod, public/blog-img is populated via scripts/copy-blog-images.sh (run in pnpm build pre-step) or via Next rewrite serving from blog/content.
  • Fallback: If cover.webp missing, hero.webp is used; same for thumb.

Generate with sharp:

npx sharp -i input.png -o blog/content/my-post/hero.webp --webp '{"quality":80}'

5. Tags & Categories

  • Category: Single, broad buckets (URL Shortening, Analytics, Engineering, AI Infrastructure, QR, etc.). Rendered as badge and breadcrumb.
  • Tags: Up to 5 free-form. Displayed as pills on cards and used for SEO keywords. Synonyms folded client-side (e.g., seo + SEOseo).
  • Filtering: Future ?tag=ai-infra will be handled in app/blog/page.tsx — currently lists all; filter is Array.filter on tags.includes.

6. Related Posts

related: ["slug-1","slug-2"] renders as pill links under article. No auto-generation yet — manually curate 2–4 highly relevant. Missing slugs are skipped (no 404).

7. Featured Posts

featured: true pins to top if homepage/blog hero is implemented. Currently all posts sorted by publishedAt desc; featured tint is CSS (border-[var(--brand)]) handled in app/blog/page.tsx.

8. Markdown & Rendering

  • Flavour: GFM via marked (tables, fenced code, autolinks).
  • Safety: app/blog/[slug]/page.tsx sanitizes HTML — strips <script>, on* handlers, javascript: URLs, and adds rel="noopener" to external links. Author Markdown is trusted but sanitized.
  • Code blocks: Fenced ```ts etc. rendered with prose-pre:bg-[#0b0b10] and horizontal scroll.
  • Links: Internal [text](/blog/other) stays SPA; external links get target="_blank" via sanitizer.

9. SEO & Structured Data

  • Per-post metadata: generateMetadata() emits title, description, canonical, openGraph, twitter.
  • JSON-LD: Article + FAQPage injected in <script type="application/ld+json">.
  • Sitemap/Robots: app/sitemap.ts should enumerate blog slugs (add in Phase 2). Currently static sitemap handles pages; extend to include ...slugs.map(s=> ({url: https://yas.sh/blog/${s}})).
  • Headings: Use ## for sections, single # is title via frontmatter. Hierarchy enforced via lint script.

10. Reading Time

readingTime is manual minutes. Estimate 200 wpm. E.g., 1800 words → 9 min. Future: auto-calc via words / 200 if omitted.

11. Troubleshooting

Issue Fix
Post not showing Folder missing index.mdx or slug mismatch; check fs.readdirSync filter !f.startsWith("_")
Image 404 Ensure hero.webp exists and public/blog-img populated; run bash scripts/copy-blog-images.sh
Invalid frontmatter YAML must be valid; quotes needed if value contains :
404 on slug Slug must match /^[a-z0-9-]+$/ — no caps, no underscores

12. Scripts

# Copy blog images to public for serving (prod)
bash scripts/copy-blog-images.sh
# Audit: list all posts with word count
node -e "const fs=require('fs');fs.readdirSync('blog/content').filter(f=>!f.startsWith('_')).forEach(s=>{const c=fs.readFileSync('blog/content/'+s+'/index.mdx','utf8');console.log(s, (c.split(/\s+/).length+' words'))})"

Owner: Team — add via folder; no approval for draft; publish is merge to main triggers redeploy.

🍪 Cookies & privacy. yas.sh uses only essential cookies to keep you signed in and remember your preferences. We do not run third-party trackers. See our cookie policy and privacy policy.
Settings