From d8a79ccbe34428104ddf28cfa37077aea4bbd56d Mon Sep 17 00:00:00 2001 From: suphicakir Date: Fri, 16 Oct 2026 00:47:50 +0300 Subject: [PATCH] feat: add design system, theme toggle, SEO head and base layout - Huemint Magazine tokens for light/dark themes with AA-safe accent variants - FOUC-free theme init + toggle (localStorage, follows OS until chosen) - Seo component (canonical, OG, Twitter, WebSite/Person JSON-LD) - Header with popover mobile menu, 4-column footer with social links - Container, GridLines, SectionHeading, Button, Badge, Card, ExternalLink - site.json / social.json validated with zod - dev-only /styleguide preview Co-Authored-By: Claude Opus 5.5 --- PROJECT_MEMORY.md | 43 +++++----- astro.config.mjs | 20 +++-- src/components/Badge.astro | 36 +++++++++ src/components/Button.astro | 41 ++++++++++ src/components/Card.astro | 23 ++++++ src/components/Container.astro | 14 ++++ src/components/ExternalLink.astro | 25 ++++++ src/components/Footer.astro | 94 ++++++++++++++++++++++ src/components/GridLines.astro | 19 +++++ src/components/Header.astro | 88 ++++++++++++++++++++ src/components/SectionHeading.astro | 30 +++++++ src/components/Seo.astro | 85 ++++++++++++++++++++ src/components/ThemeInit.astro | 26 ++++++ src/components/ThemeToggle.astro | 37 +++++++++ src/dev/Styleguide.astro | 120 ++++++++++++++++++++++++++++ src/layouts/BaseLayout.astro | 41 ++++++++++ src/lib/data.ts | 67 ++++++++++++++++ src/pages/index.astro | 22 +++-- src/styles/global.css | 120 ++++++++++++++++++++++++++++ 19 files changed, 915 insertions(+), 36 deletions(-) create mode 100644 src/components/Badge.astro create mode 100644 src/components/Button.astro create mode 100644 src/components/Card.astro create mode 100644 src/components/Container.astro create mode 100644 src/components/ExternalLink.astro create mode 100644 src/components/Footer.astro create mode 100644 src/components/GridLines.astro create mode 100644 src/components/Header.astro create mode 100644 src/components/SectionHeading.astro create mode 100644 src/components/Seo.astro create mode 100644 src/components/ThemeInit.astro create mode 100644 src/components/ThemeToggle.astro create mode 100644 src/dev/Styleguide.astro create mode 100644 src/layouts/BaseLayout.astro create mode 100644 src/lib/data.ts diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index d59190e..90b1665 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -1,13 +1,13 @@ # PROJECT_MEMORY — ta1sph.info > Claude'un bu proje için kalıcı hafızası. Her görev sonunda güncellenir. Kısa ve güncel tutulur. -> Son güncelleme: 2026-10-16 · Güncelleyen: Claude (Siteyi Kur — Aşama 1) +> Son güncelleme: 2026-10-16 · Güncelleyen: Claude (Siteyi Kur — Aşama 2) ## 1. Durum -- **Aşama:** 1 tamamlandı (iskelet). Sıradaki: **Aşama 2 — Tasarım sistemi ve layout**. -- **Tamamlanan:** Aşama 0 (ön kontrol), Aşama 1 (entegrasyonlar, script'ler, Prettier, hafıza dosyaları repoda). -- **Açık işler:** Aşama 2–8 (`references/build-plan.md`). `LICENSE` / `LICENSE-CONTENT` Aşama 6'da (README şimdiden bağlantı veriyor). +- **Aşama:** 2 tamamlandı (tasarım sistemi + layout). Sıradaki: **Aşama 3 — Ana Sayfa**. +- **Tamamlanan:** Aşama 0, 1, 2. Stil önizleme yalnızca dev'de: `http://localhost:4321/styleguide/`. +- **Açık işler:** Aşama 3–8. Footer'daki yasal sayfalar ve `/rss.xml` henüz yok (Aşama 4/6). Favicon hâlâ Astro varsayılanı (Aşama 7). `LICENSE` dosyaları Aşama 6. - **Bilinen sorunlar:** — ## 2. Ortam @@ -21,7 +21,11 @@ ## 3. Dosya Haritası - `astro.config.mjs` — site/trailingSlash/static + node adapter, mdx, sitemap (`/_styleguide/` hariç), astro-icon (tabler), Tailwind vite plugin -- `src/styles/global.css` — Tailwind girişi + typography plugin (token'lar Aşama 2'de) +- `src/styles/global.css` — tema token'ları (light/dark, `data-theme`), fontlar, `.surface-alt` (khaki bölüm), `.headline`, `.link-strong`, reduced-motion +- `src/data/site.json` (ad, açıklama, nav, footer, `ui` metinleri, postsPerPage) · `src/data/social.json` (boş `href` → gizli) · `src/lib/data.ts` (zod; hata build'i kırar) +- `src/layouts/BaseLayout.astro` — head (ThemeInit, Seo, RSS alternate), skip link, Header, `main#main`, Footer +- `src/components/` — `Seo` (title/desc/canonical/OG/Twitter + WebSite/Person JSON-LD graph, `jsonLd` prop'u ile ek düğüm), `ThemeInit` (FOUC önleyici inline), `ThemeToggle`, `Header` (mobil menü = native `popover`, JS yok), `Footer` (4 sütun), `Container`, `GridLines`, `SectionHeading` (eyebrow + kırmızı çizgi), `Button` (primary/secondary/link), `Badge` (solid/outline/category), `Card`, `ExternalLink` +- `src/dev/Styleguide.astro` — `astro.config.mjs`'deki `devRoutes` entegrasyonu ile sadece `astro dev`'de `/styleguide/` (`_` önekli sayfalar Astro'da rota olmaz) - `src/lib/slug.ts` — Türkçe karakter dönüştürücü `slugify` - `scripts/new-post.mjs` — `npm run new:post -- "Başlık"` → `src/content/blog//index.md` (draft: true); `slug.ts`'yi Node type-stripping ile import eder - `src/pages/index.astro` — geçici yer tutucu (Aşama 3'te değişecek) @@ -30,19 +34,21 @@ ## 4. Kararlar (ADR) -| Tarih | Karar | Gerekçe | -| ---------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| 2026-10-15 | Statik çıktı + `@astrojs/node` ile sadece `/api/views` dinamik | "Öne çıkan = en çok okunan" için sayaç gerekli; site geri kalanı statik ve hızlı | -| 2026-10-15 | Sayaç: SQLite (`better-sqlite3`), volume `/app/data` | Harici servis yok, ev sunucusunda basit ve kalıcı | -| 2026-10-15 | Arama: Pagefind | Statik, sunucusuz, Türkçe destekli | -| 2026-10-15 | Harita: Leaflet + OpenStreetMap | API anahtarı / ücret yok, KVKK dostu | -| 2026-10-15 | İkonlar: astro-icon + Tabler | Tutarlı çizgi seti, anten kulesi ve marka ikonları mevcut | -| 2026-10-15 | Lisans: Kod MIT, içerik CC BY-SA 4.0 | Kaynak gösterilerek serbest kullanım talebi | -| 2026-10-15 | Kanonik alan: apex `ta1sph.info`, www → 301 | SEO tekilleştirme | -| 2026-10-15 | Fontlar self-host (Fontsource: Rubik + Inter) | Performans + gizlilik | -| 2026-10-16 | Pagefind CLI postbuild (`astro-pagefind` yok) | Daha az bağımlılık; node adapter çıktısı `dist/client` | -| 2026-10-16 | Skill repoya kopyalanmadı, kullanıcı düzeyinde | Çift kopya senkron riski | -| 2026-10-15 | Locator KN41KB99HW → 41.08307 N, 28.91094 E | Harita merkezi | +| Tarih | Karar | Gerekçe | +| ---------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| 2026-10-15 | Statik çıktı + `@astrojs/node` ile sadece `/api/views` dinamik | "Öne çıkan = en çok okunan" için sayaç gerekli; site geri kalanı statik ve hızlı | +| 2026-10-15 | Sayaç: SQLite (`better-sqlite3`), volume `/app/data` | Harici servis yok, ev sunucusunda basit ve kalıcı | +| 2026-10-15 | Arama: Pagefind | Statik, sunucusuz, Türkçe destekli | +| 2026-10-15 | Harita: Leaflet + OpenStreetMap | API anahtarı / ücret yok, KVKK dostu | +| 2026-10-15 | İkonlar: astro-icon + Tabler | Tutarlı çizgi seti, anten kulesi ve marka ikonları mevcut | +| 2026-10-15 | Lisans: Kod MIT, içerik CC BY-SA 4.0 | Kaynak gösterilerek serbest kullanım talebi | +| 2026-10-15 | Kanonik alan: apex `ta1sph.info`, www → 301 | SEO tekilleştirme | +| 2026-10-15 | Fontlar self-host (Fontsource: Rubik + Inter) | Performans + gizlilik | +| 2026-10-16 | Ek token'lar: `accent-fill`/`on-accent-fill`, `accent-text` (khaki'de fg'ye döner) | Ölçüm: ink/red 4.46, red-strong/khaki 3.85 → küçük metin AA'yı geçmiyordu. Kırmızı zeminli butonlar ≥19px bold | +| 2026-10-16 | Mobil menü native `popover` | Escape/odak/light-dismiss JS'siz | +| 2026-10-16 | Pagefind CLI postbuild (`astro-pagefind` yok) | Daha az bağımlılık; node adapter çıktısı `dist/client` | +| 2026-10-16 | Skill repoya kopyalanmadı, kullanıcı düzeyinde | Çift kopya senkron riski | +| 2026-10-15 | Locator KN41KB99HW → 41.08307 N, 28.91094 E | Harita merkezi | ## 5. İçerik Envanteri @@ -52,6 +58,7 @@ ## 6. Değişiklik Günlüğü (en yeni üstte) +- 2026-10-16 | Siteyi Kur A2 | Token'lar, tema anahtarı, SEO bileşeni, layout, header/footer, temel bileşenler, site/social JSON, dev styleguide; axe temiz (2 tema) | `src/styles/`, `src/components/`, `src/layouts/`, `src/data/`, `src/lib/data.ts`, `src/dev/`, `astro.config.mjs` - 2026-10-16 | Siteyi Kur A1 | İskelet: Tailwind 4, mdx, sitemap, node, icon, rss, fontlar, pagefind, prettier, new:post | `astro.config.mjs`, `package.json`, `scripts/`, `src/lib/slug.ts`, `src/styles/`, `README.md` - 2026-10-16 | Siteyi Kur A0 | Ortam kontrolü; hafıza + docs kitten projeye kopyalandı | `CLAUDE.md`, `PROJECT_MEMORY.md`, `docs/` - 2026-10-15 | Kurulum | Skill, CLAUDE.md ve PROJECT_MEMORY.md eklendi | `.claude/`, `CLAUDE.md`, `PROJECT_MEMORY.md` diff --git a/astro.config.mjs b/astro.config.mjs index f4ee3b8..d349a09 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,6 +7,20 @@ import sitemap from '@astrojs/sitemap'; import node from '@astrojs/node'; import icon from 'astro-icon'; +/** + * Dev-only routes (style preview); never part of a production build. + * @type {import('astro').AstroIntegration} + */ +const devRoutes = { + name: 'dev-routes', + hooks: { + 'astro:config:setup': ({ command, injectRoute }) => { + if (command !== 'dev') return; + injectRoute({ pattern: '/styleguide', entrypoint: './src/dev/Styleguide.astro' }); + }, + }, +}; + // https://astro.build/config export default defineConfig({ site: 'https://ta1sph.info', @@ -14,11 +28,7 @@ export default defineConfig({ // Static by default; only routes with `export const prerender = false` (view counter API) run on the server. output: 'static', adapter: node({ mode: 'standalone' }), - integrations: [ - mdx(), - sitemap({ filter: (page) => !page.includes('/_styleguide/') }), - icon({ include: { tabler: ['*'] } }), - ], + integrations: [mdx(), sitemap(), icon({ include: { tabler: ['*'] } }), devRoutes], vite: { plugins: [tailwindcss()], }, diff --git a/src/components/Badge.astro b/src/components/Badge.astro new file mode 100644 index 0000000..53d3716 --- /dev/null +++ b/src/components/Badge.astro @@ -0,0 +1,36 @@ +--- +import { Icon } from 'astro-icon/components'; + +type Variant = 'solid' | 'outline' | 'category'; + +export interface Props { + variant?: Variant; + icon?: string; + href?: string; + class?: string; +} + +const { variant = 'solid', icon, href, class: className } = Astro.props; + +const variants: Record = { + // accent-fill (not accent) keeps small text ≥ 4.5:1 in both themes. + solid: 'bg-accent-fill text-on-accent-fill font-semibold', + outline: 'border border-line text-fg-muted', + category: + 'bg-khaki text-ink font-semibold dark:bg-bg-alt dark:text-fg dark:border dark:border-line', +}; +const Tag = href ? 'a' : 'span'; +--- + + + {icon && diff --git a/src/components/Button.astro b/src/components/Button.astro new file mode 100644 index 0000000..3133154 --- /dev/null +++ b/src/components/Button.astro @@ -0,0 +1,41 @@ +--- +import type { HTMLAttributes } from 'astro/types'; +import { Icon } from 'astro-icon/components'; + +type Variant = 'primary' | 'secondary' | 'link'; + +export interface Props extends HTMLAttributes<'a'> { + href?: string; + variant?: Variant; + icon?: string; + type?: 'button' | 'submit'; +} + +const { href, variant = 'primary', icon, type = 'button', class: className, ...rest } = Astro.props; + +// Primary uses ink on red at ≥19px bold (large text, ratio 4.46 ≥ 3). +const variants: Record = { + primary: + 'bg-accent text-on-accent px-6 py-3 text-[1.1875rem] font-bold rounded-[2px] hover:bg-fg hover:text-bg', + secondary: + 'bg-fg text-bg px-6 py-3 text-[1.1875rem] font-bold rounded-[2px] hover:bg-accent hover:text-on-accent', + link: 'link-strong text-lg', +}; +const classes = [ + 'font-display inline-flex items-center gap-2 transition-colors', + variants[variant], + className, +]; +--- + +{href ? ( + + + {icon && +) : ( + +)} diff --git a/src/components/Card.astro b/src/components/Card.astro new file mode 100644 index 0000000..735c3cd --- /dev/null +++ b/src/components/Card.astro @@ -0,0 +1,23 @@ +--- +import type { HTMLTag } from 'astro/types'; + +export interface Props { + as?: HTMLTag; + /** Adds hover lift; the card's own link should cover it via `after:absolute after:inset-0`. */ + interactive?: boolean; + class?: string; +} + +const { as: Tag = 'article', interactive = false, class: className } = Astro.props; +--- + + + + diff --git a/src/components/Container.astro b/src/components/Container.astro new file mode 100644 index 0000000..9638a76 --- /dev/null +++ b/src/components/Container.astro @@ -0,0 +1,14 @@ +--- +import type { HTMLTag } from 'astro/types'; + +export interface Props { + as?: HTMLTag; + class?: string; +} + +const { as: Tag = 'div', class: className } = Astro.props; +--- + + + + diff --git a/src/components/ExternalLink.astro b/src/components/ExternalLink.astro new file mode 100644 index 0000000..42791af --- /dev/null +++ b/src/components/ExternalLink.astro @@ -0,0 +1,25 @@ +--- +import type { HTMLAttributes } from 'astro/types'; +import { Icon } from 'astro-icon/components'; +import { site } from '../lib/data'; + +export interface Props extends Omit, 'target' | 'rel'> { + href: string; + /** Show the trailing external-link icon. */ + icon?: boolean; +} + +const { href, icon = true, class: className, ...rest } = Astro.props; +--- + + + + {icon && diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..a4d6416 --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,94 @@ +--- +import { Icon } from 'astro-icon/components'; +import { site, social } from '../lib/data'; +import Badge from './Badge.astro'; +import Container from './Container.astro'; +import ExternalLink from './ExternalLink.astro'; + +const year = new Date().getFullYear(); +const linkClass = + 'decoration-accent underline-offset-4 hover:underline decoration-2 inline-flex items-center gap-2'; +const titleClass = 'font-display mb-4 text-sm font-bold tracking-wide uppercase'; +--- + +
+ +
+ +

{site.footer.about}

+ + {site.author.callsign} · {site.author.locator} + +
+ + + + + + {social.length > 0 && ( +
+ +
    + {social.map((s) => ( +
  • + + +
  • + ))} +
+
+ )} +
+ +
+ +

+ © {year} {site.author.name} · {site.footer.license} +

+

{site.footer.signoff}

+
+
+
diff --git a/src/components/GridLines.astro b/src/components/GridLines.astro new file mode 100644 index 0000000..91048b4 --- /dev/null +++ b/src/components/GridLines.astro @@ -0,0 +1,19 @@ +--- +// Decorative vertical magazine grid lines (reference: Huemint "Magazine"). Hidden on mobile. +export interface Props { + class?: string; +} +const { class: className } = Astro.props; +--- + + diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 0000000..cfa8c0d --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,88 @@ +--- +import { Icon } from 'astro-icon/components'; +import { site } from '../lib/data'; +import Container from './Container.astro'; +import ThemeToggle from './ThemeToggle.astro'; + +const path = Astro.url.pathname; +const isActive = (href: string) => (href === '/' ? path === '/' : path.startsWith(href)); +--- + +
+ + + {site.name} + + +
+ + + + + +
+
+ + + +
diff --git a/src/components/SectionHeading.astro b/src/components/SectionHeading.astro new file mode 100644 index 0000000..a1931b8 --- /dev/null +++ b/src/components/SectionHeading.astro @@ -0,0 +1,30 @@ +--- +// Magazine section heading: small red eyebrow with a rule, then a heavy uppercase title. +export interface Props { + eyebrow?: string; + title: string; + level?: 1 | 2 | 3; + id?: string; + class?: string; +} + +const { eyebrow, title, level = 2, id, class: className } = Astro.props; +const Heading = `h${level}` as 'h1' | 'h2' | 'h3'; +const sizes = { + 1: 'text-[clamp(2.75rem,7vw,6rem)]', + 2: 'text-[clamp(2rem,4.5vw,3.5rem)]', + 3: 'text-2xl sm:text-3xl', +}; +--- + +
+ {eyebrow && ( +

+ {eyebrow} +

+ )} + + {title} + +
diff --git a/src/components/Seo.astro b/src/components/Seo.astro new file mode 100644 index 0000000..c5f0234 --- /dev/null +++ b/src/components/Seo.astro @@ -0,0 +1,85 @@ +--- +import { site, social } from '../lib/data'; + +export interface Props { + /** Page title without the site suffix. Omit on the home page. */ + title?: string; + description?: string; + /** Absolute or root-relative image URL for OG/Twitter. */ + image?: string; + imageAlt?: string; + type?: 'website' | 'article'; + publishedTime?: Date; + modifiedTime?: Date; + noindex?: boolean; + /** Extra JSON-LD nodes (BlogPosting, BreadcrumbList, …). */ + jsonLd?: Record[]; +} + +const { + title, + description = site.description, + image, + imageAlt, + type = 'website', + publishedTime, + modifiedTime, + noindex = false, + jsonLd = [], +} = Astro.props; + +const fullTitle = title ? `${title}${site.titleSeparator}${site.name}` : site.title; +const canonical = new URL(Astro.url.pathname, Astro.site ?? site.url).href; +const imageUrl = image ? new URL(image, Astro.site ?? site.url).href : undefined; +const twitterHandle = social.find((s) => s.id === 'x')?.handle; + +const personId = `${site.url}/#person`; +const graph = { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'WebSite', + '@id': `${site.url}/#website`, + url: `${site.url}/`, + name: site.title, + description: site.description, + inLanguage: site.lang, + publisher: { '@id': personId }, + }, + { + '@type': 'Person', + '@id': personId, + name: site.author.name, + alternateName: site.author.callsign, + url: `${site.url}/`, + sameAs: social.map((s) => s.href), + }, + ...jsonLd, + ], +}; +--- + +{fullTitle} + + +{noindex && } + + + + + + + +{imageUrl && } +{imageUrl && imageAlt && } +{publishedTime && } +{modifiedTime && } + + +{twitterHandle && } +{twitterHandle && } + + +{imageUrl && } + + diff --git a/src/components/ThemeToggle.astro b/src/components/ThemeToggle.astro new file mode 100644 index 0000000..bb84d68 --- /dev/null +++ b/src/components/ThemeToggle.astro @@ -0,0 +1,37 @@ +--- +import { Icon } from 'astro-icon/components'; +import { site } from '../lib/data'; +--- + + + + diff --git a/src/dev/Styleguide.astro b/src/dev/Styleguide.astro new file mode 100644 index 0000000..c76984a --- /dev/null +++ b/src/dev/Styleguide.astro @@ -0,0 +1,120 @@ +--- +// Dev-only style preview, injected at /styleguide/ by astro.config.mjs (never built for production). +// Sample copy below is placeholder text for visual testing only. +import Badge from '../components/Badge.astro'; +import Button from '../components/Button.astro'; +import Card from '../components/Card.astro'; +import Container from '../components/Container.astro'; +import ExternalLink from '../components/ExternalLink.astro'; +import GridLines from '../components/GridLines.astro'; +import SectionHeading from '../components/SectionHeading.astro'; +import BaseLayout from '../layouts/BaseLayout.astro'; + +const swatches = [ + ['bg', 'bg-bg'], + ['bg-alt', 'bg-bg-alt'], + ['surface', 'bg-surface'], + ['fg', 'bg-fg'], + ['fg-muted', 'bg-fg-muted'], + ['accent', 'bg-accent'], + ['accent-strong', 'bg-accent-strong'], + ['accent-fill', 'bg-accent-fill'], +]; +--- + + +
+ + + +
+

Two background colours

+

Paper ↔ Khaki sections, thin vertical grid lines.

+ +
+ +
+
+ +
+ + +
    + {swatches.map(([name, cls]) => ( +
  • +
    +

    {name}

    +
  • + ))} +
+
+
+ +
+ + +
+ + + + +
+
+ Updated 16 Oct 2026 + + Created 1 Oct 2026 + + + Category + +
+

+ Inline{' '} + + link + {' '} + and an{' '} + + external link + + . +

+
+
+ +
+ + +
+ {[1, 2, 3].map((n) => ( + +
+ Category {n} +

+ + Card title {n} + +

+

Muted supporting text for the card body.

+

Small accent text

+
+
+ ))} +
+
+
+
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..4159a44 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,41 @@ +--- +import '../styles/global.css'; +import Footer from '../components/Footer.astro'; +import Header from '../components/Header.astro'; +import Seo, { type Props as SeoProps } from '../components/Seo.astro'; +import ThemeInit from '../components/ThemeInit.astro'; +import { site } from '../lib/data'; + +export type Props = SeoProps; + +const props = Astro.props; +--- + + + + + + + + + + + + + + + + + + {site.ui.skipToContent} + +
+
+ +
+