Next.js on Hostinger with GitHub, Cloudflare & AI Search

Rantideb Howlader•14 min read•

Introduction

I deliberately chose not to use Vercel for this portfolio. Vercel is excellent. However, free tier limits become an issue quickly for personal projects.

Instead, I built a custom deployment pipeline. It combines three tools:

  • Hostinger: Node.js runtime and hosting.
  • Cloudflare: DNS, CDN, and DDoS protection.
  • Cloudflare Workers AI & Vectorize: Semantic AI search.

This post is a hands-on guide. I document exactly how I built this setup. It covers every command, config, and click. Follow these steps to build the same pipeline for your Next.js project.

The stack:

  • Runtime: Next.js 15 with full SSR.
  • Package manager: pnpm.
  • Hosting: Hostinger Web App deployment.
  • DNS & CDN: Cloudflare free plan.
  • AI search: Cloudflare Workers AI (@cf/baai/bge-small-en-v1.5) and Vectorize.

Phase 1: Preparing the Repository

Step 1: Push Code to GitHub

Hostinger pulls your code directly from a GitHub repository. Push your Next.js project to GitHub before doing anything else.

bash
git init
git add .
git commit -m "initial commit"
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git push -u origin main

Your repository can be public or private. Hostinger handles both formats smoothly.

Step 2: Set Up pnpm as the Package Manager

I use pnpm instead of npm. This is a hard requirement because of Hostinger's inode limits. I will explain inodes in the next step.

Make sure your project declares pnpm as the package manager in package.json:

json
{
  "packageManager": "pnpm@10.0.0"
}

You must also commit a pnpm-lock.yaml file. Hostinger runs pnpm install --frozen-lockfile during the build. The build fails if the lockfile is missing.

bash
pnpm install
git add pnpm-lock.yaml
git commit -m "add pnpm lockfile"
git push

Step 3: Add a prebuild Script for Inode Control

This is the most important lesson I learned with Hostinger. Hostinger limits the total number of files on your account. These files are called inodes.

Next.js generates thousands of tiny files in .next/cache and node_modules/.cache during every build. You will hit the inode limit after a few deployments. Then, your deployments will fail silently.

The fix is adding a prebuild script to package.json. This script wipes the cache directories before every build:

json
{
  "scripts": {
    "prebuild": "rm -rf .next/cache && rm -rf node_modules/.cache",
    "build": "next build",
    "start": "next start"
  }
}

Node package managers automatically run any script named prebuild before running build. Hostinger will clear the old cache before building the new version. This keeps your total file count flat over time.

Commit this change before moving forward:

bash
git add package.json
git commit -m "add prebuild cache cleanup for inode control"
git push

Phase 2: Deploying on Hostinger

Step 4: Create a Web App in hPanel

  1. Log into your Hostinger account and open hPanel.
  2. Go to Websites in the sidebar.
  3. Click Add website or Deploy Your Web App.
  4. You will see an Import Git repository option. Click Continue with GitHub.
  5. Authorize Hostinger to access your repositories on the GitHub OAuth page.
  6. Return to hPanel and select your repository from the dropdown menu.
  7. Select your main branch. Mine is main.

Step 5: Configure the Build Settings

Hostinger detects your Next.js application automatically. It pre-fills the build and start commands. Check them carefully:

Setting Value
Build command pnpm install && pnpm build
Start command pnpm start
Node.js version 22.x
Output directory .next

Change the commands manually if Hostinger defaults to npm.

The prebuild script handles the cache cleanup automatically. You do not need to add rm -rf commands here.

Step 6: First Deployment

Click Deploy. Hostinger will execute these steps:

  1. Clone your repository.
  2. Run pnpm install --frozen-lockfile.
  3. Run the prebuild script to clear cache.
  4. Run pnpm build.
  5. Start the server with pnpm start.

The first build takes a few minutes. You can monitor the live logs in hPanel. Hostinger provides a temporary .hostingersite.com subdomain for testing when the build finishes.

Step 7: Automatic Deployments

Hostinger creates a GitHub webhook automatically. Any push to your selected branch triggers a new deployment.

Verify this is active:

  1. Go to your GitHub repository.
  2. Navigate to Settings then Webhooks.
  3. Look for a Hostinger webhook URL with a green checkmark.

Every time I run git push origin main, Hostinger pulls the code, builds it, and deploys it automatically.

Phase 3: Pointing Your Domain Through Cloudflare

I use Cloudflare for three reasons:

  • Global CDN caching
  • Free automatic HTTPS
  • DDoS protection

The traffic flow is simple: User → Cloudflare edge → Hostinger.

Step 8: Add Your Site to Cloudflare

  1. Create a free account at dash.cloudflare.com.
  2. Click Add a Site and enter your domain name.
  3. Select the Free plan.
  4. Cloudflare scans your existing DNS records. Ensure your Hostinger A record is listed. Add it manually if it is missing.

Step 9: Update Your Domain's Nameservers

Cloudflare assigns you two custom nameservers. They look like this:

text
amy.ns.cloudflare.com
bob.ns.cloudflare.com

Go to your domain registrar. Replace the existing nameservers with the Cloudflare nameservers. Save your changes.

DNS propagation usually finishes in under 30 minutes.

Step 10: Verify DNS Records in Cloudflare

Go to the DNS tab in Cloudflare after propagation finishes. Check your records:

Type Name Content Proxy
A @ Hostinger's server IP Proxied (orange cloud)
CNAME www yourdomain.com Proxied (orange cloud)

The orange cloud icon confirms traffic is routing through Cloudflare.

Step 11: SSL and Performance Settings

Adjust these settings in Cloudflare:

  1. Go to SSL/TLS. Set encryption mode to Full (strict).
  2. Go to Speed then Optimization. Enable Auto Minify for JavaScript, CSS, and HTML.
  3. Go to Caching then Configuration. Set the Browser Cache TTL to 4 hours.

Your site now serves over HTTPS globally.

This portfolio uses semantic vector embeddings. Every blog post is indexed and stored in Cloudflare Vectorize.

I built two AI features using this data:

  1. AI Search (/api/ai-search): Readers ask questions and get relevant blog posts based on meaning, not just keywords.
  2. Per-post AI Chat (/api/blog-chat): An AI assistant on each page answers questions about the specific post.

These features use standard Next.js API routes. They do not require a separate Cloudflare Worker. Hostinger runs the full Node.js runtime, allowing direct REST API calls.

Step 12: The Indexing Script

I pre-process every blog post and convert the text into vectors. I use a Node.js script located at scripts/index-blog-embeddings.js.

The script processes every .mdx file:

1. Parse and clean the text:

javascript
const { data, content } = matter(rawFile);
 
const cleanContent = removeMd(
  content
    .replace(/```[\s\S]*?```/g, "") // strip code blocks
    .replace(/<[\s\S]*?>/g, "") // strip HTML tags
);

I strip out code blocks and HTML. I only want the core prose for semantic search.

2. Split into chunks:

javascript
function chunkText(text, minLength = 300, maxLength = 1000) {
  const paragraphs = text.split(/
 
+/);
  // ... builds chunks that respect paragraph boundaries
}

I split each post into paragraph-aware chunks. Each chunk is between 300 and 1000 characters.

3. Generate embeddings with Cloudflare Workers AI:

javascript
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/baai/bge-small-en-v1.5`;
 
const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ text: [chunk] }),
});
 
const embedding = json.result.data[0]; // 384-dimension float array

I use Cloudflare's hosted bge-small-en-v1.5 model. It is fast and requires no local ML infrastructure.

4. Upsert into Vectorize:

javascript
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/vectorize/v2/indexes/blog-search/upsert`;
 
const vectors = chunks.map((chunk, j) => ({
  id: `${slug}#${j}`,
  values: embedding,
  metadata: { slug, title, text: chunk, chunkIndex: j },
}));

The vector IDs use the format slug#chunkIndex. The script detects if a post gets shorter and deletes orphaned vectors automatically.

5. Content-hash caching:

javascript
const contentHash = crypto.createHash("sha256").update(raw, "utf8").digest("hex");
 
const cached = indexCache[slug];
if (cached && cached.hash === contentHash) {
  skippedCount++;
  continue;  // nothing changed, skip this post entirely
}

I use a SHA-256 hash instead of file modification times. Git clone resets file times on every deploy. A hash ensures we only re-index actual content changes.

Step 13: Why Use GitHub Actions?

Many people ask why I run embeddings in GitHub Actions instead of Hostinger. There are three reasons:

1. Build Time Limits. Generating embeddings requires remote API calls. Doing this during the Hostinger build would add minutes to the process and cause timeouts.

2. Avoiding Redundant Indexing. Hostinger runs a fresh git clone on every deploy. Running the script there would trigger unnecessary API calls. GitHub Actions only triggers when an .mdx file changes.

3. Separation of Concerns. Hostinger serves the application. Cloudflare stores the index. The indexing script is a background data migration job. It belongs in CI.

Step 14: The GitHub Actions Workflow

The workflow file is located at .github/workflows/index-blog-embeddings.yml:

yaml
name: Index Blog Embeddings
 
on:
  push:
    branches: ["main"]
    paths:
      - "content/**/*.mdx"
 
concurrency:
  group: index-blog-embeddings-${{ github.ref }}
  cancel-in-progress: true
 
permissions:
  contents: write
 
jobs:
  index:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: pnpm/action-setup@v3
        with:
          version: 10
 
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "pnpm"
 
      - run: pnpm install --frozen-lockfile
 
      - name: Index new/changed blog posts
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
        run: pnpm index:embeddings
 
      - name: Commit updated cache
        run: |
          if ! git diff --quiet -- scripts/.indexed-posts-cache.json; then
            git config user.name "github-actions[bot]"
            git config user.email "github-actions[bot]@users.noreply.github.com"
            git add scripts/.indexed-posts-cache.json
            git commit -m "chore: update blog embeddings cache [skip ci]"
            git push
          else
            echo "No cache changes to commit."
          fi

The [skip ci] tag is essential. It prevents the bot commit from triggering an infinite loop of GitHub Actions.

Step 15: Setting Up Cloudflare Secrets

Add these secrets to GitHub under Settings > Secrets and variables > Actions:

Secret name How to get it
CLOUDFLARE_API_TOKEN Cloudflare Dashboard → My Profile → API Tokens → Create Token → select Cloudflare AI, Vectorize (Edit permission)
CLOUDFLARE_ACCOUNT_ID Cloudflare Dashboard → home page → right sidebar, listed under "Account ID"

Step 16: First Run

Run the indexer locally the first time to bootstrap the database:

bash
CLOUDFLARE_API_TOKEN=your_token \
CLOUDFLARE_ACCOUNT_ID=your_account_id \
pnpm index:embeddings

The script will create the blog-search index and upload all vectors. Commit the cache file when it finishes:

bash
git add scripts/.indexed-posts-cache.json
git commit -m "chore: initial blog embeddings cache"
git push

Step 17: The AI Search Route

The /api/ai-search route handles user queries. It performs three steps:

Step A: Embed the query

typescript
const embeddingUrl = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/@cf/baai/bge-small-en-v1.5`;
 
const embeddingRes = await fetch(embeddingUrl, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ text: [query] }),
});
 
const queryVector = embeddingJson.result.data[0];

Step B: Query Vectorize

typescript
const vectorizeUrl = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/vectorize/v2/indexes/blog-search/query`;
 
const vectorizeRes = await fetch(vectorizeUrl, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    vector: queryVector,
    topK: 8,
    returnValues: false,
    returnMetadata: "all",
  }),
});

Step C: Group and return

The route groups matching chunks by post slug. It ranks them by similarity score and returns the results to the frontend.

Step 18: The Per-Post AI Chat Route

The /api/blog-chat route powers the chat widget on each post. It reads the MDX file from disk instead of querying Vectorize.

Read and clean the post:

typescript
const rawFile = fs.readFileSync(filePath, "utf8");
const { data, content } = matter(rawFile);
 
const cleanContent = removeMd(content.replace(/```[\s\S]*?```/g, "").replace(/<[\s\S]*?>/g, ""));

Build the system prompt:

typescript
const systemPrompt = `
You are Ranti's Blog AI assistant. Your goal is to help the reader understand the blog post titled "${title}".
Speak in the first person (as Ranti, the author of this post). Warm, friendly, professional.
 
Here is the complete text of the blog post:
=========================================
Title: ${title}
Date: ${data.publishedAt}
Content:
${cleanContent}
=========================================
 
Instructions:
- Answer questions directly related to this blog post.
- Always speak in the first person (e.g. "In my article...", "I wrote this because...").
- Keep answers helpful, concise, and focused on the article.
`;

Send to Cloudflare Workers AI:

typescript
const model = "@cf/meta/llama-3.1-8b-instruct";
const url = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/ai/run/${model}`;
 
const response = await fetch(url, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    messages: [{ role: "system", content: systemPrompt }, ...messages],
    max_tokens: maxTokens,
  }),
});

I adjust max_tokens dynamically based on the query complexity. This saves Cloudflare AI quota on simple questions.

How It All Fits Together

Here is the data flow for the AI search:

flowchart TD
    A[Reader types a search query] --> B[Next.js /api/ai-search route on Hostinger]
    B --> C[Cloudflare Workers AI\nbge-small-en-v1.5\nEmbed the query into a 384-dim vector]
    C --> D[Cloudflare Vectorize\nFind top-8 matching post chunks\nby cosine similarity]
    D --> E[Group chunks by post slug\nRank by highest similarity score]
    E --> F[Return ranked post list with snippets\nto the frontend]

Here is the data flow for the per-post AI chat:

flowchart TD
    A[Reader asks a question on a blog post page] --> B[Next.js /api/blog-chat route on Hostinger]
    B --> C[Read the full MDX post from disk\nStrip markdown and code blocks]
    C --> D[Build system prompt\nFull post text injected as context]
    D --> E[Cloudflare Workers AI\nLlama 3.1 8B Instruct\nGenerate a grounded answer]
    E --> F[Return AI response to the reader]

Here is the overall deployment and indexing architecture:

flowchart LR
    subgraph GitHub
        A[Push to main] --> B{Which files changed?}
        B -- MDX file changed --> C[GitHub Actions\nIndex Blog Embeddings workflow]
        B -- Code changed --> D[Hostinger webhook\ntriggers redeploy]
    end
 
    subgraph Cloudflare
        C --> E[Workers AI\nbge-small-en-v1.5\nGenerate embeddings]
        E --> F[Vectorize\nblog-search index\nStore vectors]
        C --> G[Commit updated cache\nwith skip ci]
    end
 
    subgraph Hostinger
        D --> H[pnpm install\nprebuild clears cache\npnpm build\npnpm start]
        H --> I[Next.js running\nSSR and API routes live]
    end
 
    I -- Search query --> E
    F -- Top-k matches --> I

Conclusion

This complete stack provides a production-grade, globally distributed, AI-powered portfolio.

  • Hostinger: Node.js runtime.
  • Cloudflare: DNS, CDN, AI, and Vectorize.
  • GitHub Actions: Automated indexing.

The core insight is that Cloudflare's AI tools are REST APIs. You can call them from any server, including a Next.js API route on Hostinger.

  • Deployments: Fully automated on every push.
  • Inode safety: Handled by the prebuild script and pnpm.
  • AI indexing: Runs purely in GitHub Actions.
  • Search and Chat: Powered by Next.js API routes hitting Cloudflare.

I highly recommend this setup if you want full control and AI capabilities without Vercel lock-in.

FAQ

Can you host Next.js on Hostinger?

Yes. Hostinger's Deploy Your Web App feature supports full Node.js runtimes. You can deploy Next.js with SSR, API routes, and server components directly. Connect your GitHub repository, and Hostinger handles the build.

Does Hostinger support Node.js and Next.js natively?

Yes. Hostinger runs Node.js natively on its Web Hosting plans. It supports Next.js, Nuxt, and Express. You do not need a VPS.

Why should I use pnpm instead of npm on Hostinger?

Use pnpm on Hostinger to manage strict inode limits. Standard npm creates tens of thousands of individual files. pnpm uses a global content store with symlinks. This drastically cuts the real file count and prevents inode ceiling errors.

How do I fix Hostinger inode limit issues with Next.js?

Add a prebuild script to your package.json: "prebuild": "rm -rf .next/cache && rm -rf node_modules/.cache". Switch from npm to pnpm. These two steps keep your inode usage flat across multiple deployments.

Is Cloudflare free to use with Hostinger?

Yes. Cloudflare's free plan works seamlessly with Hostinger. Point your domain's nameservers at Cloudflare. It proxies traffic to your server, providing free SSL, global CDN caching, and DDoS protection.

How do I add AI search to a Next.js blog?

Use Cloudflare Workers AI to generate text embeddings for each post. Store them in Cloudflare Vectorize. When a user searches, embed the query and query Vectorize for similar chunks. Return the ranked results. No GPU server is required.

Does Cloudflare Workers AI work without a Cloudflare Worker?

Yes. Cloudflare Workers AI offers a standard REST API. Any server that makes HTTPS requests can call it directly. A Next.js API route on Hostinger works perfectly.

How do I auto-index blog posts to Cloudflare Vectorize on every publish?

Create a GitHub Actions workflow. Trigger it on push to main with paths restricted to content/**/*.mdx. Run your embedding script to generate vectors and upsert them to Vectorize. Use a content-hash cache file to skip unchanged posts. Commit the cache back with a [skip ci] tag.

Was this post helpful?

Your feedback helps me improve future content

Subscribe to Newsletter

Get the latest posts delivered right to your inbox

Join 1,000+ readers. No spam, unsubscribe anytime.