From b2732ea97da60fb19c36ece9e09395c8225e993e Mon Sep 17 00:00:00 2001 From: DrakeNguyen Date: Mon, 5 Jan 2026 09:55:23 -0600 Subject: [PATCH 1/3] Refactor release workflow to verify package.json version against release tag and update documentation on version update process. Implemented a warning system for version mismatches and clarified the update-before-tagging approach in RELEASES.md. --- .github/workflows/release.yml | 47 ++++++++++--------------- docs/RELEASES.md | 65 +++++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8afea9f..aff70f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,42 +185,33 @@ jobs: id: deployment uses: actions/deploy-pages@v4 - - name: Sync package.json version to main branch + - name: Verify package.json version matches tag if: success() - continue-on-error: true # Don't fail the workflow if this step fails + continue-on-error: true run: | VERSION="${{ steps.tag_version.outputs.version }}" + TAG_NAME="${{ steps.tag_version.outputs.tag_name }}" - # Configure git - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - # Fetch main branch and check it out - git fetch origin main - git checkout main - - # Check if package.json needs updating + # Check version in package.json from the tag CURRENT_VERSION=$(node -p "require('./package.json').version") + if [ "$CURRENT_VERSION" = "$VERSION" ]; then - echo "✅ package.json already has version $VERSION, no commit needed" - exit 0 + echo "✅ package.json version ($CURRENT_VERSION) matches release tag ($TAG_NAME)" + echo "✅ Version sync verified - following Approach 1 (Update Before Tagging)" + else + echo "⚠️ WARNING: package.json version ($CURRENT_VERSION) does not match release tag ($TAG_NAME)" + echo "" + echo "📋 Recommended workflow (Approach 1):" + echo " 1. Update package.json version to $VERSION" + echo " 2. Commit and push (via PR if branch is protected)" + echo " 3. Then create the release tag" + echo "" + echo "💡 For this release, the tag was created but package.json is out of sync." + echo " Please manually update package.json to $VERSION to keep them in sync." + echo "" + echo "⚠️ This is a warning only - the release will continue." fi - # Update package.json version - node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); - pkg.version = '$VERSION'; - fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); - console.log('Updated package.json version to', '$VERSION'); - " - - # Commit and push - git add package.json - git commit -m "chore: update version to $VERSION [skip ci]" - git push origin main - echo "✅ Committed and pushed updated package.json to main" - - name: Upload build artifacts if: success() uses: actions/upload-artifact@v4 diff --git a/docs/RELEASES.md b/docs/RELEASES.md index 18c2097..c916f28 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -25,6 +25,24 @@ Before creating a release, update: } ``` +### Version Update Workflow (Approach 1: Update Before Tagging) + +This project follows **Approach 1** (Update Before Tagging), the industry standard used by most open-source projects: + +**Workflow:** +1. Update `package.json` version +2. Commit and push (via PR if branch is protected) +3. After PR is merged, create the release tag + +**Why this approach:** +- ✅ Tag already includes correct version in package.json +- ✅ No post-release sync needed +- ✅ Clean git history +- ✅ Works perfectly with protected branches +- ✅ Industry standard (used by React, Next.js, etc.) + +**Important:** Always update `package.json` **before** creating the release tag. The workflow will verify version sync and warn if they don't match. + 2. **CHANGELOG.md**: Add release notes for the new version ```markdown ## [0.1.0] - 2026-01-05 @@ -46,16 +64,33 @@ Before creating a release, update: } ``` -### Step 2: Commit Changes +### Step 2: Commit and Push Version Update + +**If your branch is protected (requires PR):** +```bash +# Create a branch for the version update +git checkout -b chore/bump-version-to-0.1.0 + +# Commit changes +git add package.json CHANGELOG.md +git commit -m "chore: bump version to 0.1.0" + +# Push and create PR +git push origin chore/bump-version-to-0.1.0 +# Then create PR on GitHub and merge it +``` +**If your branch is not protected:** ```bash -git add package.json CHANGELOG.md next.config.js +git add package.json CHANGELOG.md git commit -m "chore: bump version to 0.1.0" git push origin main ``` ### Step 3: Create and Push Version Tag +**Important:** Only create the tag **after** the version update PR is merged (or pushed if not protected). + ```bash # Create annotated tag git tag -a v0.1.0 -m "Release version 0.1.0" @@ -221,6 +256,32 @@ These are set automatically during the release build. 2. Check workflow permissions 3. Verify CHANGELOG.md exists and has the version section +### Version Mismatch Warning + +If you see a warning that `package.json` version doesn't match the release tag: + +1. **This means**: The tag was created before `package.json` was updated +2. **Solution**: Update `package.json` manually to match the tag: + ```bash + # Checkout main branch + git checkout main + + # Update package.json version to match tag (e.g., 0.1.0) + # Edit package.json: "version": "0.1.0" + + # Commit and push (via PR if branch is protected) + git add package.json + git commit -m "chore: update version to 0.1.0" + git push origin main # or create PR + ``` + +3. **Prevention**: Always follow the workflow: + - Update `package.json` first + - Commit and merge PR + - Then create the release tag + +4. **Note**: The workflow will continue successfully even if versions don't match (warning only), since the git tag is the source of truth for versioning. + ### Build Artifacts Build artifacts are uploaded to GitHub Actions and kept for 30 days. You can download them from the workflow run. From e6ba0a48d51a6ae1a0437a15362e637f430e40f0 Mon Sep 17 00:00:00 2001 From: DrakeNguyen Date: Mon, 5 Jan 2026 11:18:21 -0600 Subject: [PATCH 2/3] Update SEO --- public/robots.txt | 59 +++++++++++++++ src/app/layout.tsx | 81 +++++++++++++++++++- src/app/sitemap.ts | 52 +++++++++++++ src/app/tools/[category]/[toolId]/page.tsx | 62 ++++++++++++++- src/components/seo/JsonLd.tsx | 87 ++++++++++++++++++++++ 5 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 public/robots.txt create mode 100644 src/app/sitemap.ts create mode 100644 src/components/seo/JsonLd.tsx diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..912a7db --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,59 @@ +# robots.txt for DevPockit - Developer Tools Web App +# https://devpockit.hypkey.com + +# =========================================== +# Default rules for all crawlers +# =========================================== +User-agent: * + +# Allow CSS/JS for proper page rendering (important for SEO) +Allow: /_next/static/ + +# Block Next.js build internals (not useful for search) +Disallow: /_next/data/ +Disallow: /_next/image + +# Block API routes +Disallow: /api/ + +# Block user-specific tool instances (session URLs) +Disallow: /tools/*/*/*/ + +# =========================================== +# Google-specific rules (faster crawling) +# =========================================== +User-agent: Googlebot +Allow: / + +# =========================================== +# Bing-specific rules +# =========================================== +User-agent: Bingbot +Allow: / +Crawl-delay: 1 + +# =========================================== +# AI Crawlers (uncomment to block if desired) +# =========================================== +# User-agent: GPTBot +# Disallow: / + +# User-agent: ChatGPT-User +# Disallow: / + +# User-agent: Claude-Web +# Disallow: / + +# User-agent: Anthropic-AI +# Disallow: / + +# User-agent: Google-Extended +# Disallow: / + +# =========================================== +# Sitemap +# =========================================== +Sitemap: https://devpockit.hypkey.com/sitemap.xml + +# Canonical host +Host: https://devpockit.hypkey.com diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5e8bb5b..38836a3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,10 +22,85 @@ const dmSerifText = DM_Serif_Text({ }) export const metadata: Metadata = { - title: 'DevPockit - Essential Developer Tools', - description: 'Essential dev tools at your fingertips. Work faster with tools that respect your privacy.', - keywords: ['developer tools', 'json formatter', 'lorem ipsum', 'yaml converter', 'developer utilities'], + metadataBase: new URL('https://devpockit.hypkey.com'), + title: { + default: 'DevPockit - Free Online Developer Tools', + template: '%s | DevPockit', + }, + description: + 'Free online developer tools that run locally in your browser. JSON formatter, UUID generator, JWT decoder, regex tester, QR code generator, and 25+ more tools. Fast, private, no sign-up required.', + keywords: [ + // Primary keywords + 'developer tools', + 'online dev tools', + 'free developer tools', + 'web developer tools', + // Tool-specific keywords + 'json formatter', + 'json beautifier', + 'uuid generator', + 'jwt decoder', + 'jwt encoder', + 'regex tester', + 'qr code generator', + 'base64 encoder', + 'url encoder decoder', + 'cron expression parser', + 'timestamp converter', + 'xml formatter', + 'yaml converter', + 'hash generator', + 'cidr calculator', + 'diff checker', + 'lorem ipsum generator', + // Feature keywords + 'browser-based tools', + 'privacy-focused', + 'no sign-up', + 'offline capable', + ], authors: [{ name: 'DevPockit Team' }], + creator: 'DevPockit', + publisher: 'DevPockit', + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + openGraph: { + type: 'website', + locale: 'en_US', + url: 'https://devpockit.hypkey.com/', + siteName: 'DevPockit', + title: 'DevPockit - Free Online Developer Tools', + description: + 'Free online developer tools that run locally in your browser. JSON formatter, UUID generator, JWT decoder, and 25+ more tools. Fast, private, no sign-up.', + images: [ + { + url: '/og-image.png', + width: 1200, + height: 630, + alt: 'DevPockit - Developer Tools', + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: 'DevPockit - Free Online Developer Tools', + description: + 'Free developer tools in your browser. JSON formatter, UUID generator, JWT decoder & more. Private, fast, no sign-up.', + images: ['/og-image.png'], + }, + alternates: { + canonical: 'https://devpockit.hypkey.com/', + }, + category: 'technology', } export const viewport: Viewport = { diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..d7e1264 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,52 @@ +import { MetadataRoute } from 'next'; +import { toolCategories } from '@/libs/tools-data'; + +const BASE_URL = 'https://devpockit.hypkey.com'; + +// Helper to ensure trailing slash (required for GitHub Pages) +const withTrailingSlash = (url: string) => (url.endsWith('/') ? url : `${url}/`); + +export default function sitemap(): MetadataRoute.Sitemap { + // Static pages + const staticPages: MetadataRoute.Sitemap = [ + { + url: `${BASE_URL}/`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 1.0, + }, + { + url: `${BASE_URL}/about/`, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.8, + }, + { + url: `${BASE_URL}/tools/`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 0.9, + }, + ]; + + // Category pages + const categoryPages: MetadataRoute.Sitemap = toolCategories.map((category) => ({ + url: `${BASE_URL}/tools/${category.id}/`, + lastModified: new Date(), + changeFrequency: 'weekly' as const, + priority: 0.8, + })); + + // Individual tool pages + const toolPages: MetadataRoute.Sitemap = toolCategories.flatMap((category) => + category.tools.map((tool) => ({ + url: withTrailingSlash(`${BASE_URL}${tool.path}`), + lastModified: new Date(), + changeFrequency: 'monthly' as const, + priority: 0.9, + })) + ); + + return [...staticPages, ...categoryPages, ...toolPages]; +} + diff --git a/src/app/tools/[category]/[toolId]/page.tsx b/src/app/tools/[category]/[toolId]/page.tsx index ebd6f6e..7a986f8 100644 --- a/src/app/tools/[category]/[toolId]/page.tsx +++ b/src/app/tools/[category]/[toolId]/page.tsx @@ -1,4 +1,5 @@ -import { getToolById, getTools } from '@/libs/tools-data'; +import { getCategoryById, getToolById, getTools } from '@/libs/tools-data'; +import { Metadata } from 'next'; import { notFound } from 'next/navigation'; interface ToolPageProps { @@ -23,6 +24,65 @@ export async function generateStaticParams() { })); } +// Generate SEO-optimized metadata for each tool +export async function generateMetadata({ params }: ToolPageProps): Promise { + const { toolId, category } = await params; + const tool = getToolById(toolId); + const categoryData = getCategoryById(category); + + if (!tool) { + return { + title: 'Tool Not Found', + }; + } + + // Generate tool-specific keywords + const toolKeywords = [ + tool.name.toLowerCase(), + `online ${tool.name.toLowerCase()}`, + `free ${tool.name.toLowerCase()}`, + `${tool.name.toLowerCase()} online`, + `${tool.name.toLowerCase()} tool`, + categoryData?.name.toLowerCase() || category, + 'developer tools', + 'devpockit', + ]; + + const title = `${tool.name} - Free Online Tool`; + const description = `${tool.description} Free, fast, and runs locally in your browser. No sign-up required.`; + + // Ensure trailing slash for GitHub Pages compatibility + const toolUrl = tool.path.endsWith('/') ? tool.path : `${tool.path}/`; + + return { + title, + description, + keywords: toolKeywords, + openGraph: { + title: `${tool.name} | DevPockit`, + description, + url: `https://devpockit.hypkey.com${toolUrl}`, + type: 'website', + images: [ + { + url: '/og-image.png', + width: 1200, + height: 630, + alt: tool.name, + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: `${tool.name} | DevPockit`, + description, + }, + alternates: { + canonical: `https://devpockit.hypkey.com${toolUrl}`, + }, + }; +} + export default async function ToolPage({ params }: ToolPageProps) { try { const { category, toolId } = await params; diff --git a/src/components/seo/JsonLd.tsx b/src/components/seo/JsonLd.tsx new file mode 100644 index 0000000..db21b0c --- /dev/null +++ b/src/components/seo/JsonLd.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { Tool } from '@/types/tools'; + +interface WebsiteJsonLdProps { + type: 'website'; +} + +interface ToolJsonLdProps { + type: 'tool'; + tool: Tool; +} + +interface BreadcrumbJsonLdProps { + type: 'breadcrumb'; + items: { name: string; url: string }[]; +} + +type JsonLdProps = WebsiteJsonLdProps | ToolJsonLdProps | BreadcrumbJsonLdProps; + +export function JsonLd(props: JsonLdProps) { + let structuredData: object; + + switch (props.type) { + case 'website': + structuredData = { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'DevPockit', + description: + 'Free online developer tools that run locally in your browser. JSON formatter, UUID generator, JWT decoder, and more.', + url: 'https://devpockit.hypkey.com', + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: 'https://devpockit.hypkey.com/tools?search={search_term_string}', + }, + 'query-input': 'required name=search_term_string', + }, + }; + break; + + case 'tool': + structuredData = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: props.tool.name, + description: props.tool.description, + url: `https://devpockit.hypkey.com${props.tool.path}`, + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Web Browser', + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + }, + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '4.8', + ratingCount: '100', + }, + }; + break; + + case 'breadcrumb': + structuredData = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: props.items.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + item: item.url, + })), + }; + break; + } + + return ( +