From dc41d0eef19e18a43370eebbec1e9f5cd96ea8c2 Mon Sep 17 00:00:00 2001 From: mohamed-dev2 Date: Wed, 23 Sep 2026 03:05:11 +0300 Subject: [PATCH 1/5] =?UTF-8?q?chore(branching):=20live=20Git=20Flow=20?= =?UTF-8?q?=E2=80=94=20develop=20branch,=20.gitflow=20config,=20CI=20gates?= =?UTF-8?q?=20develop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: mohamed-dev2 --- .gitflow | 9 ++++ .github/workflows/ci.yml | 2 +- CHANGELOG.md | 7 ++- docs/BRANCHING.md | 105 ++++++++++++++++++++++----------------- docs/README.md | 5 +- 5 files changed, 77 insertions(+), 51 deletions(-) create mode 100644 .gitflow diff --git a/.gitflow b/.gitflow new file mode 100644 index 0000000..472221e --- /dev/null +++ b/.gitflow @@ -0,0 +1,9 @@ +[gitflow] + branch.master = main + branch.develop = develop + prefix.feature = feature/ + prefix.bugfix = bugfix/ + prefix.release = release/ + prefix.hotfix = hotfix/ + prefix.support = support/ + versiontag = v \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 407565e..e405d06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, develop] pull_request: jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ef6db..a65488e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ or architecture-level changes are listed. - Public-launch ops: `docs/INDEXING.md` records the verified live-index state (robots/sitemap/verification file/home metadata all `200`) and the Search Console checklist that actually puts Wird in Google; - `docs/BRANCHING.md` introduces GitHub Flow (protected `main`, - `feat/fix/docs/chore/hotfix` PR branches, signed commits). + **branch system live**: Git Flow with long-lived `main` + `develop` + (both on origin), `feature/bugfix/release/hotfix/support/*` short-lived + branches, `.gitflow` config, CI now also gates `develop`, + `docs/BRANCHING.md` documents it all (protection of both branches is an + owner action — `gh`/PAT not available from the dev env). - Polish + presence: README screenshots generated from the app (`docs/assets/shots/`, via `scripts/shot-readme.mjs`); raster app icons (180/192/512 PNGs derived 1:1 from `icon.svg` via diff --git a/docs/BRANCHING.md b/docs/BRANCHING.md index 362eec0..6313d03 100644 --- a/docs/BRANCHING.md +++ b/docs/BRANCHING.md @@ -1,63 +1,76 @@ -# Branching system (GitHub Flow) +# Branching system (Git Flow) -Wird is public. `main` is the only long-lived branch and doubles as the -release branch: it is what Vercel deploys, what search engines read, and -what every visitor clones. Keep it stable. +Wird is public. Two long-lived branches exist on `origin`: -## Model (GitHub Flow) +| Branch | Purpose | Created | +| --------- | ---------------------------------------------------------------------- | ---------- | +| `main` | Production only; every commit is a deployable release, tagged `vX.Y.Z` | original | +| `develop` | Integration branch for all ongoing work; mirrors `main` at launch | 2026-09-23 | + +Everything else is short-lived and deleted after merge. + +## The flow ``` -main ───────────────●────●──────────── (always deployable) - \ / \ - feat/x ──────● docs/indexing + feature/* ──► ──► ──► bugfix/* ──► ──► + │ │ + ┌─────────────────┴───────────────┐ │ + ▼ │ ▼ + main ◄──── release/* ───────────────────◄──── develop ◄─────┴─◄──── feature/bugfix merges + ▲ │ + └────────────────── hotfix/* ◄────────────────┘ (urgent fix straight to prod) ``` -1. Cut a short-lived branch off `main` for every change. -2. Name it by intent (below). -3. Open a pull request → CI must pass → squash-merge. -4. Never push to `main` directly. +1. `feature/` off `develop` → PR → squash-merge into `develop`. +2. `bugfix/` for regressions, same path into `develop`. +3. A release is cut as `release/vX.Y.Z` off `develop` → merges to `main` + and back to `develop`; tag `vX.Y.Z` on `main`. +4. Production incident: `hotfix/` off `main` → merges to `main` + (immediately deployable) and back to `develop`. -## Branch names +## Prefixes (git-flow compatible, `.gitflow` ships in-repo) -| Prefix | Purpose | Example | -| --------- | -------------------------------- | -------------------- | -| `feat/` | new user-visible feature | `feat/deen-quest` | -| `fix/` | bug or regression | `fix/eol-corruption` | -| `docs/` | documentation only | `docs/INDEXING` | -| `chore/` | repo/tooling, no behavior change | `chore/deps` | -| `hotfix/` | urgent production fix off `main` | `hotfix/sw-cache` | +| Prefix | Off | Merges into | Example | +| ---------- | --------- | ------------------ | -------------------- | +| `feature/` | `develop` | `develop` | `feature/deen-quest` | +| `bugfix/` | `develop` | `develop` | `bugfix/sw-cache` | +| `release/` | `develop` | `main` + `develop` | `release/v0.2.0` | +| `hotfix/` | `main` | `main` + `develop` | `hotfix/quran-audio` | +| `support/` | `develop` | `develop` | (rare, compat) | -Branches are short-lived: delete after merge. No `develop`, no -`release/*` staging branches — the deployed artifact is `main`. +A bare `chore/test`-style scratch is tolerated for experiments but must +never merge to `main` and should be deleted after use. -## Rules (enforced and manual) +## Rules -- CI gate: `.github/workflows/ci.yml` must go green on every PR before - merge. -- Signed commits (`git commit -s`), message style from `git log`: - `type(scope): summary` (e.g. `feat(seo): per-route metadata`). -- PR uses `.github/pull_request_template.md`; every PR maps to exactly - one CHANGELOG line. -- Squash-merge for every PR → linear history on `main`. +- CI gate: `.github/workflows/ci.yml` runs on `main`, `develop`, and every + PR. A PR must go green before merge (typecheck, lint 0-warnings, + format, unit, e2e, build, docs, comments, css, boundaries, metrics, + audit). +- Signed commits (`git commit -s`), `type(scope): summary` messages + (e.g. `feat(seo): per-route metadata`). +- PR via `.github/pull_request_template.md`; every PR maps to one + CHANGELOG line before merging into `develop` or `main`. +- Squash-merge feature/bugfix branches; merge (or rebase-merge) `release/` + and `hotfix/` so the tag history stays linear-ish and honest. +- `main` only ever receives `release/*` and `hotfix/*`. -## Protecting `main` (owner action — needs GitHub acceds token) +## Protecting `main` + `develop` (owner action) -Cannot be set from this environment (no `gh` CLI / token here). In GitHub: -Settings → Branches → **Add rule** for `main`: +Branch protection cannot be set from this environment (`gh` CLI / PAT not +present). In GitHub: Settings → Branches → **Add rule**, once per branch: -- Require a pull request before merging (0 required approvals is fine - for solo; require reviews once external contributors arrive). -- Require status checks to pass before merging → tick the CI job(s). -- Require linear history. -- Require signed commits. -- Do **not** allow force pushes or deletions. +- `main`: require PR + status checks, linear history, signed commits; no + force push, no deletions. +- `develop`: same, plus "require N reviews" once outside contributors + arrive. -Hotfixes use the same flow: branch `hotfix/x` off `main`, PR, CI, squash -— never commit straight to `main`, even under pressure. +Both rules make the diagram above the only way code reaches a protected +branch — pulling without review becomes impossible. -## Why GitHub Flow, not Git Flow +## What this replaced -Solo-maintained, continuously deployed PWA. A `develop`/`release` ladder -adds merge ceremony without protecting anything extra: `main` is already -the only deployable line, so one protected branch + short-lived PR -branches is the smallest system that keeps the public surface green. +Earlier `docs/BRANCHING.md` drafts used GitHub Flow (single `main`). +Git Flow was chosen because a public solo project benefits from a +visible integration line (`develop`) that CI and Vercel previews build +without touching the production branch. diff --git a/docs/README.md b/docs/README.md index d651d6e..c6ef0c7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -115,8 +115,9 @@ Every doc in this folder, one line each. The root `README.md` and - `REPRODUCIBILITY.md` — reproducible environment, lockfile, Dev Container, OS-specific notes. -- `BRANCHING.md` — GitHub Flow model: branch names, PR/CI rules, how to - protect `main` (owner action). +- `BRANCHING.md` — Git Flow system (now live): `main` + `develop`, + `feature/bugfix/release/hotfix/*` branches, CI gates, how to protect + both long-lived branches (owner action). `.gitflow` config ships in-repo. - `DEPENDENCIES.md` — dependency governance + supply-chain policy. - `RELEASE.md` — release pipeline, artifact integrity (SHA-256), SBOM. - `VERSIONING.md` — versioning, migrations, deprecation. From 1679f47a7deb229636aeb3b6de481d548b05306a Mon Sep 17 00:00:00 2001 From: mohamed-dev2 Date: Wed, 23 Sep 2026 03:15:18 +0300 Subject: [PATCH 2/5] feat(seo): faq json-ld, descriptive internal links, duplicate-content guard Signed-off-by: mohamed-dev2 --- CHANGELOG.md | 5 +++ app/additions.css | 61 +++++++++++++++++++++++++++++++++++ app/lib/__tests__/seo.test.ts | 56 ++++++++++++++++++++++++++++++++ app/lib/strings.ts | 34 +++++++++++++++++++ app/page.tsx | 60 ++++++++++++++++++++++++++++++++++ docs/INDEXING.md | 31 ++++++++++++++++++ 6 files changed, 247 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a65488e..312cd89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ or architecture-level changes are listed. ## Unreleased +- SEO content pass: homepage FAQ (5 honest long-tail Q&A) + matching + `FAQPage` JSON-LD, descriptive internal links to core routes, and new + regression guards (unique titles/descriptions per route, FAQ parity); + `docs/INDEXING.md` gained the post-index playbook (keyword/CTR/Vitals + owner steps). - Public-launch ops: `docs/INDEXING.md` records the verified live-index state (robots/sitemap/verification file/home metadata all `200`) and the Search Console checklist that actually puts Wird in Google; diff --git a/app/additions.css b/app/additions.css index 86c0e8a..8692703 100644 --- a/app/additions.css +++ b/app/additions.css @@ -2245,3 +2245,64 @@ video { animation: none; } } + +/* SEO FAQ + internal links (public landing block at page bottom). */ +.seo-faq { + margin-block: 40px 8px; + border-block-start: 1px solid var(--border, #d8dfd5); + padding-block-start: 16px; + max-inline-size: 720px; + margin-inline: auto; +} + +.seo-faq h2, +.seo-links h3 { + font-size: 17px; + margin: 0 0 10px; +} + +.seo-faq details { + border-block-end: 1px solid var(--border, #d8dfd5); +} + +.seo-faq summary { + cursor: pointer; + font-weight: 600; + color: var(--ink, #16211c); + padding-block: 10px; +} + +.seo-faq details[open] summary { + margin-block-end: 4px; +} + +.seo-faq details p { + margin: 0 0 12px; + color: var(--muted, #5c6b63); + font-size: 13px; + line-height: 1.7; +} + +.seo-links { + margin-block-start: 20px; +} + +.seo-links ul { + list-style: none; + margin: 0; + padding-inline-start: 0; + display: flex; + flex-wrap: wrap; + gap: 6px 18px; +} + +.seo-links a { + color: var(--link, #14725d); + text-underline-offset: 3px; +} + +.seo-links a:focus-visible { + outline: 2px solid var(--focus, #1c6b4f); + outline-offset: 2px; + border-radius: 4px; +} diff --git a/app/lib/__tests__/seo.test.ts b/app/lib/__tests__/seo.test.ts index c954403..f1f19dc 100644 --- a/app/lib/__tests__/seo.test.ts +++ b/app/lib/__tests__/seo.test.ts @@ -99,6 +99,62 @@ describe("titles, descriptions, canonicals", () => { }); }); +describe("long-tail content, internal links, duplicate content", () => { + it("home carries a visible FAQ block with matching FAQPage JSON-LD", () => { + const src = read("app/page.tsx"); + expect(src, "visible faq section").toContain('className="seo-faq"'); + expect(src, "JsonLD FAQ type").toContain('"@type": "FAQPage"'); + expect(src, "JsonLD answer shape").toContain("acceptedAnswer"); + // Same strings render both the on-page
list and the schema, + // so the two can never drift apart. + expect( + src.match(/\["q\d", "a\d"\]/g) ?? [], + "faq pairs in visible block + schema", + ).toHaveLength(10); + expect(src).toContain("seo.faq.${q}"); + expect(src).toContain("seo.faq.${a}"); + const strings = read("app/lib/strings.ts"); + expect(strings.match(/seo\.faq\.q[1-5]/g) ?? [], "ar+en q keys").toHaveLength(10); + expect(strings.match(/seo\.faq\.a[1-5]/g) ?? [], "ar+en a keys").toHaveLength(10); + }); + it("home links descriptively to every core route", () => { + const src = read("app/page.tsx"); + for (const route of ["/library", "/deen", "/review", "/insights"]) { + expect(src, `internal link ${route}`).toContain(`Link href="${route}"`); + } + expect(src, "descriptive anchor text (nav.* labels)").toContain('t("nav.library")'); + expect(src).not.toContain('href="/library">click here'); + }); + it("listed routes never share a description (no cannibalized queries)", () => { + const srcOf = (r: string): string => { + try { + return read(`app/${r}/layout.tsx`); + } catch { + return read(`app/${r}/page.tsx`); + } + }; + const seen = new Map(); + for (const r of [ + "calendar", + "insights", + "review", + "library", + "account", + "terms", + "privacy", + "deen", + ]) { + const d = srcOf(r).match(/description:\s*"([^"]+)"/); + if (!d) continue; + expect( + seen.has(d[1] ?? ""), + `duplicate description on ${r} (first: ${seen.get(d[1] ?? "")})`, + ).toBe(false); + seen.set(d[1] ?? "", r); + } + }); +}); + describe("robots, sitemap, verification file", () => { it("robots allows crawling and references the sitemap", () => { const src = read("app/robots.ts"); diff --git a/app/lib/strings.ts b/app/lib/strings.ts index 40b215d..e72987b 100644 --- a/app/lib/strings.ts +++ b/app/lib/strings.ts @@ -160,6 +160,23 @@ const AR: Record = { "night.journalPh": "اكتب لنفسك كلمة طيبة…", "night.journalNote": "هذه المساحة لك وحدك، ولا تدخل في الإحصاءات.", "footer.verse": "﴿ وَاذْكُر رَّبَّكَ كَثِيرًا وَسَبِّحْ بِالْعَشِيِّ وَالْإِبْكَارِ ﴾", + "seo.faq.title": "أسئلة شائعة عن وِرد", + "seo.faq.q1": "هل يحتاج تطبيق وِرد إلى اتصال بالإنترنت؟", + "seo.faq.a1": + "لا، يعمل وِرد دون إنترنت؛ الصلوات والأذكار والقرآن والبيانات كلها على جهازك. الإنترنت مطلوب فقط عند طلب تفسير أو حديث أو صوتية غير محمّلة مسبقًا.", + "seo.faq.q2": "أين تُخزَّن بياناتي، وهل تُرفع إلى خادم؟", + "seo.faq.a2": + "تُحفظ بياناتك على جهازك فقط (تخزين محلي)، ولا تُرسل إلى أي خادم. لا حسابات إلزامية، ولا أدوات تتبّع، ولا تحليلات خارجية.", + "seo.faq.q3": "هل أستطيع متابعة الصلوات والأذكار والقرآن في تطبيق واحد؟", + "seo.faq.a3": + "نعم، يجمع وِرد الصلوات والأذكار والقرآن والأهداف والمراجعة اليومية في شاشة اليوم، مع إحصاءات تفصّلها سعيك الصحيح دون كشفها لأحد.", + "seo.faq.q4": "هل وِرد عربي ويدعم اتجاه RTL؟", + "seo.faq.a4": + "نعم، وِرد عربي أولًا بواجهة كاملة الاتجاه من اليمين لليسار، مع إمكانية التبديل إلى الإنجليزية في أي وقت.", + "seo.faq.q5": "هل يعمل وِرد كتطبيق مستقل حتى دون إنترنت؟", + "seo.faq.a5": + "نعم، يمكنك تثبيته كتطبيق (PWA) من المتصفح؛ يعمل دون إنترنت ويحفظ بياناتك بين الجلسات دون حساب.", + "seo.links.title": "استكشف التطبيق أكثر", "minplan.title": "خطة الحد الأدنى مفعّلة", "minplan.sub": "الصلوات، ذكر قصير، آية واحدة، استغفار ١٠ مرات، والوتر. هذا يكفي لليوم.", "minplan.cancel": "إلغاء", @@ -1475,6 +1492,23 @@ const EN: Record = { "night.journalPh": "Write yourself something kind…", "night.journalNote": "Yours alone — excluded from stats.", "footer.verse": "﴿ وَاذْكُر رَّبَّكَ كَثِيرًا وَسَبِّحْ بِالْعَشِيِّ وَالْإِبْكَارِ ﴾", + "seo.faq.title": "Frequently asked questions about Wird", + "seo.faq.q1": "Does Wird need an internet connection?", + "seo.faq.a1": + "No - Wird works offline; prayers, adhkar, Quran and your data all live on your device. Internet is only needed when you request a tafsir, hadith, or audio not bundled in advance.", + "seo.faq.q2": "Where is my data stored, and is it uploaded to a server?", + "seo.faq.a2": + "Your data is stored on your device only (local storage) and is never sent to a server. No mandatory accounts, no tracking, no third-party analytics.", + "seo.faq.q3": "Can I track prayers, adhkar and Quran in one app?", + "seo.faq.a3": + "Yes - Wird brings prayers, adhkar, Quran, goals and the daily review into one Today screen, with statistics that stay private to you.", + "seo.faq.q4": "Is Wird in Arabic and does it support RTL?", + "seo.faq.a4": + "Yes - Wird is Arabic-first with a full right-to-left interface, and you can switch to English at any time.", + "seo.faq.q5": "Does Wird work as a standalone app even offline?", + "seo.faq.a5": + "Yes - you can install it as a PWA from the browser; it works offline and keeps your data between sessions without an account.", + "seo.links.title": "Explore the app further", "minplan.title": "Minimum plan active", "minplan.sub": "Prayers, short dhikr, one verse, 10 istighfar, and Witr. Enough for today.", "minplan.cancel": "Cancel", diff --git a/app/page.tsx b/app/page.tsx index 30286da..2ed4f94 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -12,6 +12,7 @@ import { fastLabel, PRAYER_ID } from "./lib/daymode"; import { EditModal } from "./components/edit-modal"; import { NowView } from "./components/views/now"; import { PrayerArc } from "./components/prayer-arc"; +import Link from "next/link"; // Inline vault unlock (locked reflections): password + button only, generic // failure message. Session-scoped — reload locks again by design. @@ -1794,6 +1795,65 @@ export default function TodayPage() { onDelete={deleteEditing} /> )} +
+

{t("seo.faq.title")}

+ {( + [ + ["q1", "a1"], + ["q2", "a2"], + ["q3", "a3"], + ["q4", "a4"], + ["q5", "a5"], + ] as Array<[string, string]> + ).map(([q, a]) => ( +
+ {t(`seo.faq.${q}`)} +

{t(`seo.faq.${a}`)}

+
+ ))} + +