diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4442363..4dfdca2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,8 +55,17 @@ jobs: command: pages project create soumyo-com --production-branch=main - name: Deploy to Cloudflare Pages + id: deploy uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy dist --project-name=soumyo-com --commit-dirty=true + + - name: Output Deployment URL + run: | + echo "### Deployment Complete!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Production URL:** https://soumyo.com" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Cloudflare URL:** ${{ steps.deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..20acad3 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,87 @@ +name: Preview Deployment + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: "preview-${{ github.head_ref }}" + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + preview: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.1.42" + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: bun install + + - name: Build + run: bun run build + + - name: Deploy Preview + id: deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy dist --project-name=soumyo-com --branch=${{ github.head_ref }} + + - name: Comment Preview URL + uses: actions/github-script@v7 + with: + script: | + const output = `${{ steps.deploy.outputs.deployment-url }}`; + const body = `## Preview Deployment Ready!\n\n**Preview URL:** ${output}\n\n*This preview will be automatically updated when you push new commits.*`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(c => c.body.includes('Preview Deployment Ready')); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } diff --git a/scripts/generate-headers.js b/scripts/generate-headers.js index 9ea7741..edea0eb 100644 --- a/scripts/generate-headers.js +++ b/scripts/generate-headers.js @@ -15,55 +15,43 @@ const __dirname = path.dirname(__filename); const distDir = path.join(__dirname, '../dist'); // Create the _headers file content -const headersContent = `# Security headers -/* - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - X-XSS-Protection: 1; mode=block - Referrer-Policy: strict-origin-when-cross-origin - Permissions-Policy: camera=(), microphone=(), geolocation=() - -# Cache static assets -/assets/* - Cache-Control: public, max-age=31536000, immutable - -# Cache hashed JS bundles -/chunks/*.js - Cache-Control: public, max-age=31536000, immutable -/entry.*.js - Cache-Control: public, max-age=31536000, immutable - -# Specific files flagged in PageSpeed Insights +// Order: Most specific paths first, global rules last +const headersContent = `# Font files - specific file first, then directory /fonts/inter-var.woff2 Cache-Control: public, max-age=31536000, immutable Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET Access-Control-Allow-Headers: Content-Type -# CORS headers for all font files /fonts/* + Cache-Control: public, max-age=31536000, immutable Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET Access-Control-Allow-Headers: Content-Type -# Cache fonts +# Hashed assets (immutable - Astro outputs to /_astro/) +/_astro/* + Cache-Control: public, max-age=31536000, immutable + +/assets/* + Cache-Control: public, max-age=31536000, immutable + +# Legacy hashed JS bundles +/chunks/*.js + Cache-Control: public, max-age=31536000, immutable + +# Font files at root level /*.woff2 Cache-Control: public, max-age=31536000, immutable Access-Control-Allow-Origin: * - Access-Control-Allow-Methods: GET - Access-Control-Allow-Headers: Content-Type /*.woff Cache-Control: public, max-age=31536000, immutable Access-Control-Allow-Origin: * - Access-Control-Allow-Methods: GET - Access-Control-Allow-Headers: Content-Type /*.ttf Cache-Control: public, max-age=31536000, immutable Access-Control-Allow-Origin: * - Access-Control-Allow-Methods: GET - Access-Control-Allow-Headers: Content-Type -# Cache images +# Images - 30 day cache /*.jpg Cache-Control: public, max-age=2592000 /*.jpeg @@ -79,20 +67,22 @@ const headersContent = `# Security headers /*.ico Cache-Control: public, max-age=2592000 -# Cache JS and CSS -/*.js - Cache-Control: public, max-age=2592000, must-revalidate -/*.css - Cache-Control: public, max-age=2592000, must-revalidate - -# HTML - short cache time +# HTML pages - no cache for fresh content /*.html Cache-Control: public, max-age=0, must-revalidate + / Cache-Control: public, max-age=0, must-revalidate -# Everything else +# Global rules (security headers + default cache) /* + X-Content-Type-Options: nosniff + X-Frame-Options: DENY + X-XSS-Protection: 1; mode=block + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: camera=(), microphone=(), geolocation=() + Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://us.i.posthog.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://us.i.posthog.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self' + Strict-Transport-Security: max-age=31536000; includeSubDomains Cache-Control: public, max-age=3600 `;