seo update
This commit is contained in:
513
.agents/skills/seo/SKILL.md
Normal file
513
.agents/skills/seo/SKILL.md
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
---
|
||||||
|
name: seo
|
||||||
|
description: Optimize for search engine visibility and ranking. Use when asked to "improve SEO", "optimize for search", "fix meta tags", "add structured data", "sitemap optimization", or "search engine optimization".
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: web-quality-skills
|
||||||
|
version: "1.0"
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO optimization
|
||||||
|
|
||||||
|
Search engine optimization based on Lighthouse SEO audits and Google Search guidelines. Focus on technical SEO, on-page optimization, and structured data.
|
||||||
|
|
||||||
|
## SEO fundamentals
|
||||||
|
|
||||||
|
Search ranking factors (approximate influence):
|
||||||
|
|
||||||
|
| Factor | Influence | This Skill |
|
||||||
|
|--------|-----------|------------|
|
||||||
|
| Content quality & relevance | ~40% | Partial (structure) |
|
||||||
|
| Backlinks & authority | ~25% | ✗ |
|
||||||
|
| Technical SEO | ~15% | ✓ |
|
||||||
|
| Page experience (Core Web Vitals) | ~10% | See [Core Web Vitals](../core-web-vitals/SKILL.md) |
|
||||||
|
| On-page SEO | ~10% | ✓ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical SEO
|
||||||
|
|
||||||
|
### Crawlability
|
||||||
|
|
||||||
|
**robots.txt:**
|
||||||
|
```text
|
||||||
|
# /robots.txt
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Block admin/private areas
|
||||||
|
Disallow: /admin/
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /private/
|
||||||
|
|
||||||
|
# Don't block resources needed for rendering
|
||||||
|
# ❌ Disallow: /static/
|
||||||
|
|
||||||
|
Sitemap: https://example.com/sitemap.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Meta robots:**
|
||||||
|
```html
|
||||||
|
<!-- Default: indexable, followable -->
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
|
||||||
|
<!-- Noindex specific pages -->
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
|
<!-- Indexable but don't follow links -->
|
||||||
|
<meta name="robots" content="index, nofollow">
|
||||||
|
|
||||||
|
<!-- Control snippets -->
|
||||||
|
<meta name="robots" content="max-snippet:150, max-image-preview:large">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Canonical URLs:**
|
||||||
|
```html
|
||||||
|
<!-- Prevent duplicate content issues -->
|
||||||
|
<link rel="canonical" href="https://example.com/page">
|
||||||
|
|
||||||
|
<!-- Self-referencing canonical (recommended) -->
|
||||||
|
<link rel="canonical" href="https://example.com/current-page">
|
||||||
|
|
||||||
|
<!-- For paginated content -->
|
||||||
|
<link rel="canonical" href="https://example.com/products">
|
||||||
|
<!-- Or use rel="prev" / rel="next" for explicit pagination -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### XML sitemap
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://example.com/</loc>
|
||||||
|
<lastmod>2024-01-15</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://example.com/products</loc>
|
||||||
|
<lastmod>2024-01-14</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sitemap best practices:**
|
||||||
|
- Maximum 50,000 URLs or 50MB per sitemap
|
||||||
|
- Use sitemap index for larger sites
|
||||||
|
- Include only canonical, indexable URLs
|
||||||
|
- Update `lastmod` when content changes
|
||||||
|
- Submit to Google Search Console
|
||||||
|
|
||||||
|
### URL structure
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ Good URLs:
|
||||||
|
https://example.com/products/blue-widget
|
||||||
|
https://example.com/blog/how-to-use-widgets
|
||||||
|
|
||||||
|
❌ Poor URLs:
|
||||||
|
https://example.com/p?id=12345
|
||||||
|
https://example.com/products/item/category/subcategory/blue-widget-2024-sale-discount
|
||||||
|
```
|
||||||
|
|
||||||
|
**URL guidelines:**
|
||||||
|
- Use hyphens, not underscores
|
||||||
|
- Lowercase only
|
||||||
|
- Keep short (< 75 characters)
|
||||||
|
- Include target keywords naturally
|
||||||
|
- Avoid parameters when possible
|
||||||
|
- Use HTTPS always
|
||||||
|
|
||||||
|
### HTTPS & security
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Ensure all resources use HTTPS -->
|
||||||
|
<img src="https://example.com/image.jpg">
|
||||||
|
|
||||||
|
<!-- Not: -->
|
||||||
|
<img src="http://example.com/image.jpg">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security headers for SEO trust signals:**
|
||||||
|
```
|
||||||
|
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: DENY
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## On-page SEO
|
||||||
|
|
||||||
|
### Title tags
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ❌ Missing or generic -->
|
||||||
|
<title>Page</title>
|
||||||
|
<title>Home</title>
|
||||||
|
|
||||||
|
<!-- ✅ Descriptive with primary keyword -->
|
||||||
|
<title>Blue Widgets for Sale | Premium Quality | Example Store</title>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Title tag guidelines:**
|
||||||
|
- 50-60 characters (Google truncates ~60)
|
||||||
|
- Primary keyword near the beginning
|
||||||
|
- Unique for every page
|
||||||
|
- Brand name at end (unless homepage)
|
||||||
|
- Action-oriented when appropriate
|
||||||
|
|
||||||
|
### Meta descriptions
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ❌ Missing or duplicate -->
|
||||||
|
<meta name="description" content="">
|
||||||
|
|
||||||
|
<!-- ✅ Compelling and unique -->
|
||||||
|
<meta name="description" content="Shop premium blue widgets with free shipping. 30-day returns. Rated 4.9/5 by 10,000+ customers. Order today and save 20%.">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Meta description guidelines:**
|
||||||
|
- 150-160 characters
|
||||||
|
- Include primary keyword naturally
|
||||||
|
- Compelling call-to-action
|
||||||
|
- Unique for every page
|
||||||
|
- Matches page content
|
||||||
|
|
||||||
|
### Heading structure
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ❌ Poor structure -->
|
||||||
|
<h2>Welcome to Our Store</h2>
|
||||||
|
<h4>Products</h4>
|
||||||
|
<h1>Contact Us</h1>
|
||||||
|
|
||||||
|
<!-- ✅ Proper hierarchy -->
|
||||||
|
<h1>Blue Widgets - Premium Quality</h1>
|
||||||
|
<h2>Product Features</h2>
|
||||||
|
<h3>Durability</h3>
|
||||||
|
<h3>Design</h3>
|
||||||
|
<h2>Customer Reviews</h2>
|
||||||
|
<h2>Pricing</h2>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Heading guidelines:**
|
||||||
|
- Single `<h1>` per page (the main topic)
|
||||||
|
- Logical hierarchy (don't skip levels)
|
||||||
|
- Include keywords naturally
|
||||||
|
- Descriptive, not generic
|
||||||
|
|
||||||
|
### Image SEO
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ❌ Poor image SEO -->
|
||||||
|
<img src="IMG_12345.jpg">
|
||||||
|
|
||||||
|
<!-- ✅ Optimized image -->
|
||||||
|
<img src="blue-widget-product-photo.webp"
|
||||||
|
alt="Blue widget with chrome finish, side view showing control panel"
|
||||||
|
width="800"
|
||||||
|
height="600"
|
||||||
|
loading="lazy">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Image guidelines:**
|
||||||
|
- Descriptive filenames with keywords
|
||||||
|
- Alt text describes the image content
|
||||||
|
- Compressed and properly sized
|
||||||
|
- WebP/AVIF with fallbacks
|
||||||
|
- Lazy load below-fold images
|
||||||
|
|
||||||
|
### Internal linking
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ❌ Non-descriptive -->
|
||||||
|
<a href="/products">Click here</a>
|
||||||
|
<a href="/widgets">Read more</a>
|
||||||
|
|
||||||
|
<!-- ✅ Descriptive anchor text -->
|
||||||
|
<a href="/products/blue-widgets">Browse our blue widget collection</a>
|
||||||
|
<a href="/guides/widget-maintenance">Learn how to maintain your widgets</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linking guidelines:**
|
||||||
|
- Descriptive anchor text with keywords
|
||||||
|
- Link to relevant internal pages
|
||||||
|
- Reasonable number of links per page
|
||||||
|
- Fix broken links promptly
|
||||||
|
- Use breadcrumbs for hierarchy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structured data (JSON-LD)
|
||||||
|
|
||||||
|
### Organization
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Example Company",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"logo": "https://example.com/logo.png",
|
||||||
|
"sameAs": [
|
||||||
|
"https://twitter.com/example",
|
||||||
|
"https://linkedin.com/company/example"
|
||||||
|
],
|
||||||
|
"contactPoint": {
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
"telephone": "+1-555-123-4567",
|
||||||
|
"contactType": "customer service"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Article
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
"headline": "How to Choose the Right Widget",
|
||||||
|
"description": "Complete guide to selecting widgets for your needs.",
|
||||||
|
"image": "https://example.com/article-image.jpg",
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Jane Smith",
|
||||||
|
"url": "https://example.com/authors/jane-smith"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Example Blog",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://example.com/logo.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"datePublished": "2024-01-15",
|
||||||
|
"dateModified": "2024-01-20"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Product
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Product",
|
||||||
|
"name": "Blue Widget Pro",
|
||||||
|
"image": "https://example.com/blue-widget.jpg",
|
||||||
|
"description": "Premium blue widget with advanced features.",
|
||||||
|
"brand": {
|
||||||
|
"@type": "Brand",
|
||||||
|
"name": "WidgetCo"
|
||||||
|
},
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "49.99",
|
||||||
|
"priceCurrency": "USD",
|
||||||
|
"availability": "https://schema.org/InStock",
|
||||||
|
"url": "https://example.com/products/blue-widget"
|
||||||
|
},
|
||||||
|
"aggregateRating": {
|
||||||
|
"@type": "AggregateRating",
|
||||||
|
"ratingValue": "4.8",
|
||||||
|
"reviewCount": "1250"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAQ
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "FAQPage",
|
||||||
|
"mainEntity": [
|
||||||
|
{
|
||||||
|
"@type": "Question",
|
||||||
|
"name": "What colors are available?",
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "Our widgets come in blue, red, and green."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "Question",
|
||||||
|
"name": "What is the warranty?",
|
||||||
|
"acceptedAnswer": {
|
||||||
|
"@type": "Answer",
|
||||||
|
"text": "All widgets include a 2-year warranty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breadcrumbs
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 1,
|
||||||
|
"name": "Home",
|
||||||
|
"item": "https://example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 2,
|
||||||
|
"name": "Products",
|
||||||
|
"item": "https://example.com/products"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 3,
|
||||||
|
"name": "Blue Widgets",
|
||||||
|
"item": "https://example.com/products/blue-widgets"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
Test structured data at:
|
||||||
|
- [Google Rich Results Test](https://search.google.com/test/rich-results)
|
||||||
|
- [Schema.org Validator](https://validator.schema.org/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mobile SEO
|
||||||
|
|
||||||
|
### Responsive design
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ❌ Not mobile-friendly -->
|
||||||
|
<meta name="viewport" content="width=1024">
|
||||||
|
|
||||||
|
<!-- ✅ Responsive viewport -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tap targets
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ❌ Too small for mobile */
|
||||||
|
.small-link {
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Adequate tap target */
|
||||||
|
.mobile-friendly-link {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 48px;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Font sizes
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ❌ Too small on mobile */
|
||||||
|
body {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ✅ Readable without zooming */
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## International SEO
|
||||||
|
|
||||||
|
### Hreflang tags
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- For multi-language sites -->
|
||||||
|
<link rel="alternate" hreflang="en" href="https://example.com/page">
|
||||||
|
<link rel="alternate" hreflang="es" href="https://example.com/es/page">
|
||||||
|
<link rel="alternate" hreflang="fr" href="https://example.com/fr/page">
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://example.com/page">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Language declaration
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html lang="en">
|
||||||
|
<!-- or -->
|
||||||
|
<html lang="es-MX">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SEO audit checklist
|
||||||
|
|
||||||
|
### Critical
|
||||||
|
- [ ] HTTPS enabled
|
||||||
|
- [ ] robots.txt allows crawling
|
||||||
|
- [ ] No `noindex` on important pages
|
||||||
|
- [ ] Title tags present and unique
|
||||||
|
- [ ] Single `<h1>` per page
|
||||||
|
|
||||||
|
### High priority
|
||||||
|
- [ ] Meta descriptions present
|
||||||
|
- [ ] Sitemap submitted
|
||||||
|
- [ ] Canonical URLs set
|
||||||
|
- [ ] Mobile-responsive
|
||||||
|
- [ ] Core Web Vitals passing
|
||||||
|
|
||||||
|
### Medium priority
|
||||||
|
- [ ] Structured data implemented
|
||||||
|
- [ ] Internal linking strategy
|
||||||
|
- [ ] Image alt text
|
||||||
|
- [ ] Descriptive URLs
|
||||||
|
- [ ] Breadcrumb navigation
|
||||||
|
|
||||||
|
### Ongoing
|
||||||
|
- [ ] Fix crawl errors in Search Console
|
||||||
|
- [ ] Update sitemap when content changes
|
||||||
|
- [ ] Monitor ranking changes
|
||||||
|
- [ ] Check for broken links
|
||||||
|
- [ ] Review Search Console insights
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Use |
|
||||||
|
|------|-----|
|
||||||
|
| Google Search Console | Monitor indexing, fix issues |
|
||||||
|
| Google PageSpeed Insights | Performance + Core Web Vitals |
|
||||||
|
| Rich Results Test | Validate structured data |
|
||||||
|
| Lighthouse | Full SEO audit |
|
||||||
|
| Screaming Frog | Crawl analysis |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Google Search Central](https://developers.google.com/search)
|
||||||
|
- [Schema.org](https://schema.org/)
|
||||||
|
- [Core Web Vitals](../core-web-vitals/SKILL.md)
|
||||||
|
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
"Bash(git remote:*)",
|
"Bash(git remote:*)",
|
||||||
"Bash(find /Users/jeet/Desktop/Jio/grateful-journal/src -type f -name *.ts -o -name *.tsx)",
|
"Bash(find /Users/jeet/Desktop/Jio/grateful-journal/src -type f -name *.ts -o -name *.tsx)",
|
||||||
"Bash(ls -la /Users/jeet/Desktop/Jio/grateful-journal/*.config.*)",
|
"Bash(ls -la /Users/jeet/Desktop/Jio/grateful-journal/*.config.*)",
|
||||||
"mcp__ide__getDiagnostics"
|
"mcp__ide__getDiagnostics",
|
||||||
|
"Bash(npx skills:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
.claude/skills/seo
Symbolic link
1
.claude/skills/seo
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/seo
|
||||||
28
index.html
28
index.html
@@ -20,10 +20,12 @@
|
|||||||
<title>Grateful Journal — Your Private Gratitude Journal</title>
|
<title>Grateful Journal — Your Private Gratitude Journal</title>
|
||||||
<meta name="description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time." />
|
<meta name="description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time." />
|
||||||
<meta name="keywords" content="gratitude journal, private journal, encrypted journal, daily gratitude, mindfulness, reflection" />
|
<meta name="keywords" content="gratitude journal, private journal, encrypted journal, daily gratitude, mindfulness, reflection" />
|
||||||
|
<meta name="robots" content="index, follow, max-snippet:160, max-image-preview:large" />
|
||||||
<link rel="canonical" href="https://gratefuljournal.online/" />
|
<link rel="canonical" href="https://gratefuljournal.online/" />
|
||||||
|
|
||||||
<!-- Open Graph (WhatsApp, Facebook, LinkedIn previews) -->
|
<!-- Open Graph (WhatsApp, Facebook, LinkedIn previews) -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
<meta property="og:url" content="https://gratefuljournal.online/" />
|
<meta property="og:url" content="https://gratefuljournal.online/" />
|
||||||
<meta property="og:title" content="Grateful Journal — Your Private Gratitude Journal" />
|
<meta property="og:title" content="Grateful Journal — Your Private Gratitude Journal" />
|
||||||
<meta property="og:description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts." />
|
<meta property="og:description" content="A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts." />
|
||||||
@@ -45,16 +47,40 @@
|
|||||||
"description": "A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts.",
|
"description": "A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts.",
|
||||||
"applicationCategory": "LifestyleApplication",
|
"applicationCategory": "LifestyleApplication",
|
||||||
"operatingSystem": "Web, Android, iOS",
|
"operatingSystem": "Web, Android, iOS",
|
||||||
|
"browserRequirements": "Requires JavaScript. Requires HTML5.",
|
||||||
"offers": {
|
"offers": {
|
||||||
"@type": "Offer",
|
"@type": "Offer",
|
||||||
"price": "0",
|
"price": "0",
|
||||||
"priceCurrency": "USD"
|
"priceCurrency": "USD"
|
||||||
}
|
},
|
||||||
|
"featureList": [
|
||||||
|
"End-to-end encrypted journal entries",
|
||||||
|
"Daily gratitude prompts",
|
||||||
|
"Private and secure — no ads, no tracking",
|
||||||
|
"Works offline as a PWA"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<noscript>
|
||||||
|
<main style="font-family:sans-serif;max-width:640px;margin:4rem auto;padding:1rem;color:#1a1a1a">
|
||||||
|
<h1>Grateful Journal — Your Private Gratitude Journal</h1>
|
||||||
|
<p>A private, end-to-end encrypted gratitude journal. No feeds, no noise — just you and your thoughts. Grow your gratitude one moment at a time.</p>
|
||||||
|
<h2>Features</h2>
|
||||||
|
<ul>
|
||||||
|
<li>End-to-end encrypted journal entries — only you can read them</li>
|
||||||
|
<li>Daily gratitude prompts to keep you consistent</li>
|
||||||
|
<li>No ads, no tracking, no social feed</li>
|
||||||
|
<li>Works offline as a Progressive Web App (PWA)</li>
|
||||||
|
<li>Free to use</li>
|
||||||
|
</ul>
|
||||||
|
<h2>How it works</h2>
|
||||||
|
<p>Sign in with Google, write a few things you're grateful for each day, and watch your mindset shift over time. Your entries are encrypted before they leave your device.</p>
|
||||||
|
<p><a href="https://gratefuljournal.online/">Get started — it's free</a></p>
|
||||||
|
</main>
|
||||||
|
</noscript>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -23,7 +23,17 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
# Known SPA routes — serve index.html
|
||||||
try_files $uri $uri/ /index.html;
|
location = / {
|
||||||
|
try_files /index.html =404;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
location ~ ^/(write|history|settings)(/|$) {
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static assets — serve directly, 404 if missing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Disallow: /write
|
||||||
Disallow: /
|
Disallow: /history
|
||||||
|
Disallow: /settings
|
||||||
|
|
||||||
User-agent: Googlebot
|
Sitemap: https://gratefuljournal.online/sitemap.xml
|
||||||
Allow: /login
|
|
||||||
Disallow: /
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://gratefuljournal.online/login</loc>
|
<loc>https://gratefuljournal.online/</loc>
|
||||||
|
<lastmod>2026-04-08</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
10
skills-lock.json
Normal file
10
skills-lock.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"seo": {
|
||||||
|
"source": "addyosmani/web-quality-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"computedHash": "f1fed683b76913d26fbf1aa1e008e6932f7771701fc3a79925b042236aa4681a"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2917,11 +2917,17 @@
|
|||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes tour-btn-pulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.6; transform: scale(0.96); }
|
||||||
|
}
|
||||||
|
|
||||||
.gj-tour-popover .driver-popover-next-btn {
|
.gj-tour-popover .driver-popover-next-btn {
|
||||||
background: var(--color-primary, #22c55e) !important;
|
background: var(--color-primary, #22c55e) !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
text-shadow: none !important;
|
text-shadow: none !important;
|
||||||
|
animation: tour-btn-pulse 1.2s ease-in-out infinite !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gj-tour-popover .driver-popover-prev-btn {
|
.gj-tour-popover .driver-popover-prev-btn {
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ function App() {
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/" element={<LoginPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/write"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
@@ -36,8 +37,7 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export default function BottomNav() {
|
|||||||
{/* Write */}
|
{/* Write */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`bottom-nav-btn ${isActive('/') ? 'bottom-nav-btn-active' : ''}`}
|
className={`bottom-nav-btn ${isActive('/write') ? 'bottom-nav-btn-active' : ''}`}
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/write')}
|
||||||
aria-label="Write"
|
aria-label="Write"
|
||||||
>
|
>
|
||||||
{/* Pencil / edit icon */}
|
{/* Pencil / edit icon */}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function ProtectedRoute({ children }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <Navigate to="/login" state={{ from: location }} replace />
|
return <Navigate to="/" state={{ from: location }} replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { driver, type DriveStep } from 'driver.js'
|
import { driver, type DriveStep } from 'driver.js'
|
||||||
import 'driver.js/dist/driver.css'
|
import 'driver.js/dist/driver.css'
|
||||||
@@ -128,14 +128,17 @@ function getSettingsSteps(isMobile: boolean): DriveStep[] {
|
|||||||
export function useOnboardingTour() {
|
export function useOnboardingTour() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const driverRef = useRef<ReturnType<typeof driver> | null>(null)
|
const driverRef = useRef<ReturnType<typeof driver> | null>(null)
|
||||||
|
const [isTourActive, setIsTourActive] = useState(false)
|
||||||
|
|
||||||
const startTour = useCallback(() => {
|
const startTour = useCallback(() => {
|
||||||
const isMobile = window.innerWidth < 860
|
const isMobile = window.innerWidth < 860
|
||||||
|
setIsTourActive(true)
|
||||||
|
|
||||||
const driverObj = driver({
|
const driverObj = driver({
|
||||||
...driverDefaults(),
|
...driverDefaults(),
|
||||||
onDestroyStarted: () => {
|
onDestroyStarted: () => {
|
||||||
clearPendingTourStep()
|
clearPendingTourStep()
|
||||||
|
setIsTourActive(false)
|
||||||
driverObj.destroy()
|
driverObj.destroy()
|
||||||
},
|
},
|
||||||
onNextClick: () => {
|
onNextClick: () => {
|
||||||
@@ -145,6 +148,7 @@ export function useOnboardingTour() {
|
|||||||
// Last home step → navigate to /history
|
// Last home step → navigate to /history
|
||||||
if (activeIndex === steps.length - 1) {
|
if (activeIndex === steps.length - 1) {
|
||||||
localStorage.setItem(TOUR_PENDING_KEY, 'history')
|
localStorage.setItem(TOUR_PENDING_KEY, 'history')
|
||||||
|
setIsTourActive(false)
|
||||||
driverObj.destroy()
|
driverObj.destroy()
|
||||||
navigate('/history')
|
navigate('/history')
|
||||||
return
|
return
|
||||||
@@ -206,7 +210,7 @@ export function useOnboardingTour() {
|
|||||||
if (activeIndex === steps.length - 1) {
|
if (activeIndex === steps.length - 1) {
|
||||||
clearPendingTourStep()
|
clearPendingTourStep()
|
||||||
driverObj.destroy()
|
driverObj.destroy()
|
||||||
navigate('/')
|
navigate('/write')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,5 +223,5 @@ export function useOnboardingTour() {
|
|||||||
setTimeout(() => driverObj.drive(), 300)
|
setTimeout(() => driverObj.drive(), 300)
|
||||||
}, [navigate])
|
}, [navigate])
|
||||||
|
|
||||||
return { startTour, continueTourOnHistory, continueTourOnSettings }
|
return { startTour, continueTourOnHistory, continueTourOnSettings, isTourActive }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function HomePage() {
|
|||||||
const titleInputRef = useRef<HTMLInputElement>(null)
|
const titleInputRef = useRef<HTMLInputElement>(null)
|
||||||
const contentTextareaRef = useRef<HTMLTextAreaElement>(null)
|
const contentTextareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
const { startTour } = useOnboardingTour()
|
const { startTour, isTourActive } = useOnboardingTour()
|
||||||
|
|
||||||
// Check if onboarding should be shown after login
|
// Check if onboarding should be shown after login
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,6 +52,14 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
}, [loading, user, userId, mongoUser])
|
}, [loading, user, userId, mongoUser])
|
||||||
|
|
||||||
|
// On-demand tour triggered from Settings (no DB read/write)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && user && localStorage.getItem('gj-force-tour') === 'true') {
|
||||||
|
localStorage.removeItem('gj-force-tour')
|
||||||
|
setTimeout(() => startTour(), 150)
|
||||||
|
}
|
||||||
|
}, [loading, user, startTour])
|
||||||
|
|
||||||
async function markTutorialDone() {
|
async function markTutorialDone() {
|
||||||
if (!user || !userId) return
|
if (!user || !userId) return
|
||||||
const token = await user.getIdToken()
|
const token = await user.getIdToken()
|
||||||
@@ -78,7 +86,7 @@ export default function HomePage() {
|
|||||||
<div className="home-page" style={{ alignItems: 'center', justifyContent: 'center', gap: '1rem' }}>
|
<div className="home-page" style={{ alignItems: 'center', justifyContent: 'center', gap: '1rem' }}>
|
||||||
<h1 style={{ fontFamily: '"Sniglet", system-ui', color: 'var(--color-text)' }}>Grateful Journal</h1>
|
<h1 style={{ fontFamily: '"Sniglet", system-ui', color: 'var(--color-text)' }}>Grateful Journal</h1>
|
||||||
<p style={{ color: 'var(--color-text-muted)' }}>Sign in to start your journal.</p>
|
<p style={{ color: 'var(--color-text-muted)' }}>Sign in to start your journal.</p>
|
||||||
<Link to="/login" className="home-login-link">Go to login</Link>
|
<Link to="/" className="home-login-link">Go to login</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -178,7 +186,7 @@ export default function HomePage() {
|
|||||||
onKeyDown={handleTitleKeyDown}
|
onKeyDown={handleTitleKeyDown}
|
||||||
enterKeyHint="next"
|
enterKeyHint="next"
|
||||||
ref={titleInputRef}
|
ref={titleInputRef}
|
||||||
disabled={phase !== 'idle'}
|
disabled={phase !== 'idle' || isTourActive}
|
||||||
/>
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
id="tour-content-textarea"
|
id="tour-content-textarea"
|
||||||
@@ -188,7 +196,7 @@ export default function HomePage() {
|
|||||||
onChange={(e) => setEntry(e.target.value)}
|
onChange={(e) => setEntry(e.target.value)}
|
||||||
enterKeyHint="enter"
|
enterKeyHint="enter"
|
||||||
ref={contentTextareaRef}
|
ref={contentTextareaRef}
|
||||||
disabled={phase !== 'idle'}
|
disabled={phase !== 'idle' || isTourActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,7 +235,7 @@ export default function HomePage() {
|
|||||||
id="tour-save-btn"
|
id="tour-save-btn"
|
||||||
className="journal-write-btn"
|
className="journal-write-btn"
|
||||||
onClick={handleWrite}
|
onClick={handleWrite}
|
||||||
disabled={phase !== 'idle' || !title.trim() || !entry.trim()}
|
disabled={phase !== 'idle' || isTourActive || !title.trim() || !entry.trim()}
|
||||||
>
|
>
|
||||||
{phase === 'saving' ? 'Saving...' : 'Save Entry'}
|
{phase === 'saving' ? 'Saving...' : 'Save Entry'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) return
|
if (loading) return
|
||||||
if (user) navigate('/', { replace: true })
|
if (user) navigate('/write', { replace: true })
|
||||||
}, [user, loading, navigate])
|
}, [user, loading, navigate])
|
||||||
|
|
||||||
async function handleGoogleSignIn() {
|
async function handleGoogleSignIn() {
|
||||||
|
|||||||
@@ -71,9 +71,8 @@ export default function SettingsPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleSeeTutorial = () => {
|
const handleSeeTutorial = () => {
|
||||||
localStorage.removeItem('gj-onboarding-done')
|
localStorage.setItem('gj-force-tour', 'true')
|
||||||
localStorage.removeItem('gj-tour-pending-step')
|
navigate('/write')
|
||||||
navigate('/')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = mongoUser?.displayName || user?.displayName || 'User'
|
const displayName = mongoUser?.displayName || user?.displayName || 'User'
|
||||||
|
|||||||
Reference in New Issue
Block a user