Added gitea-mirror
24
Divers/gitea-mirror/www/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
2
Divers/gitea-mirror/www/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
92
Divers/gitea-mirror/www/CLAUDE.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is the marketing website for Gitea Mirror, built with Astro and Tailwind CSS v4. It serves as a landing page to showcase the Gitea Mirror application's features and provide getting started information.
|
||||
|
||||
**Note**: This is NOT the main Gitea Mirror application. The actual application is located in the parent directory (`../`).
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
bun install # Install dependencies
|
||||
bun run dev # Start development server (port 4321)
|
||||
bun run build # Build for production
|
||||
bun run preview # Preview production build
|
||||
```
|
||||
|
||||
## Architecture & Key Concepts
|
||||
|
||||
### Technology Stack
|
||||
- **Framework**: Astro (v5.0.5) - Static site generator with React integration
|
||||
- **UI**: React (v19.0.0) + Tailwind CSS v4
|
||||
- **Runtime**: Bun
|
||||
- **Styling**: Tailwind CSS v4 with Vite plugin
|
||||
|
||||
### Project Structure
|
||||
- `/src/pages/` - Astro pages (single `index.astro` page)
|
||||
- `/src/components/` - React components for UI sections
|
||||
- `Hero.tsx` - Landing hero section
|
||||
- `Features.tsx` - Feature showcase
|
||||
- `GettingStarted.tsx` - Installation and setup guide
|
||||
- `Screenshots.tsx` - Product screenshots gallery
|
||||
- `Footer.tsx` - Page footer
|
||||
- `/src/layouts/` - Layout wrapper components
|
||||
- `/public/assets/` - Static assets (shared with main project)
|
||||
- `/public/favicon.svg` - Site favicon
|
||||
|
||||
### Key Implementation Details
|
||||
|
||||
1. **Single Page Application**: The entire website is a single page (`index.astro`) composed of React components.
|
||||
|
||||
2. **Responsive Design**: All components use Tailwind CSS for responsive layouts with mobile-first approach.
|
||||
|
||||
3. **Asset Sharing**: Screenshots and images are shared with the main Gitea Mirror project (located in `/public/assets/`).
|
||||
|
||||
4. **Component Pattern**: Each major section is a separate React component with TypeScript interfaces for props.
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
**When updating content:**
|
||||
- Hero section copy is in `Hero.tsx`
|
||||
- Features are defined in `Features.tsx` as an array
|
||||
- Getting started steps are in `GettingStarted.tsx`
|
||||
- Screenshots are referenced from `/public/assets/`
|
||||
|
||||
**When adding new sections:**
|
||||
1. Create a new component in `/src/components/`
|
||||
2. Import and add it to `index.astro`
|
||||
3. Follow the existing pattern of full-width sections with container constraints
|
||||
|
||||
**Styling conventions:**
|
||||
- Use Tailwind CSS v4 classes exclusively
|
||||
- Follow the existing color scheme (zinc/neutral grays, blue accents)
|
||||
- Maintain consistent spacing using Tailwind's spacing scale
|
||||
- Keep mobile responsiveness in mind
|
||||
|
||||
### Common Tasks
|
||||
|
||||
**Updating screenshots:**
|
||||
- Screenshots should match those in the main application
|
||||
- Place new screenshots in `/public/assets/`
|
||||
- Update the `Screenshots.tsx` component to reference new images
|
||||
|
||||
**Modifying feature list:**
|
||||
- Edit the `features` array in `Features.tsx`
|
||||
- Each feature needs: icon, title, and description
|
||||
- Icons come from `lucide-react`
|
||||
|
||||
**Changing getting started steps:**
|
||||
- Edit the content in `GettingStarted.tsx`
|
||||
- Docker and direct installation tabs are separate sections
|
||||
- Code blocks use `<pre>` and `<code>` tags with Tailwind styling
|
||||
|
||||
## Relationship to Main Project
|
||||
|
||||
This website showcases the Gitea Mirror application located in the parent directory. When making updates:
|
||||
- Ensure feature descriptions match actual capabilities
|
||||
- Keep version numbers and requirements synchronized
|
||||
- Use the same screenshots as the main application's documentation
|
||||
- Maintain consistent branding and messaging
|
||||
30
Divers/gitea-mirror/www/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Gitea Mirror Marketing Site
|
||||
|
||||
This Astro workspace powers the public marketing experience for Gitea Mirror. It includes the landing page, screenshots, call-to-action components, and the new use case library that highlights real-world workflows.
|
||||
|
||||
## Developing Locally
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
The site is available at `http://localhost:4321`. Tailwind CSS v4 handles styling; classes can be used directly inside Astro, MDX, and React components.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/pages/index.astro` – Main landing page
|
||||
- `src/components/` – Reusable UI (Header, Hero, Features, UseCases, etc.)
|
||||
- `src/lib/use-cases.ts` – Central data source for use case titles, summaries, and tags
|
||||
- `src/pages/use-cases/` – MDX guides for each use case, rendered with `UseCaseLayout`
|
||||
- `src/layouts/UseCaseLayout.astro` – Shared layout that injects the header, shader background, and footer into MDX guides
|
||||
|
||||
## Authoring Use Case Guides
|
||||
|
||||
1. Add or update a record in `src/lib/use-cases.ts`. This keeps the landing page and library listing in sync.
|
||||
2. Create a new MDX file in `src/pages/use-cases/<slug>.mdx` with the `UseCaseLayout` layout and descriptive frontmatter.
|
||||
3. Run `bun run dev` to preview the layout and ensure the new guide inherits global styles.
|
||||
|
||||
## Deployment
|
||||
|
||||
The marketing site is built with the standard Astro pipeline. Use `bun run build` to generate a production build before deploying.
|
||||
14
Divers/gitea-mirror/www/astro.config.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@astrojs/react';
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
},
|
||||
|
||||
integrations: [react(), mdx()]
|
||||
});
|
||||
1037
Divers/gitea-mirror/www/bun.lock
Normal file
21
Divers/gitea-mirror/www/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
628
Divers/gitea-mirror/www/docs/SEO_KEYWORDS.md
Normal file
@@ -0,0 +1,628 @@
|
||||
# SEO Keywords & Programmatic Content Strategy for Gitea Mirror
|
||||
|
||||
> **Goal**: Generate 5,000-15,000 organic visits/month within 6-12 months
|
||||
> **Strategy**: Low-effort, high-intent pages targeting long-tail keywords
|
||||
> **Focus**: Problem-solving content over generic tool descriptions
|
||||
|
||||
---
|
||||
|
||||
## 🎯 LOW-HANGING FRUIT: Quick Wins (Start This Week)
|
||||
|
||||
### Tier 1: Ultra Low-Effort, High-Intent Pages (1-2 hours each)
|
||||
|
||||
These are **simple template pages** with **minimal content** but **high search volume** and **buyer intent**.
|
||||
|
||||
| Page | Keyword | Monthly Searches | Difficulty | Effort | Priority |
|
||||
|------|---------|-----------------|------------|--------|----------|
|
||||
| `/use-cases/backup-github-repositories` | "backup github repositories" | 500-1K | Low (15) | 1h | ⭐⭐⭐⭐⭐ |
|
||||
| `/use-cases/migrate-github-to-gitea` | "migrate github to gitea" | 300-800 | Low (10) | 1h | ⭐⭐⭐⭐⭐ |
|
||||
| `/solutions/github-disaster-recovery` | "github disaster recovery" | 200-500 | Low (12) | 1h | ⭐⭐⭐⭐⭐ |
|
||||
| `/vs/manual-vs-automated-github-migration` | "automated github migration" | 150-400 | Very Low (8) | 1.5h | ⭐⭐⭐⭐ |
|
||||
| `/guides/setup-gitea-mirror-docker` | "gitea mirror docker setup" | 100-300 | Very Low (5) | 2h | ⭐⭐⭐⭐ |
|
||||
|
||||
**Why these work:**
|
||||
- Specific, actionable queries ("how to backup", "migrate to")
|
||||
- Low competition (KD < 15)
|
||||
- High commercial intent (ready to install)
|
||||
- Can reuse existing docs content
|
||||
|
||||
**Template for these pages:** 400-600 words, 30 minutes to write each
|
||||
|
||||
---
|
||||
|
||||
## 📊 KEYWORD STRATEGY: 3-Tier Approach
|
||||
|
||||
### Tier 1: Problem-Solving Keywords (HIGHEST PRIORITY)
|
||||
**Intent**: "I have this specific problem"
|
||||
**Effort**: Low (template-based)
|
||||
**Pages needed**: 15
|
||||
|
||||
| Primary Keyword | Secondary Keywords | Est. Traffic | Page URL |
|
||||
|----------------|-------------------|--------------|----------|
|
||||
| backup github repositories | github backup tool, automated github backup | 500/mo | `/use-cases/backup-github-repositories` |
|
||||
| migrate github to gitea | github gitea migration, import github to gitea | 400/mo | `/use-cases/migrate-github-to-gitea` |
|
||||
| github disaster recovery | backup github organization, github downtime backup | 250/mo | `/solutions/github-disaster-recovery` |
|
||||
| sync github to self-hosted | self-hosted github alternative, github to gitea sync | 200/mo | `/use-cases/sync-github-to-self-hosted-gitea` |
|
||||
| preserve github history | github history backup, archive github repos | 180/mo | `/use-cases/preserve-github-history` |
|
||||
| github vendor lock-in | avoid github lock-in, github alternatives | 150/mo | `/solutions/avoid-vendor-lock-in` |
|
||||
| github backup automation | automate github mirror, scheduled github backup | 140/mo | `/use-cases/github-backup-automation` |
|
||||
| mirror starred repositories | backup starred repos, export github stars | 120/mo | `/use-cases/starred-repos-collection` |
|
||||
| github offline access | offline git mirror, air-gapped github | 100/mo | `/solutions/need-offline-git-access` |
|
||||
| github rate limits | bypass github api limits, github api alternatives | 90/mo | `/solutions/github-rate-limits` |
|
||||
|
||||
**Total Tier 1 Traffic Potential**: ~2,500 visits/month
|
||||
|
||||
---
|
||||
|
||||
### Tier 2: Feature-Specific Keywords (MEDIUM PRIORITY)
|
||||
**Intent**: "I want to do this specific thing"
|
||||
**Effort**: Medium (requires explaining features)
|
||||
**Pages needed**: 12
|
||||
|
||||
| Primary Keyword | Est. Traffic | Page URL |
|
||||
|----------------|--------------|----------|
|
||||
| mirror github issues | 80/mo | `/features/github-issues-migration` |
|
||||
| sync github releases | 70/mo | `/features/github-releases-sync` |
|
||||
| mirror github wiki | 60/mo | `/features/wiki-migration` |
|
||||
| preserve github organization structure | 50/mo | `/features/organization-structure-preservation` |
|
||||
| mirror private github repos | 180/mo | `/features/private-repository-mirroring` |
|
||||
| github metadata migration | 45/mo | `/features/metadata-migration` |
|
||||
| scheduled github sync | 120/mo | `/features/scheduled-synchronization` |
|
||||
| batch github migration | 40/mo | `/features/batch-repository-processing` |
|
||||
| github pull request migration | 35/mo | `/features/pull-request-mirroring` |
|
||||
| git lfs mirror | 30/mo | `/features/git-lfs-support` |
|
||||
|
||||
**Total Tier 2 Traffic Potential**: ~1,200 visits/month
|
||||
|
||||
---
|
||||
|
||||
### Tier 3: Comparison Keywords (HIGH CONVERSION)
|
||||
**Intent**: "Evaluating options"
|
||||
**Effort**: Medium-High (research required)
|
||||
**Pages needed**: 8
|
||||
|
||||
| Primary Keyword | Est. Traffic | Conversion Potential | Page URL |
|
||||
|----------------|--------------|---------------------|----------|
|
||||
| github backup tools comparison | 250/mo | Very High | `/vs/github-backup-solutions` |
|
||||
| gitea vs github | 800/mo | Medium | `/vs/github-vs-gitea` |
|
||||
| manual vs automated migration | 60/mo | High | `/vs/manual-vs-automated-migration` |
|
||||
| git clone vs mirror | 45/mo | Medium | `/vs/git-clone-vs-automated-sync` |
|
||||
| gitea alternatives | 150/mo | Medium | `/alternatives` |
|
||||
| self-hosted git servers | 400/mo | Low | `/vs/self-hosted-vs-cloud-git` |
|
||||
|
||||
**Total Tier 3 Traffic Potential**: ~1,700 visits/month
|
||||
|
||||
---
|
||||
|
||||
## 🚀 IMPLEMENTATION ROADMAP: 4-Week Sprint
|
||||
|
||||
### Week 1: Foundation (5 pages)
|
||||
**Goal**: Get first pages indexed, establish content structure
|
||||
|
||||
**Day 1-2: Setup** (4 hours)
|
||||
- [ ] Create Astro content collections (`src/content/config.ts`)
|
||||
- [ ] Build page templates (use-cases, features, solutions)
|
||||
- [ ] Setup SEO component with structured data
|
||||
- [ ] Create sitemap generator
|
||||
|
||||
**Day 3-5: Core Content** (8 hours)
|
||||
- [ ] `/use-cases/backup-github-repositories` - 600 words
|
||||
- [ ] `/use-cases/migrate-github-to-gitea` - 600 words
|
||||
- [ ] `/solutions/github-disaster-recovery` - 500 words
|
||||
- [ ] `/features/automatic-github-mirroring` - 700 words
|
||||
- [ ] `/vs/manual-vs-automated-migration` - 800 words
|
||||
|
||||
**Day 6-7: Technical Setup** (3 hours)
|
||||
- [ ] Submit sitemap to Google Search Console
|
||||
- [ ] Setup Google Analytics 4
|
||||
- [ ] Add schema.org markup
|
||||
- [ ] Create robots.txt
|
||||
- [ ] Setup canonical URLs
|
||||
|
||||
**Week 1 Target**: 5 pages live, indexed by Google
|
||||
|
||||
---
|
||||
|
||||
### Week 2: Scale Content (10 pages)
|
||||
**Goal**: Batch create similar pages using templates
|
||||
|
||||
**Use Case Pages** (5 pages, 1 hour each):
|
||||
- [ ] `/use-cases/sync-github-to-self-hosted-gitea`
|
||||
- [ ] `/use-cases/preserve-github-history`
|
||||
- [ ] `/use-cases/github-backup-automation`
|
||||
- [ ] `/use-cases/starred-repos-collection`
|
||||
- [ ] `/use-cases/vendor-lock-in-prevention`
|
||||
|
||||
**Feature Pages** (5 pages, 1.5 hours each):
|
||||
- [ ] `/features/private-repository-mirroring`
|
||||
- [ ] `/features/scheduled-synchronization`
|
||||
- [ ] `/features/github-issues-migration`
|
||||
- [ ] `/features/github-releases-sync`
|
||||
- [ ] `/features/metadata-migration`
|
||||
|
||||
**Week 2 Target**: 15 total pages, monitor first impressions in GSC
|
||||
|
||||
---
|
||||
|
||||
### Week 3: Problem-Solution Focus (8 pages)
|
||||
**Goal**: Target high-intent problem queries
|
||||
|
||||
**Solution Pages** (6 pages, 45 min each):
|
||||
- [ ] `/solutions/avoid-vendor-lock-in`
|
||||
- [ ] `/solutions/need-offline-git-access`
|
||||
- [ ] `/solutions/github-rate-limits`
|
||||
- [ ] `/solutions/github-pricing-too-expensive`
|
||||
- [ ] `/solutions/comply-with-data-regulations`
|
||||
- [ ] `/solutions/preserve-deleted-github-repos`
|
||||
|
||||
**Guide Pages** (2 pages, 2 hours each):
|
||||
- [ ] `/guides/setup-gitea-mirror-docker`
|
||||
- [ ] `/guides/migrate-github-organization-to-gitea`
|
||||
|
||||
**Week 3 Target**: 23 total pages, start seeing traffic
|
||||
|
||||
---
|
||||
|
||||
### Week 4: Comparison & Polish (7 pages + optimization)
|
||||
**Goal**: High-conversion comparison content + optimization
|
||||
|
||||
**Comparison Pages** (4 pages, 2 hours each):
|
||||
- [ ] `/vs/github-backup-solutions`
|
||||
- [ ] `/vs/github-vs-gitea`
|
||||
- [ ] `/vs/self-hosted-vs-cloud-git`
|
||||
- [ ] `/alternatives`
|
||||
|
||||
**Integration Pages** (3 pages, 1 hour each):
|
||||
- [ ] `/integrations/docker-compose`
|
||||
- [ ] `/integrations/kubernetes`
|
||||
- [ ] `/integrations/helm-charts`
|
||||
|
||||
**Optimization** (8 hours):
|
||||
- [ ] Add internal linking between all pages
|
||||
- [ ] Optimize images (WebP, alt text)
|
||||
- [ ] Add FAQ sections to top 10 pages
|
||||
- [ ] Create content calendar for Month 2
|
||||
|
||||
**Week 4 Target**: 30 total pages, 50-100 visitors/week
|
||||
|
||||
---
|
||||
|
||||
## 📝 CONTENT TEMPLATES
|
||||
|
||||
### Template 1: Use Case Page (400-600 words, 30 min)
|
||||
|
||||
```markdown
|
||||
# [Use Case Title] - Gitea Mirror
|
||||
|
||||
> **In this guide**: Learn how to [solve specific problem] using Gitea Mirror's automated [feature].
|
||||
|
||||
## The Problem
|
||||
|
||||
[2-3 sentences describing the pain point]
|
||||
|
||||
**Common challenges:**
|
||||
- Challenge 1
|
||||
- Challenge 2
|
||||
- Challenge 3
|
||||
|
||||
## How Gitea Mirror Solves This
|
||||
|
||||
[3-4 sentences explaining the solution]
|
||||
|
||||
**Key capabilities:**
|
||||
- ✅ Capability 1
|
||||
- ✅ Capability 2
|
||||
- ✅ Capability 3
|
||||
|
||||
## Quick Start (5 Minutes)
|
||||
|
||||
\`\`\`bash
|
||||
# Step 1: Pull the Docker image
|
||||
docker pull giteamirror/gitea-mirror:latest
|
||||
|
||||
# Step 2: Run with environment variables
|
||||
docker run -d \\
|
||||
-e GITHUB_TOKEN=your_token \\
|
||||
-e GITEA_URL=https://gitea.example.com \\
|
||||
giteamirror/gitea-mirror
|
||||
\`\`\`
|
||||
|
||||
[2 sentences on what happens next]
|
||||
|
||||
## Real-World Example
|
||||
|
||||
[Short scenario: "A DevOps team needed to..."]
|
||||
|
||||
## Related Features
|
||||
|
||||
- [Link to feature 1]
|
||||
- [Link to feature 2]
|
||||
|
||||
## Get Started
|
||||
|
||||
[CTA button/link to GitHub repo]
|
||||
|
||||
---
|
||||
|
||||
**Keywords**: [primary], [secondary], [tertiary]
|
||||
**Last Updated**: [Date]
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Answers search query immediately
|
||||
- Shows code (high engagement)
|
||||
- Internal links (SEO juice)
|
||||
- Clear CTA
|
||||
- **Total time: 30 minutes**
|
||||
|
||||
---
|
||||
|
||||
### Template 2: Feature Page (500-700 words, 45 min)
|
||||
|
||||
```markdown
|
||||
# [Feature Name] - Gitea Mirror
|
||||
|
||||
> Automatically [feature benefit] from GitHub to Gitea with zero manual work.
|
||||
|
||||
## What Is [Feature Name]?
|
||||
|
||||
[2-3 sentences explaining the feature]
|
||||
|
||||
## Why You Need This
|
||||
|
||||
**Without Gitea Mirror:**
|
||||
- ❌ Manual problem 1
|
||||
- ❌ Manual problem 2
|
||||
- ❌ Manual problem 3
|
||||
|
||||
**With Gitea Mirror:**
|
||||
- ✅ Automated solution 1
|
||||
- ✅ Automated solution 2
|
||||
- ✅ Automated solution 3
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Step 1**: [Action]
|
||||
2. **Step 2**: [Action]
|
||||
3. **Step 3**: [Result]
|
||||
|
||||
## Configuration
|
||||
|
||||
\`\`\`yaml
|
||||
# Example configuration
|
||||
feature_enabled: true
|
||||
option1: value
|
||||
option2: value
|
||||
\`\`\`
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Use Case 1
|
||||
[Scenario where this feature helps]
|
||||
|
||||
### Use Case 2
|
||||
[Another scenario]
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Tip 1
|
||||
- Tip 2
|
||||
- Tip 3
|
||||
|
||||
## See It In Action
|
||||
|
||||
[Screenshot or GIF]
|
||||
|
||||
## Get Started
|
||||
|
||||
[CTA]
|
||||
|
||||
---
|
||||
|
||||
**Related**:
|
||||
- [Use case page]
|
||||
- [Guide page]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Template 3: Solution Page (300-500 words, 20 min)
|
||||
|
||||
```markdown
|
||||
# [Problem Statement] - Solved
|
||||
|
||||
> **The Problem**: [One sentence problem]
|
||||
> **The Solution**: Gitea Mirror's automated [approach]
|
||||
|
||||
## Why This Problem Matters
|
||||
|
||||
[2 sentences on impact]
|
||||
|
||||
**Consequences of not solving:**
|
||||
1. Consequence 1
|
||||
2. Consequence 2
|
||||
3. Consequence 3
|
||||
|
||||
## How Gitea Mirror Fixes This
|
||||
|
||||
[Explain the solution in 3-4 sentences]
|
||||
|
||||
## Implementation
|
||||
|
||||
\`\`\`bash
|
||||
# 2-3 line code snippet
|
||||
\`\`\`
|
||||
|
||||
## Success Story
|
||||
|
||||
"[Quote or short anecdote]"
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. [Link to getting started]
|
||||
2. [Link to relevant feature]
|
||||
|
||||
[CTA button]
|
||||
```
|
||||
|
||||
**Total time: 20 minutes**
|
||||
|
||||
---
|
||||
|
||||
## 🎨 SEO OPTIMIZATION CHECKLIST
|
||||
|
||||
### On-Page SEO (Per Page)
|
||||
```
|
||||
✅ Title tag: [Keyword] - Gitea Mirror (50-60 chars)
|
||||
✅ Meta description with CTA (150-160 chars)
|
||||
✅ H1 includes primary keyword
|
||||
✅ URL slug = primary keyword
|
||||
✅ First paragraph mentions keyword
|
||||
✅ H2s include semantic variations
|
||||
✅ Image alt text descriptive
|
||||
✅ Internal links (3-5 per page)
|
||||
✅ External links (1-2 authoritative sources)
|
||||
✅ Schema.org markup (SoftwareApplication)
|
||||
✅ Canonical URL set
|
||||
✅ Mobile responsive
|
||||
✅ Page speed < 3s
|
||||
```
|
||||
|
||||
### Content Quality Checks
|
||||
```
|
||||
✅ Answers search intent completely
|
||||
✅ 400-1500 word count (based on competition)
|
||||
✅ Code examples where relevant
|
||||
✅ Screenshots/visuals
|
||||
✅ Updated date visible
|
||||
✅ Clear CTA
|
||||
✅ Related content links
|
||||
✅ No keyword stuffing (1-2% density)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 TRACKING & METRICS
|
||||
|
||||
### Week 1-2 KPIs
|
||||
- [ ] All pages indexed in Google (check GSC)
|
||||
- [ ] 0 technical SEO errors (screaming frog)
|
||||
- [ ] < 3s page load time
|
||||
- [ ] Mobile usability 100/100
|
||||
|
||||
### Week 3-4 KPIs
|
||||
- [ ] 10+ impressions/day in GSC
|
||||
- [ ] 3+ clicks/day from organic
|
||||
- [ ] 1+ page ranking in top 50
|
||||
|
||||
### Month 2 Goals
|
||||
- [ ] 100+ impressions/day
|
||||
- [ ] 20+ clicks/day
|
||||
- [ ] 10+ keywords in top 50
|
||||
- [ ] 5+ keywords in top 20
|
||||
|
||||
### Month 3 Goals
|
||||
- [ ] 500+ impressions/day
|
||||
- [ ] 50+ clicks/day
|
||||
- [ ] 20+ keywords in top 20
|
||||
- [ ] 10+ keywords in top 10
|
||||
|
||||
---
|
||||
|
||||
## 🔗 INTERNAL LINKING STRATEGY
|
||||
|
||||
**Hub & Spoke Model**
|
||||
|
||||
### Hub Pages (Link FROM these everywhere)
|
||||
1. Homepage
|
||||
2. `/use-cases/migrate-github-to-gitea` (main use case)
|
||||
3. `/features/automatic-github-mirroring` (main feature)
|
||||
|
||||
### Spoke Pages (Link TO hubs + related spokes)
|
||||
- Use case pages link to: Related features, guides, solutions
|
||||
- Feature pages link to: Use cases, guides
|
||||
- Solution pages link to: Use cases, features
|
||||
- Guide pages link to: Features, use cases
|
||||
|
||||
**Example**:
|
||||
```
|
||||
/use-cases/backup-github-repositories
|
||||
→ Links to:
|
||||
- /features/scheduled-synchronization
|
||||
- /features/automatic-github-mirroring
|
||||
- /guides/setup-gitea-mirror-docker
|
||||
- /solutions/github-disaster-recovery
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 CONTENT HACKS: Work Smarter
|
||||
|
||||
### 1. Batch Similar Pages (2x faster)
|
||||
Write all "use case" pages in one session using the template. Copy structure, change specifics.
|
||||
|
||||
### 2. Reuse Existing Content
|
||||
- Main repo README → Use case pages
|
||||
- Docker docs → Guide pages
|
||||
- GitHub issues → Problem pages
|
||||
|
||||
### 3. AI-Assisted Expansion
|
||||
- Write 200-word outline manually
|
||||
- Expand with AI to 600 words
|
||||
- Edit for accuracy (10 min)
|
||||
- **Time saved: 50%**
|
||||
|
||||
### 4. Screenshot Once, Use Everywhere
|
||||
Create a `/public/screenshots/` library:
|
||||
- Dashboard view
|
||||
- Configuration screen
|
||||
- Migration in progress
|
||||
- Results page
|
||||
|
||||
Reuse across all pages.
|
||||
|
||||
### 5. Schema Markup Template
|
||||
Create one JSON-LD template, reuse with variable substitution:
|
||||
```json
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Gitea Mirror",
|
||||
"description": "[PAGE_DESCRIPTION]",
|
||||
"url": "[PAGE_URL]"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 MONTH 2-3 EXPANSION PLAN
|
||||
|
||||
### Month 2: Depth Over Breadth
|
||||
**Goal**: Make existing pages rank higher
|
||||
|
||||
**Activities**:
|
||||
- [ ] Add 200 words to each existing page
|
||||
- [ ] Add FAQ sections (5 Q&As per page)
|
||||
- [ ] Create 10 more guide pages (tutorials)
|
||||
- [ ] Add video embeds (YouTube shorts)
|
||||
- [ ] Guest post on Dev.to (backlinks)
|
||||
|
||||
**New Pages** (10):
|
||||
- 5 more use case pages
|
||||
- 5 advanced guides
|
||||
|
||||
### Month 3: Authority Building
|
||||
**Goal**: Establish Gitea Mirror as THE GitHub migration resource
|
||||
|
||||
**Activities**:
|
||||
- [ ] Ultimate Guide: "Complete GitHub to Gitea Migration Guide" (3,000 words)
|
||||
- [ ] Comparison matrix: All GitHub backup tools
|
||||
- [ ] Interactive tool: "Migration time calculator"
|
||||
- [ ] Video tutorials (5-10 minutes each)
|
||||
- [ ] Community: Add testimonials/case studies
|
||||
|
||||
**New Pages** (15):
|
||||
- 5 integration pages
|
||||
- 5 technical spec pages
|
||||
- 5 advanced solution pages
|
||||
|
||||
---
|
||||
|
||||
## 🏆 SUCCESS METRICS (6 Months)
|
||||
|
||||
### Conservative Target
|
||||
- **Pages**: 50 indexed
|
||||
- **Traffic**: 5,000 visits/month
|
||||
- **Keywords**: 30 in top 20
|
||||
- **Backlinks**: 15-20
|
||||
- **GitHub Stars**: +50 from organic
|
||||
|
||||
### Optimistic Target
|
||||
- **Pages**: 80 indexed
|
||||
- **Traffic**: 12,000 visits/month
|
||||
- **Keywords**: 50 in top 20, 20 in top 10
|
||||
- **Backlinks**: 40-50
|
||||
- **GitHub Stars**: +200 from organic
|
||||
|
||||
---
|
||||
|
||||
## 🔧 TECHNICAL SETUP (Do Once)
|
||||
|
||||
### Astro Content Collections
|
||||
```typescript
|
||||
// src/content/config.ts
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const useCases = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
keywords: z.array(z.string()),
|
||||
problem: z.string(),
|
||||
solution: z.string(),
|
||||
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
|
||||
timeToRead: z.number(),
|
||||
relatedPages: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
'use-cases': useCases,
|
||||
'features': defineCollection({ /* ... */ }),
|
||||
'guides': defineCollection({ /* ... */ }),
|
||||
'solutions': defineCollection({ /* ... */ }),
|
||||
'vs': defineCollection({ /* ... */ }),
|
||||
};
|
||||
```
|
||||
|
||||
### Dynamic Route Template
|
||||
```astro
|
||||
---
|
||||
// src/pages/use-cases/[...slug].astro
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const useCases = await getCollection('use-cases');
|
||||
return useCases.map(entry => ({
|
||||
params: { slug: entry.slug },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
<Layout title={entry.data.title} description={entry.data.description}>
|
||||
<article>
|
||||
<h1>{entry.data.title}</h1>
|
||||
<Content />
|
||||
</article>
|
||||
</Layout>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 QUICK ACTION CHECKLIST
|
||||
|
||||
**Today:**
|
||||
- [ ] Create content collections structure
|
||||
- [ ] Write first use case page (1 hour)
|
||||
- [ ] Setup Google Search Console
|
||||
|
||||
**This Week:**
|
||||
- [ ] Complete 5 high-priority pages
|
||||
- [ ] Submit sitemap
|
||||
- [ ] Add schema markup
|
||||
|
||||
**This Month:**
|
||||
- [ ] 30 pages live
|
||||
- [ ] Internal linking complete
|
||||
- [ ] First organic traffic
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: January 2025
|
||||
**Next Review**: February 2025
|
||||
**Owner**: [Your Team]
|
||||
36
Divers/gitea-mirror/www/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "www",
|
||||
"type": "module",
|
||||
"version": "1.1.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.12",
|
||||
"@astrojs/react": "^4.4.2",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@splinetool/react-spline": "^4.1.0",
|
||||
"@splinetool/runtime": "^1.12.5",
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^5.16.4",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.555.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.24.0"
|
||||
}
|
||||
4758
Divers/gitea-mirror/www/pnpm-lock.yaml
generated
Normal file
BIN
Divers/gitea-mirror/www/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
Divers/gitea-mirror/www/public/assets/activity.png
Normal file
|
After Width: | Height: | Size: 834 KiB |
BIN
Divers/gitea-mirror/www/public/assets/activity_mobile.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
Divers/gitea-mirror/www/public/assets/configuration-2.png
Normal file
|
After Width: | Height: | Size: 986 KiB |
BIN
Divers/gitea-mirror/www/public/assets/configuration.png
Normal file
|
After Width: | Height: | Size: 905 KiB |
BIN
Divers/gitea-mirror/www/public/assets/configuration_mobile.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
Divers/gitea-mirror/www/public/assets/dashboard.png
Normal file
|
After Width: | Height: | Size: 908 KiB |
BIN
Divers/gitea-mirror/www/public/assets/dashboard_mobile.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
Divers/gitea-mirror/www/public/assets/hero_logo.webp
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
Divers/gitea-mirror/www/public/assets/logo-new.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
Divers/gitea-mirror/www/public/assets/logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
Divers/gitea-mirror/www/public/assets/organisation.png
Normal file
|
After Width: | Height: | Size: 825 KiB |
BIN
Divers/gitea-mirror/www/public/assets/organisation_mobile.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
Divers/gitea-mirror/www/public/assets/repositories.png
Normal file
|
After Width: | Height: | Size: 952 KiB |
BIN
Divers/gitea-mirror/www/public/assets/repositories_mobile.png
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
Divers/gitea-mirror/www/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
3
Divers/gitea-mirror/www/public/favicon.svg
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
Divers/gitea-mirror/www/public/og-image.png
Normal file
|
After Width: | Height: | Size: 330 KiB |
8
Divers/gitea-mirror/www/public/robots.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
# Robots.txt for Gitea Mirror
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://gitea-mirror.com/sitemap.xml
|
||||
|
||||
# Crawl-delay for responsible crawling
|
||||
User-agent: *
|
||||
Crawl-delay: 1
|
||||
9
Divers/gitea-mirror/www/public/sitemap.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://gitea-mirror.com/</loc>
|
||||
<lastmod>2025-01-08</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
19
Divers/gitea-mirror/www/src/components/Button.astro
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
// Click button, get confetti!
|
||||
// Styled by Tailwind :)
|
||||
---
|
||||
|
||||
<button
|
||||
class="appearance-none py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-opacity-75"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<script>
|
||||
import confetti from 'canvas-confetti';
|
||||
const button = document.body.querySelector('button');
|
||||
|
||||
if (button) {
|
||||
button.addEventListener('click', () => confetti());
|
||||
}
|
||||
</script>
|
||||
60
Divers/gitea-mirror/www/src/components/CTA.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { GitHubStats } from './GitHubStats';
|
||||
|
||||
export function CTA() {
|
||||
return (
|
||||
<section className="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="relative overflow-hidden rounded-2xl sm:rounded-3xl bg-card/80 backdrop-blur-sm border border-primary/10 p-6 sm:p-8 md:p-12 text-center shadow-xl">
|
||||
{/* Subtle gradient accent */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5 pointer-events-none" />
|
||||
<div className="absolute -top-24 -right-24 w-48 h-48 bg-primary/20 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute -bottom-24 -left-24 w-48 h-48 bg-accent/20 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="relative">
|
||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold mb-4">
|
||||
<span className="block sm:inline">Start Protecting</span>
|
||||
<span className="text-gradient from-primary via-accent to-accent-purple block sm:inline"> Your Code Today</span>
|
||||
</h2>
|
||||
<p className="text-base sm:text-lg text-muted-foreground mb-6 sm:mb-8 max-w-2xl mx-auto px-4">
|
||||
Join developers who trust Gitea Mirror to keep their repositories safe and accessible.
|
||||
Free, open source, and ready to deploy.
|
||||
</p>
|
||||
|
||||
{/* Stats */}
|
||||
<GitHubStats />
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4">
|
||||
<Button size="lg" className="group w-full sm:w-auto min-h-[48px] bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30 transition-all duration-300" asChild>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer">
|
||||
Get Started Now
|
||||
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" className="w-full sm:w-auto min-h-[48px] bg-background/80 backdrop-blur-sm hover:bg-primary/10 hover:border-primary/30 hover:text-foreground transition-all duration-300" asChild>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/discussions" target="_blank" rel="noopener noreferrer">
|
||||
Join Community
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open source note */}
|
||||
<div className="mt-8 sm:mt-12 text-center">
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
Gitea Mirror is licensed under GPL-3.0.
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/blob/main/LICENSE"
|
||||
className="ml-1 underline hover:text-foreground transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
View License
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
138
Divers/gitea-mirror/www/src/components/FAQ.astro
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What is Gitea Mirror and why do I need it?",
|
||||
answer: "Gitea Mirror is a self-hosted tool that automatically backs up your GitHub repositories to your own Gitea server. You need it because GitHub outages, account issues, or policy changes can lock you out of your code. With Gitea Mirror, you own your backups completely—no monthly fees, no third-party storage, just full control over your repository history, issues, pull requests, and releases."
|
||||
},
|
||||
{
|
||||
question: "How is this different from BackHub or Rewind?",
|
||||
answer: "BackHub and Rewind are cloud services that cost $600-2400/year and store your code on their servers. Gitea Mirror is free, open source, and runs on your own infrastructure. You pay $0/month and have complete data ownership. The tradeoff: you manage the infrastructure yourself, while cloud services are fully managed."
|
||||
},
|
||||
{
|
||||
question: "Does Gitea Mirror backup issues, pull requests, and releases?",
|
||||
answer: "Yes! Enable 'Mirror metadata' in settings to backup issues with comments, labels, and assignees. Pull requests are mirrored as enriched issues with full metadata, commit history, and file changes (Gitea's API doesn't support creating PRs from external sources). Releases, including binary assets, and wiki pages are also backed up when enabled."
|
||||
},
|
||||
{
|
||||
question: "How long does setup take?",
|
||||
answer: "15-20 minutes with Docker. Run 'docker compose -f docker-compose.alt.yml up -d', visit localhost:4321, create an account, paste your GitHub and Gitea tokens, select repos to backup—done. The Proxmox LXC one-liner is even faster. No complex configuration files or manual scripting required."
|
||||
},
|
||||
{
|
||||
question: "What happens if GitHub goes down or I lose access?",
|
||||
answer: "You can immediately clone from your Gitea server instead. Your local backups include full commit history, branches, tags, issues, releases—everything. Recovery time is typically under 2 minutes (just point git to your Gitea URL). This is why it's called disaster recovery, not just mirroring."
|
||||
},
|
||||
{
|
||||
question: "How often does it sync with GitHub?",
|
||||
answer: "Configurable from every 15 minutes to once per day (or longer). Most users choose 1-8 hours based on how fresh they want backups. The scheduler auto-discovers new repos and respects per-repo intervals, unlike Gitea's built-in mirroring which defaults to 24 hours."
|
||||
},
|
||||
{
|
||||
question: "Can I backup starred repositories?",
|
||||
answer: "Yes! Gitea Mirror can automatically backup all your GitHub stars into a dedicated Gitea organization. Perfect for preserving important open source projects before they disappear (projects get deleted, renamed, or removed all the time)."
|
||||
},
|
||||
{
|
||||
question: "What are the system requirements?",
|
||||
answer: "Minimal: 2 vCPU, 2GB RAM, 5-10GB storage (grows with repo count). Runs on Docker, Kubernetes, Proxmox LXC, or bare metal. Works on AMD64 and ARM64 (Raspberry Pi compatible). A small homelab server or cheap VPS is plenty for personal use."
|
||||
},
|
||||
{
|
||||
question: "Is my GitHub token stored securely?",
|
||||
answer: "Yes. All GitHub and Gitea tokens are encrypted at rest using AES-256-GCM. Even if someone gains access to the SQLite database, they can't read your tokens without the encryption key. Use the ENCRYPTION_SECRET environment variable for additional security."
|
||||
},
|
||||
{
|
||||
question: "Can I backup private repositories?",
|
||||
answer: "Absolutely. Just use a GitHub personal access token (classic) with 'repo' scope enabled. Gitea Mirror will backup all repositories you have access to—public, private, and internal."
|
||||
},
|
||||
{
|
||||
question: "What if I have multiple GitHub organizations?",
|
||||
answer: "Gitea Mirror supports multiple organizations with flexible destination strategies: preserve GitHub structure in Gitea, consolidate into a single org, or use mixed mode (personal repos in one place, org repos preserve structure). Edit individual organization destinations via the dashboard."
|
||||
},
|
||||
{
|
||||
question: "Does it support Git LFS (large files)?",
|
||||
answer: "Yes! Enable 'Mirror LFS' in settings. Make sure your Gitea server has LFS enabled (LFS_START_SERVER = true) and Git v2.1.2+. Large assets like videos, datasets, and binaries are backed up alongside your code."
|
||||
},
|
||||
{
|
||||
question: "How do I restore from backup?",
|
||||
answer: "Simple: 'git clone https://your-gitea-server/owner/repo.git'. Your full history, branches, and tags are there. For issues/PRs/releases, they're already in Gitea's web interface. For complete disaster recovery, restore the data volume to a fresh Gitea Mirror instance—everything (config, sync history) is preserved."
|
||||
},
|
||||
{
|
||||
question: "Can I run this alongside a cloud backup service?",
|
||||
answer: "Yes! Many users run Gitea Mirror for local/warm backups while using cloud services for offsite redundancy. Best of both worlds: instant local recovery and geographic disaster protection. Totally compatible."
|
||||
},
|
||||
{
|
||||
question: "Is this enterprise-ready with SLA guarantees?",
|
||||
answer: "No. Gitea Mirror is a community open source project—no 24/7 support, no compliance certifications, no guaranteed uptime. It's perfect for homelabs, indie developers, and small teams comfortable with self-hosting. If you need enterprise SLAs, consider commercial solutions like BackHub or GitHub Enterprise Backup."
|
||||
}
|
||||
];
|
||||
|
||||
// Generate FAQ schema for SEO
|
||||
const faqSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": faqs.map(faq => ({
|
||||
"@type": "Question",
|
||||
"name": faq.question,
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": faq.answer
|
||||
}
|
||||
}))
|
||||
};
|
||||
---
|
||||
|
||||
<section id="faq" class="py-16 sm:py-24 px-4 sm:px-6 lg:px-8 bg-muted/30">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-12 sm:mb-16">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-widest text-muted-foreground mb-4">
|
||||
<HelpCircle className="w-4 h-4" />
|
||||
FAQ
|
||||
</span>
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
|
||||
Common Questions About GitHub Backups
|
||||
</h2>
|
||||
<p class="mt-4 text-base sm:text-lg text-muted-foreground">
|
||||
Everything you need to know about self-hosted repository backups with Gitea Mirror.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{faqs.map((faq, index) => (
|
||||
<details class="group rounded-xl border bg-background/80 p-6 shadow-sm transition-all hover:shadow-md open:ring-1 open:ring-primary/20">
|
||||
<summary class="flex cursor-pointer items-start justify-between gap-4 font-semibold text-foreground list-none">
|
||||
<span class="text-left">{faq.question}</span>
|
||||
<svg
|
||||
class="h-5 w-5 flex-shrink-0 text-muted-foreground transition-transform group-open:rotate-180"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</summary>
|
||||
<p class="mt-4 text-sm sm:text-base text-muted-foreground leading-relaxed">
|
||||
{faq.answer}
|
||||
</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-12 text-center">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Still have questions?
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/discussions" class="text-primary hover:text-primary/80 font-medium transition-colors ml-1">
|
||||
Ask in our GitHub Discussions
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ Schema for SEO -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify(faqSchema)} />
|
||||
|
||||
<style>
|
||||
details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
90
Divers/gitea-mirror/www/src/components/Features.astro
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
import {
|
||||
RefreshCw,
|
||||
Building2,
|
||||
FolderTree,
|
||||
Activity,
|
||||
Lock,
|
||||
Heart,
|
||||
} from 'lucide-react';
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Automated Mirroring",
|
||||
description: "Set it and forget it. Automatically sync your GitHub repositories to Gitea on a schedule.",
|
||||
icon: RefreshCw,
|
||||
gradient: "from-primary/10 to-accent/10",
|
||||
iconColor: "text-primary"
|
||||
},
|
||||
{
|
||||
title: "Bulk Operations",
|
||||
description: "Mirror entire organizations or user accounts with a single configuration.",
|
||||
icon: Building2,
|
||||
gradient: "from-accent/10 to-accent-teal/10",
|
||||
iconColor: "text-accent"
|
||||
},
|
||||
{
|
||||
title: "Preserve Structure",
|
||||
description: "Maintain your GitHub organization structure or customize how repos are organized.",
|
||||
icon: FolderTree,
|
||||
gradient: "from-accent-teal/10 to-primary/10",
|
||||
iconColor: "text-accent-teal"
|
||||
},
|
||||
{
|
||||
title: "Real-time Status",
|
||||
description: "Monitor mirror progress with live updates and detailed activity logs.",
|
||||
icon: Activity,
|
||||
gradient: "from-accent-coral/10 to-primary/10",
|
||||
iconColor: "text-accent-coral"
|
||||
},
|
||||
{
|
||||
title: "Secure & Private",
|
||||
description: "Self-hosted solution keeps your code on your infrastructure with full control.",
|
||||
icon: Lock,
|
||||
gradient: "from-accent-purple/10 to-primary/10",
|
||||
iconColor: "text-accent-purple"
|
||||
},
|
||||
{
|
||||
title: "Open Source",
|
||||
description: "Free, transparent, and community-driven development. Contribute and customize.",
|
||||
icon: Heart,
|
||||
gradient: "from-primary/10 to-accent-purple/10",
|
||||
iconColor: "text-primary"
|
||||
}
|
||||
];
|
||||
---
|
||||
|
||||
<section id="features" class="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center mb-12 sm:mb-16">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight px-4">
|
||||
Everything You Need for
|
||||
<span class="text-gradient from-primary to-accent block sm:inline"> Reliable Backups</span>
|
||||
</h2>
|
||||
<p class="mt-4 text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto px-4">
|
||||
Powerful features designed to keep your code safe and accessible, no matter what happens.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8">
|
||||
{features.map((feature) => {
|
||||
const Icon = feature.icon;
|
||||
return (
|
||||
<div
|
||||
class={`group relative p-6 sm:p-8 rounded-xl sm:rounded-2xl border bg-gradient-to-br ${feature.gradient} backdrop-blur-sm hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 overflow-hidden`}
|
||||
>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-transparent to-background/50 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
<div class="relative">
|
||||
<div class={`inline-flex p-2.5 sm:p-3 rounded-lg bg-background/80 backdrop-blur-sm mb-3 sm:mb-4 ${feature.iconColor} shadow-sm`}>
|
||||
<Icon className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg sm:text-xl font-semibold mb-2">{feature.title}</h3>
|
||||
<p class="text-sm sm:text-base text-muted-foreground">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
83
Divers/gitea-mirror/www/src/components/Footer.astro
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
import { Github, Book, MessageSquare, Bug } from 'lucide-react';
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: "Source Code",
|
||||
href: "https://github.com/RayLabsHQ/gitea-mirror",
|
||||
icon: Github
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
href: "https://github.com/RayLabsHQ/gitea-mirror/tree/main/docs",
|
||||
icon: Book
|
||||
},
|
||||
{
|
||||
title: "Discussions",
|
||||
href: "https://github.com/RayLabsHQ/gitea-mirror/discussions",
|
||||
icon: MessageSquare
|
||||
},
|
||||
{
|
||||
title: "Report Issue",
|
||||
href: "https://github.com/RayLabsHQ/gitea-mirror/issues",
|
||||
icon: Bug
|
||||
}
|
||||
];
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="border-t py-8 sm:py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex flex-col items-center gap-6 sm:gap-8">
|
||||
<!-- Logo and tagline -->
|
||||
<div class="text-center">
|
||||
<div class="flex items-center justify-center gap-2 mb-2">
|
||||
<img
|
||||
src="/assets/logo.png"
|
||||
alt="Gitea Mirror"
|
||||
class="w-7 h-6 md:w-10 md:h-8"
|
||||
/>
|
||||
<span class="font-semibold text-base sm:text-lg">Gitea Mirror</span>
|
||||
</div>
|
||||
<p class="text-xs sm:text-sm text-muted-foreground">
|
||||
Keep your GitHub code safe and synced
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<nav class="grid grid-cols-2 sm:flex items-center justify-center gap-4 sm:gap-6 text-center">
|
||||
{links.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<a
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center justify-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors py-2 sm:py-0"
|
||||
>
|
||||
<Icon className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
<span>{link.title}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<!-- Copyright -->
|
||||
<div class="text-center text-xs sm:text-sm text-muted-foreground px-4">
|
||||
<p>© {currentYear} Gitea Mirror. Open source under GPL-3.0 License.</p>
|
||||
<p class="mt-1">
|
||||
Made with dedication by the{' '}
|
||||
<a
|
||||
href="https://github.com/RayLabsHQ"
|
||||
class="underline hover:text-foreground transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
RayLabs team
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
60
Divers/gitea-mirror/www/src/components/GitHubButton.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Github, Star } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function GitHubButton() {
|
||||
const [stars, setStars] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStars = async () => {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/RayLabsHQ/gitea-mirror');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStars(data.stargazers_count);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch GitHub stars:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStars();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile version - compact with text */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="md:hidden hover:bg-primary/10 hover:border-primary/30 hover:text-foreground transition-all duration-300 px-3"
|
||||
asChild
|
||||
>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5">
|
||||
<Star className="w-4 h-4" />
|
||||
<span className="font-semibold">{stars || '—'}</span>
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
{/* Desktop version - full button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="hidden md:flex hover:bg-primary/10 hover:border-primary/30 hover:text-foreground transition-all duration-300"
|
||||
asChild
|
||||
>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer" className="flex items-center">
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
<span>Star on GitHub</span>
|
||||
{stars !== null && (
|
||||
<>
|
||||
<span className="mx-2 text-muted-foreground">•</span>
|
||||
<Star className="w-3 h-3 mr-1" />
|
||||
<span>{stars}</span>
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
59
Divers/gitea-mirror/www/src/components/GitHubStats.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Star, GitFork, Users } from 'lucide-react';
|
||||
|
||||
interface GitHubRepo {
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
open_issues_count: number;
|
||||
}
|
||||
|
||||
export function GitHubStats() {
|
||||
const [stats, setStats] = useState<GitHubRepo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/RayLabsHQ/gitea-mirror');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch GitHub stats:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 md:gap-8 mb-6 sm:mb-8 text-white/80 text-sm sm:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
<span className="font-semibold">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 md:gap-8 mb-6 sm:mb-8 text-foreground text-sm sm:text-base">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
<span className="font-semibold">{stats?.stargazers_count || 0} Stars</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitFork className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
<span className="font-semibold">{stats?.forks_count || 0} Forks</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
<span className="font-semibold">Active Community</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
Divers/gitea-mirror/www/src/components/Header.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import { GitHubButton } from './GitHubButton';
|
||||
|
||||
export function Header() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 10);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/#features', label: 'Features' },
|
||||
{ href: '/#use-cases', label: 'Use Cases' },
|
||||
{ href: '/#screenshots', label: 'Screenshots' },
|
||||
{ href: '/#installation', label: 'Installation' }
|
||||
];
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed left-0 right-0 z-50 transition-all duration-300 ${
|
||||
isScrolled ? 'backdrop-blur-lg bg-background/80 border-b shadow-sm' : 'bg-background/50'
|
||||
}`}
|
||||
style={{ top: 'var(--promo-banner-height, 0px)' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<a href="/" className="flex items-center gap-2 group">
|
||||
<img
|
||||
src="/assets/logo.png"
|
||||
alt="Gitea Mirror Logo"
|
||||
className="w-7 h-6 md:w-10 md:h-8"
|
||||
/>
|
||||
<span className="text-lg sm:text-xl font-bold">Gitea Mirror</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-8">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Desktop Actions */}
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
<ThemeToggle />
|
||||
<GitHubButton />
|
||||
</div>
|
||||
|
||||
{/* Mobile Actions */}
|
||||
<div className="flex md:hidden items-center gap-3">
|
||||
<GitHubButton />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
);
|
||||
}
|
||||
98
Divers/gitea-mirror/www/src/components/Hero.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Button } from "./ui/button";
|
||||
import { ArrowRight, Shield, RefreshCw, HardDrive } from "lucide-react";
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
const Spline = React.lazy(() => import('@splinetool/react-spline'));
|
||||
|
||||
export function Hero() {
|
||||
|
||||
return (
|
||||
<section className="relative min-h-[100vh] pt-20 pb-10 flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
{/* spline object */}
|
||||
<div className="spline-object absolute inset-0 max-lg:-z-10 max-h-[40rem] -translate-y-16 md:max-h-[50rem] lg:max-h-[60%] xl:max-h-[70%] 2xl:max-h-[80%] md:-translate-y-24 lg:-translate-y-28 flex items-center justify-center">
|
||||
|
||||
<div className="block md:hidden w-[80%]">
|
||||
<img
|
||||
src="/assets/hero_logo.webp"
|
||||
alt="Gitea Mirror hero image"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-2 bottom-4 h-20 w-40 bg-background hidden md:block"/>
|
||||
<Suspense fallback={
|
||||
<div className="w-full h-full md:flex items-center justify-center hidden">
|
||||
<img
|
||||
src="/assets/hero_logo.webp"
|
||||
alt="Gitea Mirror hero logo"
|
||||
className="w-[200px] h-[160px] md:w-[280px] md:h-[240px] lg:w-[360px] lg:h-[320px] xl:w-[420px] xl:h-[380px] 2xl:w-[480px] 2xl:h-[420px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<Spline
|
||||
scene="https://prod.spline.design/jl0aKWbdH9vHQnYV/scene.splinecode"
|
||||
className="hidden md:block"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
{/* div to avoid clipping in lower screen heights */}
|
||||
<div className="clip-avoid w-full h-[16rem] md:h-[20rem] lg:h-[12rem] 2xl:h-[16rem]" aria-hidden="true"></div>
|
||||
<div className="max-w-7xl mx-auto pb-20 lg:pb-60 xl:pb-24 text-center w-full">
|
||||
<h1 className="pt-10 2xl:pt-20 text-3xl xs:text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight leading-tight">
|
||||
<span className="text-foreground">Backup Your GitHub</span>
|
||||
<br />
|
||||
<span className="text-gradient from-primary via-accent to-accent-purple">
|
||||
To Self-Hosted Gitea
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="mt-4 sm:mt-6 text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-4 z-20">
|
||||
Automatic, private, and free. Own your code history forever.
|
||||
Preserve issues, PRs, releases, and wiki in your own Gitea server.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 sm:mt-8 flex flex-wrap items-center justify-center gap-3 text-xs sm:text-sm text-muted-foreground px-4 z-20">
|
||||
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary">
|
||||
<HardDrive className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
<span className="font-medium">Self-Hosted Backup</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-accent/10 text-accent">
|
||||
<RefreshCw className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
<span className="font-medium">Automated Syncing</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-accent-purple/10 text-accent-purple">
|
||||
<Shield className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
<span className="font-medium">$0/month</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Hunt Badge */}
|
||||
<div className="mt-6 sm:mt-8 flex items-center justify-center px-4 z-20">
|
||||
<a
|
||||
href="https://www.producthunt.com/products/gitea-mirror?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-gitea-mirror"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block transition-transform hover:scale-105"
|
||||
>
|
||||
<img
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1013721&theme=light&t=1757620787136"
|
||||
alt="Gitea Mirror - Automated github to gitea repository mirroring & backup | Product Hunt"
|
||||
style={{ width: '250px', height: '54px' }}
|
||||
width="250"
|
||||
height="54"
|
||||
className="dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1013721&theme=dark&t=1757620890723"
|
||||
alt="Gitea Mirror - Automated github to gitea repository mirroring & backup | Product Hunt"
|
||||
style={{ width: '250px', height: '54px' }}
|
||||
width="250"
|
||||
height="54"
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
169
Divers/gitea-mirror/www/src/components/Installation.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Copy, Check, Terminal, Container, Cloud } from 'lucide-react';
|
||||
|
||||
type InstallMethod = 'docker' | 'manual' | 'proxmox';
|
||||
|
||||
export function Installation() {
|
||||
const [activeMethod, setActiveMethod] = useState<InstallMethod>('docker');
|
||||
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
|
||||
|
||||
const copyToClipboard = async (text: string, commandId: string) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopiedCommand(commandId);
|
||||
setTimeout(() => setCopiedCommand(null), 2000);
|
||||
};
|
||||
|
||||
const installMethods = {
|
||||
docker: {
|
||||
icon: Container,
|
||||
title: "Docker",
|
||||
description: "Recommended for most users",
|
||||
steps: [
|
||||
{
|
||||
title: "Clone the repository",
|
||||
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git && cd gitea-mirror",
|
||||
id: "docker-clone"
|
||||
},
|
||||
{
|
||||
title: "Start with Docker Compose",
|
||||
command: "docker compose -f docker-compose.alt.yml up -d",
|
||||
id: "docker-start"
|
||||
},
|
||||
{
|
||||
title: "Access the application",
|
||||
command: "# Open http://localhost:4321 in your browser",
|
||||
id: "docker-access"
|
||||
}
|
||||
]
|
||||
},
|
||||
manual: {
|
||||
icon: Terminal,
|
||||
title: "Manual",
|
||||
description: "For development or custom setups",
|
||||
steps: [
|
||||
{
|
||||
title: "Install Bun runtime",
|
||||
command: "curl -fsSL https://bun.sh/install | bash",
|
||||
id: "manual-bun"
|
||||
},
|
||||
{
|
||||
title: "Clone and setup",
|
||||
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git\ncd gitea-mirror\nbun run setup",
|
||||
id: "manual-setup"
|
||||
},
|
||||
{
|
||||
title: "Start the application",
|
||||
command: "bun run dev",
|
||||
id: "manual-start"
|
||||
}
|
||||
]
|
||||
},
|
||||
proxmox: {
|
||||
icon: Cloud,
|
||||
title: "Proxmox LXC",
|
||||
description: "One-click install for Proxmox VE",
|
||||
steps: [
|
||||
{
|
||||
title: "Run the installation script",
|
||||
command: 'bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"',
|
||||
id: "proxmox-install"
|
||||
},
|
||||
{
|
||||
title: "Follow the prompts",
|
||||
command: "# The script will guide you through the setup",
|
||||
id: "proxmox-follow"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="installation" className="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8 sm:mb-16">
|
||||
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
|
||||
Get Started in Minutes
|
||||
</h2>
|
||||
<p className="mt-4 text-base sm:text-lg text-muted-foreground">
|
||||
Choose your preferred installation method
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Installation method tabs */}
|
||||
<div className="flex flex-col sm:flex-row flex-wrap justify-center gap-3 sm:gap-4 mb-8 sm:mb-12">
|
||||
{(Object.entries(installMethods) as [InstallMethod, typeof installMethods[InstallMethod]][]).map(([method, config]) => {
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<button
|
||||
key={method}
|
||||
onClick={() => setActiveMethod(method)}
|
||||
className={`flex items-center gap-3 px-4 sm:px-6 py-3 rounded-lg border transition-all min-h-[60px] ${
|
||||
activeMethod === method
|
||||
? 'bg-gradient-to-r from-primary to-accent text-primary-foreground border-transparent shadow-lg shadow-primary/25'
|
||||
: 'bg-card hover:bg-muted border-border hover:border-primary/30'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
<div className="text-left">
|
||||
<p className="font-semibold text-sm sm:text-base">{config.title}</p>
|
||||
<p className={`text-xs ${activeMethod === method ? 'text-primary-foreground/80' : 'text-muted-foreground'}`}>
|
||||
{config.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Installation steps */}
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{installMethods[activeMethod].steps.map((step, index) => (
|
||||
<div key={step.id} className="relative">
|
||||
<div className="flex items-start gap-3 sm:gap-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-md shadow-primary/20">
|
||||
<span className="text-sm font-semibold text-primary-foreground">{index + 1}</span>
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
<h3 className="font-semibold mb-2 text-sm sm:text-base">{step.title}</h3>
|
||||
<div className="relative group">
|
||||
<div className="relative overflow-hidden rounded-lg">
|
||||
<pre className="bg-muted/50 p-3 sm:p-4 pr-10 sm:pr-12 overflow-x-auto text-[11px] sm:text-sm font-mono scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent">
|
||||
<code className="block whitespace-nowrap">{step.command}</code>
|
||||
</pre>
|
||||
{/* Scroll indicator gradient for mobile */}
|
||||
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-muted/50 to-transparent pointer-events-none sm:hidden" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-1 right-1 sm:top-2 sm:right-2 w-7 h-7 sm:w-9 sm:h-9 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity z-10"
|
||||
onClick={() => copyToClipboard(step.command, step.id)}
|
||||
>
|
||||
{copiedCommand === step.id ? (
|
||||
<Check className="h-3 w-3 sm:h-4 sm:w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{index < installMethods[activeMethod].steps.length - 1 && (
|
||||
<div className="absolute left-4 top-10 bottom-0 w-[1px] bg-border -z-10" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Additional info */}
|
||||
<div className="mt-8 sm:mt-12 p-4 sm:p-6 rounded-lg bg-muted/30 border">
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
<strong className="text-foreground">First user becomes admin.</strong> After installation,
|
||||
create your account and configure GitHub and Gitea connections through the web interface.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
51
Divers/gitea-mirror/www/src/components/PromoBanner.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Calendar, Sparkles } from 'lucide-react';
|
||||
|
||||
export function PromoBanner() {
|
||||
const bannerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Update CSS custom property for header offset
|
||||
const updateOffset = () => {
|
||||
if (bannerRef.current) {
|
||||
const height = bannerRef.current.offsetHeight;
|
||||
document.documentElement.style.setProperty('--promo-banner-height', `${height}px`);
|
||||
}
|
||||
};
|
||||
|
||||
updateOffset();
|
||||
window.addEventListener('resize', updateOffset);
|
||||
return () => window.removeEventListener('resize', updateOffset);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={bannerRef}
|
||||
className="fixed top-0 left-0 right-0 z-[60] bg-gradient-to-r from-violet-600 via-purple-600 to-indigo-600 text-white"
|
||||
>
|
||||
<a
|
||||
href="https://lumical.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5 hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-x-3 text-sm">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="font-medium">New from RayLabs:</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 font-semibold">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Lumical
|
||||
</span>
|
||||
<span className="hidden sm:inline text-white/90">
|
||||
— Scan meeting invites to your calendar with AI
|
||||
</span>
|
||||
<span className="ml-1 inline-flex items-center gap-1 rounded-full bg-white/20 px-3 py-0.5 text-xs font-medium">
|
||||
Try it free
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
245
Divers/gitea-mirror/www/src/components/Screenshots.astro
Normal file
@@ -0,0 +1,245 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
// Import all images
|
||||
import dashboardDesktop from '../../public/assets/dashboard.png';
|
||||
import dashboardMobile from '../../public/assets/dashboard_mobile.png';
|
||||
import organisationDesktop from '../../public/assets/organisation.png';
|
||||
import organisationMobile from '../../public/assets/organisation_mobile.png';
|
||||
import repositoriesDesktop from '../../public/assets/repositories.png';
|
||||
import repositoriesMobile from '../../public/assets/repositories_mobile.png';
|
||||
import configurationDesktop from '../../public/assets/configuration.png';
|
||||
import configurationMobile from '../../public/assets/configuration_mobile.png';
|
||||
import activityDesktop from '../../public/assets/activity.png';
|
||||
import activityMobile from '../../public/assets/activity_mobile.png';
|
||||
|
||||
const screenshots = [
|
||||
{
|
||||
title: "Dashboard Overview",
|
||||
description: "Monitor all your mirrored repositories in one place",
|
||||
desktop: dashboardDesktop,
|
||||
mobile: dashboardMobile
|
||||
},
|
||||
{
|
||||
title: "Organization Management",
|
||||
description: "Easily manage and sync entire GitHub organizations",
|
||||
desktop: organisationDesktop,
|
||||
mobile: organisationMobile
|
||||
},
|
||||
{
|
||||
title: "Repository Control",
|
||||
description: "Fine-grained control over individual repository mirrors",
|
||||
desktop: repositoriesDesktop,
|
||||
mobile: repositoriesMobile
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
description: "Simple and intuitive configuration interface",
|
||||
desktop: configurationDesktop,
|
||||
mobile: configurationMobile
|
||||
},
|
||||
{
|
||||
title: "Activity Monitoring",
|
||||
description: "Track sync progress and view detailed logs",
|
||||
desktop: activityDesktop,
|
||||
mobile: activityMobile
|
||||
}
|
||||
];
|
||||
---
|
||||
|
||||
<section id="screenshots" class="py-16 sm:py-24 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-muted/30 via-primary/5 to-muted/30">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center mb-8 sm:mb-16">
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
|
||||
See It In Action
|
||||
</h2>
|
||||
<p class="mt-4 text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto px-4">
|
||||
A clean, intuitive interface designed for efficiency and ease of use
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-5xl mx-auto">
|
||||
<!-- Screenshot viewer -->
|
||||
<div id="screenshot-container" class="relative group">
|
||||
<div class="aspect-[9/16] sm:aspect-[16/10] overflow-hidden rounded-lg sm:rounded-2xl bg-card border shadow-lg">
|
||||
{screenshots.map((screenshot, index) => (
|
||||
<picture data-index={index} class={index === 0 ? 'block' : 'hidden'}>
|
||||
<source media="(max-width: 640px)" srcset={screenshot.mobile.src} />
|
||||
<Image
|
||||
src={screenshot.desktop}
|
||||
alt={screenshot.title}
|
||||
class="w-full h-full object-cover object-top"
|
||||
draggable={false}
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
/>
|
||||
</picture>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="screenshot-nav-prev absolute left-2 sm:left-4 top-1/2 -translate-y-1/2 opacity-0 sm:opacity-100 group-hover:opacity-100 transition-opacity hidden sm:flex"
|
||||
aria-label="Previous screenshot"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="screenshot-nav-next absolute right-2 sm:right-4 top-1/2 -translate-y-1/2 opacity-0 sm:opacity-100 group-hover:opacity-100 transition-opacity hidden sm:flex"
|
||||
aria-label="Next screenshot"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Screenshot info -->
|
||||
<div class="mt-6 sm:mt-8 text-center">
|
||||
<h3 id="screenshot-title" class="text-lg sm:text-xl font-semibold">{screenshots[0].title}</h3>
|
||||
<p id="screenshot-description" class="mt-2 text-sm sm:text-base text-muted-foreground">{screenshots[0].description}</p>
|
||||
</div>
|
||||
|
||||
<!-- Dots indicator -->
|
||||
<div class="mt-6 sm:mt-8 flex justify-center gap-2">
|
||||
{screenshots.map((_, index) => (
|
||||
<button
|
||||
data-index={index}
|
||||
class={`screenshot-dot transition-all duration-300 ${
|
||||
index === 0
|
||||
? 'w-8 h-2 bg-primary rounded-full'
|
||||
: 'w-2 h-2 bg-muted-foreground/30 hover:bg-muted-foreground/50 rounded-full'
|
||||
}`}
|
||||
aria-label={`Go to screenshot ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Mobile swipe hint -->
|
||||
<p class="mt-4 text-xs text-muted-foreground text-center sm:hidden">
|
||||
Swipe left or right to navigate
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Thumbnail grid -->
|
||||
<div class="hidden lg:grid grid-cols-5 gap-4 mt-12 px-8">
|
||||
{screenshots.map((screenshot, index) => (
|
||||
<button
|
||||
data-index={index}
|
||||
class={`screenshot-thumb relative overflow-hidden rounded-lg transition-all duration-300 ${
|
||||
index === 0
|
||||
? 'ring-2 ring-primary shadow-lg scale-105'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={screenshot.desktop}
|
||||
alt={screenshot.title}
|
||||
class="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script define:vars={{ screenshots }}>
|
||||
let currentIndex = 0;
|
||||
let touchStart = 0;
|
||||
let touchEnd = 0;
|
||||
const minSwipeDistance = 50;
|
||||
|
||||
const container = document.getElementById('screenshot-container');
|
||||
const pictures = container.querySelectorAll('picture');
|
||||
const dots = document.querySelectorAll('.screenshot-dot');
|
||||
const thumbs = document.querySelectorAll('.screenshot-thumb');
|
||||
const titleEl = document.getElementById('screenshot-title');
|
||||
const descriptionEl = document.getElementById('screenshot-description');
|
||||
const prevBtn = container.querySelector('.screenshot-nav-prev');
|
||||
const nextBtn = container.querySelector('.screenshot-nav-next');
|
||||
|
||||
function updateView(newIndex) {
|
||||
// Hide current, show new
|
||||
pictures[currentIndex].classList.add('hidden');
|
||||
pictures[newIndex].classList.remove('hidden');
|
||||
|
||||
// Update dots
|
||||
dots[currentIndex].classList.remove('w-8', 'h-2', 'bg-primary');
|
||||
dots[currentIndex].classList.add('w-2', 'h-2', 'bg-muted-foreground/30');
|
||||
dots[newIndex].classList.remove('w-2', 'h-2', 'bg-muted-foreground/30');
|
||||
dots[newIndex].classList.add('w-8', 'h-2', 'bg-primary');
|
||||
|
||||
// Update thumbnails
|
||||
if (thumbs.length > 0) {
|
||||
thumbs[currentIndex].classList.remove('ring-2', 'ring-primary', 'shadow-lg', 'scale-105');
|
||||
thumbs[currentIndex].classList.add('opacity-60');
|
||||
thumbs[newIndex].classList.remove('opacity-60');
|
||||
thumbs[newIndex].classList.add('ring-2', 'ring-primary', 'shadow-lg', 'scale-105');
|
||||
}
|
||||
|
||||
// Update text
|
||||
titleEl.textContent = screenshots[newIndex].title;
|
||||
descriptionEl.textContent = screenshots[newIndex].description;
|
||||
|
||||
currentIndex = newIndex;
|
||||
}
|
||||
|
||||
function goToPrevious() {
|
||||
const newIndex = currentIndex === 0 ? screenshots.length - 1 : currentIndex - 1;
|
||||
updateView(newIndex);
|
||||
}
|
||||
|
||||
function goToNext() {
|
||||
const newIndex = (currentIndex + 1) % screenshots.length;
|
||||
updateView(newIndex);
|
||||
}
|
||||
|
||||
// Touch handling
|
||||
container.addEventListener('touchstart', (e) => {
|
||||
touchEnd = 0;
|
||||
touchStart = e.targetTouches[0].clientX;
|
||||
});
|
||||
|
||||
container.addEventListener('touchmove', (e) => {
|
||||
touchEnd = e.targetTouches[0].clientX;
|
||||
});
|
||||
|
||||
container.addEventListener('touchend', () => {
|
||||
if (!touchStart || !touchEnd) return;
|
||||
|
||||
const distance = touchStart - touchEnd;
|
||||
const isLeftSwipe = distance > minSwipeDistance;
|
||||
const isRightSwipe = distance < -minSwipeDistance;
|
||||
|
||||
if (isLeftSwipe && currentIndex < screenshots.length - 1) {
|
||||
goToNext();
|
||||
}
|
||||
if (isRightSwipe && currentIndex > 0) {
|
||||
goToPrevious();
|
||||
}
|
||||
});
|
||||
|
||||
// Button navigation
|
||||
prevBtn?.addEventListener('click', goToPrevious);
|
||||
nextBtn?.addEventListener('click', goToNext);
|
||||
|
||||
// Dot navigation
|
||||
dots.forEach((dot, index) => {
|
||||
dot.addEventListener('click', () => updateView(index));
|
||||
});
|
||||
|
||||
// Thumbnail navigation
|
||||
thumbs.forEach((thumb, index) => {
|
||||
thumb.addEventListener('click', () => updateView(index));
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') goToPrevious();
|
||||
if (e.key === 'ArrowRight') goToNext();
|
||||
});
|
||||
</script>
|
||||
140
Divers/gitea-mirror/www/src/components/ShaderBackground.astro
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
---
|
||||
|
||||
<canvas id="shader-canvas" class="hidden lg:block absolute inset-0 w-full h-full dark:opacity-90 z-10 pointer-events-none"></canvas>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('shader-canvas') as HTMLCanvasElement | null;
|
||||
if (!canvas) {
|
||||
console.error('Canvas element not found');
|
||||
} else {
|
||||
const gl = canvas.getContext('webgl', { alpha: true });
|
||||
|
||||
if (!gl) {
|
||||
console.error('WebGL not supported!');
|
||||
} else {
|
||||
const vertexShaderSource = `
|
||||
attribute vec2 a_position;
|
||||
void main() {
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShaderSource = `
|
||||
precision mediump float;
|
||||
uniform vec2 iResolution;
|
||||
uniform float iTime;
|
||||
uniform vec3 u_color1;
|
||||
uniform vec3 u_color2;
|
||||
#define iterations 2
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / iResolution.xy;
|
||||
vec2 mirrored_uv = uv;
|
||||
|
||||
if (mirrored_uv.x > 0.5) {
|
||||
mirrored_uv.x = 1.0 - mirrored_uv.x;
|
||||
}
|
||||
|
||||
float res = 1.0;
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
res += cos(mirrored_uv.y * 12.345 - iTime * 1.0 + cos(res * 12.234) * 0.2 + cos(mirrored_uv.x * 32.2345 + cos(mirrored_uv.y * 17.234))) + cos(mirrored_uv.x * 12.345);
|
||||
}
|
||||
|
||||
vec3 c = mix(u_color1, u_color2, cos(res + cos(mirrored_uv.y * 24.3214) * 0.1 + cos(mirrored_uv.x * 6.324 + iTime * 1.0) + iTime) * 0.5 + 0.5);
|
||||
float vignette = clamp((length(mirrored_uv - 0.1 + cos(iTime * 0.9 + mirrored_uv.yx * 4.34 + mirrored_uv.xy * res) * 0.2) * 5.0 - 0.4), 0.0, 1.0);
|
||||
vec3 final_color = mix(c, vec3(0.0), vignette);
|
||||
gl_FragColor = vec4(final_color, length(final_color));
|
||||
}
|
||||
`;
|
||||
|
||||
function createShader(context: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
|
||||
const shader = context.createShader(type);
|
||||
if (!shader) return null;
|
||||
|
||||
context.shaderSource(shader, source);
|
||||
context.compileShader(shader);
|
||||
|
||||
if (!context.getShaderParameter(shader, context.COMPILE_STATUS)) {
|
||||
console.error('Shader compilation error:', context.getShaderInfoLog(shader));
|
||||
context.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
|
||||
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
|
||||
|
||||
if (vertexShader && fragmentShader) {
|
||||
const program = gl.createProgram();
|
||||
if (program) {
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('Program linking error:', gl.getProgramInfoLog(program));
|
||||
} else {
|
||||
gl.useProgram(program);
|
||||
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
||||
|
||||
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
|
||||
gl.enableVertexAttribArray(positionAttributeLocation);
|
||||
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
const resolutionLocation = gl.getUniformLocation(program, "iResolution");
|
||||
const timeLocation = gl.getUniformLocation(program, "iTime");
|
||||
const color1Location = gl.getUniformLocation(program, "u_color1");
|
||||
const color2Location = gl.getUniformLocation(program, "u_color2");
|
||||
|
||||
const color1 = [0.122, 0.502, 0.122]; // #1f801f
|
||||
const color2 = [0.059, 0.251, 0.059]; // #0f400f
|
||||
|
||||
function render(time: number): void {
|
||||
if (!gl || !program || !canvas) return;
|
||||
|
||||
time *= 0.001;
|
||||
|
||||
const displayWidth = canvas.clientWidth;
|
||||
const displayHeight = canvas.clientHeight;
|
||||
|
||||
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
|
||||
canvas.width = displayWidth;
|
||||
canvas.height = displayHeight;
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.useProgram(program);
|
||||
|
||||
if (resolutionLocation !== null) {
|
||||
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
|
||||
}
|
||||
if (timeLocation !== null) {
|
||||
gl.uniform1f(timeLocation, time);
|
||||
}
|
||||
if (color1Location !== null) {
|
||||
gl.uniform3fv(color1Location, color1);
|
||||
}
|
||||
if (color2Location !== null) {
|
||||
gl.uniform3fv(color2Location, color2);
|
||||
}
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
40
Divers/gitea-mirror/www/src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
useEffect(() => {
|
||||
// Check for saved theme preference or default to light
|
||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
|
||||
|
||||
setTheme(initialTheme);
|
||||
document.documentElement.classList.toggle('dark', initialTheme === 'dark');
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="w-10 h-10 rounded-full"
|
||||
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
<Moon className="h-5 w-5 transition-all" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5 transition-all" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
74
Divers/gitea-mirror/www/src/components/UseCases.astro
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useCases } from '@/lib/use-cases';
|
||||
---
|
||||
|
||||
<section id="use-cases" class="py-16 sm:py-24 px-4 sm:px-6 lg:px-8 bg-muted/30 border-y">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center max-w-3xl mx-auto mb-12 sm:mb-16">
|
||||
<span class="inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-widest text-muted-foreground mb-4">
|
||||
Use Cases
|
||||
</span>
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
|
||||
Proven Ways Teams Depend on
|
||||
<span class="text-gradient from-primary to-accent block sm:inline"> Gitea Mirror</span>
|
||||
</h2>
|
||||
<p class="mt-4 text-base sm:text-lg text-muted-foreground">
|
||||
Explore real-world workflows where automated mirroring removes risk, accelerates migrations, and keeps engineering teams shipping.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:gap-6 lg:gap-8 lg:grid-cols-3">
|
||||
{useCases.slice(0, 3).map((useCase) => (
|
||||
<article class="group relative flex flex-col rounded-2xl border bg-background/80 p-6 sm:p-8 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
|
||||
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-primary/5 via-accent/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div class="relative flex flex-col h-full">
|
||||
<h3 class="text-xl font-semibold mb-3">
|
||||
{useCase.title}
|
||||
</h3>
|
||||
<p class="text-sm sm:text-base text-muted-foreground mb-4">
|
||||
{useCase.summary}
|
||||
</p>
|
||||
|
||||
<dl class="grid gap-3 text-sm sm:text-base text-muted-foreground">
|
||||
<div>
|
||||
<dt class="font-semibold text-foreground">Pain Point</dt>
|
||||
<dd>{useCase.painPoint}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="font-semibold text-foreground">Outcome</dt>
|
||||
<dd>{useCase.outcome}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-2">
|
||||
{useCase.tags.map((tag) => (
|
||||
<span class="inline-flex items-center rounded-full border border-muted px-3 py-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={`/use-cases/${useCase.slug}/`}
|
||||
class="mt-auto inline-flex items-center gap-2 pt-6 text-sm font-medium text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
Read the playbook
|
||||
<ArrowRight class="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-10 text-center">
|
||||
<a
|
||||
href="/use-cases/"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-6 py-2 text-sm font-semibold text-primary transition-colors hover:bg-primary/15"
|
||||
>
|
||||
View more use cases
|
||||
<ArrowRight class="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
79
Divers/gitea-mirror/www/src/components/UseCasesList.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
import { useCases } from '@/lib/use-cases';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
const featured = useCases.slice(0, 3);
|
||||
const more = useCases.slice(3);
|
||||
|
||||
export function FeaturedUseCases() {
|
||||
return (
|
||||
<div className="mt-8 grid gap-6 lg:grid-cols-3">
|
||||
{featured.map((item) => (
|
||||
<article key={item.slug} className="group relative flex flex-col rounded-3xl border border-primary/50 bg-primary/5 p-6 sm:p-7 shadow-lg shadow-primary/10 transition-all duration-300 hover:-translate-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{item.tags.map((tag) => (
|
||||
<span key={tag} className="inline-flex items-center rounded-full border border-muted px-2.5 py-1">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="mt-4 text-xl font-semibold sm:text-2xl text-foreground">{item.title}</h2>
|
||||
<p className="mt-3 text-sm sm:text-base text-muted-foreground">{item.summary}</p>
|
||||
<div className="mt-5 grid gap-3 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground/70">Pain point</h3>
|
||||
<p>{item.painPoint}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground/70">Outcome</h3>
|
||||
<p>{item.outcome}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={`/use-cases/${item.slug}/`}
|
||||
className="mt-6 inline-flex w-max items-center gap-2 rounded-full border border-primary/50 px-4 py-2 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
|
||||
>
|
||||
View playbook
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MoreUseCases() {
|
||||
return (
|
||||
<div className="mt-8 grid gap-6 md:grid-cols-2">
|
||||
{more.map((item) => (
|
||||
<article key={item.slug} className="group relative flex flex-col rounded-3xl border border-muted bg-background/70 p-6 sm:p-7 transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-lg">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{item.tags.map((tag) => (
|
||||
<span key={tag} className="inline-flex items-center rounded-full border border-muted px-2.5 py-1">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 className="mt-4 text-xl font-semibold sm:text-2xl text-foreground/90">{item.title}</h2>
|
||||
<p className="mt-3 text-sm sm:text-base text-muted-foreground">{item.summary}</p>
|
||||
|
||||
<div className="mt-5 grid gap-3 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground/70">Pain point</h3>
|
||||
<p>{item.painPoint}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-foreground/70">Outcome</h3>
|
||||
<p>{item.outcome}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={`/use-cases/${item.slug}/`}
|
||||
className="mt-6 inline-flex w-max items-center gap-2 rounded-full border border-primary/50 px-4 py-2 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
|
||||
>
|
||||
View playbook
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
58
Divers/gitea-mirror/www/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}
|
||||
>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
115
Divers/gitea-mirror/www/src/layouts/UseCaseIndexLayout.astro
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import { Header } from '../components/Header';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
const {
|
||||
content: {
|
||||
title = 'Use Case',
|
||||
description = 'Explore how Gitea Mirror helps engineering teams stay resilient.',
|
||||
canonical = 'https://gitea-mirror.com/use-cases',
|
||||
}
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{title} · Gitea Mirror</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<script is:inline>
|
||||
const theme = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-background text-foreground antialiased">
|
||||
<Header client:load />
|
||||
<main class="pt-24 pb-20">
|
||||
<article class="use-case-content mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<slot />
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
.use-case-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.use-case-content > :is(h1, h2, h3) {
|
||||
font-weight: 700;
|
||||
color: var(--foreground);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.use-case-content h1 {
|
||||
font-size: clamp(2rem, 3vw + 1rem, 2.75rem);
|
||||
}
|
||||
|
||||
.use-case-content h2 {
|
||||
font-size: clamp(1.5rem, 2.5vw + 0.75rem, 2.125rem);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.use-case-content h3 {
|
||||
font-size: clamp(1.25rem, 1.5vw + 0.75rem, 1.5rem);
|
||||
}
|
||||
|
||||
.use-case-content p {
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.use-case-content ul,
|
||||
.use-case-content ol {
|
||||
padding-left: 1.5rem;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.use-case-content li::marker {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.use-case-content pre {
|
||||
background-color: color-mix(in srgb, var(--muted) 70%, transparent);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
font-family: 'Fira Code', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 0.95rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.use-case-content code:not(pre code) {
|
||||
background-color: color-mix(in srgb, var(--muted) 70%, transparent);
|
||||
color: var(--foreground);
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.use-case-content a {
|
||||
color: color-mix(in srgb, var(--primary) 85%, var(--accent));
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: color-mix(in srgb, var(--primary) 50%, transparent);
|
||||
}
|
||||
|
||||
.use-case-content a:hover {
|
||||
text-decoration-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
115
Divers/gitea-mirror/www/src/layouts/UseCaseLayout.astro
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import { Header } from '../components/Header';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
const {
|
||||
content: {
|
||||
title = 'Use Case',
|
||||
description = 'Explore how Gitea Mirror helps engineering teams stay resilient.',
|
||||
canonical = 'https://gitea-mirror.com/use-cases',
|
||||
}
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{title} · Gitea Mirror</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<script is:inline>
|
||||
const theme = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-background text-foreground antialiased">
|
||||
<Header client:load />
|
||||
<main class="pt-24 pb-20">
|
||||
<article class="use-case-content mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
<slot />
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
.use-case-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 3rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.use-case-content > :is(h1, h2, h3) {
|
||||
font-weight: 700;
|
||||
color: var(--foreground);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.use-case-content h1 {
|
||||
font-size: clamp(2rem, 3vw + 1rem, 2.75rem);
|
||||
}
|
||||
|
||||
.use-case-content h2 {
|
||||
font-size: clamp(1.5rem, 2.5vw + 0.75rem, 2.125rem);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.use-case-content h3 {
|
||||
font-size: clamp(1.25rem, 1.5vw + 0.75rem, 1.5rem);
|
||||
}
|
||||
|
||||
.use-case-content p {
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.use-case-content ul,
|
||||
.use-case-content ol {
|
||||
padding-left: 1.5rem;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.use-case-content li::marker {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.use-case-content pre {
|
||||
background-color: color-mix(in srgb, var(--muted) 70%, transparent);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
font-family: 'Fira Code', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 0.95rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.use-case-content code:not(pre code) {
|
||||
background-color: color-mix(in srgb, var(--muted) 70%, transparent);
|
||||
color: var(--foreground);
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.use-case-content a {
|
||||
color: color-mix(in srgb, var(--primary) 85%, var(--accent));
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: color-mix(in srgb, var(--primary) 50%, transparent);
|
||||
}
|
||||
|
||||
.use-case-content a:hover {
|
||||
text-decoration-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
16
Divers/gitea-mirror/www/src/layouts/main.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{content.title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
75
Divers/gitea-mirror/www/src/lib/use-cases.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export interface UseCase {
|
||||
slug: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
painPoint: string;
|
||||
outcome: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const useCases: UseCase[] = [
|
||||
{
|
||||
slug: 'backup-github-repositories',
|
||||
title: 'Backup GitHub Repositories',
|
||||
summary: 'Continuously mirror GitHub repositories into self-hosted Gitea so your side projects stay safe even when GitHub hiccups.',
|
||||
painPoint: 'Homelabbers rely on GitHub availability but want local backups that preserve history, metadata, and LFS assets.',
|
||||
outcome: 'Automated syncs capture full repository history, metadata, and file storage so you always have an up-to-date local copy.',
|
||||
tags: ['Redundancy', 'Continuous Sync', 'Homelab'],
|
||||
},
|
||||
{
|
||||
slug: 'deploy-with-helm-chart',
|
||||
title: 'Deploy with Helm Chart',
|
||||
summary: 'Install the project on Kubernetes in a few commands using the maintained Helm chart to keep your backup mirror humming.',
|
||||
painPoint: 'Self-hosters want reproducible Git backups without hand-rolling manifests for every cluster or upgrade.',
|
||||
outcome: 'Versioned Helm values capture backup config, making redeploys and upgrades fast, scriptable, and low-risk.',
|
||||
tags: ['Kubernetes', 'Helm', 'Homelab'],
|
||||
},
|
||||
{
|
||||
slug: 'proxmox-lxc-homelab',
|
||||
title: 'Spin Up on Proxmox LXC',
|
||||
summary: 'Run the one-liner Proxmox VE script to launch gitea-mirror inside a tuned LXC container for your lab backups.',
|
||||
painPoint: 'Proxmox homelabbers want a repeatable Git backup without manually wiring containers, volumes, and services.',
|
||||
outcome: 'The community script provisions the container, installs Bun, and wires persistence so mirroring works minutes after boot.',
|
||||
tags: ['Proxmox', 'Automation', 'Homelab'],
|
||||
},
|
||||
{
|
||||
slug: 'sync-github-to-self-hosted-gitea',
|
||||
title: 'Sync GitHub to Self-Hosted Gitea',
|
||||
summary: 'Run continuous mirrors so your homelab Gitea instance stays in lockstep with GitHub without manual pulls.',
|
||||
painPoint: 'Tinkerers want to keep a local Gitea in sync but `git pull --mirror` cron jobs break on metadata and new repos.',
|
||||
outcome: 'Gitea Mirror auto-discovers repos, syncs metadata, and respects intervals so your LAN copy matches upstream every hour.',
|
||||
tags: ['Continuous Sync', 'Self-Hosted', 'Homelab'],
|
||||
},
|
||||
{
|
||||
slug: 'preserve-github-history',
|
||||
title: 'Preserve GitHub History Forever',
|
||||
summary: 'Archive commit history, issues, and releases into Gitea so side projects survive account removals or repo deletion.',
|
||||
painPoint: 'Homelab archivists fear SaaS changes wiping years of work, but manual exports miss metadata and LFS assets.',
|
||||
outcome: 'Scheduled mirrors capture full history with metadata snapshots, giving you an air-gapped archive you control.',
|
||||
tags: ['Archival', 'Metadata', 'Homelab'],
|
||||
},
|
||||
{
|
||||
slug: 'github-backup-automation',
|
||||
title: 'Automate GitHub Backups',
|
||||
summary: 'Replace brittle scripts with policy-driven schedules, health checks, and alerts that keep your Git backups honest.',
|
||||
painPoint: 'Cron jobs and shell scripts fail silently, leaving you with stale mirrors when you need a restore most.',
|
||||
outcome: 'Gitea Mirror tracks sync status, retries failures, and exposes health endpoints so you can trust every backup window.',
|
||||
tags: ['Automation', 'Observability', 'Homelab'],
|
||||
},
|
||||
{
|
||||
slug: 'starred-repos-collection',
|
||||
title: 'Build a Starred Repo Collection',
|
||||
summary: 'Mirror starred GitHub projects into your own Gitea library so favorites stay browsable even when upstream disappears.',
|
||||
painPoint: 'Curators star dozens of repos but lose them when owners delete or rename, and there’s no offline copy.',
|
||||
outcome: 'The starred collector funnels every star into a dedicated Gitea org with metadata intact for long-term tinkering.',
|
||||
tags: ['Curation', 'Automation', 'Homelab'],
|
||||
},
|
||||
{
|
||||
slug: 'vendor-lock-in-prevention',
|
||||
title: 'Stay Ready to Leave GitHub',
|
||||
summary: 'Keep an always-current mirror so you can pivot from GitHub to self-hosted tooling whenever policies shift.',
|
||||
painPoint: 'Indie builders worry about pricing, auth changes, or ToS updates but lack a live fallback they can swap to instantly.',
|
||||
outcome: 'Continuous mirrors mean you can flip DNS to Gitea, keep working locally, and evaluate alternatives without downtime.',
|
||||
tags: ['Vendor Independence', 'Continuity', 'Homelab'],
|
||||
},
|
||||
];
|
||||
6
Divers/gitea-mirror/www/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "GitHub Backup Tools Compared: Self-Hosted vs Cloud Solutions"
|
||||
description: "Compare Gitea Mirror with BackHub, Rewind, GitHub Enterprise Backup, and manual scripts. Choose the best GitHub backup solution for your needs."
|
||||
canonical: "https://gitea-mirror.com/comparison/github-backup-tools/"
|
||||
---
|
||||
|
||||
# GitHub Backup Tools Compared: Finding the Right Solution
|
||||
|
||||
## Why GitHub backups matter
|
||||
|
||||
GitHub hosts millions of repositories, but relying on a single platform comes with risks:
|
||||
|
||||
- **Outages**: GitHub experiences downtime (most recent: November 2024, October 2024, August 2024)
|
||||
- **Account issues**: DMCA takedowns, TOS violations, or account suspensions can lock you out
|
||||
- **Accidental deletions**: One wrong click and your repo history vanishes
|
||||
- **Company changes**: Microsoft's acquisition led to policy shifts many developers disagreed with
|
||||
- **Data sovereignty**: GDPR, HIPAA, or internal policies may require local data control
|
||||
|
||||
The question isn't *if* you need backups—it's *which solution* fits your workflow and budget.
|
||||
|
||||
## Comparison at a glance
|
||||
|
||||
| Feature | Gitea Mirror | BackHub | Rewind Backups | GitHub Enterprise Backup | Manual Scripts |
|
||||
|---------|--------------|---------|----------------|-------------------------|----------------|
|
||||
| **Cost/Year** | $0 | $600+ | $240+ | $21,000+ | $0 |
|
||||
| **Self-Hosted** | ✅ | ❌ | ❌ | Optional | ✅ |
|
||||
| **Setup Time** | 15 min | 5 min | 5 min | Days | 2+ hours |
|
||||
| **Metadata (Issues/PRs)** | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Releases & Assets** | ✅ | ✅ | ✅ | ✅ | Partial |
|
||||
| **Wiki Backup** | ✅ | ✅ | ✅ | ✅ | Partial |
|
||||
| **Git LFS Support** | ✅ | ✅ | ✅ | ✅ | Manual |
|
||||
| **Auto-Discovery** | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| **Scheduled Syncing** | ✅ | ✅ | ✅ | ✅ | Cron-based |
|
||||
| **Data Ownership** | ✅ Full | ❌ | ❌ | ✅ | ✅ Full |
|
||||
| **Restore Complexity** | Low | Medium | Medium | Low | High |
|
||||
| **Multi-Org Support** | ✅ | ✅ | ✅ | ✅ | Manual |
|
||||
| **Real-Time Dashboard** | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
|
||||
## Solution breakdown
|
||||
|
||||
### Gitea Mirror (Self-Hosted, Free)
|
||||
|
||||
**Best for**: Homelab enthusiasts, indie developers, privacy-conscious teams, cost-sensitive startups
|
||||
|
||||
**How it works**: Automatically mirrors your GitHub repositories to your own Gitea server using scheduled syncs. Preserves full history, metadata (issues, PRs as enriched issues, labels, milestones), releases, and wiki content.
|
||||
|
||||
**Pros**:
|
||||
- ✅ **Zero recurring costs** - Just your hosting/hardware
|
||||
- ✅ **Complete data ownership** - Everything stays on your infrastructure
|
||||
- ✅ **Privacy-first** - No third parties touch your code
|
||||
- ✅ **Customizable** - Open source, fork it, extend it
|
||||
- ✅ **Docker-friendly** - One command deployment
|
||||
- ✅ **Multi-arch support** - AMD64 and ARM64 (Raspberry Pi compatible)
|
||||
- ✅ **No vendor lock-in** - Standard Git repos, portable
|
||||
|
||||
**Cons**:
|
||||
- ❌ You manage the infrastructure (server, backups, updates)
|
||||
- ❌ Community support only (no SLA)
|
||||
- ❌ Requires basic Docker/server knowledge
|
||||
- ❌ You're responsible for Gitea security updates
|
||||
|
||||
**Ideal user**: "I run a homelab with Proxmox/Docker and want full control over my GitHub backups without paying monthly fees."
|
||||
|
||||
**Setup**:
|
||||
```bash
|
||||
docker compose -f docker-compose.alt.yml up -d
|
||||
# Visit http://localhost:4321, create account, add tokens
|
||||
```
|
||||
|
||||
### BackHub (Cloud, $50-200/month)
|
||||
|
||||
**Best for**: Teams wanting zero infrastructure management, compliance-focused organizations
|
||||
|
||||
**How it works**: SaaS platform that backs up GitHub orgs/repos to their cloud storage. Offers point-in-time recovery and compliance features.
|
||||
|
||||
**Pros**:
|
||||
- ✅ Fully managed (zero infrastructure)
|
||||
- ✅ SOC 2 compliant
|
||||
- ✅ Easy restore interface
|
||||
- ✅ Supports all GitHub features
|
||||
|
||||
**Cons**:
|
||||
- ❌ $600-2400/year minimum
|
||||
- ❌ Your code lives on their servers
|
||||
- ❌ Vendor lock-in for restore process
|
||||
- ❌ Pricing scales with repo count
|
||||
- ❌ No self-hosted option
|
||||
|
||||
**Ideal user**: "I need GitHub backups with compliance guarantees and don't want to manage servers."
|
||||
|
||||
### Rewind Backups (Cloud, $20-100/month)
|
||||
|
||||
**Best for**: Small teams, agencies managing client repos
|
||||
|
||||
**How it works**: Cloud backup service that snapshots GitHub data daily/hourly with web-based restore.
|
||||
|
||||
**Pros**:
|
||||
- ✅ Simple setup (OAuth connection)
|
||||
- ✅ User-friendly restore UI
|
||||
- ✅ Supports multiple SaaS platforms (not just GitHub)
|
||||
|
||||
**Cons**:
|
||||
- ❌ $240-1200/year
|
||||
- ❌ Cloud-only storage
|
||||
- ❌ Restore requires their platform
|
||||
- ❌ Limited to their backup schedule
|
||||
- ❌ No local/offline access
|
||||
|
||||
**Ideal user**: "I backup multiple SaaS tools and want one dashboard for everything."
|
||||
|
||||
### GitHub Enterprise Backup Utilities (Self-Hosted, Requires GHE)
|
||||
|
||||
**Best for**: Large enterprises already on GitHub Enterprise
|
||||
|
||||
**How it works**: Official GitHub tool for backing up GitHub Enterprise Server instances. Creates encrypted backup snapshots.
|
||||
|
||||
**Pros**:
|
||||
- ✅ Official GitHub tool
|
||||
- ✅ Enterprise-grade
|
||||
- ✅ Compliance-ready
|
||||
|
||||
**Cons**:
|
||||
- ❌ Requires GitHub Enterprise license ($21k+/year for 10 users)
|
||||
- ❌ Only works with Enterprise Server, not GitHub.com
|
||||
- ❌ Complex setup and maintenance
|
||||
- ❌ Overkill for small teams
|
||||
|
||||
**Ideal user**: "We're already paying for GitHub Enterprise and need official backup tooling."
|
||||
|
||||
### Manual Git Scripts (DIY, Free but Brittle)
|
||||
|
||||
**Best for**: Minimalists with few repos and technical expertise
|
||||
|
||||
**How it works**: Cron jobs that run `git clone --mirror` for each repo.
|
||||
|
||||
**Pros**:
|
||||
- ✅ Zero cost
|
||||
- ✅ Simple concept
|
||||
- ✅ No third-party dependencies
|
||||
|
||||
**Cons**:
|
||||
- ❌ No metadata backup (issues, PRs, releases lost)
|
||||
- ❌ Manual discovery of new repos
|
||||
- ❌ No LFS support without extra work
|
||||
- ❌ Fails silently (no health checks)
|
||||
- ❌ High maintenance overhead
|
||||
- ❌ No organization/bulk operations
|
||||
- ❌ Wiki requires separate cloning logic
|
||||
|
||||
**Ideal user**: "I have 2-3 repos and enjoy writing Bash scripts."
|
||||
|
||||
## Decision framework
|
||||
|
||||
### Choose Gitea Mirror if you:
|
||||
- Want $0 recurring costs
|
||||
- Run a homelab or have spare hardware
|
||||
- Value data ownership and privacy
|
||||
- Are comfortable with Docker/basic sysadmin tasks
|
||||
- Need to back up personal repos or small org (<50 repos)
|
||||
- Want to learn self-hosting skills
|
||||
|
||||
### Choose BackHub if you:
|
||||
- Have budget for managed services ($50-200/mo)
|
||||
- Need SOC 2 compliance
|
||||
- Want zero infrastructure management
|
||||
- Back up 10+ organizations
|
||||
- Require guaranteed SLA
|
||||
|
||||
### Choose Rewind if you:
|
||||
- Back up multiple SaaS tools (not just GitHub)
|
||||
- Have limited budget ($20-50/mo)
|
||||
- Want dead-simple restore UX
|
||||
- Don't need self-hosted storage
|
||||
|
||||
### Choose GitHub Enterprise Backup if you:
|
||||
- Already have GitHub Enterprise Server
|
||||
- Need official support contracts
|
||||
- Operate at enterprise scale (hundreds of users)
|
||||
- Have compliance requirements for official tooling
|
||||
|
||||
### Choose Manual Scripts if you:
|
||||
- Have <5 repos
|
||||
- Don't care about metadata/issues/PRs
|
||||
- Enjoy scripting and troubleshooting
|
||||
- Have time to maintain cron jobs
|
||||
|
||||
## Real-world cost comparison (5-year total)
|
||||
|
||||
Assuming 50 repositories, 10 active contributors:
|
||||
|
||||
| Solution | Year 1 | Year 5 Total | Notes |
|
||||
|----------|--------|--------------|-------|
|
||||
| **Gitea Mirror** | $0-50 | $0-250 | Only hardware/hosting costs (VPS: ~$50/yr) |
|
||||
| **BackHub** | $1,200 | $6,000 | Scales with repos |
|
||||
| **Rewind** | $600 | $3,000 | Mid-tier plan |
|
||||
| **GitHub Enterprise Backup** | $21,000+ | $105,000+ | Requires GHE license |
|
||||
| **Manual Scripts** | $0 | $0 | Plus hundreds of hours maintaining |
|
||||
|
||||
## Migration paths
|
||||
|
||||
### From cloud backup to Gitea Mirror
|
||||
1. Export backup data from your cloud provider (if supported)
|
||||
2. Deploy Gitea Mirror following the [backup playbook](/use-cases/backup-github-repositories/)
|
||||
3. Let Gitea Mirror rebuild from GitHub (preserves all metadata)
|
||||
4. Cancel cloud subscription once validated
|
||||
|
||||
### From manual scripts to Gitea Mirror
|
||||
1. Note your current repo list
|
||||
2. Deploy Gitea Mirror and connect GitHub
|
||||
3. Auto-import handles discovery
|
||||
4. Delete cron jobs once sync confirmed
|
||||
|
||||
### From Gitea Mirror to other solutions
|
||||
Your repos are standard Git mirrors—clone from Gitea and push to any other service. Zero lock-in.
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### Can I use Gitea Mirror alongside a cloud backup service?
|
||||
|
||||
Absolutely! Gitea Mirror adds a self-hosted "warm backup" layer while cloud services provide offsite redundancy. Best of both worlds.
|
||||
|
||||
### What if my hardware fails?
|
||||
|
||||
Snapshot your Gitea Mirror data volume regularly using ZFS, Btrfs, or tools like Restic. Store snapshots offsite. The GitHub → Gitea Mirror sync is idempotent—spin up a new instance, restore data volume, and it resumes.
|
||||
|
||||
### How does Gitea Mirror compare for disaster recovery?
|
||||
|
||||
**RTO (Recovery Time Objective)**: Minutes—just `git clone` from your Gitea server.
|
||||
**RPO (Recovery Point Objective)**: Depends on sync interval (15 min - 24 hours typical).
|
||||
|
||||
Cloud services have similar RPO but may have slower RTO if you need to restore hundreds of repos through a web UI.
|
||||
|
||||
### Can I try Gitea Mirror without committing?
|
||||
|
||||
Yes! The Docker setup takes 15 minutes. Test with a few repos, evaluate, then scale or remove with `docker compose down -v`.
|
||||
|
||||
### Does Gitea Mirror work with GitHub Enterprise Cloud?
|
||||
|
||||
Yes, as long as you have API access and a personal access token. The GitHub API endpoint is configurable.
|
||||
|
||||
## Next steps
|
||||
|
||||
- **Ready to self-host?** Follow the [GitHub backup playbook](/use-cases/backup-github-repositories/)
|
||||
- **Need multi-tenant?** See [Proxmox LXC deployment](/use-cases/proxmox-lxc-homelab/) or [Kubernetes Helm chart](/use-cases/deploy-with-helm-chart/)
|
||||
- **Questions?** [Open a GitHub discussion](https://github.com/RayLabsHQ/gitea-mirror/discussions)
|
||||
|
||||
## Honest assessment
|
||||
|
||||
Gitea Mirror is **not** enterprise SaaS. There's no 24/7 support, no compliance certifications, no guaranteed uptime. It's a well-maintained open source project built by and for developers who value ownership and cost-effectiveness.
|
||||
|
||||
If you need a pager number when things break, choose a commercial solution. If you enjoy owning your infrastructure and solving problems, Gitea Mirror saves thousands of dollars annually while giving you complete control.
|
||||
|
||||
The best backup solution is the one you'll actually use and test. Choose based on your skills, budget, and risk tolerance—not marketing promises.
|
||||
224
Divers/gitea-mirror/www/src/pages/index.astro
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import { Header } from "../components/Header";
|
||||
import { Hero } from "../components/Hero";
|
||||
import ShaderBackground from "../components/ShaderBackground.astro";
|
||||
import Features from "../components/Features.astro";
|
||||
import UseCases from "../components/UseCases.astro";
|
||||
import Screenshots from "../components/Screenshots.astro";
|
||||
import { Installation } from "../components/Installation";
|
||||
import { CTA } from "../components/CTA";
|
||||
import FAQ from "../components/FAQ.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { PromoBanner } from "../components/PromoBanner";
|
||||
|
||||
const siteUrl = "https://gitea-mirror.com";
|
||||
const title = "GitHub Backup Tool | Self-Hosted Repository Backup to Gitea";
|
||||
const description =
|
||||
"Automatically backup GitHub repos to your own Gitea server. Preserve issues, PRs, releases & wiki. Self-hosted, Docker-ready. Free alternative to cloud backup services.";
|
||||
const keywords =
|
||||
"github backup, github backup self hosted, github repository backup, backup github to nas, github disaster recovery, offline github backup, github backup docker, automatic github backup, github account backup, gitea mirror, self-hosted git backup, repository sync, github to gitea, git mirror, code backup, self-hosted backup solution";
|
||||
|
||||
// Structured data for SEO
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
name: "Gitea Mirror",
|
||||
applicationCategory: "BackupApplication",
|
||||
operatingSystem: "Linux, macOS, Windows",
|
||||
offers: {
|
||||
"@type": "Offer",
|
||||
price: "0",
|
||||
priceCurrency: "USD",
|
||||
},
|
||||
description:
|
||||
"Automatic GitHub repository backup to self-hosted Gitea. Preserves complete history, issues, PRs, and releases. Free alternative to cloud backup services.",
|
||||
url: siteUrl,
|
||||
author: {
|
||||
"@type": "Organization",
|
||||
name: "RayLabs",
|
||||
url: "https://github.com/RayLabsHQ",
|
||||
},
|
||||
softwareVersion: "3.9.2",
|
||||
screenshot: [
|
||||
`${siteUrl}/assets/dashboard.png`,
|
||||
`${siteUrl}/assets/repositories.png`,
|
||||
`${siteUrl}/assets/organisation.png`,
|
||||
],
|
||||
featureList: [
|
||||
"Automated scheduled backups",
|
||||
"Self-hosted (full data ownership)",
|
||||
"Metadata preservation (issues, PRs, releases, wiki)",
|
||||
"Docker support",
|
||||
"Multi-repository backup",
|
||||
"Git LFS support",
|
||||
"Free and open source",
|
||||
],
|
||||
softwareRequirements: "Docker or Bun runtime",
|
||||
};
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta
|
||||
name="robots"
|
||||
content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
|
||||
/>
|
||||
<meta name="author" content="RayLabs" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={siteUrl} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={`${siteUrl}/og-image.png`} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:site_name" content="Gitea Mirror" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={siteUrl} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={`${siteUrl}/og-image.png`} />
|
||||
<meta name="twitter:creator" content="@RayLabsHQ" />
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={siteUrl} />
|
||||
|
||||
<!-- Additional Meta Tags -->
|
||||
<meta name="theme-color" content="#5b6fff" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="Gitea Mirror" />
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script
|
||||
type="application/ld+json"
|
||||
is:inline
|
||||
set:html={JSON.stringify(structuredData)}
|
||||
/>
|
||||
|
||||
<!-- Preconnect to external domains -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="https://github.com" />
|
||||
|
||||
<!-- Theme detection script (prevent flash) -->
|
||||
<script is:inline>
|
||||
const theme =
|
||||
localStorage.getItem("theme") ||
|
||||
(window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light");
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
</script>
|
||||
|
||||
<!-- 100% privacy-first analytics -->
|
||||
<script
|
||||
async
|
||||
src="https://scripts.simpleanalyticscdn.com/latest.js"
|
||||
is:inline></script>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-background text-foreground antialiased">
|
||||
<!-- <PromoBanner client:load /> -->
|
||||
<Header client:load />
|
||||
|
||||
<main>
|
||||
<div class="relative">
|
||||
<ShaderBackground />
|
||||
<Hero client:load />
|
||||
</div>
|
||||
<Features />
|
||||
<UseCases />
|
||||
<Screenshots />
|
||||
<Installation client:load />
|
||||
<FAQ />
|
||||
<CTA client:load />
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Blob animation */
|
||||
@keyframes blob {
|
||||
0% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(30px, -50px) scale(1.1);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-20px, 20px) scale(0.9);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-blob {
|
||||
animation: blob 7s infinite;
|
||||
}
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
/* Grid background pattern */
|
||||
.bg-grid-white\/10 {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0.1) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.1) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Smooth gradient animations */
|
||||
@keyframes gradient-shift {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-gradient {
|
||||
background-size: 200% 200%;
|
||||
animation: gradient-shift 15s ease infinite;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Backup GitHub Repositories with Gitea Mirror"
|
||||
description: "Run a homelab-friendly playbook to mirror GitHub into self-hosted Gitea with automated schedules, health checks, and restore drills."
|
||||
canonical: "https://gitea-mirror.com/use-cases/backup-github-repositories/"
|
||||
---
|
||||
|
||||
# Backup GitHub Repositories with Gitea Mirror
|
||||
|
||||
## Why homelabbers care
|
||||
|
||||
GitHub is great—right up until an outage, SSO change, or account lockout strands your projects. Gitea Mirror keeps a self-hosted copy of everything (history, metadata, LFS) so you can keep working locally. This playbook walks through the minimal Docker setup the project ships with and shows how to prove your backups actually work.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker Engine and Compose on the host that will run the mirror
|
||||
- A GitHub personal access token (classic) with `repo`, plus the `read:org` checkbox under `admin:org` when you mirror organizations (leave the write/admin boxes unchecked)
|
||||
- A self-hosted Gitea instance (can be on the same box) and admin or org owner credentials
|
||||
- Open ports 4321 (web UI) and 3000 (default Gitea) inside your network
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Clone the repo and start the stack
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
docker compose -f docker-compose.alt.yml up -d
|
||||
```
|
||||
|
||||
The `alt` compose file ships with sane defaults for a single-node backup mirror. It stores data in `./data`. To use a different path, edit the volume mapping (for example `- /srv/gitea-mirror:/app/data`).
|
||||
|
||||
Verify the containers:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.alt.yml ps
|
||||
docker compose -f docker-compose.alt.yml logs -f gitea-mirror
|
||||
```
|
||||
|
||||
Wait for "Server started" before moving on.
|
||||
|
||||
### 2. Generate tokens and connect GitHub
|
||||
|
||||
1. Create a GitHub personal access token (classic) with `repo` enabled and, inside the `admin:org` section, check `read:org` so the mirror can list organization repositories—leave `write:org` and `admin:org` unchecked.
|
||||
2. Log in to Gitea and create an access token for an admin/owner account with `write:repository`.
|
||||
3. Visit `http://<host>:4321` and sign up—the first user becomes admin.
|
||||
4. Complete the setup wizard:
|
||||
- Paste the GitHub PAT and Gitea URL/token.
|
||||
- Choose which GitHub owners (user/org) to track.
|
||||
- Leave sync interval at the default 1 hour to start.
|
||||
|
||||
### 3. Stage your first backup job
|
||||
|
||||
On the dashboard:
|
||||
|
||||
1. Click **Mirror Repository** for a small test project.
|
||||
2. Open Gitea and confirm the mirror appears with the right owner/org.
|
||||
3. In **Configuration → Connections**, open the **Content & Data** section to enable **Mirror metadata** and **Git LFS** if you rely on issues, wikis, or large assets.
|
||||
|
||||
For broader coverage, switch the organization strategy to **Preserve structure** so Gitea mirrors your GitHub org layout automatically.
|
||||
|
||||
### 4. Turn on automatic syncs and cleanup
|
||||
|
||||
Open **Configuration → Automation** in the web UI.
|
||||
|
||||
- Enable **Automatic syncing** and pick an interval that matches how fresh you want the mirror (start with `60 minutes`, shorten for active repos).
|
||||
- Leave the scheduler enabled—auto-discovery ships with it, so new GitHub repositories and stars are pulled in on the next pass.
|
||||
- If you want the mirror to tidy up when GitHub repos disappear, enable **Handle orphaned repositories** and keep the action on **Archive** so history stays intact.
|
||||
|
||||
<figure class="mt-8 flex flex-col items-center">
|
||||
<img
|
||||
src="/assets/configuration.png"
|
||||
alt="Automation tab in Gitea Mirror showing the automatic syncing controls for GitHub backups."
|
||||
class="w-full max-w-5xl rounded-xl border border-muted shadow-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<figcaption class="mt-3 text-sm text-muted-foreground text-center">
|
||||
Configure the scheduler and cleanup policies from the Automation tab so GitHub mirrors stay fresh without manual cron jobs.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
### 5. Prove the backup works
|
||||
|
||||
Treat the mirror like any other DR asset:
|
||||
|
||||
1. Temporarily block outbound GitHub access on your machine.
|
||||
2. Clone from Gitea instead: `git clone http://<gitea-host>/<owner>/<repo>.git`.
|
||||
3. Confirm commit history, tags, releases, and issues exist.
|
||||
4. Remove the block and document the restore steps in your homelab wiki.
|
||||
|
||||
## Health checks & monitoring
|
||||
|
||||
- The container exposes `/api/health`; add it to Uptime Kuma, Healthchecks.io, or Prometheus.
|
||||
- Mirror failures surface in the activity log; consider exporting them through the `/api/events` endpoint.
|
||||
- Watch the `data/` volume on the host (e.g. `du -sh data/`) to make sure you have headroom for mirrored repos and LFS blobs.
|
||||
|
||||
## Hardening tips
|
||||
|
||||
- Put the stack behind a reverse proxy (Traefik, Caddy, Nginx) and enable TLS.
|
||||
- Rotate both GitHub and Gitea tokens quarterly; the UI will flag expired credentials.
|
||||
- Snapshot the `data/` volume (ZFS/BTRFS) or back it up with `restic` so the mirror survives host failure.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Promote the mirror to read-only users who do not need GitHub access.
|
||||
- Layer on the [Helm](../deploy-with-helm-chart) or [Proxmox LXC](../proxmox-lxc-homelab) playbooks when you outgrow the single-node setup.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Does Gitea Mirror copy issues, pull requests, releases, and LFS?
|
||||
|
||||
Yes. Enable Mirror metadata, Mirror releases, and Git LFS from **Configuration → Connections → Content & Data**. Pull requests are mirrored as enriched issues with linked branches and metadata.
|
||||
|
||||
### How often should I sync GitHub backups?
|
||||
|
||||
Most homelabs pick 30–120 minutes. Faster schedules improve RPO but use more GitHub API quota; adjust by org/repo if only a few projects are critical.
|
||||
|
||||
### Where are backups stored and how do I restore?
|
||||
|
||||
Repositories and the SQLite DB live under the `data/` directory (or your configured volume). Restore by cloning from Gitea or by moving the volume to a fresh deployment and signing back in.
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Deploy Gitea Mirror with the Helm Chart"
|
||||
description: "Install the Gitea Mirror backup service on Kubernetes with the official Helm chart, including secrets, persistence, and upgrade workflow."
|
||||
canonical: "https://gitea-mirror.com/use-cases/deploy-with-helm-chart/"
|
||||
---
|
||||
|
||||
# Deploy Gitea Mirror with the Helm Chart
|
||||
|
||||
## Why ship it to Kubernetes
|
||||
|
||||
If your homelab already runs a cluster (k3s, Talos, MicroK8s), Helm is the fastest way to keep Gitea Mirror close to the rest of your self-hosted stack. The chart in [`helm/gitea-mirror`](https://github.com/RayLabsHQ/gitea-mirror/tree/main/helm/gitea-mirror) bundles the deployment, service, ingress, and persistence so you can version your backup mirror just like any other release.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Kubernetes 1.23+ with storage (Rook, Longhorn, local-path, etc.)
|
||||
- Helm 3.8+
|
||||
- GitHub PAT and Gitea API token ready (same scopes as the Docker playbook)
|
||||
- Namespace with outbound access to GitHub and your Gitea host
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Create a namespace (optional)
|
||||
|
||||
```bash
|
||||
kubectl create namespace gitea-mirror
|
||||
```
|
||||
|
||||
### 2. Provide credentials and install the chart
|
||||
|
||||
The [chart README](https://github.com/RayLabsHQ/gitea-mirror/blob/main/helm/gitea-mirror/README.md) documents multiple supported approaches. Choose the one that matches how you manage secrets.
|
||||
|
||||
**Inline quick start (no values file):**
|
||||
|
||||
First, clone the repository or download the chart:
|
||||
```bash
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
```
|
||||
|
||||
Then install with credentials:
|
||||
```bash
|
||||
helm upgrade --install gitea-mirror ./helm/gitea-mirror \
|
||||
--namespace gitea-mirror \
|
||||
--set "gitea-mirror.github.username=<your-gh-username>" \
|
||||
--set "gitea-mirror.github.token=<your-gh-token>" \
|
||||
--set "gitea-mirror.gitea.url=https://gitea.example.com" \
|
||||
--set "gitea-mirror.gitea.token=<your-gitea-token>"
|
||||
```
|
||||
|
||||
**Using a values file:**
|
||||
|
||||
```yaml
|
||||
# values-gitea-mirror.yaml
|
||||
gitea-mirror:
|
||||
github:
|
||||
username: "your-gh-user"
|
||||
token: "ghp_your_token"
|
||||
gitea:
|
||||
url: "https://git.lab.local"
|
||||
token: "gitea_your_token"
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 1Gi
|
||||
```
|
||||
|
||||
```bash
|
||||
helm upgrade --install gitea-mirror ./helm/gitea-mirror \
|
||||
--namespace gitea-mirror \
|
||||
--values values-gitea-mirror.yaml
|
||||
```
|
||||
|
||||
**Bring your own Secret (recommended for production):**
|
||||
|
||||
```bash
|
||||
kubectl -n gitea-mirror create secret generic gitea-mirror-secrets \
|
||||
--from-literal=GITHUB_TOKEN="ghp_your_token" \
|
||||
--from-literal=GITEA_TOKEN="gitea_your_token" \
|
||||
--from-literal=ENCRYPTION_SECRET="$(openssl rand -base64 48)"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# values-gitea-mirror.yaml
|
||||
gitea-mirror:
|
||||
existingSecret: "gitea-mirror-secrets"
|
||||
github:
|
||||
username: "your-gh-user"
|
||||
gitea:
|
||||
url: "https://git.lab.local"
|
||||
```
|
||||
|
||||
Helm renders a `Deployment`, `Service`, optional Ingress/Gateway resources, and—when persistence is enabled—a PVC mounted at `/app/data` for the SQLite database and mirrored repositories.
|
||||
|
||||
### 3. Verify the release
|
||||
|
||||
```bash
|
||||
kubectl -n gitea-mirror get pods,svc,pvc
|
||||
kubectl -n gitea-mirror logs deploy/gitea-mirror --tail=100
|
||||
```
|
||||
|
||||
Watch for `Server started` in the logs. Once ready, browse to the ingress host (or userland port-forward with `kubectl port-forward svc/gitea-mirror 4321:4321`). Complete the first-run wizard just like the Docker playbook.
|
||||
|
||||
After the pod is healthy, open **Configuration → Connections** inside the UI to add GitHub owners, choose a destination strategy, and enable metadata/LFS mirroring.
|
||||
|
||||
### 4. Keep it updated
|
||||
|
||||
- Pull chart updates when you bump the repo: `git pull` then re-run the `helm upgrade` command.
|
||||
- Override the container image tag with `--set image.tag=v3.7.2` if you need to pin (defaults to `v{appVersion}` from Chart.yaml).
|
||||
- Use Helm rollbacks if a release misbehaves: `helm rollback gitea-mirror <REVISION> -n gitea-mirror`.
|
||||
|
||||
## Observability
|
||||
|
||||
- Attach the `/api/health` endpoint to your cluster’s probing (Kubernetes probes are already configured by the chart).
|
||||
- Expose the metrics endpoint via a `ServiceMonitor` if you run Prometheus; add `extraAnnotations` to make it discoverable.
|
||||
- Watch PVC growth with `kubectl df-pv` or your storage dashboard to ensure LFS blobs do not exhaust the volume.
|
||||
|
||||
## Disaster-recovery drill
|
||||
|
||||
1. Scale the deployment down: `kubectl -n gitea-mirror scale deploy gitea-mirror --replicas=0`.
|
||||
2. Snapshot the PVC (CSI snapshots or Velero).
|
||||
3. Restore into a test namespace and scale the deployment back up.
|
||||
4. Confirm you can log in and the mirrored repositories are intact.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
helm uninstall gitea-mirror -n gitea-mirror
|
||||
kubectl delete namespace gitea-mirror
|
||||
```
|
||||
|
||||
Remove the PVC manually if you want a clean slate: `kubectl delete pvc gitea-mirror-storage -n gitea-mirror`.
|
||||
|
||||
Ready to run on bare metal instead? Head over to the [Proxmox LXC playbook](../proxmox-lxc-homelab).
|
||||
|
||||
## FAQ
|
||||
|
||||
### Where do I define GitHub owners and organizations?
|
||||
|
||||
Add owners from the **Configuration → Connections** screen after the release is running. The chart seeds credentials and defaults, but owner discovery happens in the UI.
|
||||
|
||||
### Can I manage secrets outside of Kubernetes?
|
||||
|
||||
Yes. Leave `existingSecret` empty and the chart will create a secret with the values from the file, but using a pre-created secret keeps PATs out of Git history and lets you rotate them with `kubectl apply`.
|
||||
|
||||
### How do I throttle syncs to fit my quota?
|
||||
|
||||
Adjust `gitea-mirror.automation.schedule_interval` in your values file (default: 3600 seconds = 1 hour). Lower values mean more frequent syncs; higher values create quieter schedules. You can also configure intervals per owner/repository inside the web UI.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Automate GitHub Backups"
|
||||
description: "Replace fragile cron scripts with scheduled mirrors, health checks, and logging that keep GitHub backups trustworthy."
|
||||
canonical: "https://gitea-mirror.com/use-cases/github-backup-automation/"
|
||||
---
|
||||
|
||||
# Automate GitHub Backups
|
||||
|
||||
## Why automation beats cron
|
||||
|
||||
Shell scripts and `git clone --mirror` jobs work until they don’t—usually when GitHub rotates tokens, repositories rename, or metadata gets missed. Gitea Mirror bundles scheduling, auto-discovery, and repository cleanup so your backups keep running while you sleep.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Gitea Mirror deployment with outbound HTTPS access
|
||||
- GitHub PAT + Gitea token stored in the UI or supplied via environment variables
|
||||
- A monitoring target that can poll HTTP endpoints (Healthchecks.io, Uptime Kuma, Prometheus, etc.)
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Enable automatic syncing
|
||||
|
||||
1. Go to **Configuration → Automation**.
|
||||
2. Toggle **Automatic syncing** on and choose an interval that matches your recovery point objective (start with `30 minutes` for active teams, stretch to `12 hours` for archives).
|
||||
3. Confirm the scheduler is running by checking the **Last sync** and **Next sync** timestamps in the Automation card.
|
||||
|
||||
### 2. Keep the repository list current
|
||||
|
||||
- In **Configuration → Connections**, click **Import GitHub Data** so the dashboard knows about every repository, organization, and star you selected.
|
||||
- Enable **Mirror starred repositories** if you want personal favorites backed up, and set the **Starred repos organization** for tidy storage in Gitea.
|
||||
- Use the inline destination editor on the **Repositories** page when you need a repo to land in a different Gitea organization.
|
||||
|
||||
### 3. Configure repository cleanup (optional)
|
||||
|
||||
- Still on **Configuration → Automation**, enable **Handle orphaned repositories automatically**.
|
||||
- Leave the action on **Archive** to keep a read-only backup when a GitHub repo disappears, or switch to **Delete** if you require a strict mirror.
|
||||
- Disable **Dry run** after your first test so the cleanup service can act on what it finds.
|
||||
|
||||
### 4. Monitor scheduler health
|
||||
|
||||
- Point your monitoring system at `http://<mirror-host>:4321/api/health` to track uptime.
|
||||
- Review sync failures from the **Activity Log** page or export them via `/api/events` for long-term retention.
|
||||
- Run `bun run manage-db check` during maintenance windows to verify background tasks, migrations, and queue state.
|
||||
|
||||
<figure class="mt-8 flex flex-col items-center">
|
||||
<img
|
||||
src="/assets/activity.png"
|
||||
alt="Gitea Mirror activity log displaying recent GitHub backup jobs and their status."
|
||||
class="w-full max-w-5xl rounded-xl border border-muted shadow-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<figcaption class="mt-3 text-sm text-muted-foreground text-center">
|
||||
The Activity Log highlights successful syncs and failures so you can react before GitHub backups fall behind.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
### 5. Harden credentials and runbooks
|
||||
|
||||
- Store GitHub and Gitea tokens via the Configuration UI—Gitea Mirror encrypts them at rest.
|
||||
- Rotate tokens on a schedule and note expiry dates in your homelab runbook; the dashboard surfaces failures when credentials expire.
|
||||
- Export the configuration JSON (`/api/export`) alongside your documentation so you can rebuild the mirror quickly if you need to redeploy.
|
||||
|
||||
## Validate automation
|
||||
|
||||
- Force a failure by temporarily revoking a PAT; the next scheduler run should flag the repository in the Activity Log. Restore the token and use **Sync Repository** to confirm recovery.
|
||||
- Run `bun run manage-db check` (or the UI health check) to ensure migrations and tasks are clean.
|
||||
- Spot-check the **Repositories** table or export the Activity Log CSV to confirm `Last mirrored` timestamps match your configured interval.
|
||||
|
||||
## Best practices
|
||||
|
||||
- Tune the sync interval to balance freshness with GitHub rate limits; most homelabs sit between 30 and 120 minutes.
|
||||
- Start with the cleanup action set to **Archive** until you are confident you will not remove something you still need.
|
||||
- Pair automation with the [Preserve GitHub History](../preserve-github-history/) playbook to maintain context, not just code.
|
||||
|
||||
## Related playbooks
|
||||
|
||||
- [Backup GitHub Repositories](../backup-github-repositories/)
|
||||
- [Run Gitea Mirror inside a Proxmox LXC](../proxmox-lxc-homelab/)
|
||||
|
||||
## FAQ
|
||||
|
||||
### What replaces my cron scripts?
|
||||
|
||||
The built-in scheduler handles intervals, retries, and discovery. It also powers cleanup for deleted upstream repos (Archive/Delete) once enabled.
|
||||
|
||||
### How do I get alerts if backups fail?
|
||||
|
||||
Monitor `/api/health` with Healthchecks.io or Uptime Kuma and review the Activity Log. You can export failures via `/api/events` for centralized logging.
|
||||
|
||||
### Will new repositories be discovered automatically?
|
||||
|
||||
Yes. After importing your GitHub data once, the scheduler’s discovery step keeps the inventory updated and mirrors new repositories on the next run.
|
||||
120
Divers/gitea-mirror/www/src/pages/use-cases/index.mdx
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseIndexLayout.astro
|
||||
title: "Gitea Mirror Use Cases"
|
||||
description: "Homelab-friendly playbooks that keep GitHub repos mirrored inside Gitea without promising enterprise guarantees."
|
||||
canonical: "https://gitea-mirror.com/use-cases/"
|
||||
---
|
||||
|
||||
import { ArrowRight, ShieldAlert, Sparkles, Home } from 'lucide-react';
|
||||
import { useCases } from '@/lib/use-cases';
|
||||
|
||||
<section class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="rounded-3xl border border-primary/20 bg-gradient-to-br from-primary/10 via-background to-background p-8 sm:p-12 shadow-lg">
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.25em] text-primary">
|
||||
<Sparkles class="h-4 w-4" />
|
||||
Built by and for homelab tinkerers
|
||||
</div>
|
||||
<h1 class="mt-6 text-3xl font-bold sm:text-4xl md:text-[2.75rem] md:leading-tight">
|
||||
Gitea Mirror in Action: Use Cases for Self-Hosted GitHub Backups
|
||||
</h1>
|
||||
<p class="mt-4 max-w-3xl text-base sm:text-lg text-muted-foreground">
|
||||
Gitea Mirror is an open-source side project. It’s perfect for your homelab, indie dev studio, or early-stage team that
|
||||
needs backups and optional failover. There’s <strong class="font-semibold text-foreground">no enterprise SLA</strong>—just
|
||||
practical playbooks you can own, fork, and improve.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
<div class="flex items-start gap-3 rounded-2xl border border-muted bg-background/80 p-4">
|
||||
<div>
|
||||
<h2 class="!mt-0 text-sm font-semibold uppercase tracking-wide text-foreground/80">Ideal for</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Homelabbers, solo builders, and scrappy startups that want GitHub peace of mind without managed pricing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3 rounded-2xl border border-muted bg-background/80 p-4">
|
||||
<div>
|
||||
<h2 class="!mt-0 text-sm font-semibold uppercase tracking-wide text-foreground/80">Worth noting</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Community support only. No compliance guarantees, no 24/7 pager. Kick the tires before depending on it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="use-cases" class="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center max-w-3xl mx-auto mb-12 sm:mb-16">
|
||||
<span class="inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-widest text-muted-foreground mb-4">
|
||||
Use Cases
|
||||
</span>
|
||||
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
|
||||
Real-World Gitea Mirror Workflows
|
||||
</h2>
|
||||
<p class="mt-4 text-base sm:text-lg text-muted-foreground">
|
||||
Discover how developers and teams are using Gitea Mirror to create reliable, self-hosted backups of their GitHub repositories. These use cases provide step-by-step instructions for common scenarios.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:gap-6 lg:gap-8 lg:grid-cols-3">
|
||||
{useCases.map((useCase) => (
|
||||
<article class="group relative flex flex-col rounded-2xl border bg-background/80 p-6 sm:p-8 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg">
|
||||
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-primary/5 via-accent/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div class="relative flex flex-col h-full">
|
||||
<h3 class="text-xl font-semibold mb-3">
|
||||
{useCase.title}
|
||||
</h3>
|
||||
<p class="text-sm sm:text-base text-muted-foreground mb-4">
|
||||
{useCase.summary}
|
||||
</p>
|
||||
|
||||
<dl class="grid gap-3 text-sm sm:text-base text-muted-foreground">
|
||||
<div>
|
||||
<dt class="font-semibold text-foreground">Pain Point</dt>
|
||||
<dd>{useCase.painPoint}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="font-semibold text-foreground">Outcome</dt>
|
||||
<dd>{useCase.outcome}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-2">
|
||||
{useCase.tags.map((tag) => (
|
||||
<span class="inline-flex items-center rounded-full border border-muted px-3 py-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={`/use-cases/${useCase.slug}/`}
|
||||
class="mt-auto inline-flex items-center gap-2 pt-6 text-sm font-medium text-primary transition-colors hover:text-primary/80"
|
||||
>
|
||||
Read the playbook
|
||||
<ArrowRight class="h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mx-auto mt-16 max-w-7xl px-4 sm:px-6 lg:px-8 pb-8">
|
||||
<div class="rounded-3xl border border-primary/20 bg-primary/5 p-6 sm:p-10 text-center">
|
||||
<h2 class="text-2xl font-semibold sm:text-3xl">Have a niche workflow?</h2>
|
||||
<p class="mt-3 text-sm sm:text-base text-muted-foreground">
|
||||
Fork the project, open an issue, or drop a PR. These guides are community-made—just like the tooling behind them.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/RayLabsHQ/gitea-mirror"
|
||||
class="mt-6 inline-flex items-center gap-2 rounded-full border border-primary/50 bg-background px-5 py-2 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
|
||||
>
|
||||
Contribute on GitHub
|
||||
<ArrowRight class="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Preserve GitHub History Forever"
|
||||
description: "Archive commits, issues, releases, and LFS assets into Gitea so hobby projects survive account removals or repo deletions."
|
||||
canonical: "https://gitea-mirror.com/use-cases/preserve-github-history/"
|
||||
---
|
||||
|
||||
# Preserve GitHub History Forever
|
||||
|
||||
## Keep the entire story, not just the code
|
||||
|
||||
GitHub accounts get banned, repos go private, and owners rage-delete history. If you care about the full timeline—issues, releases, wiki—Gitea Mirror snapshots everything on a schedule so the story survives in your homelab.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Running Gitea Mirror (follow the [backup playbook](../backup-github-repositories/))
|
||||
- GitHub PAT with `repo` enabled (add the `read:org` checkbox under `admin:org` when you archive organization repositories; leave write/admin unchecked)
|
||||
- Destination Gitea with enough disk for cloned repos + attachments
|
||||
- Optional: object storage or snapshots for long-term archiving of the mirror volume
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Set archival-friendly defaults
|
||||
|
||||
In **Configuration → Connections**, open **Content & Data**:
|
||||
|
||||
- Enable **Mirror metadata** and choose the components you care about (issues, pull requests, labels, milestones, wiki).
|
||||
- Enable **Mirror releases** and raise the **Latest releases** limit if you need a deeper history of release assets.
|
||||
- Toggle **Git LFS (Large File Storage)** so binaries follow the repository, assuming LFS is enabled in your Gitea instance.
|
||||
|
||||
### 2. Create an "Archive" organization in Gitea
|
||||
|
||||
1. In Gitea, create an org like `github-archive` and grant read-only access to everyone who needs the history.
|
||||
2. Back in Gitea Mirror under **Configuration → Connections**, pick the **Preserve structure** strategy (or set a destination organization) so repos land in that archive org.
|
||||
3. Tighten permissions in Gitea—disable pushes for regular users so the archive stays immutable while the service updates it via its token.
|
||||
|
||||
<figure class="mt-8 flex flex-col items-center">
|
||||
<img
|
||||
src="/assets/repositories.png"
|
||||
alt="Repositories dashboard in Gitea Mirror showing archived GitHub projects synced into Gitea."
|
||||
class="w-full max-w-5xl rounded-xl border border-muted shadow-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<figcaption class="mt-3 text-sm text-muted-foreground text-center">
|
||||
Keep every GitHub project visible in the repositories dashboard while routing mirrors into a dedicated archive organization.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
### 3. Choose retention & cadence
|
||||
|
||||
- In **Configuration → Automation**, enable **Automatic syncing** and set the interval (`1h` keeps fast-moving repos current; `12h` is usually enough for archives).
|
||||
- Turn on **Handle orphaned repositories automatically** and leave the action on **Archive** so anything deleted upstream is preserved locally but marked read-only.
|
||||
- Bump the **Latest releases** limit or run an occasional manual sync from the **Repositories** table when you need older release assets.
|
||||
|
||||
### 4. Record provenance
|
||||
|
||||
- Add a README or label inside the archive organization that captures the upstream URL, first mirrored date, and token owner.
|
||||
- Export a CSV from the **Repositories** view or hit `/api/events` quarterly so you retain a human-friendly change log.
|
||||
- Store the configuration export (`/api/export`) alongside your disaster-recovery docs in case you need to rebuild the service.
|
||||
|
||||
### 5. Back up the backup
|
||||
|
||||
- Snapshots: Use ZFS/BTRFS or Proxmox backups on the mirror’s data volume.
|
||||
- Offsite: `restic`/`rclone` the `data/` directory to a NAS or object store.
|
||||
- Test: Restore to a test Gitea instance and spot-check history every few months.
|
||||
|
||||
## Verify the archive
|
||||
|
||||
1. Delete a draft issue on GitHub.
|
||||
2. Wait for the next sync; open the issue in Gitea—you should still see the original content.
|
||||
3. Compare `git tag -l` in both remotes to ensure releases match.
|
||||
4. Use `git lfs ls-files` to confirm large assets made it across.
|
||||
|
||||
## Maintenance checklist
|
||||
|
||||
- Rotate tokens annually and document the rotation date in the repo README.
|
||||
- Monitor disk growth; configure `persistence.size` if you run the Helm chart.
|
||||
- Log anomalies—failed runs, conflicts—in your homelab journal to track trends.
|
||||
|
||||
## Related playbooks
|
||||
|
||||
- [Automate GitHub Backups](../github-backup-automation/)
|
||||
- [Build a Starred Repo Collection](../starred-repos-collection/)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Does this preserve issues, pull requests, and releases?
|
||||
|
||||
Yes—enable Mirror metadata and Mirror releases from **Configuration → Connections → Content & Data**. Pull requests copy as enriched issues, keeping discussion and labels.
|
||||
|
||||
### What happens if a GitHub repo is deleted or goes private?
|
||||
|
||||
Turn on Handle orphaned repositories automatically and use Archive to keep a read-only copy locally. Delete enforces a strict mirror, removing the repo.
|
||||
|
||||
### How much storage will I need long-term?
|
||||
|
||||
Plan for repo size plus attachments and LFS. Monitor the mirror’s `data/` volume growth and consider ZFS/BTRFS snapshots or object storage for older archives.
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Self-Hosted GitHub Backup on Proxmox LXC | Gitea Mirror Homelab Setup"
|
||||
description: "Deploy a dedicated GitHub backup appliance in your Proxmox homelab using the one-line LXC installer. Automatic syncing, snapshot-ready, homelab-optimized."
|
||||
canonical: "https://gitea-mirror.com/use-cases/proxmox-lxc-homelab/"
|
||||
---
|
||||
|
||||
# Self-Hosted GitHub Backup on Proxmox LXC
|
||||
|
||||
## The Homelab GitHub Backup Appliance
|
||||
|
||||
Running GitHub backups in your homelab means **complete data ownership** without monthly SaaS fees. When most of your infrastructure lives in Proxmox VE, the community LXC script is the fastest path from zero to a dedicated backup appliance.
|
||||
|
||||
It handles Bun runtime, systemd services, persistent storage, and future upgrades—so you can focus on keeping your GitHub history safe and synced locally. Perfect for homelabbers who want the peace of mind of offline backups without cloud dependencies.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Proxmox VE host with the [community-scripts repository](https://community-scripts.github.io/ProxmoxVE/) enabled
|
||||
- Storage pool with ~6 GB free (default script allocation) and an available bridge (usually `vmbr0`)
|
||||
- GitHub PAT and Gitea token scoped for mirroring
|
||||
- DNS or IP address for the container on your LAN
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Launch the installer
|
||||
|
||||
SSH into the Proxmox host and run:
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"
|
||||
```
|
||||
|
||||
The helper script prompts for:
|
||||
|
||||
- **Node**: the Proxmox host that will own the container
|
||||
- **Storage**: local-lvm, ZFS dataset, etc.
|
||||
- **Network bridge**: e.g. `vmbr0`
|
||||
- **IP assignment**: DHCP or static (consider static for a backup appliance)
|
||||
- **Container size**: defaults to 2 vCPU, 2 GiB RAM, 6 GiB disk—bump RAM if you mirror large orgs
|
||||
|
||||
Accept the defaults or adjust as needed. The script downloads the release, installs Bun, seeds systemd, and prints the access URL on completion.
|
||||
|
||||
### 2. First login and setup
|
||||
|
||||
1. Browse to `http://<container-ip>:4321`.
|
||||
2. Create the admin account (first user = admin).
|
||||
3. Enter your GitHub PAT and Gitea API token in the onboarding wizard.
|
||||
4. Select the GitHub owners you want mirrored—auto-discovery runs by default once scheduling is enabled, so new repositories appear automatically.
|
||||
|
||||
### 3. Validate persistence and services
|
||||
|
||||
Inside the container (`pct enter <CTID>`):
|
||||
|
||||
```bash
|
||||
systemctl status gitea-mirror
|
||||
ls /opt/gitea-mirror/data
|
||||
```
|
||||
|
||||
You should see `gitea-mirror.db` and a `repos/` directory. Data lives under `/opt/gitea-mirror/data`; back it up or snapshot the underlying storage pool regularly.
|
||||
|
||||
### 4. Expose the service (optional)
|
||||
|
||||
- Add a Proxmox firewall rule or reverse proxy entry (Traefik/Caddy/HAProxy) if you want TLS.
|
||||
- Create a DNS record (`mirror.lab.local`) pointed at the container for easier access.
|
||||
|
||||
### 5. Upgrades & maintenance
|
||||
|
||||
- Re-run the installer script; it detects existing installs, backs up `/opt/gitea-mirror/data`, downloads the latest release, and restarts the service.
|
||||
- Watch for warnings about upgrades from v2 → v3 wiping config—take a snapshot first if you still run v2 artifacts.
|
||||
- Check logs with `journalctl -u gitea-mirror -n 200` or `journalctl -u gitea-mirror -f` for live tailing.
|
||||
|
||||
## Disaster-recovery drill
|
||||
|
||||
1. In Proxmox, snapshot the container (or use ZFS/BTRFS snapshots on the storage pool).
|
||||
2. Stop GitHub access temporarily and clone from the Gitea mirror to confirm the backup works.
|
||||
3. Restore the snapshot on a different node to ensure the service boots cleanly with the preserved data volume.
|
||||
|
||||
<figure class="mt-8 flex flex-col items-center">
|
||||
<img
|
||||
src="/assets/dashboard_mobile.png"
|
||||
alt="Mobile view of the Gitea Mirror dashboard running inside a Proxmox LXC container."
|
||||
class="w-full max-w-sm rounded-xl border border-muted shadow-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<figcaption class="mt-3 text-sm text-muted-foreground text-center">
|
||||
Even inside a lightweight LXC container, the dashboard stays responsive for quick health checks on the go.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Port 4321 already used**: change `PORT` in `/opt/gitea-mirror.env` and restart with `systemctl restart gitea-mirror`.
|
||||
- **Disk full**: extend the container disk in Proxmox, then run `pct resize <CTID> rootfs +5G`.
|
||||
- **Token expired**: log in to the UI → Configuration to update GitHub/Gitea credentials.
|
||||
|
||||
Looking for a cluster-native deployment? Try the [Helm playbook](../deploy-with-helm-chart).
|
||||
|
||||
## FAQ
|
||||
|
||||
### What container size should I start with?
|
||||
|
||||
The script defaults to 2 vCPU, 2 GiB RAM, 6 GiB disk. Bump RAM for large orgs or many concurrent mirrors; you can resize disk later with `pct resize`.
|
||||
|
||||
### How do I upgrade the LXC deployment?
|
||||
|
||||
Re-run the community installer. It detects existing installs, backs up `/opt/gitea-mirror/data`, downloads the latest release, and restarts the service.
|
||||
|
||||
### How should I back up and restore?
|
||||
|
||||
Snapshot the container or back up `/opt/gitea-mirror/data`. Restore by attaching the volume to a fresh container and starting the service; your configuration is preserved.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Build a Starred Repo Collection"
|
||||
description: "Mirror your starred GitHub projects into a dedicated Gitea library so favorites remain available offline."
|
||||
canonical: "https://gitea-mirror.com/use-cases/starred-repos-collection/"
|
||||
---
|
||||
|
||||
# Build a Starred Repo Collection
|
||||
|
||||
## Curate without losing projects
|
||||
|
||||
Stars are a personal library—but they vanish when creators delete repos or your account is rate limited. Gitea Mirror can automatically capture every starred repository into a dedicated Gitea organization, complete with issues and releases, so your inspiration lives on.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Gitea Mirror up and running (Docker, LXC, or Helm)
|
||||
- GitHub PAT for the account whose stars you want mirrored
|
||||
- Destination Gitea with a personal organization ready (e.g. `stars`)
|
||||
- Optional: DNS entry for the mirror so friends can browse the collection
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Enable starred owner tracking
|
||||
|
||||
1. In the onboarding wizard—or later under **Configuration → Connections**—add your GitHub username as an owner.
|
||||
2. Enable **Mirror starred repositories** and set the **Starred repos organization** to a dedicated space such as `stars-jamie`.
|
||||
3. Use the starred content selector to choose whether you want full metadata (issues, releases, PRs, wiki) or the lightweight code-only mode.
|
||||
|
||||
### 2. Categorize the collection
|
||||
|
||||
- Create sub-organizations in Gitea (e.g. `stars-iot`, `stars-ai`) if you want separate spaces you can curate manually.
|
||||
- Starred mirrors always land in the dedicated starred org, so use repository labels or Gitea teams to slice the collection by language, topic, or priority.
|
||||
- Tag repos in Gitea with labels like `language:rust`, `topic:homelab` for quick filtering.
|
||||
|
||||
<figure class="mt-8 flex flex-col items-center">
|
||||
<img
|
||||
src="/assets/organisation.png"
|
||||
alt="Organization view in Gitea Mirror highlighting starred GitHub repositories grouped in a dedicated Gitea org."
|
||||
class="w-full max-w-5xl rounded-xl border border-muted shadow-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<figcaption class="mt-3 text-sm text-muted-foreground text-center">
|
||||
Group starred GitHub repositories into curated Gitea organizations for easier browsing and long-term preservation.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
### 3. Keep the collection fresh
|
||||
|
||||
- Enable **Automatic syncing** in **Configuration → Automation** and pick an interval around `1h` so new stars appear quickly.
|
||||
- Leave the scheduler running—auto-discovery pulls in new stars on each pass without additional configuration.
|
||||
- If you want unstarred projects archived or removed, enable **Handle orphaned repositories automatically** and choose **Archive** (recommended) or **Delete** for strict mirroring.
|
||||
|
||||
### 4. Showcase the library
|
||||
|
||||
- Point a read-only Gitea user at the org so friends can browse without write access.
|
||||
- Host the Gitea instance behind a reverse proxy with HTTPS, then link it on your blog.
|
||||
- Export the repo list weekly and share it as a curated newsletter.
|
||||
|
||||
## Verify the collection works
|
||||
|
||||
1. Star a new repo on GitHub.
|
||||
2. Wait for the star interval or run **Sync Repository** from the dashboard for an immediate refresh.
|
||||
3. Confirm the repo appears in your `stars` org with issues/releases intact.
|
||||
4. Unstar it on GitHub; if cleanup is set to **Archive**, verify the mirror is renamed with an `archived-` prefix and marked read-only. If you chose **Delete**, confirm it disappears on the next sync.
|
||||
|
||||
## Nice-to-haves
|
||||
|
||||
- Add a `README.md` in the Gitea org homepage explaining your tagging rules.
|
||||
- Pair with [Preserve GitHub History](../preserve-github-history/) so you keep metadata, not just code.
|
||||
- Use the [Helm deployment](../deploy-with-helm-chart/) if you want the collection available inside your cluster.
|
||||
|
||||
## Related playbooks
|
||||
|
||||
- [Backup GitHub Repositories](../backup-github-repositories/)
|
||||
- [Automate GitHub Backups](../github-backup-automation/)
|
||||
|
||||
## FAQ
|
||||
|
||||
### Can I mirror only the code or include metadata?
|
||||
|
||||
Yes. Use the starred content selector to choose code-only or include issues, releases, pull requests, and wikis.
|
||||
|
||||
### What happens when I unstar a repository?
|
||||
|
||||
Enable Handle orphaned repositories automatically and choose Archive (recommended) to preserve history read-only, or Delete for a strict mirror.
|
||||
|
||||
### Can others browse my collection safely?
|
||||
|
||||
Create a read-only Gitea user or org members with no write permissions, then share the organization URL behind HTTPS.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Sync GitHub to Your Self-Hosted Gitea"
|
||||
description: "Keep a homelab Gitea instance continuously updated with GitHub by using Gitea Mirror's discovery, scheduling, and metadata sync."
|
||||
canonical: "https://gitea-mirror.com/use-cases/sync-github-to-self-hosted-gitea/"
|
||||
---
|
||||
|
||||
# Sync GitHub to Your Self-Hosted Gitea
|
||||
|
||||
## Keep SaaS and self-hosted in lockstep
|
||||
|
||||
You may still collaborate on GitHub every day, yet want a LAN Gitea copy you control. Gitea Mirror bridges the two: it tracks owners, auto-discovers repos, mirrors metadata, and keeps a local instance only minutes behind upstream without hand-written cron jobs.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Gitea Mirror deployed (Docker, Proxmox LXC, or Helm)
|
||||
- GitHub PAT with `repo` enabled (add the `read:org` checkbox under `admin:org` to mirror organizations; leave write/admin unchecked)
|
||||
- Gitea API token for the target account/org
|
||||
- Stable connectivity between the mirror host and both GitHub + Gitea
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Connect accounts in the admin wizard
|
||||
|
||||
1. Sign in at `http://<mirror-host>:4321`.
|
||||
2. Open **Configuration → Connections**.
|
||||
3. Paste the GitHub PAT and choose the owners (user + orgs) you want mirrored.
|
||||
4. Add your self-hosted Gitea URL and token; pick the destination org structure (typically **Preserve structure**).
|
||||
|
||||
### 2. Import the repository inventory
|
||||
|
||||
- In **Configuration → Connections**, click **Import GitHub Data** to pull in every repository, organization, and star you've selected.
|
||||
- Enable **Mirror starred repositories** if you want personal favorites mirrored alongside org projects, and set a dedicated starred organization in Gitea.
|
||||
- Auto-discovery runs whenever the scheduler is enabled, so new GitHub repositories will appear automatically after you turn on automatic syncing.
|
||||
|
||||
### 3. Configure sync cadence
|
||||
|
||||
- Head to **Configuration → Automation** and enable **Automatic syncing**.
|
||||
- Pick an interval that matches your recovery point objective; `15–30 minutes` keeps an active GitHub org nearly live, while `2–4 hours` is fine for quieter repos.
|
||||
- Leave the scheduler running so auto-discovery and repository cleanup keep working in the background.
|
||||
|
||||
<figure class="mt-8 flex flex-col items-center">
|
||||
<img
|
||||
src="/assets/dashboard.png"
|
||||
alt="Gitea Mirror dashboard showing overall sync status for GitHub organizations and repositories."
|
||||
class="w-full max-w-5xl rounded-xl border border-muted shadow-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
<figcaption class="mt-3 text-sm text-muted-foreground text-center">
|
||||
Monitor GitHub-to-Gitea sync health from the dashboard while the scheduler keeps repositories aligned automatically.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
### 4. Mirror metadata and LFS
|
||||
|
||||
In **Configuration → Connections → Content & Data**:
|
||||
|
||||
- Enable **Mirror metadata** so issues, pull requests (as enriched issues), labels, milestones, and wikis stay in sync.
|
||||
- Enable **Mirror LFS** if your repos store binaries; confirm your Gitea instance has LFS enabled.
|
||||
- If you want deleted GitHub repos archived or removed locally, enable **Handle orphaned repositories automatically** in the Automation tab and choose the action (Archive or Delete) that matches your retention policy.
|
||||
|
||||
### 5. Validate the local mirror
|
||||
|
||||
- Select a repository in the dashboard and use the **Sync Repository** action.
|
||||
- In Gitea, verify commit history, tags, issues, and releases match GitHub.
|
||||
- Run a quick diff: `git remote add gitea http://<gitea>/<owner>/<repo>.git && git fetch gitea` then `git log origin/main..gitea/main` — it should be empty.
|
||||
|
||||
## Monitoring & health checks
|
||||
|
||||
- Watch the **Activity Log** for failed runs and retry jobs; filter by repository when you need to chase a single mirror.
|
||||
- Point Healthchecks.io, Uptime Kuma, or Prometheus at `http://<mirror-host>:4321/api/health` to confirm the service stays responsive.
|
||||
- Export `/api/repos/:id/logs` or the global `/api/events` endpoint periodically to archive sync history alongside the repositories themselves.
|
||||
|
||||
## Tips for smooth syncing
|
||||
|
||||
- Avoid running more than one mirror against the same Gitea target; let Gitea Mirror manage the schedule centrally.
|
||||
- When restructuring orgs, temporarily disable automatic syncing, move repos in Gitea, then re-enable the scheduler to avoid churn.
|
||||
- Rate limits cropping up? Move the mirror onto a different IP or configure GitHub fine-grained PATs per org.
|
||||
|
||||
## Related playbooks
|
||||
|
||||
- [Backup GitHub Repositories](../backup-github-repositories/) for the base Docker deployment
|
||||
- [Run Gitea Mirror inside a Proxmox LXC](../proxmox-lxc-homelab/) if you want the mirror on a dedicated appliance
|
||||
|
||||
## FAQ
|
||||
|
||||
### How quickly can the mirror catch up to GitHub?
|
||||
|
||||
Intervals of 15–30 minutes keep most orgs near real-time. You can shorten for critical repos, but watch GitHub API rate limits.
|
||||
|
||||
### Can I mirror multiple GitHub users and organizations?
|
||||
|
||||
Yes. Add each owner in Configuration → Connections, then choose a destination strategy (Preserve structure or a specific org) for Gitea.
|
||||
|
||||
### Is it safe to store tokens in Gitea Mirror?
|
||||
|
||||
Tokens saved via the Configuration UI are encrypted at rest. Rotate them regularly and monitor dashboard alerts for expirations.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
layout: ../../layouts/UseCaseLayout.astro
|
||||
title: "Stay Ready to Leave GitHub"
|
||||
description: "Use Gitea Mirror to keep an always-current fallback so policy or pricing changes at GitHub never stall your projects."
|
||||
canonical: "https://gitea-mirror.com/use-cases/vendor-lock-in-prevention/"
|
||||
---
|
||||
|
||||
# Stay Ready to Leave GitHub
|
||||
|
||||
## Keep your exit hatch open
|
||||
|
||||
GitHub can change pricing, authentication rules, or terms without notice. With Gitea Mirror running, you always have a live copy of code and metadata inside infrastructure you control—so switching to self-hosted Gitea, Forgejo, or something else becomes a DNS change, not a fire drill.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Production-ready Gitea instance (backed by your usual storage and TLS)
|
||||
- Gitea Mirror configured with owner discovery and metadata sync
|
||||
- Documentation of your current GitHub org structure (for validation)
|
||||
- Optional: reverse proxy + SSO for a smooth cutover experience
|
||||
|
||||
## Step-by-step
|
||||
|
||||
### 1. Mirror everything continuously
|
||||
|
||||
- In **Configuration → Connections**, add every GitHub organization you care about as an owner. Auto-discovery pulls new repositories on the next scheduler run.
|
||||
- Set the organization strategy to **Preserve structure** so Gitea mirrors the GitHub org layout.
|
||||
- Open **Content & Data** to enable metadata, releases, and Git LFS, ensuring issues, pull requests, and binaries survive the switchover.
|
||||
|
||||
### 2. Simulate a cutover
|
||||
|
||||
1. Pick a pilot team.
|
||||
2. Ask them to work exclusively out of the Gitea mirror for a sprint.
|
||||
3. Capture feedback about permissions, webhooks, CI integrations, and adjust.
|
||||
|
||||
### 3. Keep integrations dual-homed
|
||||
|
||||
- Point your CI (e.g. Woodpecker, Jenkins) at both GitHub and Gitea using mirrored tokens.
|
||||
- Maintain matching webhook payloads so automation keeps running post-swap.
|
||||
- Mirror secrets (deploy keys, bot accounts) into the Gitea org to remove blockers.
|
||||
|
||||
### 4. Document the flip procedure
|
||||
|
||||
- Write a runbook: DNS updates, webhook changes, `git remote set-url` commands.
|
||||
- Version-control the document inside the mirror so updates stay in sync.
|
||||
- Rehearse twice a year; include steps to roll back to GitHub if needed.
|
||||
|
||||
### 5. Watch for drift
|
||||
|
||||
- Review the repositories dashboard weekly for new projects, teams, or permission mismatches.
|
||||
- Check the **Automation** tab—if the scheduler is enabled, auto-discovery keeps owners current.
|
||||
- Use the Activity Log and `/api/events` export to alert on failures before your recovery point objective is breached.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- A developer can change their `origin` remote to Gitea and push without errors.
|
||||
- CI pipelines succeed when pointed exclusively at the mirror.
|
||||
- All active repos have synced within your SLA (for example, last run < 2 hours ago).
|
||||
- The runbook includes contact info, rollback steps, and verification checklists.
|
||||
|
||||
## Related playbooks
|
||||
|
||||
- [Sync GitHub to Your Self-Hosted Gitea](../sync-github-to-self-hosted-gitea/)
|
||||
- [Deploy Gitea Mirror with the Helm Chart](../deploy-with-helm-chart/)
|
||||
|
||||
## FAQ
|
||||
|
||||
### How do I confirm the mirror is still catching new repositories?
|
||||
|
||||
Check the **Configuration → Connections** screen for the `Last imported` timestamp and ensure the scheduler in **Automation** is enabled. Auto-discovery runs with each scheduled sync.
|
||||
|
||||
### What interval should I run before a potential cutover?
|
||||
|
||||
Keep `Automatic syncing` at 15–30 minutes for active organizations. That keeps the lag well under an hour if you have to pivot suddenly.
|
||||
|
||||
### Can I script the cutover?
|
||||
|
||||
Yes. Use the `/api/export` configuration JSON and the repositories CSV export to generate runbooks, then pair them with infrastructure-as-code (DNS, reverse proxy, CI) so flipping endpoints becomes repeatable.
|
||||
226
Divers/gitea-mirror/www/src/styles/global.css
Normal file
@@ -0,0 +1,226 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@custom-media --xs (width >= 475px);
|
||||
|
||||
@import "tailwindcss/theme" layer(theme);
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-accent-purple: var(--accent-purple);
|
||||
--color-accent-teal: var(--accent-teal);
|
||||
--color-accent-coral: var(--accent-coral);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
--promo-banner-height: 0px;
|
||||
--background: oklch(0.99 0 0);
|
||||
--foreground: oklch(0.15 0 0);
|
||||
--card: oklch(0.985 0 0);
|
||||
--card-foreground: oklch(0.15 0 0);
|
||||
--popover: oklch(0.985 0 0);
|
||||
--popover-foreground: oklch(0.15 0 0);
|
||||
--primary: oklch(0.55 0.25 255);
|
||||
--primary-foreground: oklch(0.99 0 0);
|
||||
--secondary: oklch(0.96 0.02 240);
|
||||
--secondary-foreground: oklch(0.15 0 0);
|
||||
--muted: oklch(0.96 0.01 240);
|
||||
--muted-foreground: oklch(0.45 0.02 240);
|
||||
--accent: oklch(0.7 0.2 190);
|
||||
--accent-foreground: oklch(0.99 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.94 0.01 240);
|
||||
--input: oklch(0.94 0.01 240);
|
||||
--ring: oklch(0.55 0.25 255);
|
||||
--chart-1: oklch(0.55 0.25 255);
|
||||
--chart-2: oklch(0.7 0.2 190);
|
||||
--chart-3: oklch(0.7 0.15 150);
|
||||
--chart-4: oklch(0.7 0.2 30);
|
||||
--chart-5: oklch(0.6 0.25 280);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.15 0 0);
|
||||
--sidebar-primary: oklch(0.55 0.25 255);
|
||||
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||
--sidebar-accent: oklch(0.96 0.02 240);
|
||||
--sidebar-accent-foreground: oklch(0.15 0 0);
|
||||
--sidebar-border: oklch(0.94 0.01 240);
|
||||
--sidebar-ring: oklch(0.55 0.25 255);
|
||||
--accent-purple: oklch(0.6 0.25 280);
|
||||
--accent-teal: oklch(0.7 0.2 190);
|
||||
--accent-coral: oklch(0.7 0.2 30);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.08 0.01 240);
|
||||
--foreground: oklch(0.98 0 0);
|
||||
--card: oklch(0.13 0.02 240);
|
||||
--card-foreground: oklch(0.98 0 0);
|
||||
--popover: oklch(0.13 0.02 240);
|
||||
--popover-foreground: oklch(0.98 0 0);
|
||||
--primary: oklch(0.7 0.25 255);
|
||||
--primary-foreground: oklch(0.08 0 0);
|
||||
--secondary: oklch(0.18 0.03 240);
|
||||
--secondary-foreground: oklch(0.98 0 0);
|
||||
--muted: oklch(0.18 0.02 240);
|
||||
--muted-foreground: oklch(0.7 0.02 240);
|
||||
--accent: oklch(0.75 0.2 190);
|
||||
--accent-foreground: oklch(0.08 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(0.22 0.02 240);
|
||||
--input: oklch(0.22 0.02 240);
|
||||
--ring: oklch(0.7 0.25 255);
|
||||
--chart-1: oklch(0.7 0.25 255);
|
||||
--chart-2: oklch(0.75 0.2 190);
|
||||
--chart-3: oklch(0.75 0.15 150);
|
||||
--chart-4: oklch(0.75 0.2 30);
|
||||
--chart-5: oklch(0.65 0.25 280);
|
||||
--sidebar: oklch(0.13 0.02 240);
|
||||
--sidebar-foreground: oklch(0.98 0 0);
|
||||
--sidebar-primary: oklch(0.7 0.25 255);
|
||||
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||
--sidebar-accent: oklch(0.18 0.03 240);
|
||||
--sidebar-accent-foreground: oklch(0.98 0 0);
|
||||
--sidebar-border: oklch(0.22 0.02 240);
|
||||
--sidebar-ring: oklch(0.7 0.25 255);
|
||||
--accent-purple: oklch(0.65 0.25 280);
|
||||
--accent-teal: oklch(0.75 0.2 190);
|
||||
--accent-coral: oklch(0.75 0.2 30);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom gradient utilities */
|
||||
@layer utilities {
|
||||
.bg-gradient-radial {
|
||||
background-image: radial-gradient(
|
||||
circle at center,
|
||||
var(--tw-gradient-stops)
|
||||
);
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
@apply bg-gradient-to-r bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.gradient-border {
|
||||
position: relative;
|
||||
background: linear-gradient(var(--background), var(--background))
|
||||
padding-box,
|
||||
linear-gradient(to right, var(--tw-gradient-stops)) border-box;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.glow-sm {
|
||||
box-shadow: 0 0 20px -5px var(--tw-shadow-color);
|
||||
}
|
||||
|
||||
.glow-md {
|
||||
box-shadow: 0 0 40px -10px var(--tw-shadow-color);
|
||||
}
|
||||
|
||||
.glow-lg {
|
||||
box-shadow: 0 0 60px -15px var(--tw-shadow-color);
|
||||
}
|
||||
|
||||
/* Accent color utilities */
|
||||
.text-accent-purple {
|
||||
color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.text-accent-teal {
|
||||
color: var(--accent-teal);
|
||||
}
|
||||
|
||||
.text-accent-coral {
|
||||
color: var(--accent-coral);
|
||||
}
|
||||
|
||||
.bg-accent-purple {
|
||||
background-color: var(--accent-purple);
|
||||
}
|
||||
|
||||
.bg-accent-teal {
|
||||
background-color: var(--accent-teal);
|
||||
}
|
||||
|
||||
.bg-accent-coral {
|
||||
background-color: var(--accent-coral);
|
||||
}
|
||||
|
||||
.from-accent-purple\/10 {
|
||||
--tw-gradient-from: oklch(from var(--accent-purple) l c h / 0.1);
|
||||
}
|
||||
|
||||
.from-accent-teal\/10 {
|
||||
--tw-gradient-from: oklch(from var(--accent-teal) l c h / 0.1);
|
||||
}
|
||||
|
||||
.from-accent-coral\/10 {
|
||||
--tw-gradient-from: oklch(from var(--accent-coral) l c h / 0.1);
|
||||
}
|
||||
|
||||
.to-accent-purple\/10 {
|
||||
--tw-gradient-to: oklch(from var(--accent-purple) l c h / 0.1);
|
||||
}
|
||||
|
||||
.to-accent-teal\/10 {
|
||||
--tw-gradient-to: oklch(from var(--accent-teal) l c h / 0.1);
|
||||
}
|
||||
|
||||
.to-accent-coral\/10 {
|
||||
--tw-gradient-to: oklch(from var(--accent-coral) l c h / 0.1);
|
||||
}
|
||||
|
||||
@media (width >= 135rem /* 2160px */) {
|
||||
.spline-object {
|
||||
max-height: 70rem /* 960px */;
|
||||
@apply -translate-y-40;
|
||||
}
|
||||
.clip-avoid {
|
||||
height: 25rem /* 320px */;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Divers/gitea-mirror/www/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||