Category: Website development

  • Building Secure, Scalable Downloads with Cloudflare R2 + Next.js for Our “Free Website” Campaign

    Building Secure, Scalable Downloads with Cloudflare R2 + Next.js for Our “Free Website” Campaign

    By the founder & developer at 3Zero Digital

    We’re launching a new initiative at 3zerodigital: free websites for freelancers and f-commerce owners, a Google Maps scraper (1-year license), plus two free eBooks—Personal Branding and Website Security. To deliver these assets reliably through our Next.js site (3zerosite), we needed file storage that’s private, fast, global, and cost-effective.

    We first considered hosting downloads via headless WooCommerce. It works, but for our use case, Cloudflare R2 was a better fit: S3-compatible APIs, private by default, and zero egress fees. Below is our end-to-end setup—copy-paste ready for teams building secure downloads with Next.js.


    TL;DR Architecture

    • Files (ZIP/PDF/PNG/ZIP, etc.) live in a private Cloudflare R2 bucket.
    • We store only a fileKey (object path, e.g. templates/landing-kit-v1.zip) in our DB—never a public URL.
    • On click, the browser calls our Next.js API route:
      1. Verify session & entitlement (does this user “own” the file?).
      2. Mint a short-lived presigned URL.
      3. 302 redirect the browser to R2 (no proxying through our server).

    Step 1 — Create the R2 Bucket and API Keys

    1. In the Cloudflare Dashboard → R2, create a bucket (e.g., my-prod-templates).
    2. Go to R2 → API Tokens and create an Account API token:
      • App token (read-only): Object Read on your bucket (used by Next.js to presign downloads).
      • Admin token (optional): Object Read & Write (for uploads/migrations).
    3. Note your Account ID. R2’s S3 endpoint looks like:https://<ACCOUNT_ID>.r2.cloudflarestorage.com

    Cloudflare R2 API Tokens

    .env

    # .env.local
    CF_R2_ACCOUNT_ID=xxxxxxxxxxxxxxxxxxxx
    CF_R2_BUCKET=my-prod-templates
    CF_R2_ACCESS_KEY_ID=AKIA... # app (read-only) token
    CF_R2_SECRET_ACCESS_KEY=... # app (read-only) token
    
    # optional (if you keep a separate admin token for uploads)
    CF_R2_ADMIN_ACCESS_KEY_ID=...
    CF_R2_ADMIN_SECRET_ACCESS_KEY=...
    

    Step 2 — Install the SDK and Create an R2 Client

    npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
    // lib/r2.ts
    import { S3Client } from '@aws-sdk/client-s3';
    
    export const r2 = new S3Client({
      region: 'auto',
      endpoint: `https://${process.env.CF_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
      credentials: {
        accessKeyId: process.env.CF_R2_ACCESS_KEY_ID!,
        secretAccessKey: process.env.CF_R2_SECRET_ACCESS_KEY!,
      },
    });
    

    Step 3 — Store fileKey in the Database (Prisma)

    Add a fileKey to your content model (we use Template):

    model Template {
      id          String   @id @default(cuid())
      name        String   @unique
      slug        String
      fileKey     String   // e.g. "templates/landing-kit-v1.zip"
      liveUrl     String
      description String?
      origPrice   Float
      salePrice   Float
      images      String[]
      deleted     Boolean  @default(false)
      createdAt   DateTime @default(now())
      updatedAt   DateTime @updatedAt
      // ...
    }
    

    What is fileKey? It’s the object path inside your R2 bucket—not a URL and not the bucket name. Example: templates/decoupled-architecture.png. You decide it at upload time (dashboard, script, or CLI) and store that string in the DB.


    Step 4 — Entitlement Check (Do They Have Access?)

    We verify that the signed-in user owns the template via a paid or active order. Customize to your business rules.

    // lib/entitlements.ts
    import prisma from '@/../prisma/db';
    
    export async function entitledOrderIdForTemplate(opts: {
      userId: string;
      templateId: string;
    }) {
      const order = await prisma.order.findFirst({
        where: {
          userId: opts.userId,
          TemplateOrderItem: {
            some: { templateId: opts.templateId }
          },
          OR: [
            { paymentStatus: 'PAID' },
            { status: { in: ['CONFIRMED', 'IN_PROGRESS', 'COMPLETED', 'FREE'] } },
          ],
          NOT: {
            status: { in: ['CANCELLED', 'REFUNDED'] }
          },
        },
        select: { id: true },
      });
    
      return order?.id ?? null;
    }
    

    Step 5 — The Secure Download API (Presigned URL → 302 Redirect)

    In Next.js App Router, params is a Promise—make sure to await it. We HEAD first to verify the key and content type, then mint a short-lived URL.

    // app/api/downloads/template/[templateId]/route.ts
    export const runtime = 'nodejs';
    
    import { NextResponse } from 'next/server';
    import { headers } from 'next/headers';
    import { auth } from '@/lib/auth';
    import prisma from '@/../prisma/db';
    import { entitledOrderIdForTemplate } from '@/lib/entitlements';
    import { r2 } from '@/lib/r2';
    import { GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
    import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
    import path from 'node:path';
    
    function safe(s: string) {
      return s.replace(/[^a-z0-9\-_. ]/gi, '_');
    }
    
    export async function GET(
      _req: Request,
      ctx: { params: Promise<{ templateId: string }> }
    ) {
      const { templateId } = await ctx.params;
      const session = await auth.api.getSession({ headers: await headers() });
      const userId = session?.user?.id;
    
      if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    
      const orderId = await entitledOrderIdForTemplate({ userId, templateId });
      if (!orderId) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
    
      const tpl = await prisma.template.findUnique({
        where: { id: templateId },
        select: { name: true, fileKey: true },
      });
    
      if (!tpl?.fileKey) return NextResponse.json({ error: 'Not found' }, { status: 404 });
    
      // HEAD to verify the object & get MIME type
      const head = await r2.send(new HeadObjectCommand({
        Bucket: process.env.CF_R2_BUCKET!,
        Key: tpl.fileKey,
      })).catch(() => null);
    
      if (!head) return NextResponse.json({ error: 'File missing or access denied' }, { status: 404 });
    
      const ext = path.extname(tpl.fileKey) || '';
      const downloadName = (tpl.name ? safe(tpl.name) : path.basename(tpl.fileKey, ext)) + ext;
      const contentType = head.ContentType || 'application/octet-stream';
    
      const cmd = new GetObjectCommand({
        Bucket: process.env.CF_R2_BUCKET!,
        Key: tpl.fileKey,
        ResponseContentDisposition: `attachment; filename="${downloadName}"`,
        ResponseContentType: contentType,
      });
    
      const ttl = Number(process.env.DOWNLOAD_TTL_SECONDS ?? 60); // configurable
      const signed = await getSignedUrl(r2, cmd, { expiresIn: ttl });
    
      const res = NextResponse.redirect(signed, 302);
      res.headers.set('Cache-Control', 'no-store');
      return res;
    }
    

    Note on big files (e.g., 500 MB): This 302 → R2 approach works efficiently. If you see retries failing on slow networks, increase DOWNLOAD_TTL_SECONDS in your environment—no code changes required.


    Step 6 — A Nicer “Download” Button (Spinner UX)

    // components/dashboard/downloads/download-button.tsx
    'use client';
    
    import * as React from 'react';
    import { Button } from '@/components/ui/button';
    import { Loader2, Download as DownloadIcon } from 'lucide-react';
    
    export default function DownloadButton({ href }: { href: string }) {
      const [loading, setLoading] = React.useState(false);
    
      return (
        <Button
          size="sm"
          onClick={(e) => {
            e.preventDefault();
            setLoading(true);
            window.location.href = href; // browser handles the file
            setTimeout(() => setLoading(false), 8000); // best-effort fallback
          }}
          disabled={loading}
          aria-busy={loading}
        >
          {loading ? (
            <>
              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              Preparing…
            </>
          ) : (
            <>
              <DownloadIcon className="mr-2 h-4 w-4" />
              Download
            </>
          )}
        </Button>
      );
    }
    

    Use it in your Downloads page:

    <DownloadButton href={`/api/downloads/template/${templateId}`} />

    3Zero Digital Downloads


    Why We Chose R2 Over Alternatives

    • Simplicity: One small API route; browser downloads directly from R2.
    • Security: Private bucket, no public file URLs, access controlled by our app.
    • Performance: We do not proxy or stream through Next.js—no server timeouts or memory spikes.
    • Cost: Zero egress fees; ideal for our campaign’s freebies and assets.

    R2 Pricing


    SEO Notes for This Post

    • Slug: /blog/cloudflare-r2-nextjs-secure-downloads
    • Meta title: Secure, Private Downloads in Next.js with Cloudflare R2 (Our 3zerodigital Setup)
    • Meta description: How 3zerodigital delivers secure downloads for our free website campaign using Cloudflare R2 and Next.js. Private bucket, presigned URLs, and copy-paste code.
    • Keywords: cloudflare r2 nextjs, s3 compatible r2, presigned url next.js, secure downloads next.js, private file delivery, prisma entitlement
    • Internal links: Link to the campaign landing page and the Downloads page.

    FAQ

    Can I keep WooCommerce in the stack?
    Yes. If you already manage products/orders in Woo, keep it. Use this R2 route purely for file delivery to reduce egress cost and keep URLs private.

    What about very large files?
    The 302 → R2 flow scales. If users on slow networks report retries failing, increase DOWNLOAD_TTL_SECONDS (e.g., 900 for 15 minutes) in your env—no code changes.

    How do we migrate existing public links?
    Upload files to R2, copy the resulting object key (e.g., templates/myfile.zip), save it to Template.fileKey, and update the UI to call the secure API route.


    Wrap-Up

    For our “Free Website for Everyone” campaign, Cloudflare R2 + Next.js gives us a secure, fast, and inexpensive download pipeline. If you need private downloads at scale—without surprise egress fees—this pattern works out of the box.

  • Boost Speed, SEO & Security with Headless WordPress and Next.js

    Boost Speed, SEO & Security with Headless WordPress and Next.js

    TL;DR: Pairing WordPress (as a headless CMS) with Next.js (as your frontend) gives you blistering performance, tighter security, and first-class SEO. You’ll ship faster pages (Core Web Vitals love), keep WordPress safely tucked away, and still enjoy the UI and extensibility you know. This guide shows why the combo works, how to wire it up (REST or GraphQL), and what patterns to use in production—complete with TypeScript examples that match your stack.

    Why go headless with WordPress + Next.js?

    Speed: Pre-render pages and cache API responses; revalidate them on a schedule or on demand. That means static-fast delivery with fresh content when you choose.

    SEO: Next.js ships optimized HTML, stable images, and predictable hydration. When you pull Yoast’s yoast_head_json into your head, you keep all your titles, meta, Open Graph, and schema in sync. Google’s page experience guidance highlights Core Web Vitals as key user-experience signals—fast, stable pages help you win.

    Security: Decoupling reduces your public attack surface. With Jamstack-style hosting (static files at the edge, APIs behind the scenes), there are simply fewer servers to harden.

    Architecture at a glance

    • WordPress = content backend (editors use admin, you expose content via REST or GraphQL)
    • Next.js = React frontend using the App Router, ISR, caching, and revalidation. Deploy to your favorite edge platform
    • SEO = pull yoast_head_json from Yoast’s REST API and render its tags or map to Next’s metadata
    • (Optional) E-commerce = WooCommerce via REST or WPGraphQL for WooCommerce

    Performance wins you can bank on

    Incremental Static Regeneration (ISR)

    Build once, refresh in the background on a timer.

    // Revalidate this page every 60 minutes
    export const revalidate = 3600

    Or at the fetch level:

    await fetch(`${API}/wp-json/wp/v2/posts`, { next: { revalidate: 3600 } })

    Both are first-class in the App Router.

    On-demand / tag revalidation

    Invalidate cached data by tag from a webhook (e.g., when a post publishes).

    // /app/api/revalidate/route.ts
    import { NextResponse } from 'next/server'
    import { revalidateTag } from 'next/cache'
    
    export async function POST(request: Request) {
      const { tag } = await request.json()
      revalidateTag(tag) // e.g. 'posts', 'post:123'
      return NextResponse.json({ revalidated: true, tag })
    }

    Use the plugin WP Webhooks to send a signal whenever something happens in WordPress—for example, when posts, pages, or comments are created, updated, or deleted.

    Image Optimization

    Use <Image> for responsive, lazy-loaded, properly sized images with CLS protection. Configure remote hosts if images live on WordPress.

    Edge-friendly caching

    Frameworks and hosts support persistent caches and global KV (e.g., Cloudflare Pages/Workers KV).

    SEO that actually sticks in a headless build

    Core Web Vitals: LCP/INP/CLS are user-experience signals in Search; improving them improves your chances. Headless + Next.js makes this easier via pre-rendering and image stability.

    Yoast in headless: Use Yoast’s REST API to retrieve all meta + schema for a URL via yoast_head_json. Render those tags directly or map them into generateMetadata. You can toggle the endpoint in Yoast settings.

    Pro tip: Keep canonical URLs consistent with your public domain and ensure your Next.js routes mirror WordPress permalinks to avoid duplicate content.

    Security: shrink your attack surface

    Public site = static assets + edge runtime. Admin/API stay private behind a separate origin, firewall, or VPN. Fewer moving parts exposed → fewer places to harden.

    Keep WordPress core, plugins, and themes updated, and disable unused endpoints. (Yoast’s REST endpoint can be toggled.)

    Two API paths: REST vs GraphQL

    Need Use REST (built-in) Use GraphQL (WPGraphQL)
    “Works out of the box” ✅ WordPress REST API ➖ requires plugin
    Query only what you need ➖ multiple endpoints / shaping ✅ single endpoint, precise queries
    Yoast SEO metadata yoast_head_json ✅ combine WPGraphQL + Yoast REST
    WooCommerce ✅ REST or Woo ✅ WPGraphQL for WooCommerce

    Production patterns (with code that matches your types)

    You shared strong TypeScript models and a WordPressAPI wrapper that enriches posts with authors, media, categories, tags, and yoast_head_json. Here’s how to wire it into the App Router.

    1) Fetch posts with caching + tags

    // lib/wordpress.ts (using your WordPressAPI instance)
    import { wordpress, type WordPressPost } from '@/lib/wordpress'
    
    // Server-friendly helper with cache tags
    export async function getLatestPosts() {
      const res = await wordpress.getPosts({
        perPage: 10,
        orderBy: 'date',
        order: 'desc',
      })
      return res.posts as WordPressPost[]
    }

    With your internal fetches already using next: { revalidate: 3600 }, you can tag downstream fetches by wrapping them or by tagging at the route level and invalidating via revalidateTag('posts').

    2) Render Yoast metadata safely

    // app/blog/[slug]/page.tsx
    import type { Metadata } from 'next'
    import { wordpress } from '@/lib/wordpress'
    
    export async function generateMetadata(
      { params }: { params: { slug: string } }
    ): Promise<Metadata> {
      const post = await wordpress.getPostBySlug(params.slug)
      if (!post?.yoastSEO) return { title: post?.title ?? 'Blog' }
    
      // Minimal mapping from Yoast JSON → Next Metadata
      const y = post.yoastSEO
      return {
        title: y.title ?? post.title,
        description: y.og_description ?? y.twitter_description ?? post.excerpt,
        openGraph: {
          title: y.og_title ?? y.title,
          description: y.og_description,
          url: post.link,
          images: y.og_image?.map((img: any) => ({ url: img.url })) ?? [],
          type: 'article',
        },
        twitter: {
          card: y.twitter_card ?? 'summary_large_image',
          title: y.twitter_title ?? y.title,
          description: y.twitter_description ?? y.og_description,
          images: y.twitter_image ? [y.twitter_image] : undefined,
        },
        alternates: { canonical: y.canonical ?? post.link },
      }
    }
    
    export default async function PostPage({ params }: { params: { slug: string } }) {
      const post = await wordpress.getPostBySlug(params.slug)
      if (!post) return null
    
      return (
        <article>
          <h1>{post.title}</h1>
          {/* Rendered HTML from WP: sanitize/transform as needed */}
          <div dangerouslySetInnerHTML={{ __html: post.content }} />
        </article>
      )
    }

    Yoast’s REST API exposes exactly the SEO metadata you need for headless; mapping to Next’s Metadata keeps things type-safe.

    3) On-demand revalidation from WordPress

    Create a small plugin or webhook in WordPress that hits your POST /api/revalidate endpoint when a post publishes/updates, passing tags like “posts” and “post:{id}”.

    // /app/api/revalidate/route.ts
    import { NextResponse } from 'next/server'
    import { revalidateTag } from 'next/cache'
    
    export async function POST(req: Request) {
      const { tags } = await req.json()
      ;(Array.isArray(tags) ? tags : [tags]).forEach((t) => revalidateTag(t))
      return NextResponse.json({ ok: true, tags })
    }

    This keeps pages fast (static) and fresh (on demand).

    4) Images from WordPress, optimized by Next.js

    import Image from 'next/image'
    
    <Image
      src={post.featuredImage?.sizes.full ?? post.featuredImage?.url ?? ''}
      alt={post.featuredImage?.alt ?? post.title}
      width={post.featuredImage?.width ?? 1200}
      height={post.featuredImage?.height ?? 630}
      priority
    />

    Remember to allow your WordPress domain in next.config.js under images.remotePatterns.

    WooCommerce? You’ve got options

    • REST: stable, widely supported, straightforward
    • GraphQL: install WPGraphQL + WPGraphQL for WooCommerce for a strongly typed schema and fewer over-fetches

    Example: use GraphQL for product lists and carts, REST for webhooks or payment provider callbacks.

    Content workflow niceties

    • Previews: add a /preview route that fetches draft content with a preview token
    • Server Actions: mutate data (e.g., form submissions) without API routes; great for headless forms and gated features
    • Cache strategy: use time-based revalidation for newsy content and tag-based for editorial control

    SEO checklist for headless WordPress

    • Map yoast_head_json → Next metadata (title, description, Open Graph, Twitter, canonical)
    • Optimize images with <Image>; define remote patterns
    • Ship fast HTML (ISR) and keep it fresh (revalidate on schedule or on demand)
    • Monitor Core Web Vitals (Search Console + field tools)

    Security checklist

    • Hide WordPress admin behind a separate origin/WAF; only your Next.js app talks to it. (Decoupling = fewer exposed surfaces.)
    • Keep plugins minimal; disable APIs you don’t need (Yoast endpoint toggle exists)
    • Use HTTPS everywhere; rotate application passwords/JWT secrets; log & rate-limit API access

    Example keyword targets (use naturally)

    Primary: headless wordpress, headless cms, wordpress rest api, wpgraphql, headless wordpress seo, headless wordpress next.js

    Supporting: wordpress api performance, headless wordpress core web vitals, headless woocommerce, decoupled wordpress, jamstack wordpress

    These map to real developer and buyer intent, spanning architecture, SEO, and e-commerce.

    FAQs

    Is headless WordPress good for SEO?

    Yes—if you implement metadata and structured data (e.g., via Yoast’s REST output) and keep pages fast and stable (Core Web Vitals).

    REST or GraphQL—what should I use?

    REST is built-in and pairs perfectly with Yoast metadata. GraphQL (WPGraphQL) is great for shaping responses and minimizing over-fetching, and WooCommerce has a dedicated GraphQL extension.

    How do I keep content fresh without slow SSR?

    Use ISR with time-based (revalidate) or tag-based revalidateTag() from webhooks.

    Can I do e-commerce?

    Yes—WooCommerce works headlessly via REST or WPGraphQL for WooCommerce.

    Further reading

    Copy-paste snippets you can adapt today

    1) Tiny fetch helper with tags (pairs with your WordPressAPI)

    // lib/wp-data.ts
    export async function getPostBySlug(slug: string) {
      const res = await fetch(
        `${process.env.NEXT_PUBLIC_BLOG_API_URL}/wp-json/wp/v2/posts?slug=${slug}&_embed=true`,
        { next: { revalidate: 1800, tags: ['posts', `post:${slug}`] } }
      )
      const [post] = await res.json()
      return post
    }

    (Use revalidateTag('posts') on publish from a WordPress webhook.)

    2) Server Action for CF7 or simple contact forms

    'use server'
    
    export async function submitContact(formData: FormData) {
      // call WordPress endpoint or a serverless function
      // returns success and optionally revalidates a tag
      // revalidateTag('contact-thanks')
    }

    (Server Actions run on the server and remove the need for an API route in many cases.)

    Final thought

    Headless WordPress with Next.js delivers performance that converts, SEO that scales, and security you can sleep on. Start small—migrate your blog or marketing pages—then expand to product catalogs and forms. You’ll feel the lift immediately.

  • A Complete Guide to Next.js Server Actions and WordPress Contact Form 7 Integration for Building Modern Headless Forms

    A Complete Guide to Next.js Server Actions and WordPress Contact Form 7 Integration for Building Modern Headless Forms

    Headless architecture is now the best way to build websites that load quickly, can handle a lot of traffic, and are optimized for search engines. Next.js and WordPress together as a headless CMS give developers the best of both worlds: WordPress’s content management flexibility and the speed benefits of modern frontend frameworks.

    This detailed guide shows how to set up a full headless contact form solution using Next.js 14+ server actions, Contact Form 7’s REST API, and TypeScript. It is based on a tutorial from CSS-Tricks that has been proven to work.

    We’ll go over the details of how to implement this approach, how to optimize it, and the SEO benefits that make it better for modern web apps.

    Why Use Headless Forms Instead of Regular WordPress Forms?

    Performance Benefits That Help SEO

    WordPress forms that are built in the traditional way are very closely linked to the WordPress frontend, which can have a big effect on how well the site works. Headless forms have a number of benefits that directly affect how high your site ranks in search engines:

    • Lightning-Fast Load Times: You can get rid of server-side bottlenecks by separating forms from WordPress’s PHP rendering engine. Next.js’s optimized JavaScript execution and server-side rendering (SSR) can raise Core Web Vitals scores by as much as 40%.
    • Better Mobile Experience: Google’s main ranking factor is mobile-first indexing, so headless forms are great at giving fast, responsive experiences on all devices. The API-first method makes sure that performance is always the same, no matter what size screen or device you have.
    • Better SEO Performance: Headless forms help search engines rank pages higher by making them load faster, improving user experience metrics, and using cleaner HTML markup that search engines can easily read.

    Modern Development Benefits

    • TypeScript Safety: Headless forms with Next.js fully support TypeScript, unlike regular WordPress forms that use PHP and sometimes inconsistent JavaScript. This cuts down on runtime errors and makes developers more productive.
    • Component Reusability: Make form components once and use them in many projects, platforms, or even mobile apps. This method cuts down on development time and maintenance costs by a lot.
    • Advanced State Management: Use React’s state management features with Next.js server actions to handle forms in a more advanced way, such as real-time validation, progressive enhancement, and optimistic updates.

    Understanding Contact Form 7’s REST API

    Contact Form 7, which powers more than 5 million WordPress sites, has a strong REST API that makes headless integration easy. The plugin has endpoints that take care of form submissions, validation, and response management without needing to build a custom backend.

    API Endpoint Structure

    The Contact Form 7 REST API has a predictable pattern:

    https://your-wordpress-site.com/wp-json/contact-form-7/v1/contact-forms/{FORM_ID}/feedback

    Important: Starting with version 5.8 of Contact Form 7, form IDs are hashed to keep them safe. You can still find the numeric ID you need for API calls in the form’s edit URL in the WordPress admin panel, though.

    Required Fields for API Submission

    For submissions to be processed correctly, Contact Form 7’s API needs certain fields:

    • Form field data: The actual fields on your form, like name, email, message, etc.
    • _wpcf7: The form ID
    • _wpcf7_locale: The language locale, like “en_US”
    • _wpcf7_unit_tag: A unique ID for the form
    • _wpcf7_container_post: The ID of the post where the form is embedded (usually “0” for headless)

    Next.js Server Actions: The Modern Way to Handle Forms

    Server actions were added to Next.js 14+, changing the way we handle form submissions. Server actions have a number of benefits over regular API routes:

    Security Benefits

    • Built-in CSRF Protection: Server actions automatically include CSRF tokens, which protect against cross-site request forgery attacks without any extra setup.
    • Server-Side Execution: The logic for processing forms runs only on the server, which keeps sensitive operations and API keys out of client-side code.
    • Input Sanitization: Server actions promote good data validation and sanitization practices, which lowers the risk of security holes.

    Enhanced Developer Experience

    • Simplified Code Structure: You don’t need separate API routes anymore; you can define server actions next to your components or in separate action files.
    • Progressive Enhancement: Forms work even if JavaScript is turned off, which makes them easier to use and works on more devices.
    • Automatic Error Handling: Built-in error boundaries and status management make it easier to handle errors than traditional fetch-based methods.

    Implementation: Building the Complete Headless Form Solution

    The implementation integrates Next.js server actions with Contact Form 7, following the approach demonstrated in the CSS-Tricks tutorial:

    Server Action Implementation

    The server action handles all the backend communication with the Contact Form 7 REST API:

    "use server";
    
    export async function sendContactForm(data: {
      "your-name": string;
      "your-email": string;
      "your-subject": string;
      "your-message": string;
      "honeypot"?: string;
    }) {
      // Server-side honeypot validation - reject if honeypot is filled
      if (data.honeypot && data.honeypot.trim() !== '') {
        return { success: false, message: "Spam detected" };
      }
    
      const formId = process.env.CF7_FORM_ID || '87';
      const siteUrl = process.env.NEXT_PUBLIC_BLOG_API_URL;
    
      if (!formId || !siteUrl) {
        throw new Error("Missing FormId and/or WordPress Site URL");
      }
    
      const url = `${siteUrl}/wp-json/contact-form-7/v1/contact-forms/${formId}/feedback`;
    
      const formData = new FormData();
      formData.append('your-name', data['your-name']);
      formData.append('your-email', data['your-email']);
      formData.append('your-subject', data['your-subject']);
      formData.append('your-message', data['your-message']);
    
      // Required CF7 fields
      formData.append('_wpcf7', formId);
      formData.append('_wpcf7_locale', 'en_US');
      formData.append('_wpcf7_unit_tag', `wpcf7-f${formId}-o1`);
      formData.append('_wpcf7_container_post', '0');
    
      try {
        const response = await fetch(url, {
          method: "POST",
          body: formData,
        });
    
        const result = await response.json();
    
        if (result.status === "mail_sent") {
          return { success: true };
        } else {
          return { success: false, message: result.message };
        }
      } catch (error) {
        return { success: false, message: "There was a server error." };
      }
    }

    Client Component with Enhanced User Experience

    The client-side implementation shows how to use modern React patterns while maintaining accessibility and built-in spam protection through honeypot fields:

    "use client";
    
    import { useState } from "react";
    import Button from '@/components/ui/button';
    import { Input } from '@/components/ui/input';
    import { Textarea } from '@/components/ui/textarea';
    import { Send } from "lucide-react";
    import { submitContactForm } from './action';
    
    const ContactForm = () => {
      const [name, setName] = useState('');
      const [email, setEmail] = useState('');
      const [subject, setSubject] = useState('');
      const [message, setMessage] = useState('');
      const [honeypot, setHoneypot] = useState(''); // Honeypot field
      const [isSubmitting, setIsSubmitting] = useState(false);
      const [success, setSuccess] = useState(false);
      const [error, setError] = useState('');
    
      const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
    
        // Honeypot spam protection
        if (honeypot !== '') return;
    
        setIsSubmitting(true);
        setError('');
        setSuccess(false);
    
        const formData = {
          "your-name": name,
          "your-email": email,
          "your-subject": subject,
          "your-message": message,
          "honeypot": honeypot, // Include honeypot in server action call
        };
    
        const result = await submitContactForm(formData);
    
        setIsSubmitting(false);
    
        if (result.success) {
          setSuccess(true);
          // Clear form fields
          setName('');
          setEmail('');
          setSubject('');
          setMessage('');
        } else {
          setError(result.message || "Submission failed. Please try again.");
        }
      };
    
      return (
        <div className='bg-white/5 backdrop-blur-xl p-8 border border-white/10 rounded-3xl'>
          <form onSubmit={handleSubmit} className='space-y-6'>
            <Input
              placeholder='Your Name'
              className='bg-white/10 border-white/20 text-white placeholder:text-white/50'
              value={name}
              onChange={(e) => setName(e.target.value)}
              required
            />
            <Input
              type='email'
              placeholder='Your Email'
              className='bg-white/10 border-white/20 text-white placeholder:text-white/50'
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
            <Input
              placeholder='Subject'
              className='bg-white/10 border-white/20 text-white placeholder:text-white/50'
              value={subject}
              onChange={(e) => setSubject(e.target.value)}
              required
            />
            <Textarea
              placeholder='Your Message'
              className='bg-white/10 border-white/20 min-h-[150px] text-white placeholder:text-white/50'
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              required
            />
    
            {/* Honeypot field for spam protection */}
            <Input
              type='text'
              name='hp'
              className='hidden'
              value={honeypot}
              onChange={(e) => setHoneypot(e.target.value)}
              tabIndex={-1}
              autoComplete="off"
            />
    
            <Button
              type='submit'
              disabled={isSubmitting}
              className='bg-gradient-to-r from-purple-600 hover:from-purple-700 to-cyan-600 hover:to-cyan-700 w-full'
            >
              {isSubmitting ? (
                "Sending..."
              ) : (
                <>
                  <Send className='mr-2 w-4 h-4' /> Send Message
                </>
              )}
            </Button>
          </form>
    
          {success && (
            <p className='mt-4 text-green-500'>Message sent successfully!</p>
          )}
          {error && <p className='mt-4 text-red-500'>{error}</p>}
        </div>
      );
    };
    
    export default ContactForm;

    Advanced Features and Security Considerations

    Comprehensive Spam Protection Strategy

    The implementation includes several layers of spam protection that are necessary for production environments:

    • Honeypot Fields: The hidden honeypot field catches automated bots that fill out all form fields indiscriminately. This validation is performed on both client and server sides – the client-side check prevents unnecessary API calls, while the server-side validation ensures security even if the client-side code is bypassed. This method is highly effective while being less intrusive than CAPTCHA.
    • Server-Side Validation: Contact Form 7’s built-in validation runs on the server, preventing malicious users from bypassing client-side restrictions.
    • Rate Limiting: Consider implementing rate limiting in production to prevent abuse and protect your WordPress backend from being overwhelmed.

    Error Handling and User Experience Enhancement

    • Graceful Degradation: Forms continue to work even when JavaScript fails to load, thanks to server actions’ progressive enhancement capabilities.
    • Comprehensive Error States: The implementation handles various scenarios including server errors, network failures, validation failures, and WordPress downtime.
    • Clear User Feedback: Immediate, understandable success and error messages keep users informed about their form submission status.

    SEO Optimization for Headless Forms

    • Improved Core Web Vitals: Headless forms contribute to better Largest Contentful Paint (LCP), First Input Delay (FID), and Cumulative Layout Shift (CLS) scores. These user experience metrics are increasingly important in search engine ranking algorithms.
    • Clean Markup Structure: Next.js generates semantic, accessible HTML that search engines can easily crawl and understand. This structured markup improves search engine indexing capabilities.
    • Enhanced Mobile Performance: Mobile-first design principles ensure optimal performance across all devices, aligning with Google’s mobile-first indexing approach.

    Troubleshooting Common Implementation Issues

    Contact Form 7 Configuration Problems

    • Hashed ID Resolution: Since CF7 version 5.8, form IDs are hashed in shortcodes, but the numeric ID required for API calls can still be found in the form’s edit URL.
    • CORS Configuration: Ensure your WordPress site allows cross-origin requests from your Next.js domain. This typically requires server configuration or WordPress plugin setup.
    • API Availability: Verify that the Contact Form 7 REST API is enabled and accessible. Some security plugins or hosting providers may restrict API access.

    Next.js Server Action Considerations

    • FormData Handling: Server actions automatically receive FormData objects, but may require special handling for complex nested data structures.
    • Error Boundary Integration: Implement proper error boundaries to handle server action failures gracefully and provide meaningful user feedback.
    • Environment Variable Management: Ensure sensitive configuration like form IDs and WordPress URLs are properly secured in environment variables.

    Best Practices and Recommendations

    • Input Validation: Always validate and sanitize user input on both client and server sides to prevent security vulnerabilities.
    • Data Protection Compliance: Ensure your implementation complies with GDPR, CCPA, and other applicable data protection regulations.
    • Regular Security Updates: Keep Contact Form 7, WordPress, and Next.js dependencies updated to address security vulnerabilities.

    Conclusion

    The integration of Next.js server actions with Contact Form 7’s REST API represents a modern, scalable approach to web form development. This architecture delivers improved performance, security, and SEO outcomes while maintaining the content management advantages that make WordPress the world’s most popular CMS.

    By implementing headless forms, you’re establishing a foundation for future growth that can adapt to new platforms, technologies, and user expectations. The decoupled architecture ensures your forms remain fast, secure, and maintainable as your business scales.

    The implementation demonstrated provides a production-ready foundation that can be extended with additional features like file uploads, multi-step forms, conditional logic, and advanced validation rules. As the headless CMS ecosystem continues to evolve, this approach positions applications for long-term success in an increasingly competitive digital landscape.

  • Adding Borders to Clipped Shapes in CSS

    Adding Borders to Clipped Shapes in CSS

    Introduction

    CSS clip-path is a powerful tool for creating non-rectangular shapes, like hexagons, triangles, or custom polygons. However, one common frustration is that standard borders (using the border property) don’t play nicely with clipped elements. The border gets clipped along with the content, or it doesn’t conform to the shape at all. This leads to hacks and workarounds.

    In this post, I’ll dive into a clever technique for adding a border to a clipped hexagonal box. I encountered this while styling a hexagon with text inside, and the solution—drawn from insights on Stack Overflow—involves layering shapes to simulate the border. The intuition is straightforward once you grasp it, and it’s reusable for any clipped shape. By the end, you’ll understand why it works and how to adapt it for future projects.

    The Problem: Why Borders and Clip-Path Don’t Mix

    Let’s start with the basics. Suppose you have a div styled as a hexagon using clip-path:

    #hexagonal-box {
        width: 250px;
        height: 100px;
        background-color: #4F5962;
        clip-path: polygon(15% 0%, 88% 0%, 100% 50%, 88% 100%, 15% 100%, 0% 50%);
    }

    This creates a nice hexagon. Now, if you try adding a border:

    #hexagonal-box {
        /* ... */
        border: 5px solid red;
    }

    It doesn’t work as expected. The border is applied to the rectangular bounding box before clipping, so you end up with a clipped rectangle border that doesn’t follow the hexagon’s edges. Even worse, the corners might look jagged or incomplete.

    Clip-path essentially “cuts” the element after borders and backgrounds are rendered, so borders can’t hug the irregular shape. This is a limitation of CSS—borders are rectangular by nature.

    The Intuition: Layering for a Faux Border

    The key insight is to treat the border as a separate, larger shape layered underneath the main content. Instead of relying on the border property, we create an illusion:

    1. Create the “border” layer: Make a slightly larger version of the shape (e.g., the hexagon plus border width) and color it with your border color (e.g., red).
    2. Overlay the content layer: Place the original-sized shape on top, colored with the background (e.g., #4F5962), effectively “covering” the inner part of the larger shape. This leaves only the outer rim visible as the border.
    3. Position precisely: Adjust dimensions, positions, and clip-path percentages to ensure the overlay aligns perfectly, creating a uniform border width.

    This mimics how borders work on rectangles but adapts it to polygons. It’s like drawing a bigger outline and filling the inside with the content color.

    Why does this work?

    • Clip-path allows precise control over polygon points, so scaling the shape uniformly expands it like a border.
    • Using absolute positioning (e.g., via ::before or a child element) keeps everything contained.
    • It’s performant and doesn’t rely on SVGs or images, staying pure CSS.

    This technique scales to other shapes too—triangles, stars, etc.—as long as you can define a larger clip-path version.

    Code Breakdown: Implementing the Hexagonal Border

    Here’s the full working example. We’ll use the ::before pseudo-element for the content layer (inner hexagon) and the main element for the border layer (outer hexagon).

    HTML Structure

    Keep it simple—just a div with an h2 for centered text:

    <div id="hexagonal-box">
        <h2>Increase Profit and Reduce Costs</h2>
    </div>

    CSS Implementation

    #hexagonal-box {
        position: relative;
        width: 262px; /* Original width + 2 * border width (250 + 2*6) */
        height: 110px; /* Original height + 2 * border width (100 + 2*5) */
        background-color: red; /* Border color */
        margin: 57.74px 0; /* Adjust for pseudo-elements if needed */
        clip-path: polygon(15.74% 0%, 87.41% 0%, 100% 50%, 87.41% 100%, 15.74% 100%, 0% 50%);
        text-align: center;
        color: #333;
        border-radius: 15px; /* Optional: Smooth corners */
        overflow: hidden;
    }
    
    #hexagonal-box::before {
        content: "";
        position: absolute;
        z-index: 1;
        left: 6px; /* Half border width, adjusted for alignment */
        top: 5px; /* Border width */
        width: 250px; /* Original width */
        height: 100px; /* Original height */
        background-color: #4F5962; /* Inner background color */
        clip-path: polygon(15% 0%, 88% 0%, 100% 50%, 88% 100%, 15% 100%, 0% 50%);
        border-radius: 15px; /* Match smoothing */
    }
    
    #hexagonal-box h2 {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        margin: 0;
        font-size: 1.2em;
        z-index: 2; /* Ensure text is on top */
    }
    
    /* Optional: Additional pseudo-elements for triangles or effects */
    #hexagonal-box::after {
        content: "";
        position: absolute;
        width: 0;
        border-left: 100px solid transparent;
        border-right: 100px solid transparent;
        top: 100%;
        border-top: 57.74px solid #ADD8E6; /* Example for a bottom triangle */
        border-radius: 15px;
    }

    Key Adjustments Explained

    • Dimensions: For a 5px border, increase width by ~12px (5px * 2 sides, but tuned to 262px for precision) and height by 10px. Test visually, as polygons aren’t perfectly uniform.
    • Clip-path Percentages: The outer hexagon uses slightly adjusted percentages (e.g., 15.74% instead of 15%) to expand proportionally. Calculate these by factoring in the border width relative to the size.
    • Positioning: The inner layer (::before) is offset by the border width (left: 6px, top: 5px) to center it. Use z-index to stack correctly.
    • Overflow and Radius: overflow: hidden and border-radius help with any clipping artifacts or smoothing.

    This code is adapted from a Stack Overflow discussion (link in references), where the community highlighted this layering approach as a reliable workaround.

    Potential Enhancements and Adaptations

    • Variable Border Width: Make widths dynamic with CSS variables (e.g., --border-width: 5px;) and calc() for sizes.
    • Other Shapes: For a triangle, define a larger polygon and overlay the smaller one. The math is similar—scale points outward.
    • Performance Tip: Avoid overusing pseudo-elements; for complex pages, consider a wrapper div instead.
    • Browser Compatibility: Clip-path works in modern browsers, but test in Edge/IE if needed (polyfills exist).

    If you need a dynamic border color or animations, this method extends well—animate the background colors or scales.

    Conclusion

    The intuition behind bordering clipped shapes is all about optical illusion through layering: a big colored shape minus a smaller inner one equals a border. It’s elegant, CSS-native, and versatile. Next time you hit a clip-path border snag, refer back here—tweak the sizes, adjust the polygons, and layer away. Happy styling!

    References

    Posted on July 27, 2025