F2E, Full-Stack To Indie Hacker

Using Stitch, Next.js and OpenNext to Build a Personal Homepage

Actually, an even earlier version of my personal homepage looked like the one below. At the time, it was mostly a toy for me to experiment with AI programming when I first started, and it basically had no functionality.

The new version of the personal homepage is currently deployed and online, and can be accessed directly at daolanx.com.

Compared to before, my ideas are more mature now. Regardless of its form, a personal homepage needs to focus on a few key points: 1. Service-oriented: It is not a toy, nor an artwork, but a medium for personal promotion; 2. High performance; 3. SEO-friendly. The new version has been enhanced in all these aspects.

1. UI Design: Using Stitch to Design and Export Code

I still used Stitch to complete the product design. If no designers are involved, I recommend exporting the zip directly, which will generate HTML based on the design. Compared to generating design software formats and then exporting, directly converting HTML into the target tech stack webpage is actually more accurate and easier.

2.Information Aggregation: Server Components Calling APIs to Aggregate Personal Portfolios and Blogs

The new version’s portfolios come from the demo.daolanx.com API, and the blogs come from the daolanx.me API. By calling interfaces through Server Components and wrapping the outer layer with Suspense, it achieves better access speeds and SEO performance.

<main className="pt-20">
  ...
  <Suspense fallback={<PortfoliosSkeleton />}>
    <Portfolios />
  </Suspense>
  <Suspense fallback={<ArticlesSkeleton />}>
    <Articles />
  </Suspense>
  ...
</main>

3. Performance Optimization: Lighthouse from 70+ to 90+

3.1 Before and After Optimization Comparison

Mediocre performance before optimization

The code generated by AI could basically reach a runnable state, but the performance score was around 70+.

90+ after optimization

To achieve higher performance, a series of adjustments needed to be made. You can refer to Lighthouse , which provides a lot of useful advice.

3.2 Loading Performance Optimization (Loading)

Goal: Shorten the fetch and render time of critical resources, prioritizing the loading speed of above-the-fold content.

  • Eliminated LCP detection delays caused by animations: Removed the opacity: 0 (FadeIn) fade-in animation of elements in the Hero section. Previously, this hidden effect would passively delay Lighthouse’s LCP timer.
  • Preloading core images: Added the priority attribute to the Portfolio collection images above-the-fold, forcing the browser to prioritize establishing data channels and preloading.
  • Precise image distribution: Re-optimized deviceSizes and imageSizes configurations to ensure that different devices can get perfectly adapted images for their resolutions, avoiding bandwidth waste.
  • Native image component upgrade: Replaced all native <img> tags with next/image, combined with fill + sizes attributes to achieve responsive loading while locking the image placeholder to prevent page layout shifts or jitters.

3.3 Rendering Performance Optimization (Rendering)

Goal: Eliminate render-blocking resources, reduce main thread burden, and ensure the timeliness of First Paint (FP).

  • Lazy loading non-critical components: Adopted next/dynamic (ssr: false) combined with requestIdleCallback to defer the loading of ParticleCanvas until the browser is completely idle.
  • DOM structure position adjustment: Moved ParticleCanvas to the very bottom of the page’s HTML structure, completely removing its blocking effect on First Paint.
  • Skipping particle canvas on mobile: Directly skipped the loading and rendering of ParticleCanvas by detecting via isDesktop(), avoiding unnecessary calculations and drawing on lower-performance mobile devices.
  • Avoiding forced synchronous layouts (Reflow): eplaced the reading of window.innerWidth in breakpoint.ts with the matchMedia API to avoid triggering forced synchronous layouts; additionally, cached size variables in the canvas’s animation loop to prevent frequent DOM reads from causing browser reflows.

3.4 Visual Stability Optimization (Visual Stability)

Goal: Eliminate Cumulative Layout Shift (CLS) and resolve flickering and jumping issues caused by font loading.

  • Tailwind theme standardization: Unified the calling of CSS variables from next/font in the Tailwind configuration to control the font family, ensuring efficient and consistent rendering.

3.5 SEO Optimization (Search Engine Optimization)

Goal: Ensure search engines correctly index multilingual pages to improve discoverability and ranking performance in international markets.

  • Dynamic Canonical URLs: eplaced the hardcoded / with dynamic paths based on the current language (e.g., /en, /zh), ensuring each language version has an independent canonical address to prevent search engines from treating multilingual pages as duplicate content.
  • hreflang multilingual tags: Generated <link rel="alternate" hreflang="..."> tags for each language version through the alternates.languages configuration to inform search engines of the relationship between language versions, ensuring users see the correct language version for their region in search results.

3.6 Accessibility

Goal: Guarantee a complete access experience for users with low vision and users of assistive technologies.

  • Allowing user zooming: Removed maximumScale: 1 from the viewport configuration to restore the browser’s native zoom capabilities, providing necessary magnification support for users with low vision.
  • Semantic Logo links: Added aria-label="Dax - Home" to the site’s Logo link so screen reader users can clearly identify the purpose of this link when navigating.
  • Accessible labeling for project links: Added independent aria-labels to the “Live Demo” and “View Source” links for each project, ensuring that the specific project the link points to is accurately conveyed even when read out of context.

4. Deployment

4.1 Why OpenNext

The deployment platform officially recommended by Next.js is Vercel, but for personal projects, Vercel’s free tier and pricing aren’t always the optimal solution. Cloudflare Workers offers a global edge network, a generous free tier, and extremely low cold start times, making it a great alternative.

The problem is: Next.js relies on the Node.js runtime, while Cloudflare Workers runs on the V8 engine. There are fundamental compatibility differences between the two.

OpenNext was created to solve this very problem. It is an open-source adapter capable of converting Next.js build artifacts into a format compatible with Cloudflare Workers, allowing your Next.js application to be deployed directly to Cloudflare’s edge network.

OpenNext provides an official Getting Started Guide that covers the basic flow of installation, configuration, and deployment. This article won’t repeat those details, but rather share the details to note and the pitfalls encountered when using OpenNext in an actual project.

4.2 Package Configuration

Dependencies

Install two core dependencies:

{
  "dependencies": {
    "@opennextjs/cloudflare": "1.14.0",
    "next": "16.0.7"
  },
  "devDependencies": {
    "wrangler": "4.56.0"
  }
}
  • @opennextjs/cloudflare: The core adapter responsible for converting Next.js build artifacts into the Cloudflare Worker format.
  • next: The Next.js framework itself.
  • wrangler: Cloudflare Workers CLI, used for deployment (version must be ≥ 3.99.0).

Script Configuration

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
    "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
    "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
  }
}

A few key scripts explained:

  • pnpm dev: Local development server using Next.js dev mode with fast hot reload. The initOpenNextCloudflareForDev() in next.config.ts bridges Cloudflare bindings.
  • pnpm build: Standard Next.js build that generates the .next/ directory. This is a prerequisite for preview and deploy.
  • pnpm preview: Runs opennextjs-cloudflare build to generate Worker-compatible output, then simulates the Cloudflare Worker environment locally. Use this to verify compatibility before shipping.
  • pnpm deploy: Builds first, then deploys to Cloudflare Workers. For production use.
  • pnpm cf-typegen: Generates TypeScript type definitions from wrangler.jsonc into env.d.ts, ensuring type hints for Cloudflare bindings.

4.3 Local Dev vs Preview: Two Modes

OpenNext provides two local runtime modes:

ModeCommandPurpose
Dev modepnpm devDaily development, fast hot reload
Preview modepnpm previewPre-release verification, simulating the real Worker environment

Recommendation: Use pnpm dev for daily development, and use pnpm preview to verify before publishing. The preview mode genuinely simulates Cloudflare’s runtime environment and can uncover issues that wouldn’t appear in development mode.

4.4 Corresponding Build and Deployment Configurations on the Cloudflare Platform Dashboard

  • Build: npx opennextjs-cloudflare build
    Dedicated to the Cloudflare Dashboard CI pipeline. Internally, OpenNext will automatically run next build first to generate the .next/ artifact, and then convert it into a format compatible with Cloudflare Workers. Once the Build is complete, the platform automatically takes over the deployment process, with no manual trigger required for upload.
  • Deploy: npm run deploy
    Used for local manual deployment. Equivalent to executing opennextjs-cloudflare build && opennextjs-cloudflare deploy, which completes the build conversion and uses wrangler to push to Cloudflare Workers sequentially in a single command, ideal for developers verifying releases on their local machines.

4.5 Image Optimization: Custom Loader is Required

Cloudflare Workers cannot run Sharp (Next.js’s default image optimization library), so you need to provide a custom image loader:

// src/lib/image-loader.ts
export default function imageLoader({
  src,
  width,
  quality,
}: {
  src: string
  width: number
  quality?: number
}) {
  const params = new URLSearchParams()
  params.set("w", String(width))
  if (quality) params.set("q", String(quality))
  return `${src}?${params.toString()}`
}

Then configure it in next.config.ts:

images: {
  loader: "custom",
  loaderFile: "./src/lib/image-loader.ts",
}

Best Practices for Production Environments:

Host static assets on Cloudflare R2 storage buckets and bind a custom domain (like assets.yourdomain.com), pointing the image URL directly to R2’s public access endpoint. R2 is on the same network as the Cloudflare CDN, so image requests are responded to directly from edge nodes without needing to return to the origin, inherently possessing global low-latency distribution capabilities. At the same time, R2 waives egress traffic fees, making it suitable for high-frequency image loading scenarios.

The final src path looks like this:

https://assets.yourdomain.com/portfolio/project-01.webp?w=1200&q=80

R2 is responsible for storage and distribution, the custom Loader is responsible for concatenating size and quality parameters, and the Next.js Image component handles responsive sizes and placeholder layouts. Each of the three performs its own duties, fully replacing Sharp’s runtime image processing capabilities.

5. References