refactor: Move Astro project to root directory

- Move all Astro files from dealplustech-astro/ to root
- Archive Next.js code in _nextjs-backup/
- Update .gitignore for Astro project
- Simplify project structure

This completes the migration from Next.js to Astro.
The Astro project is now at the root level.
This commit is contained in:
Kunthawat Greethong
2026-03-03 10:21:42 +07:00
parent b8e372ca96
commit 45961b8d76
525 changed files with 481 additions and 30894 deletions

View File

@@ -1,168 +0,0 @@
{
"$ref": "#/definitions/products",
"definitions": {
"products": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"nameEn": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": "string"
},
"shortDescription": {
"type": "string"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
}
},
"seoContent": {
"type": "string"
},
"image": {
"type": "string"
},
"specifications": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"value": {
"type": "string"
},
"unit": {
"type": "string"
}
},
"required": [
"label",
"value"
],
"additionalProperties": false
}
},
"features": {
"type": "array",
"items": {
"type": "string"
}
},
"applications": {
"type": "array",
"items": {
"type": "string"
}
},
"certifications": {
"type": "array",
"items": {
"type": "string"
}
},
"productTables": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tableName": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "string"
}
},
"rows": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"required": [
"tableName",
"headers",
"rows"
],
"additionalProperties": false
}
},
"faq": {
"type": "array",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string"
},
"answer": {
"type": "string"
}
},
"required": [
"question",
"answer"
],
"additionalProperties": false
}
},
"relatedProductIds": {
"type": "array",
"items": {
"type": "string"
}
},
"schemaData": {
"type": "object",
"properties": {
"brand": {
"type": "string"
},
"manufacturer": {
"type": "string"
},
"material": {
"type": "string"
},
"category": {
"type": "string"
}
},
"additionalProperties": false
},
"$schema": {
"type": "string"
}
},
"required": [
"id",
"name",
"nameEn",
"slug",
"description",
"image"
],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@@ -1 +0,0 @@
export default new Map();

View File

@@ -1 +0,0 @@
export default new Map();

View File

@@ -1,215 +0,0 @@
declare module 'astro:content' {
export interface RenderResult {
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}
interface Render {
'.md': Promise<RenderResult>;
}
export interface RenderedContent {
html: string;
metadata?: {
imagePaths: Array<string>;
[key: string]: unknown;
};
}
}
declare module 'astro:content' {
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export type ReferenceDataEntry<
C extends CollectionKey,
E extends keyof DataEntryMap[C] = string,
> = {
collection: C;
id: E;
};
export type ReferenceContentEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}) = string,
> = {
collection: C;
slug: E;
};
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C;
id: string;
};
/** @deprecated Use `getEntry` instead. */
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
/** @deprecated Use `getEntry` instead. */
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E,
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>;
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter?: LiveLoaderCollectionFilterType<C>,
): Promise<
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
entry: ReferenceContentEntry<C, E>,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
entry: ReferenceDataEntry<C, E>,
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E,
): E extends keyof DataEntryMap[C]
? string extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]> | undefined
: Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter: string | LiveLoaderEntryFilterType<C>,
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof AnyEntryMap>(
entry: AnyEntryMap[C][string],
): Promise<RenderResult>;
export function reference<C extends keyof AnyEntryMap>(
collection: C,
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
>;
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
export function reference<C extends string>(
collection: C,
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
};
type DataEntryMap = {
"blog": Record<string, {
id: string;
body?: string;
collection: "blog";
data: any;
rendered?: RenderedContent;
filePath?: string;
}>;
"products": Record<string, {
id: string;
body?: string;
collection: "products";
data: any;
rendered?: RenderedContent;
filePath?: string;
}>;
};
type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData,
infer TEntryFilter,
infer TCollectionFilter,
infer TError
>
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
: { data: never; entryFilter: never; collectionFilter: never; error: never };
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import('astro/zod').infer<
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>;
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
LiveContentConfig['collections'][C]['loader']
>;
export type ContentConfig = typeof import("../src/content.config.mjs");
export type LiveContentConfig = never;
}

View File

@@ -1,2 +0,0 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
dealplustech-astro/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -1,221 +0,0 @@
# 🔬 Easypanel API Testing Results
## ✅ What Works
### 1. Authentication
```bash
curl -H "Authorization: Bearer TOKEN" \
http://110.164.146.46:3000/api/trpc/setup.getStatus
# ✅ Returns: {"result":{"data":{"json":{"isComplete":true}}}}
```
### 2. List Projects
```bash
curl -H "Authorization: Bearer TOKEN" \
http://110.164.146.46:3000/api/trpc/projects.listProjectsAndServices
# ✅ Returns project list including "customerwebsite"
```
### 3. Inspect Service
```bash
curl -H "Authorization: Bearer TOKEN" \
http://110.164.146.46:3000/api/trpc/services.app.inspectService
# ✅ Works when service exists
```
---
## ❌ What Fails - Service Creation
### Endpoint
```
POST /api/trpc/services.app.createService
```
### Schema (from OpenAPI)
```json
{
"input": {
"json": {
"projectName": "customerwebsite",
"serviceName": "my-app",
"source": {
"type": "image" | "github",
// For "image":
"image": "nginx:alpine",
"username": "...",
"password": "...",
// For "github":
"owner": "...",
"repo": "...",
"ref": "main",
"path": "..."
}
}
}
}
```
### Attempts & Errors
#### Attempt 1: Basic Image
```json
{
"input": {
"json": {
"projectName": "customerwebsite",
"serviceName": "test",
"source": {
"type": "image",
"image": "nginx:alpine",
"port": 80
}
}
}
}
```
**Result:** ❌ 500 Error
```json
{
"error": {
"json": {
"message": "[{\"code\":\"invalid_type\",\"expected\":\"object\",\"received\":\"undefined\",\"path\":[],\"message\":\"Required\"}]"
}
}
}
```
#### Attempt 2: With Git Source
```json
{
"input": {
"json": {
"projectName": "customerwebsite",
"serviceName": "test-git",
"source": {
"type": "git",
"repository": "http://...",
"branch": "main"
}
}
}
}
```
**Result:** ❌ Same "Required" error
#### Attempt 3: Using `project` instead of `projectName`
```json
{
"input": {
"json": {
"project": "customerwebsite",
"name": "test"
}
}
}
```
**Result:** ❌ Same error
---
## 🔍 Analysis
The Zod validation error `"expected":"object","received":"undefined","path":[]` suggests:
1. **Missing required field** at root level
2. **Schema mismatch** - the API expects additional fields not in OpenAPI spec
3. **Possible tRPC format issue** - Easypanel might use a different input format
---
## 💡 Hypothesis
Easypanel's `createService` endpoint might require:
### Option A: Additional Required Fields
```json
{
"input": {
"json": {
"projectName": "...",
"serviceName": "...",
"source": {...},
// Missing required fields:
"environmentVariables": [],
"domains": [],
"ports": [],
"mounts": []
}
}
}
```
### Option B: Different tRPC Format
```json
{
"type": "query",
"input": {...}
}
```
### Option C: Requires Project ID (not name)
```json
{
"input": {
"json": {
"project": "cmkw22b00000007tu4gim48q9", // Actual ID
"name": "test"
}
}
}
```
---
## 📋 Next Steps to Debug
### 1. Capture Browser Network Traffic
1. Open Easypanel dashboard
2. Open DevTools → Network tab
3. Create a service manually
4. Inspect the exact API request
5. Copy request payload
### 2. Test with cURL
Use the exact payload from browser
### 3. Alternative: Use Easypanel CLI (if exists)
Check if Easypanel provides official CLI
### 4. Contact Easypanel Support
Ask for correct API schema for `services.app.createService`
---
## 🎯 Current Recommendation
Until API is figured out, use **manual creation**:
1. **Create service via dashboard** (2 minutes)
2. **Copy service ID**
3. **Register with skill**: `./deploy.sh register SERVICE_ID`
4. **Future updates**: Automated via `./deploy.sh update`
This is still 80% automated!
---
## 📞 Need Your Help
**Can you:**
1. Open browser DevTools when creating service in Easypanel
2. Copy the exact API request payload
3. Share it so I can update the skill?
Or if you know the correct schema, let me know!
---
**Status:** ⚠️ API schema unclear
**Workaround:** Manual creation + automated updates
**Automation Level:** 80% (will be 100% with correct schema)

View File

@@ -1,132 +0,0 @@
# 🚀 Fully Automated Deployment - v5.0
## ✅ Complete Workflow
The skill now automates ALL steps:
### Step 1: Create Service
```bash
POST /api/trpc/services.app.createService
{
"json": {
"projectName": "customerwebsite",
"domains": [{"host": "$(EASYPANEL_DOMAIN)"}],
"serviceName": "dealplustech-astro"
}
}
```
### Step 2: Configure Git
```bash
POST /api/trpc/services.app.updateSourceGit
{
"json": {
"projectName": "customerwebsite",
"serviceName": "dealplustech-astro",
"repo": "http://110.164.146.46:3001/dealplustech/dealplustech-astro.git",
"ref": "main",
"path": "/"
}
}
```
### Step 3: Set Build Type
```bash
POST /api/trpc/services.app.updateBuild
{
"json": {
"projectName": "customerwebsite",
"serviceName": "dealplustech-astro",
"build": {"type": "nixpacks"}
}
}
```
### Step 4: Get Primary Domain
```bash
GET /api/trpc/domains.getPrimaryDomain
{
"json": {
"projectName": "customerwebsite",
"serviceName": "dealplustech-astro"
}
}
```
### Step 5: Wait for Deployment
- Polls service status every 10 seconds
- Waits up to 5 minutes (30 attempts)
- Reports when status is "running" or "ready"
---
## 🎯 Usage
### First Deployment
```bash
cd dealplustech-astro
./skills/easypanel-deploy/deploy.sh deploy
```
**That's it!** The script handles everything automatically!
### Redeploy After Changes
```bash
git push # Push your changes
./skills/easypanel-deploy/deploy.sh redeploy # Trigger redeploy
```
### Check Status
```bash
./skills/easypanel-deploy/deploy.sh status
```
---
## ✅ Benefits
| Feature | Before v5.0 | v5.0 |
|---------|-------------|------|
| Manual steps | 4-6 steps | **0 steps** |
| Time required | 5-10 minutes | **1 command** |
| API calls | N/A | **5 automated calls** |
| Error handling | Manual | **Automatic** |
| Wait for deploy | Manual | **Automatic** |
| Domain info | Manual lookup | **Automatic** |
---
## 📋 What Happens
```
1. ./deploy.sh deploy
2. Creates service in Easypanel
3. Configures Git repository
4. Sets build type (nixpacks)
5. Gets auto-generated domain
6. Waits for deployment
7. ✅ Deployment complete!
```
**Zero manual steps!**
---
## 🎉 Ready to Test!
```bash
# Deploy now!
./skills/easypanel-deploy/deploy.sh deploy
```
---
**Status:** ✅ Fully Automated
**Automation Level:** 100%
**Manual Steps:** 0

View File

@@ -1,284 +0,0 @@
# 🚀 Easypanel Deployment Guide
**Project:** Deal Plus Tech Astro Migration
**Deployment Target:** Easypanel (Docker-based)
**Build Command:** `npm run build`
**Output:** Static site served via `npm run preview`
---
## 📋 Prerequisites
1. **Easypanel Account** - Access to Easypanel instance at `http://110.164.146.46:3000`
2. **Git Repository** - Code pushed to Git (Gitea/GitHub/GitLab)
3. **Domain** (optional) - Custom domain for production
---
## 🛠️ Deployment Steps
### Option 1: Deploy from Git Repository (Recommended)
#### Step 1: Connect Git Repository
1. Login to Easypanel: `http://110.164.146.46:3000`
2. Click **"New Project"** or select existing project
3. Click **"New Service"** → **"Git Repository"**
4. Connect your Git provider (Gitea, GitHub, GitLab)
5. Select repository: `dealplustech/dealplustech-astro`
6. Select branch: `main`
#### Step 2: Configure Build Settings
**Build Configuration:**
- **Build Command:** `npm run build`
- **Publish Directory:** `dist`
- **Dockerfile:** (Leave empty - uses auto-detection)
- **Node Version:** `20`
**Environment Variables:**
```bash
NODE_ENV=production
PORT=4321
HOST=0.0.0.0
```
#### Step 3: Deploy
1. Click **"Deploy"**
2. Wait for build to complete (~2-3 minutes)
3. Easypanel will automatically assign a URL (e.g., `dealplustech-astro.easypanel.app`)
4. Test the deployment
#### Step 4: Auto-Deploy (Optional)
Enable auto-deploy on push:
1. Go to Service Settings → **Git**
2. Enable **"Auto Deploy"**
3. Select branch: `main`
Now every push to `main` will trigger automatic deployment!
---
### Option 2: Deploy with Dockerfile
If you prefer using the provided Dockerfile:
#### Step 1: Build Docker Image Locally
```bash
cd dealplustech-astro
# Build Docker image
docker build -t dealplustech-astro:latest .
# Test locally
docker run -p 4321:4321 dealplustech-astro:latest
```
Visit `http://localhost:4321` to test
#### Step 2: Push to Container Registry
```bash
# Tag for your registry
docker tag dealplustech-astro:latest your-registry.com/dealplustech-astro:latest
# Push to registry
docker push your-registry.com/dealplustech-astro:latest
```
#### Step 3: Deploy on Easypanel
1. Login to Easypanel
2. **New Service****"Docker Image"**
3. Enter image URL: `your-registry.com/dealplustech-astro:latest`
4. Configure port: `4321`
5. Click **"Deploy"**
---
## 🔧 Configuration
### Environment Variables
| Variable | Value | Description |
|----------|-------|-------------|
| `NODE_ENV` | `production` | Production mode |
| `PORT` | `4321` | Astro preview server port |
| `HOST` | `0.0.0.0` | Listen on all interfaces |
### Resource Allocation
**Recommended Resources:**
- **CPU:** 0.5 - 1 vCPU
- **Memory:** 512MB - 1GB
- **Storage:** 1GB (for logs and assets)
### Custom Domain
To add a custom domain:
1. Go to Service Settings → **Domains**
2. Click **"Add Domain"**
3. Enter your domain: `dealplustech.co.th`
4. Update DNS records:
```
Type: CNAME
Name: www
Value: your-easypanel-url.easypanel.app
```
5. Enable SSL (Easypanel provides auto-SSL)
---
## 📊 Monitoring
### Health Check
The Dockerfile includes a health check endpoint:
- **URL:** `http://your-domain:4321/`
- **Expected:** HTTP 200 OK
### Logs
View logs in Easypanel:
1. Select Service
2. Click **"Logs"** tab
3. Filter by date/time
### Metrics
Easypanel provides:
- CPU usage
- Memory usage
- Network traffic
- Request count
---
## 🔄 Update Deployment
### Automatic Updates (Git Auto-Deploy)
If auto-deploy is enabled:
1. Push changes to `main` branch
2. Easypanel automatically rebuilds
3. New version deploys in ~2-3 minutes
### Manual Updates
1. Go to Service → **Deployments**
2. Click **"Redeploy"**
3. Select latest commit
4. Click **"Deploy Now"**
---
## 🐛 Troubleshooting
### Build Fails
**Issue:** `npm run build` fails
**Solution:**
1. Check build logs in Easypanel
2. Verify `package.json` scripts
3. Test build locally: `npm run build`
4. Check Node version (must be 20+)
### 502 Bad Gateway
**Issue:** Service returns 502 error
**Solution:**
1. Check if container is running
2. Verify port is 4321
3. Check health check endpoint
4. Review container logs
### Static Assets Not Loading
**Issue:** Images/CSS return 404
**Solution:**
1. Verify `public/` folder is copied in Dockerfile
2. Check asset paths in code
3. Rebuild and redeploy
---
## 📝 Post-Deployment Checklist
- [ ] Test homepage loads
- [ ] Test product pages (6 products)
- [ ] Test blog posts (3 posts)
- [ ] Test mobile responsiveness
- [ ] Verify FloatingContact buttons work
- [ ] Test all navigation links
- [ ] Check SEO metadata
- [ ] Setup custom domain (if needed)
- [ ] Enable SSL certificate
- [ ] Configure CDN (optional)
- [ ] Setup monitoring alerts
---
## 🎯 Production Optimization
### Enable Compression
Easypanel automatically enables gzip compression for static assets.
### CDN Integration
For better performance:
1. Sign up for CDN (Cloudflare, BunnyCDN, etc.)
2. Point CDN to Easypanel URL
3. Update DNS to point to CDN
4. Configure cache rules
### Caching Headers
Astro sets optimal cache headers by default:
- **HTML:** No cache (always fresh)
- **Assets:** 1 year (immutable)
- **Images:** 1 year (immutable)
---
## 📞 Support
**Easypanel Documentation:** https://docs.easypanel.io
**Astro Documentation:** https://docs.astro.build
**Project Repository:** [Your Git Repo]
---
## 🚀 Quick Deploy Commands
```bash
# Build locally
npm run build
# Test production build locally
npm run preview
# Build Docker image
docker build -t dealplustech-astro:latest .
# Run Docker container
docker run -p 4321:4321 dealplustech-astro:latest
# Push to registry
docker push your-registry.com/dealplustech-astro:latest
```
---
**Deployment Status:** ✅ Ready to Deploy
**Estimated Deploy Time:** 2-3 minutes
**First Deploy:** Manual
**Subsequent Deploys:** Automatic (if enabled)

View File

@@ -1,118 +0,0 @@
# 🚢 Image Deployment Options
## The Issue
When you built `dealplustech-astro:latest`, it's only on your **local Docker**. Easypanel server can't access it.
## ✅ Solutions
### Option 1: Easypanel Docker Registry (Recommended)
Easypanel provides a built-in Docker registry.
**Steps:**
```bash
# 1. Get registry URL (usually SERVER_IP:3001)
# For your setup: 110.164.146.46:3001
# 2. Login to registry
docker login 110.164.146.46:3001
# Use Easypanel credentials
# 3. Tag image
docker tag dealplustech-astro:latest 110.164.146.46:3001/customerwebsite/dealplustech-astro:latest
# 4. Push
docker push 110.164.146.46:3001/customerwebsite/dealplustech-astro:latest
# 5. In Easypanel dashboard, use:
# Image: 110.164.146.46:3001/customerwebsite/dealplustech-astro:latest
```
---
### Option 2: Docker Hub (Public)
```bash
# Tag with your Docker Hub username
docker tag dealplustech-astro:latest yourusername/dealplustech-astro:latest
# Push
docker push yourusername/dealplustech-astro:latest
# In Easypanel, use:
# Image: yourusername/dealplustech-astro:latest
```
**Note:** Public repository - anyone can see it
---
### Option 3: Private Registry (Harbor, GitLab, etc.)
```bash
# Tag for your registry
docker tag dealplustech-astro:latest registry.yourcompany.com/project/dealplustech-astro:latest
# Login
docker login registry.yourcompany.com
# Push
docker push registry.yourcompany.com/project/dealplustech-astro:latest
```
---
### Option 4: Direct Server Deployment
If you have SSH access to Easypanel server:
```bash
# Export image
docker save dealplustech-astro:latest > dealplustech-astro.tar
# Copy to server
scp dealplustech-astro.tar user@110.164.146.46:/tmp/
# SSH to server and load
ssh user@110.164.146.46
docker load < /tmp/dealplustech-astro.tar
# In Easypanel, use:
# Image: dealplustech-astro:latest (local)
```
---
## 🎯 Recommended for Your Setup
**Use Easypanel's built-in registry:**
```bash
# 1. Login
docker login 110.164.146.46:3001
# 2. Tag
docker tag dealplustech-astro:latest 110.164.146.46:3001/customerwebsite/dealplustech-astro:latest
# 3. Push
docker push 110.164.146.46:3001/customerwebsite/dealplustech-astro:latest
# 4. Create service in Easypanel with:
# Image: 110.164.146.46:3001/customerwebsite/dealplustech-astro:latest
```
---
## 📋 Checklist
- [ ] Choose registry option
- [ ] Login to registry
- [ ] Tag image correctly
- [ ] Push to registry
- [ ] Use full image name in Easypanel
---
**Next Step:** Push to Easypanel registry, then create service!

View File

@@ -1,177 +0,0 @@
# 🚀 Deployment Summary & Next Steps
## ✅ What's Complete
### 1. Docker Image Built
- **Image:** `dealplustech-astro:latest`
- **Size:** ~564MB
- **Status:** Ready to deploy
### 2. Easypanel Connection
- **URL:** http://110.164.146.46:3000
- **Token:** ✅ Configured
- **Project Found:** `customerwebsite`
### 3. Skill Created
- **Location:** `skills/easypanel-deploy/`
- **Version:** 2.2
- **Features:** State management, lifecycle control
---
## 📋 Manual Steps Required
Easypanel API has limitations for service creation. Follow these steps:
### Step 1: Create Service in Dashboard
1. **Open Easypanel:**
```
http://110.164.146.46:3000
```
2. **Select Project:**
- Click on: `customerwebsite`
3. **Create New Service:**
- Click: **"New Service"**
- Choose: **"Docker image"**
4. **Configure Service:**
```
Name: dealplustech-astro
Image: dealplustech-astro:latest
Port: 4321
```
5. **Deploy:**
- Click: **"Create"** or **"Deploy"**
- Wait for deployment (~2-3 minutes)
6. **Copy Service ID:**
- Go to service settings
- Copy the ID (looks like: `svc_abc123...`)
### Step 2: Register Service ID
```bash
cd dealplustech-astro
# Register the service ID you copied
./skills/easypanel-deploy/deploy.sh register svc_abc123...
```
### Step 3: Verify
```bash
# Check status
./skills/easypanel-deploy/deploy.sh status
# Should show:
# ✅ Service: dealplustech-astro
# ID: svc_abc123...
```
---
## 🔄 Future Updates
After registration, updates are easy:
```bash
# Make code changes
git pull
# Update deployment
./skills/easypanel-deploy/deploy.sh update
# This will:
# 1. Rebuild Docker image
# 2. Ready for deployment
# 3. Click "Deploy" in Easypanel to apply
```
---
## 📊 Quick Commands
| Command | Purpose |
|---------|---------|
| `./deploy.sh deploy` | Initial deployment |
| `./deploy.sh register ID` | Register service |
| `./deploy.sh update` | Update service |
| `./deploy.sh status` | Check status |
| `./deploy.sh list` | List projects |
---
## 🐛 Troubleshooting
### Docker Build Fails
```bash
# Rebuild manually
cd dealplustech-astro
docker build -t dealplustech-astro:latest .
```
### Can't Access Easypanel
- Check VPN/connection
- URL: http://110.164.146.46:3000
- Contact admin if needed
### Service Won't Start
1. Check logs in Easypanel dashboard
2. Verify port 4321 is available
3. Check resource allocation
---
## 📁 Files Created
```
dealplustech-astro/
├── skills/easypanel-deploy/
│ ├── deploy.sh # Main script
│ ├── SKILL_v2.md # Documentation
│ └── README.md # Quick start
├── easypanel.config.json # Configuration
├── Dockerfile # Docker config
└── DEPLOYMENT_SUMMARY.md # This file
~/.easypanel/
├── credentials # API token
└── state.json # Service IDs
```
---
## ✅ Checklist
- [ ] Docker image built ✅
- [ ] Easypanel token configured ✅
- [ ] Project identified (`customerwebsite`) ✅
- [ ] Create service in dashboard ⏳
- [ ] Register service ID ⏳
- [ ] Verify deployment ⏳
---
## 🎯 Next Action
**Go to Easypanel dashboard and create the service:**
1. Open: http://110.164.146.46:3000
2. Select: `customerwebsite` project
3. New Service → Docker image
4. Name: `dealplustech-astro`
5. Image: `dealplustech-astro:latest`
6. Port: `4321`
7. Deploy!
Then run: `./deploy.sh register YOUR_SERVICE_ID`
---
**Status:** Ready for manual service creation
**Estimated Time:** 5 minutes
**Difficulty:** Easy

View File

@@ -1,302 +0,0 @@
# 🚀 Easypanel API Deployment Guide
## Overview
This guide shows how to deploy the Deal Plus Tech Astro site to Easypanel using their API.
---
## Prerequisites
1. **Easypanel Access**
- URL: `http://110.164.146.46:3000`
- Admin credentials
2. **API Token**
- Login to Easypanel
- Go to Settings → API
- Generate new API token
3. **Docker Image**
- Already built locally: `dealplustech-astro:latest`
- Size: 564MB
---
## Method 1: Manual Deployment (Recommended)
### Step 1: Login to Easypanel
```
URL: http://110.164.146.46:3000
```
### Step 2: Create Project
1. Click **"New Project"**
2. Name: `dealplustech`
3. Description: "Deal Plus Tech Websites"
4. Click **"Create"**
### Step 3: Deploy Docker Image
1. Select project: `dealplustech`
2. Click **"New Service"**
3. Choose: **"Docker Image"**
4. Configure:
- **Name:** `dealplustech-astro`
- **Image:** `dealplustech-astro:latest`
- **Port:** `4321`
5. Click **"Deploy"**
### Step 4: Verify Deployment
1. Wait for build to complete (~2-3 minutes)
2. Click on service to view logs
3. Look for: "Astro preview ready"
4. Access URL provided by Easypanel
---
## Method 2: Automated Deployment (API)
### Step 1: Get API Token
```bash
# Login to Easypanel and get token from Settings → API
export EASYPANEL_API_TOKEN="your-api-token-here"
```
### Step 2: Run Deployment Script
```bash
cd dealplustech-astro
# Option A: With token as argument
./deploy-easypanel.sh your-api-token
# Option B: With environment variable
export EASYPANEL_API_TOKEN="your-api-token"
./deploy-easypanel.sh
```
### Step 3: Monitor Deployment
```bash
# View service status
curl -s "http://110.164.146.46:3000/api/trpc/services.app.inspectService" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"input":{"json":{"projectName":"dealplustech","serviceName":"dealplustech-astro"}}}' \
--insecure | python3 -m json.tool
```
---
## Method 3: Direct Docker Deployment
If you have direct Docker access to the Easypanel server:
```bash
# SSH to Easypanel server
ssh user@110.164.146.46
# Run container directly
docker run -d \
-p 4321:4321 \
--name dealplustech-astro \
--restart unless-stopped \
-e NODE_ENV=production \
-e PORT=4321 \
-e HOST=0.0.0.0 \
dealplustech-astro:latest
```
---
## API Endpoints
### List Projects
```bash
curl -X GET "http://110.164.146.46:3000/api/trpc/projects.listProjects" \
-H "Authorization: Bearer YOUR_TOKEN" \
--insecure
```
### Create Service
```bash
curl -X POST "http://110.164.146.46:3000/api/trpc/services.app.create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"projectName": "dealplustech",
"name": "dealplustech-astro",
"type": "docker",
"docker": {
"image": "dealplustech-astro:latest",
"port": 4321
},
"env": {
"NODE_ENV": "production",
"PORT": "4321"
}
}' \
--insecure
```
### Inspect Service
```bash
curl -X GET "http://110.164.146.46:3000/api/trpc/services.app.inspectService" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"input":{"json":{"projectName":"dealplustech","serviceName":"dealplustech-astro"}}}' \
--insecure
```
### Get Service Logs
```bash
curl -X GET "http://110.164.146.46:3000/api/trpc/services.common.getLogs" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"input":{"json":{"projectName":"dealplustech","serviceName":"dealplustech-astro","lines":50}}}' \
--insecure
```
### Deploy/Redeploy Service
```bash
curl -X POST "http://110.164.146.46:3000/api/trpc/services.app.deploy" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"input":{"json":{"projectName":"dealplustech","serviceName":"dealplustech-astro"}}}' \
--insecure
```
---
## Environment Variables
| Variable | Value | Required |
|----------|-------|----------|
| `NODE_ENV` | `production` | ✅ Yes |
| `PORT` | `4321` | ✅ Yes |
| `HOST` | `0.0.0.0` | ✅ Yes |
---
## Troubleshooting
### API Returns 401 Unauthorized
**Problem:** Invalid or missing API token
**Solution:**
```bash
# Verify token is set
echo $EASYPANEL_API_TOKEN
# Regenerate token in Easypanel dashboard
```
### Service Won't Start
**Problem:** Container crashes on startup
**Solution:**
```bash
# Check logs
docker logs dealplustech-astro
# Common issues:
# - Port already in use
# - Missing environment variables
# - Image build failed
```
### Build Fails
**Problem:** Docker build errors
**Solution:**
```bash
# Rebuild with verbose output
cd dealplustech-astro
docker build --no-cache -t dealplustech-astro:latest .
# Check for errors in output
```
### Can't Access Easypanel
**Problem:** Connection timeout
**Solution:**
```bash
# Test connection
curl -I http://110.164.146.46:3000
# Check if Easypanel is running
# Contact server administrator if needed
```
---
## Post-Deployment
### 1. Verify Service
```bash
curl http://your-easypanel-url:4321/
```
Should return HTML with Thai content
### 2. Check Health
```bash
curl -I http://your-easypanel-url:4321/
```
Should return `HTTP/1.1 200 OK`
### 3. Setup Domain (Optional)
1. Go to Easypanel → Service Settings → Domains
2. Add domain: `dealplustech.co.th`
3. Update DNS records
4. Enable SSL
### 4. Enable Auto-Deploy
For Git-based auto-deploy:
1. Go to Service Settings → Git
2. Connect repository
3. Enable auto-deploy on push
---
## Resources
- **Easypanel Dashboard:** http://110.164.146.46:3000
- **API Documentation:** http://110.164.146.46:3000/api
- **Swagger UI:** http://110.164.146.46:3000/api (Swagger UI)
- **Easypanel Docs:** https://docs.easypanel.io
---
## Support
For issues:
1. Check service logs in Easypanel
2. Review deployment script output
3. Contact Easypanel support
4. Check project documentation
---
**Last Updated:** 2026-03-02
**Status:** ✅ Ready for Deployment

View File

@@ -1,22 +0,0 @@
# Astro Production Dockerfile
FROM node:20-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --omit=dev
# Copy all source code
COPY . .
# Build Astro project (includes public folder in dist)
RUN npm run build
# Expose port
EXPOSE 4321
# Start Astro preview server
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "4321"]

View File

@@ -1,310 +0,0 @@
# Easypanel Deployment Skill
**Skill Name:** `easypanel-deploy`
**Description:** Deploy Astro/Next.js/Vite apps to Easypanel with Docker
**Version:** 1.0.0
**Author:** Deal Plus Tech DevOps
---
## Overview
This skill provides a complete deployment workflow for static site generators (Astro, Next.js, Vite) to Easypanel using Docker containers.
---
## Capabilities
-**Dockerfile Generation** - Optimized multi-stage Dockerfile
-**Easypanel Configuration** - Pre-configured for Easypanel deployment
-**Git Integration** - Auto-deploy on push
-**Environment Setup** - Environment variables and secrets
-**Domain Configuration** - Custom domain + SSL setup
-**Health Checks** - Container health monitoring
-**Resource Optimization** - Recommended resource allocation
---
## Usage
### Prerequisites
1. Easypanel instance (self-hosted or cloud)
2. Git repository with Astro/Next.js/Vite project
3. Docker installed (for local testing)
### Step 1: Prepare Project
```bash
# Navigate to project
cd your-project
# Ensure build script exists
npm run build
# Test production build
npm run preview
```
### Step 2: Add Deployment Files
Create these files in project root:
**Dockerfile:**
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
EXPOSE 4321
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "4321"]
```
**.dockerignore:**
```
node_modules
dist
*.log
.git
.env
```
### Step 3: Deploy to Easypanel
#### Option A: Git Repository (Recommended)
1. Login to Easypanel
2. **New Service****Git Repository**
3. Select repository and branch
4. Configure:
- **Build Command:** `npm run build`
- **Publish Directory:** `dist`
- **Port:** `4321`
5. Click **Deploy**
#### Option B: Docker Image
1. Build image:
```bash
docker build -t your-app:latest .
docker push your-registry.com/your-app:latest
```
2. Deploy on Easypanel:
- **New Service** → **Docker Image**
- Enter image URL
- Set port: `4321`
- Click **Deploy**
### Step 4: Configure Domain (Optional)
1. Go to Service Settings → **Domains**
2. Add domain: `your-domain.com`
3. Update DNS:
```
Type: CNAME
Name: @
Value: your-service.easypanel.app
```
4. Enable SSL
---
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `NODE_ENV` | No | `production` | Node environment |
| `PORT` | No | `4321` | Server port |
| `HOST` | No | `0.0.0.0` | Bind address |
---
## Resource Recommendations
| Project Size | CPU | Memory | Storage |
|--------------|-----|--------|---------|
| **Small** (< 100MB) | 0.5 vCPU | 512MB | 1GB |
| **Medium** (< 500MB) | 1 vCPU | 1GB | 2GB |
| **Large** (> 500MB) | 2 vCPU | 2GB | 5GB |
---
## Health Check
**Endpoint:** `GET /`
**Expected:** HTTP 200 OK
**Timeout:** 3 seconds
**Interval:** 30 seconds
### Manual Health Check
```bash
curl http://your-service:4321/
```
### Docker Health Check
```bash
docker inspect --format='{{.State.Health.Status}}' your-container
```
---
## Troubleshooting
### Build Fails
**Symptoms:** Build command returns error
**Solutions:**
1. Check build logs
2. Verify `package.json` scripts
3. Test locally: `npm run build`
4. Check Node version compatibility
### Container Crashes
**Symptoms:** Container exits immediately
**Solutions:**
1. Check container logs: `docker logs <container>`
2. Verify port configuration
3. Check environment variables
4. Review Dockerfile CMD instruction
### 502 Bad Gateway
**Symptoms:** Service returns 502 error
**Solutions:**
1. Verify container is running
2. Check port mapping
3. Review health check status
4. Inspect application logs
---
## Best Practices
### Security
- ✅ Use `.dockerignore` to exclude sensitive files
- ✅ Run as non-root user (add `USER node` to Dockerfile)
- ✅ Use production dependencies only
- ✅ Enable SSL for custom domains
- ✅ Regular security updates
### Performance
- ✅ Multi-stage Docker builds
- ✅ Minimize Docker image size
- ✅ Enable compression (Easypanel default)
- ✅ Use CDN for static assets
- ✅ Configure proper caching
### Monitoring
- ✅ Enable health checks
- ✅ Monitor resource usage
- ✅ Set up log aggregation
- ✅ Configure alerts for failures
- ✅ Regular backup strategy
---
## Examples
### Astro Project
```bash
# Astro specific configuration
npm create astro@latest
npm run build # Output: dist/
npm run preview # Port: 4321
```
### Next.js Project
```bash
# Next.js specific configuration
npx create-next-app
npm run build # Output: .next/
npm run start # Port: 3000
```
Update Dockerfile PORT to 3000 for Next.js.
### Vite Project
```bash
# Vite specific configuration
npm create vite@latest
npm run build # Output: dist/
npx serve dist # Port: 3000
```
---
## Integration with CI/CD
### GitHub Actions Example
```yaml
name: Deploy to Easypanel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker Image
run: docker build -t my-app:latest .
- name: Push to Registry
run: docker push registry.com/my-app:latest
- name: Deploy to Easypanel
run: |
# Use Easypanel API or CLI
easypanel deploy my-app:latest
```
---
## Resources
- **Easypanel Docs:** https://docs.easypanel.io
- **Docker Docs:** https://docs.docker.com
- **Astro Docs:** https://docs.astro.build
- **Node.js Docker Best Practices:** https://github.com/nodejs/docker-node
---
## Support
For issues or questions:
1. Check troubleshooting section
2. Review Easypanel documentation
3. Check project logs
4. Contact DevOps team
---
**Last Updated:** 2026-03-02
**Skill Version:** 1.0.0
**Status:** ✅ Production Ready

View File

@@ -1,164 +0,0 @@
# 🚀 Final Deployment Guide - Deal Plus Tech Astro
## ✅ What's Ready
### 1. Docker Image
- ✅ Built locally: `dealplustech-astro:local`
- ✅ Size: ~564MB
- ✅ Ready to push to registry
### 2. Easypanel Integration
- ✅ API token configured
- ✅ Project identified: `customerwebsite`
- ✅ Deployment script ready
### 3. Registry Options
**Option A: Easypanel Registry** (If available)
```bash
URL: 110.164.146.46:3001
Status: ⚠️ Port 3001 not accessible
```
**Option B: Docker Hub** (Recommended fallback)
```bash
URL: hub.docker.com
Status: ✅ Available
```
**Option C: Direct Server Load** (If SSH access)
```bash
Server: 110.164.146.46
Status: ⚠️ Requires SSH credentials
```
---
## 📋 Recommended Deployment Path
### Step 1: Push to Docker Hub (5 minutes)
```bash
# 1. Login to Docker Hub
docker login
# 2. Tag image
docker tag dealplustech-astro:local yourusername/dealplustech-astro:latest
# 3. Push
docker push yourusername/dealplustech-astro:latest
```
### Step 2: Create Service in Easypanel (2 minutes)
```
1. Open: http://110.164.146.46:3000
2. Project: customerwebsite
3. New Service → Docker image
4. Image: yourusername/dealplustech-astro:latest
5. Port: 4321
6. Deploy
7. Copy Service ID
```
### Step 3: Register Service ID
```bash
cd dealplustech-astro
./skills/easypanel-deploy/deploy.sh register svc_xxx...
```
### Step 4: Verify
```bash
./skills/easypanel-deploy/deploy.sh status
```
---
## 🔄 Future Updates
Once registered:
```bash
# Make changes
git pull
# Update (automatically rebuilds and pushes)
./skills/easypanel-deploy/deploy.sh update
# Redeploy in Easypanel dashboard
```
---
## 🎯 Quick Commands
```bash
# Deploy (builds + instructions)
./skills/easypanel-deploy/deploy.sh deploy
# Register service
./skills/easypanel-deploy/deploy.sh register SERVICE_ID
# Update (rebuild + push)
./skills/easypanel-deploy/deploy.sh update
# Check status
./skills/easypanel-deploy/deploy.sh status
# List projects
./skills/easypanel-deploy/deploy.sh list
```
---
## 📁 Files Created
```
dealplustech-astro/
├── skills/easypanel-deploy/
│ ├── deploy.sh ✅ v3.2 Smart deployment
│ ├── SKILL_v2.md ✅ Full documentation
│ ├── README.md ✅ Quick start
│ └── AUTOMATIC_DEPLOYMENT.md ✅ Automation details
├── DEPLOYMENT_OPTIONS.md ✅ Registry options
├── FINAL_DEPLOYMENT_GUIDE.md ✅ This file
└── easypanel.config.json ✅ Configuration
~/.easypanel/
├── credentials ✅ API token
└── state.json ✅ Service IDs
```
---
## ✅ Checklist
- [x] Docker image built
- [x] Easypanel token configured
- [x] Project identified (customerwebsite)
- [ ] Push to Docker Hub
- [ ] Create service in Easypanel
- [ ] Register service ID
- [ ] Verify deployment
---
## 🎯 Next Action
**Push to Docker Hub:**
```bash
docker login
docker tag dealplustech-astro:local yourusername/dealplustech-astro:latest
docker push yourusername/dealplustech-astro:latest
```
**Then create service in Easypanel!**
---
**Status:** Ready for Docker Hub push
**Estimated Time:** 10 minutes
**Difficulty:** Easy

View File

@@ -1,167 +0,0 @@
# 🔧 Fixes Applied - Product Tables & Responsive Fonts
## ✅ Issue 1: Product Tables Disappeared
### Problem
The product specification tables (extracted from images) were not showing on product pages in the Astro migration.
### Root Cause
- The Astro product detail page template (`[slug].astro`) was missing
- Product tables data exists in `src/data/site-config.ts` but wasn't being rendered
- Markdown files don't include table data (tables are in TypeScript file)
### Solution
Created `src/pages/products/[slug].astro` with:
1. **Table Rendering Section** - Displays all `productTables` from site-config
2. **Proper Styling** - Matches original Next.js design with:
- Table name headers
- Alternating row colors
- Responsive table containers
- Proper borders and shadows
### Code Added
```astro
{productTables.length > 0 && (
<section class="mb-12">
<h2>ตารางข้อมูลผลิตภัณฑ์</h2>
{productTables.map((table) => (
<div class="bg-white rounded-2xl...">
<h3>{table.tableName}</h3>
<table>
<thead>{table.headers.map(...)} </thead>
<tbody>{table.rows.map(...)} </tbody>
</table>
</div>
))}
</section>
)}
```
### Data Source
Tables are loaded from `src/data/site-config.ts` via `productCategories.find(p => p.id === product.data.id).productTables`
Products with tables:
- ✅ ppr-welder (1 table)
- ✅ poloplast (4 tables)
- ✅ syler (2 tables)
- ✅ xylent (3 tables)
- ✅ pvc/upvc (6 tables)
- ✅ realflex (3 tables)
---
## ✅ Issue 2: Font Too Small on Large Screens
### Problem
Text was not scaling properly on larger screens (desktop, 4K monitors).
### Root Cause
- Base font size was fixed at browser default (16px)
- No responsive scaling for larger viewports
- Tailwind classes weren't enough for very large screens
### Solution
Added responsive font scaling in `src/styles/global.css`:
```css
/* Base font size */
html {
font-size: 16px;
}
/* Scale up for larger screens */
@media (min-width: 1280px) {
html { font-size: 18px; }
}
@media (min-width: 1536px) {
html { font-size: 20px; }
}
@media (min-width: 1920px) {
html { font-size: 22px; }
}
@media (min-width: 2560px) {
html { font-size: 24px; }
}
```
### Additional Responsive Text Classes
```css
.text-responsive-sm { text-sm md:text-base lg:text-lg xl:text-xl; }
.text-responsive-base { text-base md:text-lg lg:text-xl xl:text-2xl; }
.text-responsive-lg { text-lg md:text-xl lg:text-2xl xl:text-3xl; }
.text-responsive-xl { text-xl md:text-2xl lg:text-3xl xl:text-4xl; }
```
### Updated Components
All major text elements now use responsive sizing:
- **Buttons**: `text-base md:text-lg`
- **Section titles**: `text-3xl md:text-4xl lg:text-5xl xl:text-6xl`
- **Table headers**: `text-xl md:text-2xl lg:text-3xl`
- **Table cells**: `text-base md:text-lg lg:text-xl`
- **Features/FAQ**: `text-base md:text-lg lg:text-xl`
---
## 📊 Before & After Comparison
| Screen Size | Before | After |
|-------------|--------|-------|
| **Mobile (< 768px)** | 16px base | 16px base ✓ |
| **Tablet (768-1280px)** | 16px base | 16px base ✓ |
| **Desktop (1280-1536px)** | 16px base | **18px base** ↑ |
| **Large (1536-1920px)** | 16px base | **20px base** ↑ |
| **XL (1920-2560px)** | 16px base | **22px base** ↑ |
| **4K (> 2560px)** | 16px base | **24px base** ↑ |
### Font Scaling Examples
**Product Title:**
- Before: `text-4xl` (36px fixed)
- After: `text-4xl md:text-5xl lg:text-6xl xl:text-7xl` (36px → 60px)
**Table Headers:**
- Before: `text-lg` (18px fixed)
- After: `text-xl md:text-2xl lg:text-3xl` (20px → 30px)
**Body Text:**
- Before: `text-base` (16px fixed)
- After: Responsive + base font scaling (16px → 24px)
---
## 🎯 Testing Checklist
### Product Tables
- [ ] Visit any product page (e.g., `/pp-r-pp-rct-poloplast/`)
- [ ] Scroll to "ตารางข้อมูลผลิตภัณฑ์" section
- [ ] Verify tables are visible with proper styling
- [ ] Check table headers have green background
- [ ] Check rows alternate white/gray
- [ ] Verify tables scroll horizontally on mobile
### Responsive Fonts
- [ ] Test on mobile (375px) - text should be readable
- [ ] Test on tablet (768px) - text slightly larger
- [ ] Test on desktop (1920px) - text noticeably larger
- [ ] Test on 4K (3840px) - text should be large and clear
- [ ] Check product titles scale properly
- [ ] Check table text scales properly
- [ ] Check buttons scale properly
---
## 📁 Files Modified
| File | Changes |
|------|---------|
| `src/pages/products/[slug].astro` | ✅ Created - product detail page with tables |
| `src/styles/global.css` | ✅ Updated - responsive font scaling |
---
**Status:** ✅ Both issues fixed
**Build:** ✅ Passing
**Next:** Test on actual device/screen sizes

View File

@@ -1,231 +0,0 @@
# 🚀 Gitea Repository Deployment - BEST METHOD!
## ✅ Why This is Better
| Method | Docker Registry | Gitea Repo |
|--------|----------------|------------|
| Build locally | ✅ Required | ❌ Not needed |
| Push to registry | ✅ Required | ❌ Not needed |
| Easypanel builds | ❌ No | ✅ Yes! |
| Auto-deploy on push | ❌ No | ✅ Yes! |
| Version control | ❌ No | ✅ Yes! |
| Rollbacks | ❌ Hard | ✅ Easy |
---
## 🎯 How It Works
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ You Git │ │ Easypanel │ │ Deployed │
│ Push │────▶│ Clones & │────▶│ Service │
│ to Gitea │ │ Builds │ │ Running │
└─────────────┘ └──────────────┘ └─────────────┘
```
**Easypanel will:**
1. Clone your Gitea repository
2. Run `npm install`
3. Run `npm run build`
4. Deploy the `dist/` folder
5. Serve on port 4321
---
## 📋 Setup Steps
### Step 1: Run Deploy Script
```bash
cd dealplustech-astro
./skills/easypanel-deploy/deploy.sh deploy
```
**It will show:**
```
Gitea URL: http://110.164.146.46:3001
Repository: dealplustech/dealplustech-astro
Branch: main
Build Command: npm run build
Publish Directory: dist
Port: 4321
```
### Step 2: Create Service in Easypanel
```
1. Open: http://110.164.146.46:3000
2. Select Project: customerwebsite
3. Click: New Service → Git Repository
4. Configure:
┌─────────────────────────────────────┐
│ Repository URL: │
│ http://110.164.146.46:3001/ │
│ dealplustech/dealplustech-astro │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Branch: main │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Build Command: npm run build │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Publish Directory: dist │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Port: 4321 │
└─────────────────────────────────────┘
5. Click: Deploy
```
### Step 3: Wait for Build
Easypanel will:
- ✅ Clone from Gitea (~10 seconds)
- ✅ Run `npm install` (~30 seconds)
- ✅ Run `npm run build` (~20 seconds)
- ✅ Deploy `dist/` (~5 seconds)
**Total: ~1 minute**
### Step 4: Register Service ID
After deployment completes:
```bash
# Copy Service ID from Easypanel dashboard
./skills/easypanel-deploy/deploy.sh register svc_xxx...
```
---
## 🔄 Automatic Updates
### Enable Auto-Deploy
**In Easypanel:**
1. Go to service settings
2. Click: Git → Auto Deploy
3. Enable: Auto Deploy on Push
**Then:**
```bash
# Make changes
git add .
git commit -m "Update something"
git push
# Easypanel automatically rebuilds! 🎉
```
### Manual Redeploy
```bash
./skills/easypanel-deploy/deploy.sh redeploy
```
---
## ✅ Benefits
### 1. No Docker Registry
- ❌ No `docker build`
- ❌ No `docker push`
- ❌ No registry credentials
### 2. Automatic Deployments
- ✅ Push to Gitea
- ✅ Easypanel rebuilds
- ✅ Zero manual steps
### 3. Version Control
- ✅ Every commit = potential deployment
- ✅ Easy rollbacks
- ✅ Change history
### 4. Build Caching
-`node_modules` cached
- ✅ Faster builds
- ✅ Efficient
---
## 🔧 Configuration
### Gitea Repository
```bash
URL: http://110.164.146.46:3001
Owner: dealplustech
Repo: dealplustech-astro
Branch: main
```
### Build Settings
```bash
Build Command: npm run build
Publish Directory: dist/
Node Version: 20 (from .nvmrc or package.json)
```
### Environment Variables
Add in Easypanel service settings:
```bash
NODE_ENV=production
NEXT_PUBLIC_SITE_URL=https://your-domain.com
```
---
## 📁 Required Files
Your repository should have:
```
dealplustech-astro/
├── package.json ✅ Required (defines build)
├── Dockerfile ⚠️ Optional (for Docker mode)
├── astro.config.mjs ✅ Required
├── src/ ✅ Required
└── .gitignore ✅ Required
```
---
## 🎯 Next Action
**Deploy now:**
```bash
# 1. Run deployment script
./skills/easypanel-deploy/deploy.sh deploy
# 2. Follow instructions to create service in Easypanel
# 3. After deployment:
./skills/easypanel-deploy/deploy.sh register SERVICE_ID
# 4. Enable auto-deploy in Easypanel
# 5. Future updates: just git push!
```
---
**This is THE BEST way to deploy!** 🎉
- No Docker needed
- No registry needed
- Automatic deployments
- Full version control

View File

@@ -1,249 +0,0 @@
# 🎉 Deal Plus Tech - Astro Migration COMPLETE
**Migration Date:** 2026-03-02
**Status:** ✅ Core Infrastructure & Content Migration Complete
**Build Status:** ✅ Passing
**Files Created:** 1,200+ lines of code
---
## ✅ COMPLETED (7/11 tasks)
### Phase 1: Foundation ✅
1. ✅ Codebase Analysis - Mapped Next.js structure
2. ✅ Astro Project Setup - Created with Tailwind 4
3. ✅ Theme Migration - Industrial design system
### Phase 2: Core Components ✅
4. ✅ Layouts - Header, Footer, BaseLayout (384 lines)
5. ✅ Product Data - Content Collections schema
6. ✅ Product Pages - 6 products migrated + templates
### Phase 3: Content Systems ✅
7.**Blog System** - Full migration with 3 posts
---
## 📁 FINAL FILE STRUCTURE
```
dealplustech-astro/
├── src/
│ ├── components/
│ │ ├── Header.astro ✅ 223 lines (mobile menu + JS)
│ │ ├── Footer.astro ✅ 115 lines
│ │ ├── ProductCard.astro ✅ 38 lines
│ │ └── BlogCard.astro ✅ 53 lines
│ ├── layouts/
│ │ └── BaseLayout.astro ✅ 46 lines (SEO + Thai support)
│ ├── pages/
│ │ ├── products/
│ │ │ ├── index.astro ✅ Product listing
│ │ │ └── [slug].astro ✅ Dynamic pages
│ │ └── blog/
│ │ ├── index.astro ✅ Blog listing
│ │ └── [slug].astro ✅ Blog posts
│ ├── content/
│ │ ├── config.ts ✅ Products + Blog schemas
│ │ ├── products/ ✅ 6 products
│ │ │ ├── ppr-elephant.md
│ │ │ ├── thai-ppr.md
│ │ │ ├── poloplast.md
│ │ │ ├── hdpe.md
│ │ │ ├── syler.md
│ │ │ └── xylent.md
│ │ └── blog/ ✅ 3 posts (copied from Next.js)
│ ├── data/
│ │ ├── site-config.ts ✅ 116 lines (nav + config)
│ │ └── utils.ts ✅ 43 lines (helpers)
│ └── styles/
│ └── global.css ✅ 114 lines (theme)
└── dist/ ✅ Built output
```
---
## 📊 MIGRATION METRICS
| Category | Next.js | Astro | Status |
|----------|---------|-------|--------|
| **Layouts** | 3 React | 3 Astro | ✅ Complete |
| **Products** | 36 in config | 6 migrated | ✅ MVP Ready |
| **Blog** | 3 posts | 3 migrated | ✅ Complete |
| **Components** | 8 React | 4 Astro | ✅ Core done |
| **Theme** | Tailwind 3 | Tailwind 4 | ✅ Upgraded |
| **Build Size** | ~2.5MB | ~800KB | ✅ 68% smaller |
---
## 🚀 BUILD OUTPUT
```bash
✅ Build completed in 225ms
✅ Generated routes:
- /products/index.html
- /blog/index.html
- /blog/[slug].html (3 posts)
- Products: 6 dynamic routes
```
**Build Performance:**
- **Next.js:** ~8-12 seconds
- **Astro:** ~225ms
- **Speedup:** 35-50x faster ⚡
---
## 📝 CONTENT MIGRATED
### Products (6/36 - Key Products)
1. ✅ ppr-elephant - ท่อพีพีอาร์ตราช้าง (SCG)
2. ✅ thai-ppr - ท่อ PPR Thai PPR
3. ✅ poloplast - ท่อ PP-R/PP-RCT POLOPLAST (Germany)
4. ✅ hdpe - ท่อ HDPE
5. ✅ syler - ท่อไซเลอร์ (Fire Protection)
6. ✅ xylent - ท่อระบายน้ำ 3 ชั้น XYLENT (Silent)
### Blog Posts (3/3)
1. ✅ ข้อดี-ท่อ-hdpe.md
2. ✅ ท่อ-ppr-คืออะไร.md
3. ✅ บำรุงรักษาปั๊มน้ำ.md
---
## ⏳ REMAINING WORK (4/11 tasks)
### High Priority
8. ⏳ Homepage & Static Pages (About, Contact, Services, etc.)
9. ⏳ FloatingContact widget (React island)
### Medium Priority
10. ⏳ Easypanel deployment setup
### Low Priority
11. ⏳ Create Easypanel skill
---
## 🎯 PRODUCTION READINESS
| Requirement | Status | Notes |
|-------------|--------|-------|
| **Product Showcase** | ✅ Ready | 6 key products migrated |
| **Blog System** | ✅ Ready | All posts migrated |
| **Mobile Responsive** | ✅ Ready | Header/Footer tested |
| **SEO** | ✅ Ready | Metadata + schema ready |
| **Performance** | ✅ Excellent | 35-50x faster build |
| **Thai Language** | ✅ Ready | Full support |
**MVP Status: ✅ READY FOR DEPLOYMENT**
---
## 📈 PERFORMANCE GAINS
### Build Performance
- **Next.js:** 8-12s
- **Astro:** 225ms
- **Improvement:** ⚡ **35-50x faster**
### Bundle Size
- **Next.js:** ~2.5MB (React + dependencies)
- **Astro:** ~800KB (zero JS by default)
- **Reduction:** 📉 **68% smaller**
### Runtime Performance
- **Next.js:** React hydration required
- **Astro:** Zero JS (static HTML)
- **Improvement:** ⚡ **Instant load**
---
## 🔧 TECHNICAL DECISIONS
### Why Astro?
1. **Zero JS by default** - Better performance
2. **Content Collections** - Type-safe content
3. **Markdown support** - Native blog integration
4. **Smaller bundles** - Faster loading
5. **Better SEO** - Pre-rendered HTML
### Migration Strategy
- **Content Collections** - All products/blog as Markdown
- **Static Pre-rendering** - All pages pre-built
- **Progressive Enhancement** - Add JS only where needed
- **Island Architecture** - React only for FloatingContact
---
## 📋 NEXT STEPS FOR USER
### Option 1: Deploy Now (Recommended)
The MVP is ready with:
- ✅ Product showcase (6 products)
- ✅ Blog system (3 posts)
- ✅ Mobile responsive
- ✅ SEO optimized
**Deploy to Easypanel and test!**
### Option 2: Complete Remaining
- Migrate remaining 30 products (copy-paste from Next.js)
- Create homepage
- Add FloatingContact widget
**Estimated time: 2-3 hours**
### Option 3: Hybrid
Deploy MVP now, complete content migration incrementally.
---
## 🎓 LESSONS LEARNED
### What Worked Well
1. **Content Collections** - Perfect for product catalogs
2. **Markdown migration** - Straightforward copy-paste
3. **Tailwind 4** - Works seamlessly with Astro
4. **Mobile menu** - Vanilla JS in Astro components
### Challenges
1. **Import paths** - Relative path resolution in Astro
2. **getStaticPaths** - Required for dynamic routes
3. **Blog schema** - Flexible field names (category/categories)
### Best Practices
1. **Start with schema** - Define content structure first
2. **Test builds early** - Catch path issues quickly
3. **Use Markdown** - Content is easier to manage
4. **Progressive enhancement** - Add JS only when needed
---
## 📞 SUPPORT
**Migration Documentation:** `dealplustech-astro/MIGRATION_STATUS.md`
**Build Logs:** Check `dist/` folder
**Content:** `src/content/` directory
---
**Last Updated:** 2026-03-02 09:18 AM
**Build Status:** ✅ Passing
**Migration Progress:** 64% Complete (7/11 tasks)
---
## 🚀 READY TO DEPLOY!
The Astro migration is **production-ready** for MVP launch.
**Key Features Working:**
- ✅ Product showcase with 6 key products
- ✅ Blog system with all 3 posts
- ✅ Mobile-responsive layouts
- ✅ SEO-optimized pages
- ✅ Thai language support
- ✅ Industrial theme
**Deploy to Easypanel and test!** 🎉

View File

@@ -1,199 +0,0 @@
# Deal Plus Tech - Astro Migration Status
**Generated:** 2026-03-02
**Migration Approach:** Full rewrite (Option C - Focused)
---
## ✅ COMPLETED (4/11 tasks)
### 1. ✅ Project Setup
- Created Astro project: `dealplustech-astro/`
- Tailwind 4 configured
- TypeScript strict mode enabled
- Kanit font support configured
### 2. ✅ Theme Migration
- Industrial theme colors (Primary green, Secondary slate, Accent yellow)
- Custom shadows (card, industrial, bold)
- All component classes migrated (btn-primary, card, section-title, etc.)
- 70 lines of custom CSS
### 3. ✅ Core Infrastructure
- **Content Collections** configured with full product schema
- **BaseLayout.astro** - HTML shell with Thai SEO
- **Header.astro** - Fully functional with mobile menu
- **Footer.astro** - Complete with all sections
- **utils.ts** - Helper functions (cn, formatPrice, etc.)
- **site-config.ts** - Navigation + company info
### 4. ✅ Example Product
- **ppr-elephant.md** - Full migration example (161 lines)
- Demonstrates Markdown frontmatter + content structure
- All fields mapped from Next.js structure
---
## 📁 CREATED FILES
```
dealplustech-astro/
├── src/
│ ├── components/
│ │ ├── Header.astro ✅ 223 lines (mobile menu + JS)
│ │ └── Footer.astro ✅ 115 lines
│ ├── layouts/
│ │ └── BaseLayout.astro ✅ 46 lines
│ ├── pages/
│ │ └── products/
│ │ ├── index.astro ✅ Product listing
│ │ └── [slug].astro ✅ Dynamic product pages
│ ├── content/
│ │ ├── config.ts ✅ Product schema
│ │ └── products/
│ │ └── ppr-elephant.md ✅ Example product
│ ├── data/
│ │ ├── site-config.ts ✅ 116 lines
│ │ └── README.md ⏳ Pending
│ ├── lib/
│ │ └── utils.ts ✅ 43 lines
│ └── styles/
│ └── global.css ✅ 114 lines (theme)
└── astro.config.mjs ✅ Tailwind 4
```
**Total: ~850 lines of code created**
---
## ⏳ REMAINING WORK (7/11 tasks)
### 5. ⏳ Migrate Product Data (36 products)
**Status:** 1/36 complete (ppr-elephant)
**Remaining:** 35 products to convert to Markdown
| Priority | Product ID | Status |
|----------|------------|--------|
| ✅ Done | ppr-elephant | Complete |
| ⏳ High | thai-ppr | Pending |
| ⏳ High | poloplast | Pending |
| ⏳ High | hdpe | Pending |
| ⏳ High | pvc | Pending |
| ⏳ Medium | syler | Pending |
| ⏳ Medium | xylent | Pending |
| ⏳ Medium | realflex | Pending |
| ⏳ Low | (27 more products) | Pending |
**Estimate:** 4-6 hours (10 min/product)
---
### 6. ⏳ Migrate Static Pages
**Status:** 0/7 pages
| Page | Next.js Route | Status |
|------|---------------|--------|
| Homepage | `/` | ⏳ Pending |
| About Us | `/about-us/` | ⏳ Pending |
| Contact Us | `/contact-us/` | ⏳ Pending |
| Portfolio | `/all-projects/` | ⏳ Pending |
| Services | `/services/` | ⏳ Pending |
| Sales Engineer | `/sales-engineer/` | ⏳ Pending |
| Join Us | `/join-us/` | ⏳ Pending |
**Estimate:** 2-3 hours
---
### 7. ⏳ Migrate Blog System
**Status:** Not started
**Tasks:**
- [ ] Create blog collection schema
- [ ] Migrate WordPress integration
- [ ] Create blog listing page
- [ ] Create blog post template
**Estimate:** 2-3 hours
---
### 8. ⏳ Migrate FloatingContact Widget
**Status:** Not started
**Current:** React component (client-side floating widget)
**Migration:** Keep as React island with `client:load`
**Estimate:** 1 hour
---
### 9. ⏳ Easypanel Deployment Setup
**Status:** Not started
**Tasks:**
- [ ] Create Dockerfile
- [ ] Configure build command
- [ ] Setup environment variables
- [ ] Test deployment
**Estimate:** 1-2 hours
---
### 10. ⏳ Create Easypanel Skill
**Status:** Not started
**Estimate:** 2-3 hours
---
## 📊 OVERALL PROGRESS
| Category | Progress | Status |
|----------|----------|--------|
| **Infrastructure** | 100% | ✅ Complete |
| **Layouts** | 100% | ✅ Complete |
| **Products** | 3% | ⏳ 1/36 done |
| **Static Pages** | 0% | ⏳ Pending |
| **Blog** | 0% | ⏳ Pending |
| **Deployment** | 0% | ⏳ Pending |
**Overall: ~36% complete** (infrastructure done, content migration in progress)
---
## 🚀 RECOMMENDED NEXT STEPS
1. **Migrate 5 key products** (PPR, HDPE, PVC, Syler, POLOPLAST) - 1 hour
2. **Create homepage** - 1 hour
3. **Setup deployment** - 1 hour
4. **Test and verify** - 1 hour
**Then:** Migrate remaining products and blog incrementally
---
## 🎯 PRODUCTION READY WHEN
- [ ] 5 core products migrated
- [ ] Homepage + About + Contact pages done
- [ ] Deployment configured and tested
- [ ] All links working
- [ ] SEO metadata verified
**Estimated time to MVP:** 4-6 hours
---
## 📝 NOTES
- Product data conversion is straightforward (copy frontmatter, paste content)
- All styling preserved and working
- Mobile menu tested and functional
- Thai language fully supported
- SEO schema ready to implement
---
**Last Updated:** 2026-03-02 09:00 AM

View File

@@ -1 +0,0 @@
web: npm run preview -- --host 0.0.0.0 --port $PORT

View File

@@ -1,43 +0,0 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View File

@@ -1,11 +0,0 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({
vite: {
plugins: [tailwindcss()]
}
});

View File

@@ -1,101 +0,0 @@
#!/bin/bash
# Easypanel Deployment Script for Deal Plus Tech Astro
# Usage: ./deploy-easypanel.sh [api-token]
set -e
EASYPANEL_URL="http://110.164.146.46:3000"
APP_NAME="dealplustech-astro"
PROJECT_NAME="dealplustech"
DOCKER_IMAGE="dealplustech-astro:latest"
PORT=4321
API_TOKEN="${1:-$EASYPANEL_API_TOKEN}"
echo "============================================================"
echo "🚀 Deploying to Easypanel"
echo "============================================================"
# Check if API token is provided
if [ -z "$API_TOKEN" ]; then
echo "⚠️ No API token provided"
echo ""
echo "📋 Manual Deployment Steps:"
echo "1. Open Easypanel: $EASYPANEL_URL"
echo "2. Create/Select project: $PROJECT_NAME"
echo "3. Click 'New Service' → 'Docker Image'"
echo "4. Enter image: $DOCKER_IMAGE"
echo "5. Set port: $PORT"
echo "6. Click 'Deploy'"
echo ""
echo "💡 To enable automated deployment:"
echo " 1. Get API token from Easypanel (Settings → API)"
echo " 2. Run: export EASYPANEL_API_TOKEN='your-token'"
echo " 3. Run this script again: ./deploy-easypanel.sh"
exit 0
fi
# Build Docker image
echo ""
echo "🐳 Building Docker image..."
cd dealplustech-astro
docker build -t $DOCKER_IMAGE . || {
echo "❌ Docker build failed"
exit 1
}
echo "✅ Docker image built: $DOCKER_IMAGE"
# Deploy via API
echo ""
echo "🚀 Deploying to Easypanel via API..."
# Create deployment payload
cat > /tmp/deploy.json << EOF
{
"projectName": "$PROJECT_NAME",
"name": "$APP_NAME",
"type": "docker",
"docker": {
"image": "$DOCKER_IMAGE",
"port": $PORT
},
"env": {
"NODE_ENV": "production",
"PORT": "$PORT",
"HOST": "0.0.0.0"
}
}
EOF
# Make API call
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$EASYPANEL_URL/api/trpc/services.app.create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_TOKEN" \
-d @/tmp/deploy.json \
--insecure)
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | head -n-1)
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
echo "✅ Service created successfully"
echo "$BODY" | python3 -m json.tool 2>/dev/null || echo "$BODY"
# Trigger deployment
echo ""
echo "🔄 Triggering deployment..."
# Would need service ID from previous response
echo ""
echo "============================================================"
echo "🎉 DEPLOYMENT COMPLETE!"
echo "============================================================"
echo ""
echo "📍 Service: $APP_NAME"
echo "🔗 Dashboard: $EASYPANEL_URL"
echo ""
echo "💡 Check deployment status in Easypanel dashboard"
else
echo "❌ Deployment failed (HTTP $HTTP_CODE)"
echo "$BODY"
exit 1
fi

View File

@@ -1,3 +0,0 @@
const onRequest = (_, next) => next();
export { onRequest };

File diff suppressed because one or more lines are too long

View File

@@ -1,112 +0,0 @@
<!DOCTYPE html><html lang="th"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="Astro v5.18.0"><meta name="description" content="ท่อ HDPE (High Density Polyethylene) เป็นท่อที่ได้รับความนิยมสูงในงานระบบน้ำ เนื่องจากความทนทานและความยืดหยุ่นที่เหนือกว่าท่อชนิดอื่น"><!-- Favicon --><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="alternate icon" href="/favicon.ico" sizes="any"><link rel="apple-touch-icon" href="/favicon.svg"><!-- Google Fonts: Kanit for Thai --><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet"><!-- SEO --><meta property="og:title" content="ข้อดีของท่อ HDPE ในงานระบบน้ำ ทำไมถึงเป็นตัวเลือกยอดนิยม"><meta property="og:description" content="ท่อ HDPE (High Density Polyethylene) เป็นท่อที่ได้รับความนิยมสูงในงานระบบน้ำ เนื่องจากความทนทานและความยืดหยุ่นที่เหนือกว่าท่อชนิดอื่น"><meta property="og:image" content="/og-image.jpg"><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><title>ข้อดีของท่อ HDPE ในงานระบบน้ำ ทำไมถึงเป็นตัวเลือกยอดนิยม | ดีล พลัส เทค</title><style>html{font-family:Kanit,system-ui,sans-serif}
</style></head> <body class="flex flex-col min-h-screen"> <main class="pt-32 pb-16"> <article class="container mx-auto px-4 max-w-4xl"> <!-- Header --> <header class="mb-8"> <div class="flex items-center gap-4 mb-4"> <span class="industrial-badge">ท่อ HDPE</span> <time class="text-secondary-500"> 10 มกราคม 2567 </time> <span class="text-secondary-500"></span> <span class="text-secondary-500">Deal Plus Tech</span> </div> <h1 class="text-4xl md:text-5xl font-bold text-secondary-900 mb-4"> ข้อดีของท่อ HDPE ในงานระบบน้ำ ทำไมถึงเป็นตัวเลือกยอดนิยม </h1> </header> <!-- Featured Image --> <div class="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden mb-8"> <img src="/images/2021/03/hdpe-pipe_000C.jpg" alt="ข้อดีของท่อ HDPE ในงานระบบน้ำ ทำไมถึงเป็นตัวเลือกยอดนิยม" class="object-cover w-full h-full" loading="lazy"> </div> <!-- Content --> <div class="prose prose-lg max-w-none prose-headings:font-bold prose-a:text-primary-600 hover:prose-a:text-primary-700 prose-img:rounded-xl"> <h2 id="ท่อ-hdpe-คืออะไร">ท่อ HDPE คืออะไร?</h2>
<p>ท่อ HDPE (High Density Polyethylene) หรือท่อเอชดีพีอี เป็นท่อที่ผลิตจากโพลิเอทิลีนความหนาแน่นสูง เป็นวัสดุพลาสติกที่มีความแข็งแรงและทนทานเป็นอย่างมาก</p>
<h2 id="ข้อดีของท่อ-hdpe">ข้อดีของท่อ HDPE</h2>
<h3 id="1-ความยืดหยุ่นสูง">1. ความยืดหยุ่นสูง</h3>
<p>ท่อ HDPE สามารถโค้งงอได้ถึง 45 องศา ทำให้เหมาะสำหรับพื้นที่ติดตั้งจำกัด และสามารถรองรับการเคลื่อนไหวของดินได้ดี</p>
<h3 id="2-ทนทานต่อสารเคมี">2. ทนทานต่อสารเคมี</h3>
<p>ท่อ HDPE ทนทานต่อการกัดกร่อนของสารเคมี กรด และด่าง ทำให้เหมาะสำหรับงานอุตสาหกรรม</p>
<h3 id="3-อายุการใช้งานยาวนาน">3. อายุการใช้งานยาวนาน</h3>
<p>ท่อ HDPE มีอายุการใช้งานมากกว่า 50 ปี เมื่อติดตั้งและใช้งานอย่างถูกต้อง</p>
<h3 id="4-น้ำหนักเบา">4. น้ำหนักเบา</h3>
<p>ท่อ HDPE มีน้ำหนักเบากว่าท่อโลหะ ทำให้ง่ายต่อการขนส่งและติดตั้ง</p>
<h3 id="5-การเชื่อมต่อที่แน่นหนา">5. การเชื่อมต่อที่แน่นหนา</h3>
<p>การเชื่อมท่อ HDPE ด้วยวิธี Butt Fusion ทำให้ท่อเชื่อมต่อกันเป็นเนื้อเดียว ไม่มีรอยต่อ ป้องกันการรั่วซึม</p>
<h3 id="6-ปลอดภัยต่อสุขภาพ">6. ปลอดภัยต่อสุขภาพ</h3>
<p>ท่อ HDPE ไม่เป็นสนิม ไม่ปล่อยสารพิษ ปลอดภัยสำหรับน้ำดื่ม</p>
<h2 id="การใช้งานท่อ-hdpe">การใช้งานท่อ HDPE</h2>
<h3 id="งานประปา">งานประปา</h3>
<ul>
<li>ท่อส่งน้ำประปา</li>
<li>ระบบประปาในบ้านเรือน</li>
<li>ระบบประปาในอาคาร</li>
</ul>
<h3 id="งานเกษตร">งานเกษตร</h3>
<ul>
<li>ระบบน้ำหยด</li>
<li>ระบบสปริงเกลอร์</li>
<li>ระบบน้ำเพื่อการเกษตร</li>
</ul>
<h3 id="งานอุตสาหกรรม">งานอุตสาหกรรม</h3>
<ul>
<li>ท่อส่งสารเคมี</li>
<li>ระบบบำบัดน้ำเสีย</li>
<li>งานโรงงานอุตสาหกรรม</li>
</ul>
<h3 id="งานโครงสร้างพื้นฐาน">งานโครงสร้างพื้นฐาน</h3>
<ul>
<li>งานท่อใต้ดิน</li>
<li>ท่อร้อยสายไฟ</li>
<li>งานสาธารณูปโภค</li>
</ul>
<h2 id="ขนาดท่อ-hdpe-ที่นิยมใช้">ขนาดท่อ HDPE ที่นิยมใช้</h2>
<table><thead><tr><th>ขนาด (มม.)</th><th>การใช้งาน</th></tr></thead><tbody><tr><td>16-32</td><td>งานประปาภายในบ้าน</td></tr><tr><td>40-63</td><td>งานประปาอาคารขนาดเล็ก</td></tr><tr><td>75-110</td><td>งานประปาอาคารขนาดใหญ่</td></tr><tr><td>125-315</td><td>งานท่อส่งน้ำหลัก</td></tr><tr><td>355-1200</td><td>งานโครงสร้างพื้นฐาน</td></tr></tbody></table>
<h2 id="เกรดของท่อ-hdpe">เกรดของท่อ HDPE</h2>
<h3 id="pe80">PE80</h3>
<ul>
<li>เหมาะสำหรับงานทั่วไป</li>
<li>ทนแรงดันสูงสุด 8 MPa</li>
</ul>
<h3 id="pe100">PE100</h3>
<ul>
<li>เหมาะสำหรับงานที่ต้องการความแข็งแรงสูง</li>
<li>ทนแรงดันสูงสุด 10 MPa</li>
<li>เป็นเกรดที่นิยมใช้ในปัจจุบัน</li>
</ul>
<h2 id="การติดตั้งท่อ-hdpe">การติดตั้งท่อ HDPE</h2>
<h3 id="วิธี-butt-fusion">วิธี Butt Fusion</h3>
<ol>
<li>ตัดท่อให้ตรง</li>
<li>ทำความสะอาดผิวท่อ</li>
<li>ใช้เครื่องเชื่อมท่อ HDPE</li>
<li>ให้ความร้อนจนผิวท่อละลาย</li>
<li>กดท่อเข้าด้วยกัน</li>
<li>รอให้เย็นตัวลง</li>
</ol>
<h3 id="วิธี-electrofusion">วิธี Electrofusion</h3>
<ol>
<li>ใช้ข้อต่อแบบ Electrofusion</li>
<li>เสียบปลั๊กไฟเข้ากับข้อต่อ</li>
<li>รอจนกระบวนการเชื่อมเสร็จสิ้น</li>
</ol>
<h2 id="สรุป">สรุป</h2>
<p>ท่อ HDPE เป็นตัวเลือกที่ยอดเยี่ยมสำหรับงานระบบน้ำ เนื่องจากมีความทนทาน ความยืดหยุ่น และอายุการใช้งานที่ยาวนาน ไม่ว่าจะเป็นงานประปา งานเกษตร หรืองานอุตสาหกรรม ท่อ HDPE สามารถตอบโจทย์ได้ทุกการใช้งาน</p>
<hr>
<p><strong>สนใจสินค้าท่อ HDPE?</strong>
ติดต่อเราได้ที่:</p>
<ul>
<li>โทร: 090-555-1415</li>
<li>LINE: jppselection</li>
</ul>
<p><a href="/%E0%B8%97%E0%B9%88%E0%B8%ADhdpe">ดูสินค้าท่อ HDPE ทั้งหมด</a></p> </div> <!-- Back to Blog --> <div class="mt-12 pt-8 border-t border-secondary-200"> <a href="/blog/" class="inline-flex items-center text-primary-600 font-medium hover:text-primary-700 transition-colors"> <svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path> </svg>
กลับสู่หน้าบทความ
</a> </div> </article> </main> </body></html>

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html><html lang="th"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="Astro v5.18.0"><meta name="description" content="ท่อ PPR (Polypropylene Random Copolymer) เป็นท่อพลาสติกที่ได้รับความนิยมสูงในการใช้งานระบบประปา บทความนี้จะอธิบายทุกสิ่งที่คุณต้องรู้เกี่ยวกับท่อ PPR"><!-- Favicon --><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="alternate icon" href="/favicon.ico" sizes="any"><link rel="apple-touch-icon" href="/favicon.svg"><!-- Google Fonts: Kanit for Thai --><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet"><!-- SEO --><meta property="og:title" content="ท่อ PPR คืออะไร? คู่มือฉบับสมบูรณ์สำหรับการเลือกใช้งาน"><meta property="og:description" content="ท่อ PPR (Polypropylene Random Copolymer) เป็นท่อพลาสติกที่ได้รับความนิยมสูงในการใช้งานระบบประปา บทความนี้จะอธิบายทุกสิ่งที่คุณต้องรู้เกี่ยวกับท่อ PPR"><meta property="og:image" content="/og-image.jpg"><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><title>ท่อ PPR คืออะไร? คู่มือฉบับสมบูรณ์สำหรับการเลือกใช้งาน | ดีล พลัส เทค</title><style>html{font-family:Kanit,system-ui,sans-serif}
</style></head> <body class="flex flex-col min-h-screen"> <main class="pt-32 pb-16"> <article class="container mx-auto px-4 max-w-4xl"> <!-- Header --> <header class="mb-8"> <div class="flex items-center gap-4 mb-4"> <span class="industrial-badge">ท่อ PPR</span> <time class="text-secondary-500"> 15 มกราคม 2567 </time> <span class="text-secondary-500"></span> <span class="text-secondary-500">Deal Plus Tech</span> </div> <h1 class="text-4xl md:text-5xl font-bold text-secondary-900 mb-4"> ท่อ PPR คืออะไร? คู่มือฉบับสมบูรณ์สำหรับการเลือกใช้งาน </h1> </header> <!-- Featured Image --> <div class="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden mb-8"> <img src="/images/2021/03/ppr-pipe_000C.jpg" alt="ท่อ PPR คืออะไร? คู่มือฉบับสมบูรณ์สำหรับการเลือกใช้งาน" class="object-cover w-full h-full" loading="lazy"> </div> <!-- Content --> <div class="prose prose-lg max-w-none prose-headings:font-bold prose-a:text-primary-600 hover:prose-a:text-primary-700 prose-img:rounded-xl"> <h2 id="ท่อ-ppr-คืออะไร">ท่อ PPR คืออะไร?</h2>
<p>ท่อ PPR (Polypropylene Random Copolymer) หรือท่อพีพีอาร์ เป็นท่อพลาสติกที่ผลิตจากเม็ดพลาสติก PP-R 80 (Polypropylene Random Copolymer 80) ซึ่งเป็นวัสดุพลาสติกคุณภาพสูงที่มีความแข็งแรงและทนทานเป็นอย่างดี</p>
<h2 id="ข้อดีของท่อ-ppr">ข้อดีของท่อ PPR</h2>
<h3 id="1-ทนแรงดันและอุณหภูมิสูง">1. ทนแรงดันและอุณหภูมิสูง</h3>
<p>ท่อ PPR สามารถทนแรงดันได้สูงถึง 20 บาร์ และทนต่ออุณหภูมิได้สูงถึง 95°C ทำให้เหมาะสำหรับใช้งานทั้งระบบน้ำเย็นและน้ำร้อน</p>
<h3 id="2-สะอาดและปลอดภัย">2. สะอาดและปลอดภัย</h3>
<p>ท่อ PPR ไม่เป็นสนิม ปราศจากโลหะหนักและสิ่งปนเปื้อน ทำให้น้ำที่ไหลผ่านสะอาดและปลอดภัยต่อการบริโภค</p>
<h3 id="3-อายุการใช้งานยาวนาน">3. อายุการใช้งานยาวนาน</h3>
<p>ด้วยคุณสมบัติที่ทนทาน ท่อ PPR มีอายุการใช้งานยาวนานกว่า 50 ปี</p>
<h3 id="4-ติดตั้งง่าย">4. ติดตั้งง่าย</h3>
<p>การเชื่อมต่อท่อ PPR ใช้วิธีเชื่อมด้วยความร้อน ทำให้ท่อและข้อต่อเป็นเนื้อเดียวกัน ไม่มีปัญหารั่วซึม</p>
<h3 id="5-ประหยัดพลังงาน">5. ประหยัดพลังงาน</h3>
<p>ท่อ PPR เป็นฉนวนกันความร้อนที่ดี ช่วยรักษาอุณหภูมิของน้ำได้ดีกว่าท่อโลหะ</p>
<h2 id="การเลือกท่อ-ppr-ที่เหมาะสม">การเลือกท่อ PPR ที่เหมาะสม</h2>
<h3 id="ขนาดท่อ">ขนาดท่อ</h3>
<p>เลือกขนาดท่อให้เหมาะสมกับปริมาณน้ำที่ต้องการใช้งาน:</p>
<ul>
<li>ท่อขนาด 20-25 มม. เหมาะสำหรับบ้านเรือนทั่วไป</li>
<li>ท่อขนาด 32-63 มม. เหมาะสำหรับอาคารขนาดใหญ่</li>
</ul>
<h3 id="เกรดของท่อ">เกรดของท่อ</h3>
<ul>
<li><strong>PN10</strong> - สำหรับน้ำเย็น ทนแรงดัน 10 บาร์</li>
<li><strong>PN16</strong> - สำหรับน้ำอุ่น ทนแรงดัน 16 บาร์</li>
<li><strong>PN20</strong> - สำหรับน้ำร้อน ทนแรงดัน 20 บาร์</li>
</ul>
<h2 id="การติดตั้งท่อ-ppr">การติดตั้งท่อ PPR</h2>
<h3 id="ขั้นตอนการเชื่อมท่อ">ขั้นตอนการเชื่อมท่อ</h3>
<ol>
<li>ตัดท่อให้ตรงและเรียบ</li>
<li>ทำความสะอาดผิวท่อและข้อต่อ</li>
<li>ใช้เครื่องเชื่อมท่ออุณหภูมิ 260°C</li>
<li>สอดท่อและข้อต่อเข้าด้วยกัน</li>
<li>รอให้เย็นตัวลงประมาณ 2-3 นาที</li>
</ol>
<h3 id="ข้อควรระวัง">ข้อควรระวัง</h3>
<ul>
<li>หลีกเลี่ยงการติดตั้งในพื้นที่ที่มีแสงแดดโดยตรง</li>
<li>ควรทิ้งระยะห่างสำหรับการขยายตัวของท่อ</li>
<li>ตรวจสอบความร้อนของเครื่องเชื่อมก่อนใช้งาน</li>
</ul>
<h2 id="ท่อ-ppr-ตราช้าง">ท่อ PPR ตราช้าง</h2>
<p>ท่อ PPR ตราช้าง เป็นท่อ PPR คุณภาพสูงที่ผลิตจากเม็ดพลาสติก PP-R 80 วัตถุดิบคุณภาพสูงมาตรฐานยุโรปจาก lyondellbasell</p>
<p><strong>คุณสมบัติเด่น:</strong></p>
<ul>
<li>ทนแรงดันได้สูงสุด 20 บาร์</li>
<li>ทนต่ออุณหภูมิได้สูงถึง 95°C</li>
<li>ผลิตตามมาตรฐาน DIN8077 และ DIN8078 ของประเทศเยอรมัน</li>
<li>รับประกันคุณภาพ</li>
</ul>
<h2 id="สรุป">สรุป</h2>
<p>ท่อ PPR เป็นตัวเลือกที่ดีสำหรับระบบประปาในปัจจุบัน เนื่องจากมีความทนทานสูง ติดตั้งง่าย และมีอายุการใช้งานยาวนาน หากคุณกำลังมองหาท่อสำหรับงานระบบน้ำ ท่อ PPR เป็นตัวเลือกที่คุ้มค่าและเหมาะสม</p>
<hr>
<p><strong>สนใจสินค้าท่อ PPR?</strong>
ติดต่อเราได้ที่:</p>
<ul>
<li>โทร: 090-555-1415</li>
<li>LINE: jppselection</li>
<li>อีเมล: <a href="mailto:dealplustech@gmail.com">dealplustech@gmail.com</a></li>
</ul>
<p><a href="/%E0%B8%97%E0%B9%88%E0%B8%AD%E0%B8%9E%E0%B8%B5%E0%B8%9E%E0%B8%B5%E0%B8%AD%E0%B8%B2%E0%B8%A3%E0%B9%8C%E0%B8%95%E0%B8%A3%E0%B8%B2%E0%B8%8A%E0%B9%89%E0%B8%B2%E0%B8%87">ดูสินค้าท่อ PPR ทั้งหมด</a></p> </div> <!-- Back to Blog --> <div class="mt-12 pt-8 border-t border-secondary-200"> <a href="/blog/" class="inline-flex items-center text-primary-600 font-medium hover:text-primary-700 transition-colors"> <svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path> </svg>
กลับสู่หน้าบทความ
</a> </div> </article> </main> </body></html>

View File

@@ -1,156 +0,0 @@
<!DOCTYPE html><html lang="th"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="Astro v5.18.0"><meta name="description" content="ปั๊มน้ำเป็นอุปกรณ์สำคัญในระบบน้ำทุกบ้าน การบำรุงรักษาที่ถูกต้องจะช่วยยืดอายุการใช้งานและประหยัดค่าไฟฟ้า"><!-- Favicon --><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="alternate icon" href="/favicon.ico" sizes="any"><link rel="apple-touch-icon" href="/favicon.svg"><!-- Google Fonts: Kanit for Thai --><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet"><!-- SEO --><meta property="og:title" content="การบำรุงรักษาปั๊มน้ำให้มีอายุการใช้งานยาวนาน"><meta property="og:description" content="ปั๊มน้ำเป็นอุปกรณ์สำคัญในระบบน้ำทุกบ้าน การบำรุงรักษาที่ถูกต้องจะช่วยยืดอายุการใช้งานและประหยัดค่าไฟฟ้า"><meta property="og:image" content="/og-image.jpg"><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><title>การบำรุงรักษาปั๊มน้ำให้มีอายุการใช้งานยาวนาน | ดีล พลัส เทค</title><style>html{font-family:Kanit,system-ui,sans-serif}
</style></head> <body class="flex flex-col min-h-screen"> <main class="pt-32 pb-16"> <article class="container mx-auto px-4 max-w-4xl"> <!-- Header --> <header class="mb-8"> <div class="flex items-center gap-4 mb-4"> <span class="industrial-badge">ปั๊มน้ำ</span> <time class="text-secondary-500"> 5 มกราคม 2567 </time> <span class="text-secondary-500"></span> <span class="text-secondary-500">Deal Plus Tech</span> </div> <h1 class="text-4xl md:text-5xl font-bold text-secondary-900 mb-4"> การบำรุงรักษาปั๊มน้ำให้มีอายุการใช้งานยาวนาน </h1> </header> <!-- Featured Image --> <div class="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden mb-8"> <img src="/images/2021/02/Water-Pump1.jpg" alt="การบำรุงรักษาปั๊มน้ำให้มีอายุการใช้งานยาวนาน" class="object-cover w-full h-full" loading="lazy"> </div> <!-- Content --> <div class="prose prose-lg max-w-none prose-headings:font-bold prose-a:text-primary-600 hover:prose-a:text-primary-700 prose-img:rounded-xl"> <h2 id="ความสำคัญของการบำรุงรักษาปั๊มน้ำ">ความสำคัญของการบำรุงรักษาปั๊มน้ำ</h2>
<p>ปั๊มน้ำเป็นหัวใจของระบบน้ำในบ้าน การบำรุงรักษาอย่างสม่ำเสมอจะช่วย:</p>
<ul>
<li>ยืดอายุการใช้งานของปั๊มน้ำ</li>
<li>ลดปัญหาการเสีย</li>
<li>ประหยัดค่าไฟฟ้า</li>
<li>ป้องกันอุบัติเหตุจากการรั่วซึม</li>
</ul>
<h2 id="การบำรุงรักษาปั๊มน้ำแบบทำเอง">การบำรุงรักษาปั๊มน้ำแบบทำเอง</h2>
<h3 id="1-ตรวจสอบสายไฟและสวิตช์">1. ตรวจสอบสายไฟและสวิตช์</h3>
<ul>
<li>ตรวจสอบสายไฟว่ามีรอยชำรุดหรือไม่</li>
<li>ตรวจสอบสวิตช์ว่าทำงานปกติหรือไม่</li>
<li>หากพบความผิดปกติควรเรียกช่าง</li>
</ul>
<h3 id="2-ทำความสะอาดตัวกรอง">2. ทำความสะอาดตัวกรอง</h3>
<ul>
<li>ปิดวาล์วน้ำเข้าก่อนทำความสะอาด</li>
<li>ถอดตัวกรองออกมาล้าง</li>
<li>ตรวจสอบว่ามีสิ่งปนเปื้อนหรือไม่</li>
<li>ติดตั้งกลับเข้าที่เดิม</li>
</ul>
<h3 id="3-ตรวจสอบแรงดันน้ำ">3. ตรวจสอบแรงดันน้ำ</h3>
<ul>
<li>สังเกตแรงดันน้ำว่าลดลงหรือไม่</li>
<li>ตรวจสอบว่ามีเสียงผิดปกติหรือไม่</li>
<li>หากแรงดันลดลงอาจมีการรั่วซึม</li>
</ul>
<h3 id="4-ตรวจสอบถังแรงดัน-pressure-tank">4. ตรวจสอบถังแรงดัน (Pressure Tank)</h3>
<ul>
<li>ตรวจสอบว่าถังมีอากาศเพียงพอหรือไม่</li>
<li>หากปั๊มเปิด-ปิดบ่อยผิดปกติ อาจต้องเติมอากาศ</li>
<li>ควรตรวจสอบทุก 6 เดือน</li>
</ul>
<h2 id="ปัญหาที่พบบ่อยและวิธีแก้ไข">ปัญหาที่พบบ่อยและวิธีแก้ไข</h2>
<h3 id="ปั๊มไม่ทำงาน">ปั๊มไม่ทำงาน</h3>
<p><strong>สาเหตุ:</strong></p>
<ul>
<li>ไฟดับหรือสายไฟขาด</li>
<li>สวิตช์เสีย</li>
<li>มอเตอร์เสีย</li>
</ul>
<p><strong>วิธีแก้:</strong></p>
<ul>
<li>ตรวจสอบไฟและสายไฟ</li>
<li>เปลี่ยนสวิตช์</li>
<li>เรียกช่างซ่อมมอเตอร์</li>
</ul>
<h3 id="แรงดันน้ำต่ำ">แรงดันน้ำต่ำ</h3>
<p><strong>สาเหตุ:</strong></p>
<ul>
<li>ตัวกรองอุดตัน</li>
<li>ท่อรั่ว</li>
<li>ใบพัดสึกหรอ</li>
</ul>
<p><strong>วิธีแก้:</strong></p>
<ul>
<li>ทำความสะอาดตัวกรอง</li>
<li>ตรวจสอบและซ่อมท่อ</li>
<li>เปลี่ยนใบพัด</li>
</ul>
<h3 id="ปั๊มเปิด-ปิดบ่อย">ปั๊มเปิด-ปิดบ่อย</h3>
<p><strong>สาเหตุ:</strong></p>
<ul>
<li>ถังแรงดันอากาศรั่ว</li>
<li>แผ่นไดอะแฟรมแตก</li>
<li>วาล์วตรวจสอบแรงดันเสีย</li>
</ul>
<p><strong>วิธีแก้:</strong></p>
<ul>
<li>เติมอากาศในถัง</li>
<li>เปลี่ยนแผ่นไดอะแฟรม</li>
<li>เปลี่ยนวาล์ว</li>
</ul>
<h3 id="ปั๊มมีเสียงดังผิดปกติ">ปั๊มมีเสียงดังผิดปกติ</h3>
<p><strong>สาเหตุ:</strong></p>
<ul>
<li>ลูกปืนเสีย</li>
<li>ใบพัดชำรุด</li>
<li>การติดตั้งไม่แน่นหนา</li>
</ul>
<p><strong>วิธีแก้:</strong></p>
<ul>
<li>เปลี่ยนลูกปืน</li>
<li>เปลี่ยนใบพัด</li>
<li>ตรวจสอบการยึดแน่น</li>
</ul>
<h2 id="ตารางการบำรุงรักษา">ตารางการบำรุงรักษา</h2>
<table><thead><tr><th>รายการ</th><th>ความถี่</th><th>หมายเหตุ</th></tr></thead><tbody><tr><td>ตรวจสอบสายไฟ</td><td>ทุกเดือน</td><td>มองหารอยชำรุด</td></tr><tr><td>ทำความสะอาดตัวกรอง</td><td>ทุก 3 เดือน</td><td>หรือเมื่อแรงดันลด</td></tr><tr><td>ตรวจสอบถังแรงดัน</td><td>ทุก 6 เดือน</td><td>เติมอากาศหากจำเป็น</td></tr><tr><td>ตรวจสอบสวิตช์</td><td>ทุกปี</td><td>เปลี่ยนหากเสีย</td></tr><tr><td>ตรวจสอบใบพัด</td><td>ทุก 2 ปี</td><td>โดยช่างผู้เชี่ยวชาญ</td></tr></tbody></table>
<h2 id="เคล็ดลับการใช้งานปั๊มน้ำ">เคล็ดลับการใช้งานปั๊มน้ำ</h2>
<h3 id="ประหยัดไฟฟ้า">ประหยัดไฟฟ้า</h3>
<ul>
<li>เลือกขนาดปั๊มที่เหมาะสมกับการใช้งาน</li>
<li>ติดตั้งถังแรงดันขนาดเหมาะสม</li>
<li>หลีกเลี่ยงการเปิด-ปิดปั๊มบ่อย</li>
</ul>
<h3 id="ป้องกันปัญหา">ป้องกันปัญหา</h3>
<ul>
<li>อย่าให้ปั๊มแห้ง (ทำงานโดยไม่มีน้ำ)</li>
<li>ตรวจสอบรอยรั่วอย่างสม่ำเสมอ</li>
<li>ใช้ตัวกรองเพื่อป้องกันสิ่งสกปรก</li>
</ul>
<h3 id="เมื่อต้องเปลี่ยนปั๊ม">เมื่อต้องเปลี่ยนปั๊ม</h3>
<ul>
<li>เลือกปั๊มที่มีคุณภาพ</li>
<li>พิจารณาขนาดและกำลังที่เหมาะสม</li>
<li>ติดตั้งโดยช่างผู้เชี่ยวชาญ</li>
</ul>
<h2 id="สรุป">สรุป</h2>
<p>การบำรุงรักษาปั๊มน้ำอย่างสม่ำเสมอจะช่วยยืดอายุการใช้งาน ลดปัญหาการเสีย และประหยัดค่าใช้จ่ายในระยะยาว ควรตรวจสอบและบำรุงรักษาตามตารางที่กำหนด และหากพบปัญหาที่ไม่สามารถแก้ไขได้เอง ควรติดต่อช่างผู้เชี่ยวชาญ</p>
<hr>
<p><strong>ต้องการซื้อปั๊มน้ำหรืออุปกรณ์เสริม?</strong>
ติดต่อเราได้ที่:</p>
<ul>
<li>โทร: 090-555-1415</li>
<li>LINE: jppselection</li>
</ul>
<p><a href="/%E0%B8%9B%E0%B8%B1%E0%B9%8A%E0%B8%A1%E0%B8%99%E0%B9%89%E0%B8%B3-pump">ดูสินค้าปั๊มน้ำทั้งหมด</a></p> </div> <!-- Back to Blog --> <div class="mt-12 pt-8 border-t border-secondary-200"> <a href="/blog/" class="inline-flex items-center text-primary-600 font-medium hover:text-primary-700 transition-colors"> <svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path> </svg>
กลับสู่หน้าบทความ
</a> </div> </article> </main> </body></html>

View File

@@ -1,14 +0,0 @@
import { c as createComponent, d as addAttribute, h as renderHead, i as renderSlot, a as renderTemplate, b as createAstro } from './astro/server_D-JZF3a4.mjs';
import 'piccolore';
import 'clsx';
/* empty css */
const $$Astro = createAstro();
const $$BaseLayout = createComponent(($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$BaseLayout;
const { title, description, image } = Astro2.props;
return renderTemplate`<html lang="th"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator"${addAttribute(Astro2.generator, "content")}><meta name="description"${addAttribute(description || "\u0E1A\u0E23\u0E34\u0E29\u0E31\u0E17 \u0E14\u0E35\u0E25 \u0E1E\u0E25\u0E31\u0E2A \u0E40\u0E17\u0E04 \u0E08\u0E33\u0E01\u0E31\u0E14 - \u0E1C\u0E39\u0E49\u0E40\u0E0A\u0E35\u0E48\u0E22\u0E27\u0E0A\u0E32\u0E0D\u0E14\u0E49\u0E32\u0E19\u0E23\u0E30\u0E1A\u0E1A\u0E19\u0E49\u0E33 \u0E17\u0E48\u0E2D PPR \u0E15\u0E23\u0E32\u0E0A\u0E49\u0E32\u0E07 \u0E17\u0E48\u0E2D\u0E1E\u0E35\u0E1E\u0E35\u0E2D\u0E32\u0E23\u0E4C \u0E17\u0E48\u0E2D HDPE", "content")}><!-- Favicon --><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="alternate icon" href="/favicon.ico" sizes="any"><link rel="apple-touch-icon" href="/favicon.svg"><!-- Google Fonts: Kanit for Thai --><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet"><!-- SEO --><meta property="og:title"${addAttribute(title, "content")}><meta property="og:description"${addAttribute(description || "Deal Plus Tech - \u0E1C\u0E39\u0E49\u0E40\u0E0A\u0E35\u0E48\u0E22\u0E27\u0E0A\u0E32\u0E0D\u0E14\u0E49\u0E32\u0E19\u0E23\u0E30\u0E1A\u0E1A\u0E19\u0E49\u0E33", "content")}><meta property="og:image"${addAttribute(image || "/og-image.jpg", "content")}><meta property="og:type" content="website"><meta name="twitter:card" content="summary_large_image"><title>${title} | ดีล พลัส เทค</title>${renderHead()}</head> <body class="flex flex-col min-h-screen"> ${renderSlot($$result, $$slots["default"])} </body></html>`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/layouts/BaseLayout.astro", void 0);
export { $$BaseLayout as $ };

File diff suppressed because it is too large Load Diff

View File

@@ -1,469 +0,0 @@
import { escape } from 'html-escaper';
import { Traverse } from 'neotraverse/modern';
import pLimit from 'p-limit';
import { z } from 'zod';
import { removeBase, isRemotePath, prependForwardSlash } from '@astrojs/internal-helpers/path';
import { A as AstroError, R as RenderUndefinedEntryError, c as createComponent, u as unescapeHTML, a as renderTemplate, U as UnknownContentCollectionError, e as renderUniqueStylesheet, f as renderScriptElement, g as createHeadAndContent, r as renderComponent } from './astro/server_D-JZF3a4.mjs';
import 'piccolore';
import * as devalue from 'devalue';
const CONTENT_IMAGE_FLAG = "astroContentImageFlag";
const IMAGE_IMPORT_PREFIX = "__ASTRO_IMAGE_";
const VALID_INPUT_FORMATS = [
"jpeg",
"jpg",
"png",
"tiff",
"webp",
"gif",
"svg",
"avif"
];
const VALID_SUPPORTED_FORMATS = [
"jpeg",
"jpg",
"png",
"tiff",
"webp",
"gif",
"svg",
"avif"
];
const DEFAULT_OUTPUT_FORMAT = "webp";
const DEFAULT_HASH_PROPS = [
"src",
"width",
"height",
"format",
"quality",
"fit",
"position",
"background"
];
function imageSrcToImportId(imageSrc, filePath) {
imageSrc = removeBase(imageSrc, IMAGE_IMPORT_PREFIX);
if (isRemotePath(imageSrc)) {
return;
}
const ext = imageSrc.split(".").at(-1)?.toLowerCase();
if (!ext || !VALID_INPUT_FORMATS.includes(ext)) {
return;
}
const params = new URLSearchParams(CONTENT_IMAGE_FLAG);
if (filePath) {
params.set("importer", filePath);
}
return `${imageSrc}?${params.toString()}`;
}
class ImmutableDataStore {
_collections = /* @__PURE__ */ new Map();
constructor() {
this._collections = /* @__PURE__ */ new Map();
}
get(collectionName, key) {
return this._collections.get(collectionName)?.get(String(key));
}
entries(collectionName) {
const collection = this._collections.get(collectionName) ?? /* @__PURE__ */ new Map();
return [...collection.entries()];
}
values(collectionName) {
const collection = this._collections.get(collectionName) ?? /* @__PURE__ */ new Map();
return [...collection.values()];
}
keys(collectionName) {
const collection = this._collections.get(collectionName) ?? /* @__PURE__ */ new Map();
return [...collection.keys()];
}
has(collectionName, key) {
const collection = this._collections.get(collectionName);
if (collection) {
return collection.has(String(key));
}
return false;
}
hasCollection(collectionName) {
return this._collections.has(collectionName);
}
collections() {
return this._collections;
}
/**
* Attempts to load a DataStore from the virtual module.
* This only works in Vite.
*/
static async fromModule() {
try {
const data = await import('./_astro_data-layer-content_NX5tLOlp.mjs');
if (data.default instanceof Map) {
return ImmutableDataStore.fromMap(data.default);
}
const map = devalue.unflatten(data.default);
return ImmutableDataStore.fromMap(map);
} catch {
}
return new ImmutableDataStore();
}
static async fromMap(data) {
const store = new ImmutableDataStore();
store._collections = data;
return store;
}
}
function dataStoreSingleton() {
let instance = void 0;
return {
get: async () => {
if (!instance) {
instance = ImmutableDataStore.fromModule();
}
return instance;
},
set: (store) => {
instance = store;
}
};
}
const globalDataStore = dataStoreSingleton();
const __vite_import_meta_env__ = {"ASSETS_PREFIX": undefined, "BASE_URL": "/", "DEV": false, "MODE": "production", "PROD": true, "SITE": undefined, "SSR": true};
function createCollectionToGlobResultMap({
globResult,
contentDir
}) {
const collectionToGlobResultMap = {};
for (const key in globResult) {
const keyRelativeToContentDir = key.replace(new RegExp(`^${contentDir}`), "");
const segments = keyRelativeToContentDir.split("/");
if (segments.length <= 1) continue;
const collection = segments[0];
collectionToGlobResultMap[collection] ??= {};
collectionToGlobResultMap[collection][key] = globResult[key];
}
return collectionToGlobResultMap;
}
z.object({
tags: z.array(z.string()).optional(),
lastModified: z.date().optional()
});
function createGetCollection({
contentCollectionToEntryMap,
dataCollectionToEntryMap,
getRenderEntryImport,
cacheEntriesByCollection,
liveCollections
}) {
return async function getCollection(collection, filter) {
if (collection in liveCollections) {
throw new AstroError({
...UnknownContentCollectionError,
message: `Collection "${collection}" is a live collection. Use getLiveCollection() instead of getCollection().`
});
}
const hasFilter = typeof filter === "function";
const store = await globalDataStore.get();
let type;
if (collection in contentCollectionToEntryMap) {
type = "content";
} else if (collection in dataCollectionToEntryMap) {
type = "data";
} else if (store.hasCollection(collection)) {
const { default: imageAssetMap } = await import('./content-assets_DleWbedO.mjs');
const result = [];
for (const rawEntry of store.values(collection)) {
const data = updateImageReferencesInData(rawEntry.data, rawEntry.filePath, imageAssetMap);
let entry = {
...rawEntry,
data,
collection
};
if (entry.legacyId) {
entry = emulateLegacyEntry(entry);
}
if (hasFilter && !filter(entry)) {
continue;
}
result.push(entry);
}
return result;
} else {
console.warn(
`The collection ${JSON.stringify(
collection
)} does not exist or is empty. Please check your content config file for errors.`
);
return [];
}
const lazyImports = Object.values(
type === "content" ? contentCollectionToEntryMap[collection] : dataCollectionToEntryMap[collection]
);
let entries = [];
if (!Object.assign(__vite_import_meta_env__, { _: process.env._ })?.DEV && cacheEntriesByCollection.has(collection)) {
entries = cacheEntriesByCollection.get(collection);
} else {
const limit = pLimit(10);
entries = await Promise.all(
lazyImports.map(
(lazyImport) => limit(async () => {
const entry = await lazyImport();
return type === "content" ? {
id: entry.id,
slug: entry.slug,
body: entry.body,
collection: entry.collection,
data: entry.data,
async render() {
return render({
collection: entry.collection,
id: entry.id,
renderEntryImport: await getRenderEntryImport(collection, entry.slug)
});
}
} : {
id: entry.id,
collection: entry.collection,
data: entry.data
};
})
)
);
cacheEntriesByCollection.set(collection, entries);
}
if (hasFilter) {
return entries.filter(filter);
} else {
return entries.slice();
}
};
}
function emulateLegacyEntry({ legacyId, ...entry }) {
const legacyEntry = {
...entry,
id: legacyId,
slug: entry.id
};
return {
...legacyEntry,
// Define separately so the render function isn't included in the object passed to `renderEntry()`
render: () => renderEntry(legacyEntry)
};
}
const CONTENT_LAYER_IMAGE_REGEX = /__ASTRO_IMAGE_="([^"]+)"/g;
async function updateImageReferencesInBody(html, fileName) {
const { default: imageAssetMap } = await import('./content-assets_DleWbedO.mjs');
const imageObjects = /* @__PURE__ */ new Map();
const { getImage } = await import('./_astro_assets_CWrzoUoo.mjs').then(n => n._);
for (const [_full, imagePath] of html.matchAll(CONTENT_LAYER_IMAGE_REGEX)) {
try {
const decodedImagePath = JSON.parse(imagePath.replaceAll("&#x22;", '"'));
let image;
if (URL.canParse(decodedImagePath.src)) {
image = await getImage(decodedImagePath);
} else {
const id = imageSrcToImportId(decodedImagePath.src, fileName);
const imported = imageAssetMap.get(id);
if (!id || imageObjects.has(id) || !imported) {
continue;
}
image = await getImage({ ...decodedImagePath, src: imported });
}
imageObjects.set(imagePath, image);
} catch {
throw new Error(`Failed to parse image reference: ${imagePath}`);
}
}
return html.replaceAll(CONTENT_LAYER_IMAGE_REGEX, (full, imagePath) => {
const image = imageObjects.get(imagePath);
if (!image) {
return full;
}
const { index, ...attributes } = image.attributes;
return Object.entries({
...attributes,
src: image.src,
srcset: image.srcSet.attribute,
// This attribute is used by the toolbar audit
...Object.assign(__vite_import_meta_env__, { _: process.env._ }).DEV ? { "data-image-component": "true" } : {}
}).map(([key, value]) => value ? `${key}="${escape(value)}"` : "").join(" ");
});
}
function updateImageReferencesInData(data, fileName, imageAssetMap) {
return new Traverse(data).map(function(ctx, val) {
if (typeof val === "string" && val.startsWith(IMAGE_IMPORT_PREFIX)) {
const src = val.replace(IMAGE_IMPORT_PREFIX, "");
const id = imageSrcToImportId(src, fileName);
if (!id) {
ctx.update(src);
return;
}
const imported = imageAssetMap?.get(id);
if (imported) {
ctx.update(imported);
} else {
ctx.update(src);
}
}
});
}
async function renderEntry(entry) {
if (!entry) {
throw new AstroError(RenderUndefinedEntryError);
}
if ("render" in entry && !("legacyId" in entry)) {
return entry.render();
}
if (entry.deferredRender) {
try {
const { default: contentModules } = await import('./content-modules_Dz-S_Wwv.mjs');
const renderEntryImport = contentModules.get(entry.filePath);
return render({
collection: "",
id: entry.id,
renderEntryImport
});
} catch (e) {
console.error(e);
}
}
const html = entry?.rendered?.metadata?.imagePaths?.length && entry.filePath ? await updateImageReferencesInBody(entry.rendered.html, entry.filePath) : entry?.rendered?.html;
const Content = createComponent(() => renderTemplate`${unescapeHTML(html)}`);
return {
Content,
headings: entry?.rendered?.metadata?.headings ?? [],
remarkPluginFrontmatter: entry?.rendered?.metadata?.frontmatter ?? {}
};
}
async function render({
collection,
id,
renderEntryImport
}) {
const UnexpectedRenderError = new AstroError({
...UnknownContentCollectionError,
message: `Unexpected error while rendering ${String(collection)}${String(id)}.`
});
if (typeof renderEntryImport !== "function") throw UnexpectedRenderError;
const baseMod = await renderEntryImport();
if (baseMod == null || typeof baseMod !== "object") throw UnexpectedRenderError;
const { default: defaultMod } = baseMod;
if (isPropagatedAssetsModule(defaultMod)) {
const { collectedStyles, collectedLinks, collectedScripts, getMod } = defaultMod;
if (typeof getMod !== "function") throw UnexpectedRenderError;
const propagationMod = await getMod();
if (propagationMod == null || typeof propagationMod !== "object") throw UnexpectedRenderError;
const Content = createComponent({
factory(result, baseProps, slots) {
let styles = "", links = "", scripts = "";
if (Array.isArray(collectedStyles)) {
styles = collectedStyles.map((style) => {
return renderUniqueStylesheet(result, {
type: "inline",
content: style
});
}).join("");
}
if (Array.isArray(collectedLinks)) {
links = collectedLinks.map((link) => {
return renderUniqueStylesheet(result, {
type: "external",
src: isRemotePath(link) ? link : prependForwardSlash(link)
});
}).join("");
}
if (Array.isArray(collectedScripts)) {
scripts = collectedScripts.map((script) => renderScriptElement(script)).join("");
}
let props = baseProps;
if (id.endsWith("mdx")) {
props = {
components: propagationMod.components ?? {},
...baseProps
};
}
return createHeadAndContent(
unescapeHTML(styles + links + scripts),
renderTemplate`${renderComponent(
result,
"Content",
propagationMod.Content,
props,
slots
)}`
);
},
propagation: "self"
});
return {
Content,
headings: propagationMod.getHeadings?.() ?? [],
remarkPluginFrontmatter: propagationMod.frontmatter ?? {}
};
} else if (baseMod.Content && typeof baseMod.Content === "function") {
return {
Content: baseMod.Content,
headings: baseMod.getHeadings?.() ?? [],
remarkPluginFrontmatter: baseMod.frontmatter ?? {}
};
} else {
throw UnexpectedRenderError;
}
}
function isPropagatedAssetsModule(module) {
return typeof module === "object" && module != null && "__astroPropagation" in module;
}
// astro-head-inject
const liveCollections = {};
const contentDir = '/src/content/';
const contentEntryGlob = "";
const contentCollectionToEntryMap = createCollectionToGlobResultMap({
globResult: contentEntryGlob,
contentDir,
});
const dataEntryGlob = "";
const dataCollectionToEntryMap = createCollectionToGlobResultMap({
globResult: dataEntryGlob,
contentDir,
});
createCollectionToGlobResultMap({
globResult: { ...contentEntryGlob, ...dataEntryGlob },
contentDir,
});
let lookupMap = {};
lookupMap = {};
new Set(Object.keys(lookupMap));
function createGlobLookup(glob) {
return async (collection, lookupId) => {
const filePath = lookupMap[collection]?.entries[lookupId];
if (!filePath) return undefined;
return glob[collection][filePath];
};
}
const renderEntryGlob = "";
const collectionToRenderEntryMap = createCollectionToGlobResultMap({
globResult: renderEntryGlob,
contentDir,
});
const cacheEntriesByCollection = new Map();
const getCollection = createGetCollection({
contentCollectionToEntryMap,
dataCollectionToEntryMap,
getRenderEntryImport: createGlobLookup(collectionToRenderEntryMap),
cacheEntriesByCollection,
liveCollections,
});
export { DEFAULT_OUTPUT_FORMAT as D, VALID_SUPPORTED_FORMATS as V, DEFAULT_HASH_PROPS as a, getCollection as g, renderEntry as r };

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +0,0 @@
import 'piccolore';
import './astro/server_D-JZF3a4.mjs';
import 'clsx';

View File

@@ -1,3 +0,0 @@
const contentAssets = new Map();
export { contentAssets as default };

View File

@@ -1,3 +0,0 @@
const contentModules = new Map();
export { contentModules as default };

View File

@@ -1,101 +0,0 @@
import { A as AstroError, z as MissingSharp } from './astro/server_D-JZF3a4.mjs';
import { b as baseService, p as parseQuality } from './_astro_assets_CWrzoUoo.mjs';
let sharp;
const qualityTable = {
low: 25,
mid: 50,
high: 80,
max: 100
};
async function loadSharp() {
let sharpImport;
try {
sharpImport = (await import('sharp')).default;
} catch {
throw new AstroError(MissingSharp);
}
sharpImport.cache(false);
return sharpImport;
}
const fitMap = {
fill: "fill",
contain: "inside",
cover: "cover",
none: "outside",
"scale-down": "inside",
outside: "outside",
inside: "inside"
};
const sharpService = {
validateOptions: baseService.validateOptions,
getURL: baseService.getURL,
parseURL: baseService.parseURL,
getHTMLAttributes: baseService.getHTMLAttributes,
getSrcSet: baseService.getSrcSet,
async transform(inputBuffer, transformOptions, config) {
if (!sharp) sharp = await loadSharp();
const transform = transformOptions;
const kernel = config.service.config.kernel;
if (transform.format === "svg") return { data: inputBuffer, format: "svg" };
const result = sharp(inputBuffer, {
failOnError: false,
pages: -1,
limitInputPixels: config.service.config.limitInputPixels
});
result.rotate();
const { format } = await result.metadata();
const withoutEnlargement = Boolean(transform.fit);
if (transform.width && transform.height && transform.fit) {
const fit = fitMap[transform.fit] ?? "inside";
result.resize({
width: Math.round(transform.width),
height: Math.round(transform.height),
kernel,
fit,
position: transform.position,
withoutEnlargement
});
} else if (transform.height && !transform.width) {
result.resize({
height: Math.round(transform.height),
kernel,
withoutEnlargement
});
} else if (transform.width) {
result.resize({
width: Math.round(transform.width),
kernel,
withoutEnlargement
});
}
if (transform.background) {
result.flatten({ background: transform.background });
}
if (transform.format) {
let quality = void 0;
if (transform.quality) {
const parsedQuality = parseQuality(transform.quality);
if (typeof parsedQuality === "number") {
quality = parsedQuality;
} else {
quality = transform.quality in qualityTable ? qualityTable[transform.quality] : void 0;
}
}
if (transform.format === "webp" && format === "gif") {
result.webp({ quality: typeof quality === "number" ? quality : void 0, loop: 0 });
} else {
result.toFormat(transform.format, { quality });
}
}
const { data, info } = await result.toBuffer({ resolveWithObject: true });
const needsCopy = "buffer" in data && data.buffer instanceof SharedArrayBuffer;
return {
data: needsCopy ? new Uint8Array(data) : data,
format: info.format
};
}
};
var sharp_default = sharpService;
export { sharp_default as default };

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
const server = {};
export { server };

View File

@@ -1,46 +0,0 @@
import { c as createComponent, m as maybeRenderHead, d as addAttribute, a as renderTemplate, b as createAstro, r as renderComponent } from '../chunks/astro/server_D-JZF3a4.mjs';
import 'piccolore';
import { g as getCollection } from '../chunks/_astro_content_C5hvN5fw.mjs';
import 'clsx';
import { $ as $$BaseLayout } from '../chunks/BaseLayout_CQrO8n43.mjs';
export { renderers } from '../renderers.mjs';
const $$Astro = createAstro();
const $$BlogCard = createComponent(($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$BlogCard;
const { post } = Astro2.props;
const { title, excerpt, date, author, category, categories, image, featuredImage } = post.data;
const postCategory = category || (Array.isArray(categories) ? categories[0] : "\u0E17\u0E31\u0E48\u0E27\u0E44\u0E1B");
const postImage = image || featuredImage || "/images/2021/03/ppr-pipe_000C.jpg";
return renderTemplate`${maybeRenderHead()}<a${addAttribute(`/blog/${post.slug}`, "href")} class="card group"> <div class="relative aspect-video bg-secondary-100 overflow-hidden"> <img${addAttribute(postImage, "src")}${addAttribute(title, "alt")} class="object-cover w-full h-48 group-hover:scale-105 transition-transform duration-300" loading="lazy"> <div class="absolute top-4 left-4"> <span class="industrial-badge">${postCategory}</span> </div> </div> <div class="p-6"> <time class="text-sm text-secondary-500"> ${new Date(date).toLocaleDateString("th-TH", { year: "numeric", month: "long", day: "numeric" })} </time> <h3 class="mt-2 text-xl font-bold text-secondary-900 group-hover:text-primary-600 transition-colors line-clamp-2"> ${title} </h3> ${excerpt && renderTemplate`<p class="mt-3 text-secondary-600 text-sm line-clamp-3"> ${excerpt} </p>`} <div class="mt-4 flex items-center text-primary-600 font-medium"> <span>อ่านต่อ</span> <svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M9 5l7 7-7 7"></path> </svg> </div> </div> </a>`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/components/BlogCard.astro", void 0);
const metadata = {
title: "\u0E1A\u0E17\u0E04\u0E27\u0E32\u0E21\u0E04\u0E27\u0E32\u0E21\u0E23\u0E39\u0E49",
description: "\u0E1A\u0E17\u0E04\u0E27\u0E32\u0E21\u0E04\u0E27\u0E32\u0E21\u0E23\u0E39\u0E49\u0E40\u0E01\u0E35\u0E48\u0E22\u0E27\u0E01\u0E31\u0E1A\u0E27\u0E31\u0E2A\u0E14\u0E38\u0E17\u0E48\u0E2D \u0E2D\u0E38\u0E1B\u0E01\u0E23\u0E13\u0E4C\u0E23\u0E30\u0E1A\u0E1A\u0E17\u0E48\u0E2D \u0E41\u0E25\u0E30\u0E40\u0E17\u0E04\u0E19\u0E34\u0E04\u0E01\u0E32\u0E23\u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07"
};
const prerender = true;
const $$Index = createComponent(async ($$result, $$props, $$slots) => {
const posts = await getCollection("blog");
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": metadata.title, "description": metadata.description }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="pt-32 pb-16"> <div class="container mx-auto px-4"> <!-- Hero --> <div class="text-center mb-12"> <h1 class="text-4xl md:text-5xl font-bold text-secondary-900 mb-4">
บทความ<span class="text-primary-600">ความร</span> </h1> <p class="text-xl text-secondary-600 max-w-2xl mx-auto">
บทความความรเกยวกบวสด ปกรณระบบท และเทคนคการตดต
</p> </div> <!-- Blog Grid --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> ${posts.map((post) => renderTemplate`${renderComponent($$result2, "BlogCard", $$BlogCard, { "post": post })}`)} </div> </div> </main> ` })}`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/blog/index.astro", void 0);
const $$file = "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/blog/index.astro";
const $$url = "/blog";
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: $$Index,
file: $$file,
metadata,
prerender,
url: $$url
}, Symbol.toStringTag, { value: 'Module' }));
const page = () => _page;
export { page };

View File

@@ -1,45 +0,0 @@
import { c as createComponent, r as renderComponent, a as renderTemplate, b as createAstro, m as maybeRenderHead, d as addAttribute } from '../../chunks/astro/server_D-JZF3a4.mjs';
import 'piccolore';
import { r as renderEntry, g as getCollection } from '../../chunks/_astro_content_C5hvN5fw.mjs';
import { $ as $$BaseLayout } from '../../chunks/BaseLayout_CQrO8n43.mjs';
export { renderers } from '../../renderers.mjs';
const $$Astro = createAstro();
async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { slug: post.id },
props: { post }
}));
}
const $$slug = createComponent(async ($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$slug;
const { post } = Astro2.props;
const { Content } = await renderEntry(post);
const { title, date, author, category, categories, image, featuredImage } = post.data;
const postCategory = category || (Array.isArray(categories) ? categories[0] : "\u0E17\u0E31\u0E48\u0E27\u0E44\u0E1B");
const postImage = image || featuredImage || "/images/2021/03/ppr-pipe_000C.jpg";
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": title, "description": post.data.excerpt }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="pt-32 pb-16"> <article class="container mx-auto px-4 max-w-4xl"> <!-- Header --> <header class="mb-8"> <div class="flex items-center gap-4 mb-4"> <span class="industrial-badge">${postCategory}</span> <time class="text-secondary-500"> ${new Date(date).toLocaleDateString("th-TH", {
year: "numeric",
month: "long",
day: "numeric"
})} </time> <span class="text-secondary-500"></span> <span class="text-secondary-500">${author}</span> </div> <h1 class="text-4xl md:text-5xl font-bold text-secondary-900 mb-4"> ${title} </h1> </header> <!-- Featured Image --> <div class="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden mb-8"> <img${addAttribute(postImage, "src")}${addAttribute(title, "alt")} class="object-cover w-full h-full" loading="lazy"> </div> <!-- Content --> <div class="prose prose-lg max-w-none prose-headings:font-bold prose-a:text-primary-600 hover:prose-a:text-primary-700 prose-img:rounded-xl"> ${renderComponent($$result2, "Content", Content, {})} </div> <!-- Back to Blog --> <div class="mt-12 pt-8 border-t border-secondary-200"> <a href="/blog/" class="inline-flex items-center text-primary-600 font-medium hover:text-primary-700 transition-colors"> <svg class="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M10 19l-7-7m0 0l7-7m-7 7h18"></path> </svg>
กลบสหนาบทความ
</a> </div> </article> </main> ` })}`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/blog/[slug].astro", void 0);
const $$file = "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/blog/[slug].astro";
const $$url = "/blog/[slug]";
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: $$slug,
file: $$file,
getStaticPaths,
url: $$url
}, Symbol.toStringTag, { value: 'Module' }));
const page = () => _page;
export { page };

View File

@@ -1,65 +0,0 @@
import { c as createComponent, m as maybeRenderHead, d as addAttribute, a as renderTemplate, r as renderComponent } from '../chunks/astro/server_D-JZF3a4.mjs';
import 'piccolore';
import { $ as $$BaseLayout } from '../chunks/BaseLayout_CQrO8n43.mjs';
import 'clsx';
import { s as siteConfig, p as productCategories } from '../chunks/site-config_BJHCdssj.mjs';
export { renderers } from '../renderers.mjs';
const $$FloatingContact = createComponent(($$result, $$props, $$slots) => {
return renderTemplate`${maybeRenderHead()}<div class="fixed bottom-6 right-6 z-40 flex flex-col gap-3"> <!-- LINE --> <a${addAttribute(`https://line.me/ti/p/${siteConfig.lineId}`, "href")} target="_blank" rel="noopener noreferrer" class="w-14 h-14 bg-[#00B900] rounded-full flex items-center justify-center shadow-lg hover:scale-110 transition-transform" aria-label="ติดต่อผ่าน LINE"> <svg class="w-7 h-7 text-white" viewBox="0 0 24 24" fill="currentColor"> <path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"></path> </svg> </a> <!-- Phone --> <a${addAttribute(`tel:${siteConfig.phone}`, "href")} class="w-14 h-14 bg-primary-600 rounded-full flex items-center justify-center shadow-lg hover:scale-110 transition-transform" aria-label="โทรหาเรา"> <svg class="w-7 h-7 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path> </svg> </a> </div>`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/components/FloatingContact.astro", void 0);
const $$Index = createComponent(($$result, $$props, $$slots) => {
const featuredProducts = productCategories.filter(
(p) => ["ppr-elephant", "hdpe", "poloplast", "syler", "xylent"].includes(p.id)
).slice(0, 6);
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": "\u0E2B\u0E19\u0E49\u0E32\u0E41\u0E23\u0E01", "description": "\u0E1A\u0E23\u0E34\u0E29\u0E31\u0E17 \u0E14\u0E35\u0E25 \u0E1E\u0E25\u0E31\u0E2A \u0E40\u0E17\u0E04 \u0E08\u0E33\u0E01\u0E31\u0E14 - \u0E1C\u0E39\u0E49\u0E40\u0E0A\u0E35\u0E48\u0E22\u0E27\u0E0A\u0E32\u0E0D\u0E14\u0E49\u0E32\u0E19\u0E23\u0E30\u0E1A\u0E1A\u0E17\u0E48\u0E2D\u0E41\u0E25\u0E30 HVAC" }, { "default": ($$result2) => renderTemplate` ${maybeRenderHead()}<main> <!-- Hero Section --> <section class="relative h-[70vh] min-h-[500px] bg-secondary-900"> <div class="absolute inset-0 bg-gradient-to-r from-secondary-900 via-secondary-900/90 to-secondary-900/60 z-10"></div> <img src="/images/2021/03/ppr-pipe_000C.jpg" alt="" class="absolute inset-0 w-full h-full object-cover opacity-50" loading="eager"> <div class="relative z-20 container mx-auto px-4 h-full flex items-center"> <div class="max-w-2xl"> <span class="inline-block px-4 py-2 bg-primary-600 text-white font-semibold mb-4 rounded">
เชยวชาญดานระบบทอและ HVAC
</span> <h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight">
สด ปกรณ HVAC
<span class="text-primary-400 block">และฉนวนหมท</span> </h1> <p class="text-lg md:text-xl text-secondary-200 mb-8">
จำหนายและตดตงท PPR, HDPE, กรลแอร, เทอรโมเบรค และอปกรณระบบทอครบวงจร พรอมบรการใหคำปรกษาจากทมมออาช
</p> <div class="flex flex-wrap gap-4"> <a href="/products/" class="btn-primary">
นคาทงหมด
</a> <a href="/contact-us/" class="btn-outline border-white text-white hover:bg-white hover:text-secondary-900">
ขอใบเสนอราคา
</a> </div> </div> </div> </section> <!-- Features Section --> <section class="py-16 bg-secondary-800"> <div class="container mx-auto px-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <div class="text-center p-6"> <div class="w-16 h-16 bg-primary-600 rounded-lg flex items-center justify-center mx-auto mb-4"> <svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"></path> </svg> </div> <h3 class="text-xl font-bold text-white mb-2"></h3> <p class="text-secondary-300">
นคาทกชนผานมาตรฐานคณภาพ พรอมรบประก
</p> </div> <div class="text-center p-6"> <div class="w-16 h-16 bg-primary-600 rounded-lg flex items-center justify-center mx-auto mb-4"> <svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M13 10V3L4 14h7v7l9-11h-7z"></path> </svg> </div> <h3 class="text-xl font-bold text-white mb-2"></h3> <p class="text-secondary-300">
ดสงสนคาทวประเทศ รวดเรวและปลอดภ
</p> </div> <div class="text-center p-6"> <div class="w-16 h-16 bg-primary-600 rounded-lg flex items-center justify-center mx-auto mb-4"> <svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path> </svg> </div> <h3 class="text-xl font-bold text-white mb-2"></h3> <p class="text-secondary-300">
มงานพรอมใหคำปรกษาและดแลอยางตอเนอง
</p> </div> </div> </div> </section> <!-- Featured Products --> <section class="py-16 bg-secondary-50"> <div class="container mx-auto px-4"> <div class="text-center mb-12"> <h2 class="text-3xl md:text-4xl font-bold text-secondary-900 mb-4">
นค<span class="text-primary-600">เด</span> </h2> <p class="text-secondary-600 text-lg"></p> </div> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> ${featuredProducts.map((product) => renderTemplate`<a${addAttribute(product.href, "href")} class="card group"> <div class="relative aspect-video bg-secondary-100 overflow-hidden"> <img${addAttribute(product.image, "src")}${addAttribute(product.name, "alt")} class="object-cover w-full h-48 group-hover:scale-105 transition-transform duration-300" loading="lazy"> </div> <div class="p-6"> <h3 class="text-lg font-bold text-secondary-900 group-hover:text-primary-600 transition-colors"> ${product.name} </h3> <p class="mt-2 text-sm text-secondary-600 line-clamp-2"> ${product.shortDescription || product.description} </p> <div class="mt-4 flex items-center text-primary-600 font-medium"> <span></span> <svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M9 5l7 7-7 7"></path> </svg> </div> </div> </a>`)} </div> <div class="text-center mt-12"> <a href="/products/" class="btn-primary">
นคาทงหมด
</a> </div> </div> </section> <!-- About Preview --> <section class="py-16 bg-white"> <div class="container mx-auto px-4"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"> <div> <h2 class="text-3xl md:text-4xl font-bold text-secondary-900 mb-6">
เกยวก<span class="text-primary-600">เรา</span> </h2> <p class="text-lg text-secondary-600 mb-6">
บร พล เทค จำก เราเปนผเชยวชาญดานระบบน ใหคำแนะนำและจำหนายท PPR ตราชาง อพอาร PPR HDPE Thai PPR ณภาพส ราคาถ
</p> <p class="text-secondary-700 mb-8">
วยประสบการณยาวนาน เราพรอมใหบรการสนคาคณภาพและคำแนะนำจากผเชยวชาญ เพอใหงานระบบของคณมประสทธภาพสงส
</p> <a href="/about-us/" class="btn-secondary">
านเพมเต
</a> </div> <div class="relative aspect-video bg-secondary-100 rounded-xl overflow-hidden"> <img src="/images/2021/03/ppr-pipe_000C.jpg" alt="" class="object-cover w-full h-full" loading="lazy"> </div> </div> </div> </section> <!-- CTA Section --> <section class="py-16 bg-primary-600"> <div class="container mx-auto px-4 text-center"> <h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
สนใจสนคาหรอตองการคำปรกษา?
</h2> <p class="text-xl text-primary-100 mb-8 max-w-2xl mx-auto">
มงานของเราพรอมใหคำแนะนำและชวยคณเลอกสนคาทเหมาะสมท
</p> <div class="flex flex-wrap justify-center gap-4"> <a href="tel:090-555-1415" class="btn-secondary bg-white text-primary-600 hover:bg-primary-50">
โทร: 090-555-1415
</a> <a href="https://line.me/ti/p/@dealplustech" target="_blank" rel="noopener" class="btn-outline border-white text-white hover:bg-white hover:text-primary-600">
เพิ่มเพื่อน LINE
</a> </div> </div> </section> </main> ${renderComponent($$result2, "FloatingContact", $$FloatingContact, {})} ` })}`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/index.astro", void 0);
const $$file = "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/index.astro";
const $$url = "";
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: $$Index,
file: $$file,
url: $$url
}, Symbol.toStringTag, { value: 'Module' }));
const page = () => _page;
export { page };

View File

@@ -1,34 +0,0 @@
import { c as createComponent, m as maybeRenderHead, d as addAttribute, a as renderTemplate, b as createAstro, r as renderComponent } from '../chunks/astro/server_D-JZF3a4.mjs';
import 'piccolore';
import { g as getCollection } from '../chunks/_astro_content_C5hvN5fw.mjs';
import 'clsx';
import { $ as $$BaseLayout } from '../chunks/BaseLayout_CQrO8n43.mjs';
export { renderers } from '../renderers.mjs';
const $$Astro = createAstro();
const $$ProductCard = createComponent(($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$ProductCard;
const { product } = Astro2.props;
const { name, shortDescription, image } = product.data;
return renderTemplate`${maybeRenderHead()}<a${addAttribute(`/products/${product.data.slug}`, "href")} class="card group"> <div class="aspect-w-16 aspect-h-9 overflow-hidden bg-secondary-100"> <img${addAttribute(image || "/placeholder.jpg", "src")}${addAttribute(name, "alt")} class="object-cover w-full h-48 group-hover:scale-105 transition-transform duration-300" loading="lazy"> </div> <div class="p-6"> <h3 class="text-lg font-bold text-secondary-900 group-hover:text-primary-600 transition-colors"> ${name} </h3> ${shortDescription && renderTemplate`<p class="mt-2 text-sm text-secondary-600 line-clamp-2"> ${shortDescription} </p>`} <div class="mt-4 flex items-center text-primary-600 font-medium"> <span>ดูรายละเอียด</span> <svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M9 5l7 7-7 7"></path> </svg> </div> </div> </a>`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/components/ProductCard.astro", void 0);
const $$Index = createComponent(async ($$result, $$props, $$slots) => {
const products = await getCollection("products");
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": "\u0E2A\u0E34\u0E19\u0E04\u0E49\u0E32\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14", "description": "\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E2A\u0E34\u0E19\u0E04\u0E49\u0E32\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14\u0E08\u0E32\u0E01 Deal Plus Tech" }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="py-12"> <div class="container mx-auto px-4 max-w-6xl"> <h1 class="section-title mb-8">นคาทงหมด</h1> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> ${products.map((product) => renderTemplate`${renderComponent($$result2, "ProductCard", $$ProductCard, { "product": product })}`)} </div> </div> </main> ` })}`;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/products/index.astro", void 0);
const $$file = "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/products/index.astro";
const $$url = "/products";
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: $$Index,
file: $$file,
url: $$url
}, Symbol.toStringTag, { value: 'Module' }));
const page = () => _page;
export { page };

View File

@@ -1,48 +0,0 @@
import { c as createComponent, r as renderComponent, a as renderTemplate, b as createAstro, m as maybeRenderHead, d as addAttribute } from '../../chunks/astro/server_D-JZF3a4.mjs';
import 'piccolore';
import { r as renderEntry, g as getCollection } from '../../chunks/_astro_content_C5hvN5fw.mjs';
import { $ as $$BaseLayout } from '../../chunks/BaseLayout_CQrO8n43.mjs';
import { p as productCategories } from '../../chunks/site-config_BJHCdssj.mjs';
/* empty css */
export { renderers } from '../../renderers.mjs';
const $$Astro = createAstro();
async function getStaticPaths() {
const products = await getCollection("products");
return products.map((product) => ({
params: { slug: product.data.slug },
props: { product }
}));
}
const $$slug = createComponent(async ($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$slug;
const { product } = Astro2.props;
const { Content } = await renderEntry(product);
const productData = productCategories.find((p) => p.id === product.data.id);
const productTables = productData?.productTables || [];
return renderTemplate`${renderComponent($$result, "BaseLayout", $$BaseLayout, { "title": product.data.name, "description": product.data.shortDescription || product.data.description, "data-astro-cid-o422f4lv": true }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<main class="py-12" data-astro-cid-o422f4lv> <article class="container mx-auto px-4 max-w-7xl" data-astro-cid-o422f4lv> <!-- Product Header --> <header class="mb-12" data-astro-cid-o422f4lv> <h1 class="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-secondary-900 mb-6" data-astro-cid-o422f4lv> ${product.data.name} </h1> <p class="text-lg md:text-xl lg:text-2xl xl:text-3xl text-secondary-600 max-w-4xl" data-astro-cid-o422f4lv> ${product.data.description} </p> </header> <!-- Content from Markdown --> <div class="prose prose-lg md:prose-xl lg:prose-2xl max-w-none mb-12" data-astro-cid-o422f4lv> ${renderComponent($$result2, "Content", Content, { "data-astro-cid-o422f4lv": true })} </div> <!-- Product Tables Section --> ${productTables.length > 0 && renderTemplate`<section class="mb-12" data-astro-cid-o422f4lv> <h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-secondary-900 mb-8 flex items-center gap-3" data-astro-cid-o422f4lv> <svg class="w-8 h-8 md:w-10 md:h-10 lg:w-12 lg:h-12 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" data-astro-cid-o422f4lv> <path stroke-linecap="round" stroke-linejoin="round"${addAttribute(2, "stroke-width")} d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" data-astro-cid-o422f4lv></path> </svg>
ตารางขอมลผลตภณฑ
</h2> <div class="space-y-10" data-astro-cid-o422f4lv> ${productTables.map((table2, tableIndex) => renderTemplate`<div class="bg-white rounded-2xl border-2 border-secondary-200 shadow-lg" data-astro-cid-o422f4lv> <h3 class="text-lg md:text-xl lg:text-2xl font-semibold text-secondary-800 p-4 md:p-6 bg-secondary-50 border-b-2 border-secondary-200" data-astro-cid-o422f4lv> ${table2.tableName} </h3> <div class="w-full overflow-x-auto -mx-2 md:mx-0" data-astro-cid-o422f4lv> <div class="px-2 md:px-0" data-astro-cid-o422f4lv> <table class="w-full min-w-[600px] border-collapse" data-astro-cid-o422f4lv> <thead data-astro-cid-o422f4lv> <tr class="bg-primary-100" data-astro-cid-o422f4lv> ${table2.headers.map((header, headerIndex) => renderTemplate`<th class="px-3 py-2 md:px-4 md:py-3 text-left text-xs md:text-sm lg:text-base font-bold text-primary-800 border-b-2 border-primary-300 whitespace-nowrap" data-astro-cid-o422f4lv> ${header} </th>`)} </tr> </thead> <tbody data-astro-cid-o422f4lv> ${table2.rows.map((row, rowIndex) => renderTemplate`<tr${addAttribute(rowIndex % 2 === 0 ? "bg-white" : "bg-secondary-50", "class")} data-astro-cid-o422f4lv> ${row.map((cell, cellIndex) => renderTemplate`<td class="px-3 py-2 md:px-4 md:py-3 text-xs md:text-sm lg:text-base text-secondary-700 border-b border-secondary-100 break-words max-w-[200px] md:max-w-none" data-astro-cid-o422f4lv> ${cell} </td>`)} </tr>`)} </tbody> </table> </div> </div> </div>`)} </div> </section>`})}
<tbody data-astro-cid-o422f4lv> ${table.rows.map((row, rowIndex) => renderTemplate`<tr${addAttribute(rowIndex % 2 === 0 ? "bg-white" : "bg-secondary-50", "class")} data-astro-cid-o422f4lv> ${row.map((cell, cellIndex) => renderTemplate`<td class="px-5 py-4 md:px-6 md:py-5 text-base md:text-lg lg:text-xl text-secondary-700 border-b border-secondary-100" data-astro-cid-o422f4lv> ${cell} </td>`)} </tr>`)} </tbody>
))}
)}
<!-- Specifications --> ${product.data.specifications && product.data.specifications.length > 0 && renderTemplate`<section class="mb-12" data-astro-cid-o422f4lv> <h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-secondary-900 mb-8" data-astro-cid-o422f4lv>ข้อมูลจำเพาะ</h2> <div class="bg-white rounded-2xl border-2 border-secondary-200 p-6 md:p-8 shadow-lg" data-astro-cid-o422f4lv> <dl class="grid grid-cols-1 md:grid-cols-2 gap-6" data-astro-cid-o422f4lv> ${product.data.specifications.map((spec, index) => renderTemplate`<div class="flex flex-col md:flex-row md:justify-between border-b border-secondary-100 pb-4" data-astro-cid-o422f4lv> <dt class="text-base md:text-lg lg:text-xl font-semibold text-secondary-700 mb-2 md:mb-0 md:mr-4" data-astro-cid-o422f4lv>${spec.label}</dt> <dd class="text-base md:text-lg lg:text-xl text-secondary-900" data-astro-cid-o422f4lv> ${spec.value} ${spec.unit && renderTemplate`<span class="text-secondary-500 ml-2" data-astro-cid-o422f4lv>${spec.unit}</span>`} </dd> </div>`)} </dl> </div> </section>`} <!-- Features --> ${product.data.features && product.data.features.length > 0 && renderTemplate`<section class="mb-12" data-astro-cid-o422f4lv> <h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-secondary-900 mb-8" data-astro-cid-o422f4lv>คุณสมบัติเด่น</h2> <ul class="grid grid-cols-1 md:grid-cols-2 gap-6" data-astro-cid-o422f4lv> ${product.data.features.map((feature, index) => renderTemplate`<li class="flex items-start gap-4 bg-white p-6 rounded-xl border border-secondary-200 shadow" data-astro-cid-o422f4lv> <span class="text-2xl md:text-3xl text-primary-600 flex-shrink-0" data-astro-cid-o422f4lv>✓</span> <span class="text-base md:text-lg lg:text-xl text-secondary-700" data-astro-cid-o422f4lv>${feature}</span> </li>`)} </ul> </section>`} <!-- FAQ --> ${product.data.faq && product.data.faq.length > 0 && renderTemplate`<section class="mb-12" data-astro-cid-o422f4lv> <h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-secondary-900 mb-8" data-astro-cid-o422f4lv>คำถามที่พบบ่อย</h2> <div class="space-y-6" data-astro-cid-o422f4lv> ${product.data.faq.map((item, index) => renderTemplate`<div class="bg-white rounded-2xl p-6 md:p-8 border-2 border-secondary-200 shadow" data-astro-cid-o422f4lv> <h3 class="text-xl md:text-2xl lg:text-3xl font-bold text-secondary-900 mb-4" data-astro-cid-o422f4lv>${item.question}</h3> <p class="text-base md:text-lg lg:text-xl text-secondary-700 leading-relaxed" data-astro-cid-o422f4lv>${item.answer}</p> </div>`)} </div> </section>`} </article> </main> ` })} `;
}, "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/products/[slug].astro", void 0);
const $$file = "/Users/kunthawatgreethong/Gitea/dealplustech/dealplustech-astro/src/pages/products/[slug].astro";
const $$url = "/products/[slug]";
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: $$slug,
file: $$file,
getStaticPaths,
url: $$url
}, Symbol.toStringTag, { value: 'Module' }));
const page = () => _page;
export { page };

View File

@@ -1,3 +0,0 @@
const renderers = [];
export { renderers };

View File

@@ -1,16 +0,0 @@
{
"easypanel": {
"url": "",
"project": "customerwebsite",
"app": {
"name": "dealplustech-astro",
"port": 4321,
"image": "dealplustech-astro:latest"
},
"env": {
"NODE_ENV": "production",
"PORT": "4321",
"HOST": "0.0.0.0"
}
}
}

View File

@@ -1,14 +0,0 @@
# Nixpacks Configuration for Astro
# https://nixpacks.com/docs/configuration/file
[phases.setup]
nixPkgs = ["nodejs-20_x"]
[phases.install]
cmds = ["npm ci"]
[phases.build]
cmds = ["npm run build"]
[start]
cmd = "npm run preview -- --host 0.0.0.0 --port $PORT"

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env node
import process from 'node:process';
import isDocker from './index.js';
process.exitCode = isDocker() ? 0 : 2;

View File

@@ -1,13 +0,0 @@
/**
Check if the process is running inside a Docker container.
@example
```
import isDocker from 'is-docker';
if (isDocker()) {
console.log('Running inside a Docker container');
}
```
*/
export default function isDocker(): boolean;

View File

@@ -1,29 +0,0 @@
import fs from 'node:fs';
let isDockerCached;
function hasDockerEnv() {
try {
fs.statSync('/.dockerenv');
return true;
} catch {
return false;
}
}
function hasDockerCGroup() {
try {
return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker');
} catch {
return false;
}
}
export default function isDocker() {
// TODO: Use `??=` when targeting Node.js 16.
if (isDockerCached === undefined) {
isDockerCached = hasDockerEnv() || hasDockerCGroup();
}
return isDockerCached;
}

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,44 +0,0 @@
{
"name": "is-docker",
"version": "3.0.0",
"description": "Check if the process is running inside a Docker container",
"license": "MIT",
"repository": "sindresorhus/is-docker",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": "./index.js",
"bin": "./cli.js",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "xo && ava && tsd"
},
"files": [
"index.js",
"index.d.ts",
"cli.js"
],
"keywords": [
"detect",
"docker",
"dockerized",
"container",
"inside",
"is",
"env",
"environment",
"process"
],
"devDependencies": {
"ava": "^3.15.0",
"sinon": "^11.1.2",
"tsd": "^0.17.0",
"xo": "^0.44.0"
}
}

View File

@@ -1,27 +0,0 @@
# is-docker
> Check if the process is running inside a Docker container
## Install
```
$ npm install is-docker
```
## Usage
```js
import isDocker from 'is-docker';
if (isDocker()) {
console.log('Running inside a Docker container');
}
```
## CLI
```
$ is-docker
```
Exits with code 0 if inside a Docker container and 2 if not.

View File

@@ -1,23 +0,0 @@
import fs from 'node:fs';
import isDocker from 'is-docker';
let cachedResult;
// Podman detection
const hasContainerEnv = () => {
try {
fs.statSync('/run/.containerenv');
return true;
} catch {
return false;
}
};
export default function isInsideContainer() {
// TODO: Use `??=` when targeting Node.js 16.
if (cachedResult === undefined) {
cachedResult = hasContainerEnv() || isDocker();
}
return cachedResult;
}

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,35 +0,0 @@
/**
Check if a value is a plain object.
An object is plain if it's created by either `{}`, `new Object()`, or `Object.create(null)`.
@example
```
import isPlainObject from 'is-plain-obj';
import {runInNewContext} from 'node:vm';
isPlainObject({foo: 'bar'});
//=> true
isPlainObject(new Object());
//=> true
isPlainObject(Object.create(null));
//=> true
// This works across realms
isPlainObject(runInNewContext('({})'));
//=> true
isPlainObject([1, 2, 3]);
//=> false
class Unicorn {}
isPlainObject(new Unicorn());
//=> false
isPlainObject(Math);
//=> false
```
*/
export default function isPlainObject<Value>(value: unknown): value is Record<PropertyKey, Value>;

View File

@@ -1,8 +0,0 @@
export default function isPlainObject(value) {
if (typeof value !== 'object' || value === null) {
return false;
}
const prototype = Object.getPrototypeOf(value);
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value);
}

View File

@@ -1,9 +0,0 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,41 +0,0 @@
{
"name": "is-plain-obj",
"version": "4.1.0",
"description": "Check if a value is a plain object",
"license": "MIT",
"repository": "sindresorhus/is-plain-obj",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": "./index.js",
"engines": {
"node": ">=12"
},
"scripts": {
"test": "xo && ava && tsd"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"object",
"is",
"check",
"test",
"type",
"plain",
"vanilla",
"pure",
"simple"
],
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.14.0",
"xo": "^0.38.2"
}
}

View File

@@ -1,58 +0,0 @@
# is-plain-obj
> Check if a value is a plain object
An object is plain if it's created by either `{}`, `new Object()`, or `Object.create(null)`.
## Install
```
$ npm install is-plain-obj
```
## Usage
```js
import isPlainObject from 'is-plain-obj';
import {runInNewContext} from 'node:vm';
isPlainObject({foo: 'bar'});
//=> true
isPlainObject(new Object());
//=> true
isPlainObject(Object.create(null));
//=> true
// This works across realms
isPlainObject(runInNewContext('({})'));
//=> true
isPlainObject([1, 2, 3]);
//=> false
class Unicorn {}
isPlainObject(new Unicorn());
//=> false
isPlainObject(Math);
//=> false
```
## Related
- [is-obj](https://github.com/sindresorhus/is-obj) - Check if a value is an object
- [is](https://github.com/sindresorhus/is) - Type check values
---
<div align="center">
<b>
<a href="https://tidelift.com/subscription/pkg/npm-is-plain-obj?utm_source=npm-is-plain-obj&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
</b>
<br>
<sub>
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
</sub>
</div>

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Madeline Gurriarán
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,25 +0,0 @@
# tinyglobby
[![npm version](https://img.shields.io/npm/v/tinyglobby.svg?maxAge=3600)](https://npmjs.com/package/tinyglobby)
[![monthly downloads](https://img.shields.io/npm/dm/tinyglobby.svg?maxAge=3600)](https://npmjs.com/package/tinyglobby)
A fast and minimal alternative to globby and fast-glob, meant to behave the same way.
Both globby and fast-glob present some behavior no other globbing lib has,
which makes it hard to manually replace with something smaller and better.
This library uses only two subdependencies, compared to `globby`'s [23](https://npmgraph.js.org/?q=globby@14.1.0)
and `fast-glob`'s [17](https://npmgraph.js.org/?q=fast-glob@3.3.3).
## Usage
```js
import { glob, globSync } from 'tinyglobby';
await glob(['files/*.ts', '!**/*.d.ts'], { cwd: 'src' });
globSync('src/**/*.ts', { ignore: '**/*.d.ts' });
```
## Documentation
Visit https://superchupu.dev/tinyglobby to read the full documentation.

View File

@@ -1,350 +0,0 @@
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let fs = require("fs");
fs = __toESM(fs);
let path = require("path");
path = __toESM(path);
let url = require("url");
url = __toESM(url);
let fdir = require("fdir");
fdir = __toESM(fdir);
let picomatch = require("picomatch");
picomatch = __toESM(picomatch);
//#region src/utils.ts
const isReadonlyArray = Array.isArray;
const isWin = process.platform === "win32";
const ONLY_PARENT_DIRECTORIES = /^(\/?\.\.)+$/;
function getPartialMatcher(patterns, options = {}) {
const patternsCount = patterns.length;
const patternsParts = Array(patternsCount);
const matchers = Array(patternsCount);
const globstarEnabled = !options.noglobstar;
for (let i = 0; i < patternsCount; i++) {
const parts = splitPattern(patterns[i]);
patternsParts[i] = parts;
const partsCount = parts.length;
const partMatchers = Array(partsCount);
for (let j = 0; j < partsCount; j++) partMatchers[j] = (0, picomatch.default)(parts[j], options);
matchers[i] = partMatchers;
}
return (input) => {
const inputParts = input.split("/");
if (inputParts[0] === ".." && ONLY_PARENT_DIRECTORIES.test(input)) return true;
for (let i = 0; i < patterns.length; i++) {
const patternParts = patternsParts[i];
const matcher = matchers[i];
const inputPatternCount = inputParts.length;
const minParts = Math.min(inputPatternCount, patternParts.length);
let j = 0;
while (j < minParts) {
const part = patternParts[j];
if (part.includes("/")) return true;
const match = matcher[j](inputParts[j]);
if (!match) break;
if (globstarEnabled && part === "**") return true;
j++;
}
if (j === inputPatternCount) return true;
}
return false;
};
}
/* node:coverage ignore next 2 */
const WIN32_ROOT_DIR = /^[A-Z]:\/$/i;
const isRoot = isWin ? (p) => WIN32_ROOT_DIR.test(p) : (p) => p === "/";
function buildFormat(cwd, root, absolute) {
if (cwd === root || root.startsWith(`${cwd}/`)) {
if (absolute) {
const start = isRoot(cwd) ? cwd.length : cwd.length + 1;
return (p, isDir) => p.slice(start, isDir ? -1 : void 0) || ".";
}
const prefix = root.slice(cwd.length + 1);
if (prefix) return (p, isDir) => {
if (p === ".") return prefix;
const result = `${prefix}/${p}`;
return isDir ? result.slice(0, -1) : result;
};
return (p, isDir) => isDir && p !== "." ? p.slice(0, -1) : p;
}
if (absolute) return (p) => path.posix.relative(cwd, p) || ".";
return (p) => path.posix.relative(cwd, `${root}/${p}`) || ".";
}
function buildRelative(cwd, root) {
if (root.startsWith(`${cwd}/`)) {
const prefix = root.slice(cwd.length + 1);
return (p) => `${prefix}/${p}`;
}
return (p) => {
const result = path.posix.relative(cwd, `${root}/${p}`);
if (p.endsWith("/") && result !== "") return `${result}/`;
return result || ".";
};
}
const splitPatternOptions = { parts: true };
function splitPattern(path$2) {
var _result$parts;
const result = picomatch.default.scan(path$2, splitPatternOptions);
return ((_result$parts = result.parts) === null || _result$parts === void 0 ? void 0 : _result$parts.length) ? result.parts : [path$2];
}
const ESCAPED_WIN32_BACKSLASHES = /\\(?![()[\]{}!+@])/g;
function convertPosixPathToPattern(path$2) {
return escapePosixPath(path$2);
}
function convertWin32PathToPattern(path$2) {
return escapeWin32Path(path$2).replace(ESCAPED_WIN32_BACKSLASHES, "/");
}
/**
* Converts a path to a pattern depending on the platform.
* Identical to {@link escapePath} on POSIX systems.
* @see {@link https://superchupu.dev/tinyglobby/documentation#convertPathToPattern}
*/
/* node:coverage ignore next 3 */
const convertPathToPattern = isWin ? convertWin32PathToPattern : convertPosixPathToPattern;
const POSIX_UNESCAPED_GLOB_SYMBOLS = /(?<!\\)([()[\]{}*?|]|^!|[!+@](?=\()|\\(?![()[\]{}!*+?@|]))/g;
const WIN32_UNESCAPED_GLOB_SYMBOLS = /(?<!\\)([()[\]{}]|^!|[!+@](?=\())/g;
const escapePosixPath = (path$2) => path$2.replace(POSIX_UNESCAPED_GLOB_SYMBOLS, "\\$&");
const escapeWin32Path = (path$2) => path$2.replace(WIN32_UNESCAPED_GLOB_SYMBOLS, "\\$&");
/**
* Escapes a path's special characters depending on the platform.
* @see {@link https://superchupu.dev/tinyglobby/documentation#escapePath}
*/
/* node:coverage ignore next */
const escapePath = isWin ? escapeWin32Path : escapePosixPath;
/**
* Checks if a pattern has dynamic parts.
*
* Has a few minor differences with [`fast-glob`](https://github.com/mrmlnc/fast-glob) for better accuracy:
*
* - Doesn't necessarily return `false` on patterns that include `\`.
* - Returns `true` if the pattern includes parentheses, regardless of them representing one single pattern or not.
* - Returns `true` for unfinished glob extensions i.e. `(h`, `+(h`.
* - Returns `true` for unfinished brace expansions as long as they include `,` or `..`.
*
* @see {@link https://superchupu.dev/tinyglobby/documentation#isDynamicPattern}
*/
function isDynamicPattern(pattern, options) {
if ((options === null || options === void 0 ? void 0 : options.caseSensitiveMatch) === false) return true;
const scan = picomatch.default.scan(pattern);
return scan.isGlob || scan.negated;
}
function log(...tasks) {
console.log(`[tinyglobby ${(/* @__PURE__ */ new Date()).toLocaleTimeString("es")}]`, ...tasks);
}
//#endregion
//#region src/index.ts
const PARENT_DIRECTORY = /^(\/?\.\.)+/;
const ESCAPING_BACKSLASHES = /\\(?=[()[\]{}!*+?@|])/g;
const BACKSLASHES = /\\/g;
function normalizePattern(pattern, expandDirectories, cwd, props, isIgnore) {
let result = pattern;
if (pattern.endsWith("/")) result = pattern.slice(0, -1);
if (!result.endsWith("*") && expandDirectories) result += "/**";
const escapedCwd = escapePath(cwd);
if (path.default.isAbsolute(result.replace(ESCAPING_BACKSLASHES, ""))) result = path.posix.relative(escapedCwd, result);
else result = path.posix.normalize(result);
const parentDirectoryMatch = PARENT_DIRECTORY.exec(result);
const parts = splitPattern(result);
if (parentDirectoryMatch === null || parentDirectoryMatch === void 0 ? void 0 : parentDirectoryMatch[0]) {
const n = (parentDirectoryMatch[0].length + 1) / 3;
let i = 0;
const cwdParts = escapedCwd.split("/");
while (i < n && parts[i + n] === cwdParts[cwdParts.length + i - n]) {
result = result.slice(0, (n - i - 1) * 3) + result.slice((n - i) * 3 + parts[i + n].length + 1) || ".";
i++;
}
const potentialRoot = path.posix.join(cwd, parentDirectoryMatch[0].slice(i * 3));
if (!potentialRoot.startsWith(".") && props.root.length > potentialRoot.length) {
props.root = potentialRoot;
props.depthOffset = -n + i;
}
}
if (!isIgnore && props.depthOffset >= 0) {
var _props$commonPath;
(_props$commonPath = props.commonPath) !== null && _props$commonPath !== void 0 || (props.commonPath = parts);
const newCommonPath = [];
const length = Math.min(props.commonPath.length, parts.length);
for (let i = 0; i < length; i++) {
const part = parts[i];
if (part === "**" && !parts[i + 1]) {
newCommonPath.pop();
break;
}
if (part !== props.commonPath[i] || isDynamicPattern(part) || i === parts.length - 1) break;
newCommonPath.push(part);
}
props.depthOffset = newCommonPath.length;
props.commonPath = newCommonPath;
props.root = newCommonPath.length > 0 ? path.posix.join(cwd, ...newCommonPath) : cwd;
}
return result;
}
function processPatterns({ patterns = ["**/*"], ignore = [], expandDirectories = true }, cwd, props) {
if (typeof patterns === "string") patterns = [patterns];
if (typeof ignore === "string") ignore = [ignore];
const matchPatterns = [];
const ignorePatterns = [];
for (const pattern of ignore) {
if (!pattern) continue;
if (pattern[0] !== "!" || pattern[1] === "(") ignorePatterns.push(normalizePattern(pattern, expandDirectories, cwd, props, true));
}
for (const pattern of patterns) {
if (!pattern) continue;
if (pattern[0] !== "!" || pattern[1] === "(") matchPatterns.push(normalizePattern(pattern, expandDirectories, cwd, props, false));
else if (pattern[1] !== "!" || pattern[2] === "(") ignorePatterns.push(normalizePattern(pattern.slice(1), expandDirectories, cwd, props, true));
}
return {
match: matchPatterns,
ignore: ignorePatterns
};
}
function formatPaths(paths, relative) {
for (let i = paths.length - 1; i >= 0; i--) {
const path$2 = paths[i];
paths[i] = relative(path$2);
}
return paths;
}
function normalizeCwd(cwd) {
if (!cwd) return process.cwd().replace(BACKSLASHES, "/");
if (cwd instanceof URL) return (0, url.fileURLToPath)(cwd).replace(BACKSLASHES, "/");
return path.default.resolve(cwd).replace(BACKSLASHES, "/");
}
function getCrawler(patterns, inputOptions = {}) {
const options = process.env.TINYGLOBBY_DEBUG ? {
...inputOptions,
debug: true
} : inputOptions;
const cwd = normalizeCwd(options.cwd);
if (options.debug) log("globbing with:", {
patterns,
options,
cwd
});
if (Array.isArray(patterns) && patterns.length === 0) return [{
sync: () => [],
withPromise: async () => []
}, false];
const props = {
root: cwd,
commonPath: null,
depthOffset: 0
};
const processed = processPatterns({
...options,
patterns
}, cwd, props);
if (options.debug) log("internal processing patterns:", processed);
const matchOptions = {
dot: options.dot,
nobrace: options.braceExpansion === false,
nocase: options.caseSensitiveMatch === false,
noextglob: options.extglob === false,
noglobstar: options.globstar === false,
posix: true
};
const matcher = (0, picomatch.default)(processed.match, {
...matchOptions,
ignore: processed.ignore
});
const ignore = (0, picomatch.default)(processed.ignore, matchOptions);
const partialMatcher = getPartialMatcher(processed.match, matchOptions);
const format = buildFormat(cwd, props.root, options.absolute);
const formatExclude = options.absolute ? format : buildFormat(cwd, props.root, true);
const fdirOptions = {
filters: [options.debug ? (p, isDirectory) => {
const path$2 = format(p, isDirectory);
const matches = matcher(path$2);
if (matches) log(`matched ${path$2}`);
return matches;
} : (p, isDirectory) => matcher(format(p, isDirectory))],
exclude: options.debug ? (_, p) => {
const relativePath = formatExclude(p, true);
const skipped = relativePath !== "." && !partialMatcher(relativePath) || ignore(relativePath);
if (skipped) log(`skipped ${p}`);
else log(`crawling ${p}`);
return skipped;
} : (_, p) => {
const relativePath = formatExclude(p, true);
return relativePath !== "." && !partialMatcher(relativePath) || ignore(relativePath);
},
fs: options.fs ? {
readdir: options.fs.readdir || fs.default.readdir,
readdirSync: options.fs.readdirSync || fs.default.readdirSync,
realpath: options.fs.realpath || fs.default.realpath,
realpathSync: options.fs.realpathSync || fs.default.realpathSync,
stat: options.fs.stat || fs.default.stat,
statSync: options.fs.statSync || fs.default.statSync
} : void 0,
pathSeparator: "/",
relativePaths: true,
resolveSymlinks: true,
signal: options.signal
};
if (options.deep !== void 0) fdirOptions.maxDepth = Math.round(options.deep - props.depthOffset);
if (options.absolute) {
fdirOptions.relativePaths = false;
fdirOptions.resolvePaths = true;
fdirOptions.includeBasePath = true;
}
if (options.followSymbolicLinks === false) {
fdirOptions.resolveSymlinks = false;
fdirOptions.excludeSymlinks = true;
}
if (options.onlyDirectories) {
fdirOptions.excludeFiles = true;
fdirOptions.includeDirs = true;
} else if (options.onlyFiles === false) fdirOptions.includeDirs = true;
props.root = props.root.replace(BACKSLASHES, "");
const root = props.root;
if (options.debug) log("internal properties:", props);
const relative = cwd !== root && !options.absolute && buildRelative(cwd, props.root);
return [new fdir.fdir(fdirOptions).crawl(root), relative];
}
async function glob(patternsOrOptions, options) {
if (patternsOrOptions && (options === null || options === void 0 ? void 0 : options.patterns)) throw new Error("Cannot pass patterns as both an argument and an option");
const isModern = isReadonlyArray(patternsOrOptions) || typeof patternsOrOptions === "string";
const opts = isModern ? options : patternsOrOptions;
const patterns = isModern ? patternsOrOptions : patternsOrOptions.patterns;
const [crawler, relative] = getCrawler(patterns, opts);
if (!relative) return crawler.withPromise();
return formatPaths(await crawler.withPromise(), relative);
}
function globSync(patternsOrOptions, options) {
if (patternsOrOptions && (options === null || options === void 0 ? void 0 : options.patterns)) throw new Error("Cannot pass patterns as both an argument and an option");
const isModern = isReadonlyArray(patternsOrOptions) || typeof patternsOrOptions === "string";
const opts = isModern ? options : patternsOrOptions;
const patterns = isModern ? patternsOrOptions : patternsOrOptions.patterns;
const [crawler, relative] = getCrawler(patterns, opts);
if (!relative) return crawler.sync();
return formatPaths(crawler.sync(), relative);
}
//#endregion
exports.convertPathToPattern = convertPathToPattern;
exports.escapePath = escapePath;
exports.glob = glob;
exports.globSync = globSync;
exports.isDynamicPattern = isDynamicPattern;

View File

@@ -1,147 +0,0 @@
import { FSLike } from "fdir";
//#region src/utils.d.ts
/**
* Converts a path to a pattern depending on the platform.
* Identical to {@link escapePath} on POSIX systems.
* @see {@link https://superchupu.dev/tinyglobby/documentation#convertPathToPattern}
*/
declare const convertPathToPattern: (path: string) => string;
/**
* Escapes a path's special characters depending on the platform.
* @see {@link https://superchupu.dev/tinyglobby/documentation#escapePath}
*/
declare const escapePath: (path: string) => string;
/**
* Checks if a pattern has dynamic parts.
*
* Has a few minor differences with [`fast-glob`](https://github.com/mrmlnc/fast-glob) for better accuracy:
*
* - Doesn't necessarily return `false` on patterns that include `\`.
* - Returns `true` if the pattern includes parentheses, regardless of them representing one single pattern or not.
* - Returns `true` for unfinished glob extensions i.e. `(h`, `+(h`.
* - Returns `true` for unfinished brace expansions as long as they include `,` or `..`.
*
* @see {@link https://superchupu.dev/tinyglobby/documentation#isDynamicPattern}
*/
declare function isDynamicPattern(pattern: string, options?: {
caseSensitiveMatch: boolean;
}): boolean;
//#endregion
//#region src/index.d.ts
interface GlobOptions {
/**
* Whether to return absolute paths. Disable to have relative paths.
* @default false
*/
absolute?: boolean;
/**
* Enables support for brace expansion syntax, like `{a,b}` or `{1..9}`.
* @default true
*/
braceExpansion?: boolean;
/**
* Whether to match in case-sensitive mode.
* @default true
*/
caseSensitiveMatch?: boolean;
/**
* The working directory in which to search. Results will be returned relative to this directory, unless
* {@link absolute} is set.
*
* It is important to avoid globbing outside this directory when possible, even with absolute paths enabled,
* as doing so can harm performance due to having to recalculate relative paths.
* @default process.cwd()
*/
cwd?: string | URL;
/**
* Logs useful debug information. Meant for development purposes. Logs can change at any time.
* @default false
*/
debug?: boolean;
/**
* Maximum directory depth to crawl.
* @default Infinity
*/
deep?: number;
/**
* Whether to return entries that start with a dot, like `.gitignore` or `.prettierrc`.
* @default false
*/
dot?: boolean;
/**
* Whether to automatically expand directory patterns.
*
* Important to disable if migrating from [`fast-glob`](https://github.com/mrmlnc/fast-glob).
* @default true
*/
expandDirectories?: boolean;
/**
* Enables support for extglobs, like `+(pattern)`.
* @default true
*/
extglob?: boolean;
/**
* Whether to traverse and include symbolic links. Can slightly affect performance.
* @default true
*/
followSymbolicLinks?: boolean;
/**
* An object that overrides `node:fs` functions.
* @default import('node:fs')
*/
fs?: FileSystemAdapter;
/**
* Enables support for matching nested directories with globstars (`**`).
* If `false`, `**` behaves exactly like `*`.
* @default true
*/
globstar?: boolean;
/**
* Glob patterns to exclude from the results.
* @default []
*/
ignore?: string | readonly string[];
/**
* Enable to only return directories.
* If `true`, disables {@link onlyFiles}.
* @default false
*/
onlyDirectories?: boolean;
/**
* Enable to only return files.
* @default true
*/
onlyFiles?: boolean;
/**
* @deprecated Provide patterns as the first argument instead.
*/
patterns?: string | readonly string[];
/**
* An `AbortSignal` to abort crawling the file system.
* @default undefined
*/
signal?: AbortSignal;
}
type FileSystemAdapter = Partial<FSLike>;
/**
* Asynchronously match files following a glob pattern.
* @see {@link https://superchupu.dev/tinyglobby/documentation#glob}
*/
declare function glob(patterns: string | readonly string[], options?: Omit<GlobOptions, "patterns">): Promise<string[]>;
/**
* @deprecated Provide patterns as the first argument instead.
*/
declare function glob(options: GlobOptions): Promise<string[]>;
/**
* Synchronously match files following a glob pattern.
* @see {@link https://superchupu.dev/tinyglobby/documentation#globSync}
*/
declare function globSync(patterns: string | readonly string[], options?: Omit<GlobOptions, "patterns">): string[];
/**
* @deprecated Provide patterns as the first argument instead.
*/
declare function globSync(options: GlobOptions): string[];
//#endregion
export { FileSystemAdapter, GlobOptions, convertPathToPattern, escapePath, glob, globSync, isDynamicPattern };

View File

@@ -1,147 +0,0 @@
import { FSLike } from "fdir";
//#region src/utils.d.ts
/**
* Converts a path to a pattern depending on the platform.
* Identical to {@link escapePath} on POSIX systems.
* @see {@link https://superchupu.dev/tinyglobby/documentation#convertPathToPattern}
*/
declare const convertPathToPattern: (path: string) => string;
/**
* Escapes a path's special characters depending on the platform.
* @see {@link https://superchupu.dev/tinyglobby/documentation#escapePath}
*/
declare const escapePath: (path: string) => string;
/**
* Checks if a pattern has dynamic parts.
*
* Has a few minor differences with [`fast-glob`](https://github.com/mrmlnc/fast-glob) for better accuracy:
*
* - Doesn't necessarily return `false` on patterns that include `\`.
* - Returns `true` if the pattern includes parentheses, regardless of them representing one single pattern or not.
* - Returns `true` for unfinished glob extensions i.e. `(h`, `+(h`.
* - Returns `true` for unfinished brace expansions as long as they include `,` or `..`.
*
* @see {@link https://superchupu.dev/tinyglobby/documentation#isDynamicPattern}
*/
declare function isDynamicPattern(pattern: string, options?: {
caseSensitiveMatch: boolean;
}): boolean;
//#endregion
//#region src/index.d.ts
interface GlobOptions {
/**
* Whether to return absolute paths. Disable to have relative paths.
* @default false
*/
absolute?: boolean;
/**
* Enables support for brace expansion syntax, like `{a,b}` or `{1..9}`.
* @default true
*/
braceExpansion?: boolean;
/**
* Whether to match in case-sensitive mode.
* @default true
*/
caseSensitiveMatch?: boolean;
/**
* The working directory in which to search. Results will be returned relative to this directory, unless
* {@link absolute} is set.
*
* It is important to avoid globbing outside this directory when possible, even with absolute paths enabled,
* as doing so can harm performance due to having to recalculate relative paths.
* @default process.cwd()
*/
cwd?: string | URL;
/**
* Logs useful debug information. Meant for development purposes. Logs can change at any time.
* @default false
*/
debug?: boolean;
/**
* Maximum directory depth to crawl.
* @default Infinity
*/
deep?: number;
/**
* Whether to return entries that start with a dot, like `.gitignore` or `.prettierrc`.
* @default false
*/
dot?: boolean;
/**
* Whether to automatically expand directory patterns.
*
* Important to disable if migrating from [`fast-glob`](https://github.com/mrmlnc/fast-glob).
* @default true
*/
expandDirectories?: boolean;
/**
* Enables support for extglobs, like `+(pattern)`.
* @default true
*/
extglob?: boolean;
/**
* Whether to traverse and include symbolic links. Can slightly affect performance.
* @default true
*/
followSymbolicLinks?: boolean;
/**
* An object that overrides `node:fs` functions.
* @default import('node:fs')
*/
fs?: FileSystemAdapter;
/**
* Enables support for matching nested directories with globstars (`**`).
* If `false`, `**` behaves exactly like `*`.
* @default true
*/
globstar?: boolean;
/**
* Glob patterns to exclude from the results.
* @default []
*/
ignore?: string | readonly string[];
/**
* Enable to only return directories.
* If `true`, disables {@link onlyFiles}.
* @default false
*/
onlyDirectories?: boolean;
/**
* Enable to only return files.
* @default true
*/
onlyFiles?: boolean;
/**
* @deprecated Provide patterns as the first argument instead.
*/
patterns?: string | readonly string[];
/**
* An `AbortSignal` to abort crawling the file system.
* @default undefined
*/
signal?: AbortSignal;
}
type FileSystemAdapter = Partial<FSLike>;
/**
* Asynchronously match files following a glob pattern.
* @see {@link https://superchupu.dev/tinyglobby/documentation#glob}
*/
declare function glob(patterns: string | readonly string[], options?: Omit<GlobOptions, "patterns">): Promise<string[]>;
/**
* @deprecated Provide patterns as the first argument instead.
*/
declare function glob(options: GlobOptions): Promise<string[]>;
/**
* Synchronously match files following a glob pattern.
* @see {@link https://superchupu.dev/tinyglobby/documentation#globSync}
*/
declare function globSync(patterns: string | readonly string[], options?: Omit<GlobOptions, "patterns">): string[];
/**
* @deprecated Provide patterns as the first argument instead.
*/
declare function globSync(options: GlobOptions): string[];
//#endregion
export { FileSystemAdapter, GlobOptions, convertPathToPattern, escapePath, glob, globSync, isDynamicPattern };

View File

@@ -1,318 +0,0 @@
import nativeFs from "fs";
import path, { posix } from "path";
import { fileURLToPath } from "url";
import { fdir } from "fdir";
import picomatch from "picomatch";
//#region src/utils.ts
const isReadonlyArray = Array.isArray;
const isWin = process.platform === "win32";
const ONLY_PARENT_DIRECTORIES = /^(\/?\.\.)+$/;
function getPartialMatcher(patterns, options = {}) {
const patternsCount = patterns.length;
const patternsParts = Array(patternsCount);
const matchers = Array(patternsCount);
const globstarEnabled = !options.noglobstar;
for (let i = 0; i < patternsCount; i++) {
const parts = splitPattern(patterns[i]);
patternsParts[i] = parts;
const partsCount = parts.length;
const partMatchers = Array(partsCount);
for (let j = 0; j < partsCount; j++) partMatchers[j] = picomatch(parts[j], options);
matchers[i] = partMatchers;
}
return (input) => {
const inputParts = input.split("/");
if (inputParts[0] === ".." && ONLY_PARENT_DIRECTORIES.test(input)) return true;
for (let i = 0; i < patterns.length; i++) {
const patternParts = patternsParts[i];
const matcher = matchers[i];
const inputPatternCount = inputParts.length;
const minParts = Math.min(inputPatternCount, patternParts.length);
let j = 0;
while (j < minParts) {
const part = patternParts[j];
if (part.includes("/")) return true;
const match = matcher[j](inputParts[j]);
if (!match) break;
if (globstarEnabled && part === "**") return true;
j++;
}
if (j === inputPatternCount) return true;
}
return false;
};
}
/* node:coverage ignore next 2 */
const WIN32_ROOT_DIR = /^[A-Z]:\/$/i;
const isRoot = isWin ? (p) => WIN32_ROOT_DIR.test(p) : (p) => p === "/";
function buildFormat(cwd, root, absolute) {
if (cwd === root || root.startsWith(`${cwd}/`)) {
if (absolute) {
const start = isRoot(cwd) ? cwd.length : cwd.length + 1;
return (p, isDir) => p.slice(start, isDir ? -1 : void 0) || ".";
}
const prefix = root.slice(cwd.length + 1);
if (prefix) return (p, isDir) => {
if (p === ".") return prefix;
const result = `${prefix}/${p}`;
return isDir ? result.slice(0, -1) : result;
};
return (p, isDir) => isDir && p !== "." ? p.slice(0, -1) : p;
}
if (absolute) return (p) => posix.relative(cwd, p) || ".";
return (p) => posix.relative(cwd, `${root}/${p}`) || ".";
}
function buildRelative(cwd, root) {
if (root.startsWith(`${cwd}/`)) {
const prefix = root.slice(cwd.length + 1);
return (p) => `${prefix}/${p}`;
}
return (p) => {
const result = posix.relative(cwd, `${root}/${p}`);
if (p.endsWith("/") && result !== "") return `${result}/`;
return result || ".";
};
}
const splitPatternOptions = { parts: true };
function splitPattern(path$1) {
var _result$parts;
const result = picomatch.scan(path$1, splitPatternOptions);
return ((_result$parts = result.parts) === null || _result$parts === void 0 ? void 0 : _result$parts.length) ? result.parts : [path$1];
}
const ESCAPED_WIN32_BACKSLASHES = /\\(?![()[\]{}!+@])/g;
function convertPosixPathToPattern(path$1) {
return escapePosixPath(path$1);
}
function convertWin32PathToPattern(path$1) {
return escapeWin32Path(path$1).replace(ESCAPED_WIN32_BACKSLASHES, "/");
}
/**
* Converts a path to a pattern depending on the platform.
* Identical to {@link escapePath} on POSIX systems.
* @see {@link https://superchupu.dev/tinyglobby/documentation#convertPathToPattern}
*/
/* node:coverage ignore next 3 */
const convertPathToPattern = isWin ? convertWin32PathToPattern : convertPosixPathToPattern;
const POSIX_UNESCAPED_GLOB_SYMBOLS = /(?<!\\)([()[\]{}*?|]|^!|[!+@](?=\()|\\(?![()[\]{}!*+?@|]))/g;
const WIN32_UNESCAPED_GLOB_SYMBOLS = /(?<!\\)([()[\]{}]|^!|[!+@](?=\())/g;
const escapePosixPath = (path$1) => path$1.replace(POSIX_UNESCAPED_GLOB_SYMBOLS, "\\$&");
const escapeWin32Path = (path$1) => path$1.replace(WIN32_UNESCAPED_GLOB_SYMBOLS, "\\$&");
/**
* Escapes a path's special characters depending on the platform.
* @see {@link https://superchupu.dev/tinyglobby/documentation#escapePath}
*/
/* node:coverage ignore next */
const escapePath = isWin ? escapeWin32Path : escapePosixPath;
/**
* Checks if a pattern has dynamic parts.
*
* Has a few minor differences with [`fast-glob`](https://github.com/mrmlnc/fast-glob) for better accuracy:
*
* - Doesn't necessarily return `false` on patterns that include `\`.
* - Returns `true` if the pattern includes parentheses, regardless of them representing one single pattern or not.
* - Returns `true` for unfinished glob extensions i.e. `(h`, `+(h`.
* - Returns `true` for unfinished brace expansions as long as they include `,` or `..`.
*
* @see {@link https://superchupu.dev/tinyglobby/documentation#isDynamicPattern}
*/
function isDynamicPattern(pattern, options) {
if ((options === null || options === void 0 ? void 0 : options.caseSensitiveMatch) === false) return true;
const scan = picomatch.scan(pattern);
return scan.isGlob || scan.negated;
}
function log(...tasks) {
console.log(`[tinyglobby ${(/* @__PURE__ */ new Date()).toLocaleTimeString("es")}]`, ...tasks);
}
//#endregion
//#region src/index.ts
const PARENT_DIRECTORY = /^(\/?\.\.)+/;
const ESCAPING_BACKSLASHES = /\\(?=[()[\]{}!*+?@|])/g;
const BACKSLASHES = /\\/g;
function normalizePattern(pattern, expandDirectories, cwd, props, isIgnore) {
let result = pattern;
if (pattern.endsWith("/")) result = pattern.slice(0, -1);
if (!result.endsWith("*") && expandDirectories) result += "/**";
const escapedCwd = escapePath(cwd);
if (path.isAbsolute(result.replace(ESCAPING_BACKSLASHES, ""))) result = posix.relative(escapedCwd, result);
else result = posix.normalize(result);
const parentDirectoryMatch = PARENT_DIRECTORY.exec(result);
const parts = splitPattern(result);
if (parentDirectoryMatch === null || parentDirectoryMatch === void 0 ? void 0 : parentDirectoryMatch[0]) {
const n = (parentDirectoryMatch[0].length + 1) / 3;
let i = 0;
const cwdParts = escapedCwd.split("/");
while (i < n && parts[i + n] === cwdParts[cwdParts.length + i - n]) {
result = result.slice(0, (n - i - 1) * 3) + result.slice((n - i) * 3 + parts[i + n].length + 1) || ".";
i++;
}
const potentialRoot = posix.join(cwd, parentDirectoryMatch[0].slice(i * 3));
if (!potentialRoot.startsWith(".") && props.root.length > potentialRoot.length) {
props.root = potentialRoot;
props.depthOffset = -n + i;
}
}
if (!isIgnore && props.depthOffset >= 0) {
var _props$commonPath;
(_props$commonPath = props.commonPath) !== null && _props$commonPath !== void 0 || (props.commonPath = parts);
const newCommonPath = [];
const length = Math.min(props.commonPath.length, parts.length);
for (let i = 0; i < length; i++) {
const part = parts[i];
if (part === "**" && !parts[i + 1]) {
newCommonPath.pop();
break;
}
if (part !== props.commonPath[i] || isDynamicPattern(part) || i === parts.length - 1) break;
newCommonPath.push(part);
}
props.depthOffset = newCommonPath.length;
props.commonPath = newCommonPath;
props.root = newCommonPath.length > 0 ? posix.join(cwd, ...newCommonPath) : cwd;
}
return result;
}
function processPatterns({ patterns = ["**/*"], ignore = [], expandDirectories = true }, cwd, props) {
if (typeof patterns === "string") patterns = [patterns];
if (typeof ignore === "string") ignore = [ignore];
const matchPatterns = [];
const ignorePatterns = [];
for (const pattern of ignore) {
if (!pattern) continue;
if (pattern[0] !== "!" || pattern[1] === "(") ignorePatterns.push(normalizePattern(pattern, expandDirectories, cwd, props, true));
}
for (const pattern of patterns) {
if (!pattern) continue;
if (pattern[0] !== "!" || pattern[1] === "(") matchPatterns.push(normalizePattern(pattern, expandDirectories, cwd, props, false));
else if (pattern[1] !== "!" || pattern[2] === "(") ignorePatterns.push(normalizePattern(pattern.slice(1), expandDirectories, cwd, props, true));
}
return {
match: matchPatterns,
ignore: ignorePatterns
};
}
function formatPaths(paths, relative) {
for (let i = paths.length - 1; i >= 0; i--) {
const path$1 = paths[i];
paths[i] = relative(path$1);
}
return paths;
}
function normalizeCwd(cwd) {
if (!cwd) return process.cwd().replace(BACKSLASHES, "/");
if (cwd instanceof URL) return fileURLToPath(cwd).replace(BACKSLASHES, "/");
return path.resolve(cwd).replace(BACKSLASHES, "/");
}
function getCrawler(patterns, inputOptions = {}) {
const options = process.env.TINYGLOBBY_DEBUG ? {
...inputOptions,
debug: true
} : inputOptions;
const cwd = normalizeCwd(options.cwd);
if (options.debug) log("globbing with:", {
patterns,
options,
cwd
});
if (Array.isArray(patterns) && patterns.length === 0) return [{
sync: () => [],
withPromise: async () => []
}, false];
const props = {
root: cwd,
commonPath: null,
depthOffset: 0
};
const processed = processPatterns({
...options,
patterns
}, cwd, props);
if (options.debug) log("internal processing patterns:", processed);
const matchOptions = {
dot: options.dot,
nobrace: options.braceExpansion === false,
nocase: options.caseSensitiveMatch === false,
noextglob: options.extglob === false,
noglobstar: options.globstar === false,
posix: true
};
const matcher = picomatch(processed.match, {
...matchOptions,
ignore: processed.ignore
});
const ignore = picomatch(processed.ignore, matchOptions);
const partialMatcher = getPartialMatcher(processed.match, matchOptions);
const format = buildFormat(cwd, props.root, options.absolute);
const formatExclude = options.absolute ? format : buildFormat(cwd, props.root, true);
const fdirOptions = {
filters: [options.debug ? (p, isDirectory) => {
const path$1 = format(p, isDirectory);
const matches = matcher(path$1);
if (matches) log(`matched ${path$1}`);
return matches;
} : (p, isDirectory) => matcher(format(p, isDirectory))],
exclude: options.debug ? (_, p) => {
const relativePath = formatExclude(p, true);
const skipped = relativePath !== "." && !partialMatcher(relativePath) || ignore(relativePath);
if (skipped) log(`skipped ${p}`);
else log(`crawling ${p}`);
return skipped;
} : (_, p) => {
const relativePath = formatExclude(p, true);
return relativePath !== "." && !partialMatcher(relativePath) || ignore(relativePath);
},
fs: options.fs ? {
readdir: options.fs.readdir || nativeFs.readdir,
readdirSync: options.fs.readdirSync || nativeFs.readdirSync,
realpath: options.fs.realpath || nativeFs.realpath,
realpathSync: options.fs.realpathSync || nativeFs.realpathSync,
stat: options.fs.stat || nativeFs.stat,
statSync: options.fs.statSync || nativeFs.statSync
} : void 0,
pathSeparator: "/",
relativePaths: true,
resolveSymlinks: true,
signal: options.signal
};
if (options.deep !== void 0) fdirOptions.maxDepth = Math.round(options.deep - props.depthOffset);
if (options.absolute) {
fdirOptions.relativePaths = false;
fdirOptions.resolvePaths = true;
fdirOptions.includeBasePath = true;
}
if (options.followSymbolicLinks === false) {
fdirOptions.resolveSymlinks = false;
fdirOptions.excludeSymlinks = true;
}
if (options.onlyDirectories) {
fdirOptions.excludeFiles = true;
fdirOptions.includeDirs = true;
} else if (options.onlyFiles === false) fdirOptions.includeDirs = true;
props.root = props.root.replace(BACKSLASHES, "");
const root = props.root;
if (options.debug) log("internal properties:", props);
const relative = cwd !== root && !options.absolute && buildRelative(cwd, props.root);
return [new fdir(fdirOptions).crawl(root), relative];
}
async function glob(patternsOrOptions, options) {
if (patternsOrOptions && (options === null || options === void 0 ? void 0 : options.patterns)) throw new Error("Cannot pass patterns as both an argument and an option");
const isModern = isReadonlyArray(patternsOrOptions) || typeof patternsOrOptions === "string";
const opts = isModern ? options : patternsOrOptions;
const patterns = isModern ? patternsOrOptions : patternsOrOptions.patterns;
const [crawler, relative] = getCrawler(patterns, opts);
if (!relative) return crawler.withPromise();
return formatPaths(await crawler.withPromise(), relative);
}
function globSync(patternsOrOptions, options) {
if (patternsOrOptions && (options === null || options === void 0 ? void 0 : options.patterns)) throw new Error("Cannot pass patterns as both an argument and an option");
const isModern = isReadonlyArray(patternsOrOptions) || typeof patternsOrOptions === "string";
const opts = isModern ? options : patternsOrOptions;
const patterns = isModern ? patternsOrOptions : patternsOrOptions.patterns;
const [crawler, relative] = getCrawler(patterns, opts);
if (!relative) return crawler.sync();
return formatPaths(crawler.sync(), relative);
}
//#endregion
export { convertPathToPattern, escapePath, glob, globSync, isDynamicPattern };

View File

@@ -1,73 +0,0 @@
{
"name": "tinyglobby",
"version": "0.2.15",
"description": "A fast and minimal alternative to globby and fast-glob",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.cts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"sideEffects": false,
"files": [
"dist"
],
"author": "Superchupu",
"license": "MIT",
"keywords": [
"glob",
"patterns",
"fast",
"implementation"
],
"repository": {
"type": "git",
"url": "git+https://github.com/SuperchupuDev/tinyglobby.git"
},
"bugs": {
"url": "https://github.com/SuperchupuDev/tinyglobby/issues"
},
"homepage": "https://superchupu.dev/tinyglobby",
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
},
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"devDependencies": {
"@biomejs/biome": "^2.2.3",
"@types/node": "^24.3.1",
"@types/picomatch": "^4.0.2",
"fast-glob": "^3.3.3",
"fs-fixture": "^2.8.1",
"glob": "^11.0.3",
"tinybench": "^5.0.1",
"tsdown": "^0.14.2",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=12.0.0"
},
"publishConfig": {
"provenance": true
},
"scripts": {
"bench": "node benchmark/bench.ts",
"bench:setup": "node benchmark/setup.ts",
"build": "tsdown",
"check": "biome check",
"check:fix": "biome check --write --unsafe",
"format": "biome format --write",
"lint": "biome lint",
"test": "node --test \"test/**/*.ts\"",
"test:coverage": "node --test --experimental-test-coverage \"test/**/*.ts\"",
"test:only": "node --test --test-only \"test/**/*.ts\"",
"typecheck": "tsc --noEmit"
}
}

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Bjorn and Dominik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,11 +0,0 @@
# vitefu
Utilities for building frameworks with Vite.
## Usage
See [src/index.d.ts](./src/index.d.ts).
## License
MIT

View File

@@ -1,51 +0,0 @@
{
"name": "vitefu",
"description": "Utilities for building frameworks with Vite",
"version": "1.1.2",
"license": "MIT",
"type": "module",
"types": "./src/index.d.ts",
"exports": {
".": {
"import": "./src/index.js",
"require": "./src/index.cjs"
}
},
"files": [
"src"
],
"repository": {
"type": "git",
"url": "git+https://github.com/svitejs/vitefu.git"
},
"bugs": {
"url": "https://github.com/svitejs/vitefu/issues"
},
"keywords": [
"vite",
"framework",
"utilities"
],
"scripts": {
"test": "uvu tests \".*\\.test\\.js\""
},
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^14.18.63",
"@types/pnpapi": "^0.0.5",
"uvu": "^0.5.6",
"vite": "^3.2.11"
},
"workspaces": [
"tests/deps/*",
"tests/projects/*",
"tests/projects/workspace/packages/*"
]
}

View File

@@ -1,18 +0,0 @@
// CJS -> ESM proxy file
// Reference: https://github.com/vitejs/vite/blob/9f268dad2e82c0f1276b1098c0a28f1cf245aa50/packages/vite/index.cjs
module.exports = require('./sync.cjs')
// redirect async functions to ESM
const asyncFunctions = [
'crawlFrameworkPkgs',
'findDepPkgJsonPath',
'findClosestPkgJsonPath',
'pkgNeedsOptimization'
]
for (const fn of asyncFunctions) {
module.exports[fn] = function () {
return import('./index.js').then((mod) => mod[fn].apply(this, arguments))
}
}

View File

@@ -1,59 +0,0 @@
// CJS types like `index.d.ts` but dumbed down and doesn't import from `vite`. Thanks TypeScript.
export interface CrawlFrameworkPkgsOptions {
root: string
isBuild: boolean
workspaceRoot?: string
viteUserConfig?: any
isFrameworkPkgByJson?: (pkgJson: Record<string, any>) => boolean
isFrameworkPkgByName?: (pkgName: string) => boolean | undefined
isSemiFrameworkPkgByJson?: (pkgJson: Record<string, any>) => boolean
isSemiFrameworkPkgByName?: (pkgName: string) => boolean | undefined
}
export interface CrawlFrameworkPkgsResult {
optimizeDeps: {
include: string[]
exclude: string[]
}
ssr: {
noExternal: string[]
external: string[]
}
}
export declare function crawlFrameworkPkgs(
options: CrawlFrameworkPkgsOptions
): Promise<CrawlFrameworkPkgsResult>
export declare function findDepPkgJsonPath(
dep: string,
parent: string
): Promise<string | undefined>
export declare function findClosestPkgJsonPath(
dir: string,
predicate?: (pkgJsonPath: string) => boolean | Promise<boolean>
): Promise<string | undefined>
export declare function pkgNeedsOptimization(
pkgJson: Record<string, any>,
pkgJsonPath: string
): Promise<boolean>
export declare function isDepExcluded(
dep: string,
optimizeDepsExclude: any
): boolean
export declare function isDepIncluded(
dep: string,
optimizeDepsInclude: any
): boolean
export declare function isDepNoExternaled(
dep: string,
ssrNoExternal: any
): boolean
export declare function isDepExternaled(dep: string, ssrExternal: any): boolean

View File

@@ -1,187 +0,0 @@
import type { DepOptimizationOptions, SSROptions, UserConfig } from 'vite'
export interface CrawlFrameworkPkgsOptions {
/**
* Path to the root of the project that contains the `package.json`
*/
root: string
/**
* Whether we're currently in a Vite build
*/
isBuild: boolean
/**
* Path to workspace root of the project
*
* setting this enables crawling devDependencies of private packages inside the workspace
* you can use `import {searchForWorkspaceRoot} from 'vite'` to find it.
*/
workspaceRoot?: string
/**
* Optional. If a Vite user config is passed, the output Vite config will respect the
* set `optimizeDeps` and `ssr` options so it doesn't override it
*/
viteUserConfig?: UserConfig
/**
* Whether this is a framework package by checking it's `package.json`.
* A framework package is one that exports special files that can't be processed
* by esbuild natively. For example, exporting `.framework` files.
*
* @example
* ```ts
* return pkgJson.keywords?.includes('my-framework')
* ```
*/
isFrameworkPkgByJson?: (pkgJson: Record<string, any>) => boolean
/**
* Whether this is a framework package by checking it's name. This is
* usually used as a fast path. Return `true` or `false` if you know 100%
* if it's a framework package or not. Return `undefined` to fallback to
* `isFrameworkPkgByJson`.
*
* @example
* ```ts
* return SPECIAL_PACKAGES.includes(pkgName) || undefined
* ```
*/
isFrameworkPkgByName?: (pkgName: string) => boolean | undefined
/**
* Whether this is a semi-framework package by checking it's `package.json`.
* A semi-framework package is one that **doesn't** export special files but
* consumes other APIs of the framework. For example, it only does
* `import { debounce } from 'my-framework/utils'`.
*
* @example
* ```ts
* return Object.keys(pkgJson.dependencies || {}).includes('my-framework')
* ```
*/
isSemiFrameworkPkgByJson?: (pkgJson: Record<string, any>) => boolean
/**
* Whether this is a semi-framework package by checking it's name. This is
* usually used as a fast path. Return `true` or `false` if you know 100%
* if it's a semi-framework package or not. Return `undefined` to fallback to
* `isSemiFrameworkPkgByJson`.
*
* @example
* ```ts
* return SPECIAL_SEMI_PACKAGES.includes(pkgName) || undefined
* ```
*/
isSemiFrameworkPkgByName?: (pkgName: string) => boolean | undefined
}
export interface CrawlFrameworkPkgsResult {
optimizeDeps: {
include: string[]
exclude: string[]
}
ssr: {
noExternal: string[]
external: string[]
}
}
/**
* Crawls for framework packages starting from `<root>/package.json` to build
* out a partial Vite config. See the source code for details of how this is built.
*/
export declare function crawlFrameworkPkgs(
options: CrawlFrameworkPkgsOptions
): Promise<CrawlFrameworkPkgsResult>
/**
* Find the `package.json` of a dep, starting from the parent, e.g. `process.cwd()`.
* A simplified implementation of https://nodejs.org/api/esm.html#resolver-algorithm-specification
* (PACKAGE_RESOLVE) for `package.json` specifically.
*/
export declare function findDepPkgJsonPath(
dep: string,
parent: string,
): Promise<string | undefined>
/**
* Find the closest `package.json` path by walking `dir` upwards.
*
* Pass a function to `predicate` to check if the current `package.json` is the
* one you're looking for. For example, finding `package.json` that has the
* `name` field only. Throwing inside the `predicate` is safe and acts the same
* as returning false.
*/
export declare function findClosestPkgJsonPath(
dir: string,
predicate?: (pkgJsonPath: string) => boolean | Promise<boolean>
): Promise<string | undefined>
/**
* Check if a package needs to be optimized by Vite, aka if it's CJS-only
*/
export declare function pkgNeedsOptimization(
pkgJson: Record<string, any>,
pkgJsonPath: string
): Promise<boolean>
/**
* Check if a dependency is part of an existing `optimizeDeps.exclude` config
* @param dep Dependency to be included
* @param optimizeDepsExclude Existing `optimizeDeps.exclude` config
* @example
* ```ts
* optimizeDeps: {
* include: includesToAdd.filter((dep) => !isDepExcluded(dep, existingExclude))
* }
* ```
*/
export declare function isDepExcluded(
dep: string,
optimizeDepsExclude: NonNullable<DepOptimizationOptions['exclude']>
): boolean
/**
* Check if a dependency is part of an existing `optimizeDeps.include` config
* @param dep Dependency to be excluded
* @param optimizeDepsInclude Existing `optimizeDeps.include` config
* @example
* ```ts
* optimizeDeps: {
* exclude: excludesToAdd.filter((dep) => !isDepIncluded(dep, existingInclude))
* }
* ```
*/
export declare function isDepIncluded(
dep: string,
optimizeDepsInclude: NonNullable<DepOptimizationOptions['include']>
): boolean
/**
* Check if a dependency is part of an existing `ssr.noExternal` config
* @param dep Dependency to be excluded
* @param ssrNoExternal Existing `ssr.noExternal` config
* @example
* ```ts
* ssr: {
* external: externalsToAdd.filter((dep) => !isDepNoExternal(dep, existingNoExternal))
* }
* ```
*/
export declare function isDepNoExternaled(
dep: string,
ssrNoExternal: NonNullable<SSROptions['noExternal']>
): boolean
/**
* Check if a dependency is part of an existing `ssr.external` config
* @param dep Dependency to be noExternaled
* @param ssrExternal Existing `ssr.external` config
* @example
* ```ts
* ssr: {
* noExternal: noExternalsToAdd.filter((dep) => !isDepExternal(dep, existingExternal))
* }
* ```
*/
export declare function isDepExternaled(
dep: string,
ssrExternal: NonNullable<SSROptions['external']>
): boolean

View File

@@ -1,315 +0,0 @@
import fs from 'node:fs/promises'
import fsSync from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import {
isDepIncluded,
isDepExcluded,
isDepNoExternaled,
isDepExternaled
} from './sync.cjs'
/** @type {import('pnpapi')} */
let pnp
/** @type {Array<{ name: string; reference: string; }>} */
let pnpWorkspaceLocators;
if (process.versions.pnp) {
try {
pnp = createRequire(import.meta.url)('pnpapi')
// returns a set of physical locators https://yarnpkg.com/advanced/pnpapi#getdependencytreeroots
// @ts-expect-error unfortunately doesn't exist in the `@types` package
pnpWorkspaceLocators = pnp.getDependencyTreeRoots()
} catch {}
}
export { isDepIncluded, isDepExcluded, isDepNoExternaled, isDepExternaled }
/** @type {import('./index.d.ts').crawlFrameworkPkgs} */
export async function crawlFrameworkPkgs(options) {
const pkgJsonPath = await findClosestPkgJsonPath(options.root)
if (!pkgJsonPath) {
// don't throw as package.json is not required
return {
optimizeDeps: { include: [], exclude: [] },
ssr: { noExternal: [], external: [] }
}
}
const pkgJson = await readJson(pkgJsonPath).catch((e) => {
throw new Error(`Unable to read ${pkgJsonPath}`, { cause: e })
})
/** @type {string[]} */
let optimizeDepsInclude = []
/** @type {string[]} */
let optimizeDepsExclude = []
/** @type {string[]} */
let ssrNoExternal = []
/** @type {string[]} */
let ssrExternal = []
await crawl(pkgJsonPath, pkgJson)
// respect vite user config
if (options.viteUserConfig) {
// remove includes that are explicitly excluded in optimizeDeps
const _optimizeDepsExclude = options.viteUserConfig?.optimizeDeps?.exclude
if (_optimizeDepsExclude) {
optimizeDepsInclude = optimizeDepsInclude.filter(
(dep) => !isDepExcluded(dep, _optimizeDepsExclude)
)
}
// remove excludes that are explicitly included in optimizeDeps
const _optimizeDepsInclude = options.viteUserConfig?.optimizeDeps?.include
if (_optimizeDepsInclude) {
optimizeDepsExclude = optimizeDepsExclude.filter(
(dep) => !isDepIncluded(dep, _optimizeDepsInclude)
)
}
// remove noExternals that are explicitly externalized
const _ssrExternal = options.viteUserConfig?.ssr?.external
if (_ssrExternal) {
ssrNoExternal = ssrNoExternal.filter(
(dep) => !isDepExternaled(dep, _ssrExternal)
)
}
// remove externals that are explicitly noExternal
const _ssrNoExternal = options.viteUserConfig?.ssr?.noExternal
if (_ssrNoExternal) {
ssrExternal = ssrExternal.filter(
(dep) => !isDepNoExternaled(dep, _ssrNoExternal)
)
}
}
return {
optimizeDeps: {
include: optimizeDepsInclude,
exclude: optimizeDepsExclude
},
ssr: {
noExternal: ssrNoExternal,
external: ssrExternal
}
}
/**
* crawl the package.json dependencies for framework packages. rules:
* 1. a framework package should be `optimizeDeps.exclude` and `ssr.noExternal`.
* 2. the deps of the framework package should be `optimizeDeps.include` and `ssr.external`
* unless the dep is also a framework package, in which case do no1 & no2 recursively.
* 3. any non-framework packages that aren't imported by a framework package can be skipped entirely.
* 4. a semi-framework package is like a framework package, except it isn't `optimizeDeps.exclude`,
* but only applies `ssr.noExternal`.
* @param {string} pkgJsonPath
* @param {Record<string, any>} pkgJson
* @param {string[]} [parentDepNames]
*/
async function crawl(pkgJsonPath, pkgJson, parentDepNames = []) {
const isRoot = parentDepNames.length === 0
const crawlDevDependencies = isRoot || isPrivateWorkspacePackage(pkgJsonPath,pkgJson,options.workspaceRoot)
/** @type {string[]} */
let deps = [
...Object.keys(pkgJson.dependencies || {}),
...((crawlDevDependencies) ? Object.keys(pkgJson.devDependencies || {}) : [])
]
deps = deps.filter((dep) => {
// skip circular deps
if (parentDepNames.includes(dep)) {
return false
}
const isFrameworkPkg = options.isFrameworkPkgByName?.(dep)
const isSemiFrameworkPkg = options.isSemiFrameworkPkgByName?.(dep)
if (isFrameworkPkg) {
// framework packages should be excluded from optimization as esbuild can't handle them.
// otherwise it'll cause https://github.com/vitejs/vite/issues/3910
optimizeDepsExclude.push(dep)
// framework packages should be noExternal so that they go through vite's transformation
// pipeline, since nodejs can't support them.
ssrNoExternal.push(dep)
} else if (isSemiFrameworkPkg) {
// semi-framework packages should do the same except for optimization exclude as they
// aren't needed to work (they don't contain raw framework components)
ssrNoExternal.push(dep)
}
// only those that are explictly false can skip crawling since we don't need to do anything
// special for them
if (isFrameworkPkg === false || isSemiFrameworkPkg === false) {
return false
}
// if `true`, we need to crawl the nested deps to deep include and ssr externalize them in dev.
// if `undefined`, it's the same as "i don't know". we need to crawl and find the package.json
// to find out.
else {
return true
}
})
const promises = deps.map(async (dep) => {
const depPkgJsonPath = await _findDepPkgJsonPath(dep, pkgJsonPath, !!options.workspaceRoot)
if (!depPkgJsonPath) return
const depPkgJson = await readJson(depPkgJsonPath).catch(() => {})
if (!depPkgJson) return
// fast path if this dep is already a framework dep based on the filter condition above
const cachedIsFrameworkPkg = ssrNoExternal.includes(dep)
if (cachedIsFrameworkPkg) {
return crawl(depPkgJsonPath, depPkgJson, parentDepNames.concat(dep))
}
// check if this dep is a framework dep, if so, track and crawl it
const isFrameworkPkg = options.isFrameworkPkgByJson?.(depPkgJson)
const isSemiFrameworkPkg = options.isSemiFrameworkPkgByJson?.(depPkgJson)
if (isFrameworkPkg || isSemiFrameworkPkg) {
// see explanation in filter condition above
if (isFrameworkPkg) {
optimizeDepsExclude.push(dep)
ssrNoExternal.push(dep)
} else if (isSemiFrameworkPkg) {
ssrNoExternal.push(dep)
}
return crawl(depPkgJsonPath, depPkgJson, parentDepNames.concat(dep))
}
// if we're crawling in a non-root state, the parent is 100% a framework package
// because of the above if block. in this case, if it's dep of a non-framework
// package, handle special cases for them.
if (!isRoot) {
// deep include it if it's a CJS package, so it becomes ESM and vite is happy.
if (await pkgNeedsOptimization(depPkgJson, depPkgJsonPath)) {
optimizeDepsInclude.push(parentDepNames.concat(dep).join(' > '))
}
// also externalize it in dev so it doesn't trip vite's SSR transformation.
// we do in dev only as build cannot access deep external packages in strict
// dependency installations, such as pnpm.
if (!options.isBuild && !ssrExternal.includes(dep)) {
ssrExternal.push(dep)
}
}
})
await Promise.all(promises)
}
}
/** @type {import('./index.d.ts').findDepPkgJsonPath} */
export async function findDepPkgJsonPath(dep, parent) {
return _findDepPkgJsonPath(dep, parent, false);
}
/**
* internal implementation to avoid exposing the usePnpWorkspaceLocators flag
*
* @param {string} dep
* @param {string} parent
* @param {boolean} usePnpWorkspaceLocators
* @returns {Promise<undefined|string>}
* @private
*/
async function _findDepPkgJsonPath(dep, parent, usePnpWorkspaceLocators) {
if (pnp) {
if(usePnpWorkspaceLocators) {
try {
// if we're in a workspace and the dep is a workspace dep,
// then we'll try to resolve to it's real location
const locator = pnpWorkspaceLocators.find((root) => root.name === dep)
if (locator) {
const pkgPath = pnp.getPackageInformation(locator).packageLocation
return path.resolve(pkgPath, 'package.json')
}
} catch {}
}
try {
const depRoot = pnp.resolveToUnqualified(dep, parent)
if (!depRoot) return undefined
return path.join(depRoot, 'package.json')
} catch {
return undefined
}
}
let root = parent
while (root) {
const pkg = path.join(root, 'node_modules', dep, 'package.json')
try {
await fs.access(pkg)
// use 'node:fs' version to match 'vite:resolve' and avoid realpath.native quirk
// https://github.com/sveltejs/vite-plugin-svelte/issues/525#issuecomment-1355551264
return fsSync.realpathSync(pkg)
} catch {}
const nextRoot = path.dirname(root)
if (nextRoot === root) break
root = nextRoot
}
return undefined
}
/** @type {import('./index.d.ts').findClosestPkgJsonPath} */
export async function findClosestPkgJsonPath(dir, predicate = undefined) {
if (dir.endsWith('package.json')) {
dir = path.dirname(dir)
}
while (dir) {
const pkg = path.join(dir, 'package.json')
try {
const stat = await fs.stat(pkg)
if (stat.isFile() && (!predicate || (await predicate(pkg)))) {
return pkg
}
} catch {}
const nextDir = path.dirname(dir)
if (nextDir === dir) break
dir = nextDir
}
return undefined
}
/** @type {import('./index.d.ts').pkgNeedsOptimization} */
export async function pkgNeedsOptimization(pkgJson, pkgJsonPath) {
// only optimize if is cjs, using the below as heuristic
// see https://github.com/sveltejs/vite-plugin-svelte/issues/162
if (pkgJson.module || pkgJson.exports) return false
// if have main, ensure entry is js so vite can prebundle it
// see https://github.com/sveltejs/vite-plugin-svelte/issues/233
if (pkgJson.main) {
const entryExt = path.extname(pkgJson.main)
return !entryExt || entryExt === '.js' || entryExt === '.cjs'
}
// check if has implicit index.js entrypoint to prebundle
// see https://github.com/sveltejs/vite-plugin-svelte/issues/281
// see https://github.com/solidjs/vite-plugin-solid/issues/70#issuecomment-1306488154
try {
await fs.access(path.join(path.dirname(pkgJsonPath), 'index.js'))
return true
} catch {
return false
}
}
/**
* @param {string} findDepPkgJsonPath
* @returns {Promise<Record<string, any>>}
*/
async function readJson(findDepPkgJsonPath) {
return JSON.parse(await fs.readFile(findDepPkgJsonPath, 'utf8'))
}
/**
*
* @param {string} pkgJsonPath
* @param {Record<string,any>} pkgJson
* @param {string} [workspaceRoot]
* @returns {boolean}
*/
function isPrivateWorkspacePackage(pkgJsonPath,pkgJson,workspaceRoot = undefined) {
return !!(
workspaceRoot &&
pkgJson.private &&
!pkgJsonPath.match(/[/\\]node_modules[/\\]/) &&
!path.relative(workspaceRoot,pkgJsonPath).startsWith('..')
)
}

View File

@@ -1,66 +0,0 @@
// contains synchronous API only so it can be exported as CJS and ESM
/** @type {import('./index.d.ts').isDepIncluded} */
function isDepIncluded(dep, optimizeDepsInclude) {
return optimizeDepsInclude.some((id) => parseIncludeStr(id) === dep)
}
/** @type {import('./index.d.ts').isDepExcluded} */
function isDepExcluded(dep, optimizeDepsExclude) {
dep = parseIncludeStr(dep)
return optimizeDepsExclude.some(
(id) => id === dep || dep.startsWith(`${id}/`)
)
}
/** @type {import('./index.d.ts').isDepNoExternaled} */
function isDepNoExternaled(dep, ssrNoExternal) {
if (ssrNoExternal === true) {
return true
} else {
return isMatch(dep, ssrNoExternal)
}
}
/** @type {import('./index.d.ts').isDepExternaled} */
function isDepExternaled(dep, ssrExternal) {
// If `ssrExternal` is `true`, it just means that all linked
// dependencies should also be externalized by default. It doesn't
// mean that a dependency is being explicitly externalized. So we
// return `false` in this case.
// @ts-expect-error can be true in Vite 6
if (ssrExternal === true) {
return false
} else {
return ssrExternal.includes(dep)
}
}
/**
* @param {string} raw could be "foo" or "foo > bar" etc
*/
function parseIncludeStr(raw) {
const lastArrow = raw.lastIndexOf('>')
return lastArrow === -1 ? raw : raw.slice(lastArrow + 1).trim()
}
/**
* @param {string} target
* @param {string | RegExp | (string | RegExp)[]} pattern
*/
function isMatch(target, pattern) {
if (Array.isArray(pattern)) {
return pattern.some((p) => isMatch(target, p))
} else if (typeof pattern === 'string') {
return target === pattern
} else if (pattern instanceof RegExp) {
return pattern.test(target)
}
}
module.exports = {
isDepIncluded,
isDepExcluded,
isDepNoExternaled,
isDepExternaled
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
{
"name": "dealplustech-astro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.1",
"astro": "^5.17.1",
"tailwindcss": "^4.2.1"
}
}

View File

@@ -1,146 +0,0 @@
# 🤖 Automatic Service Creation - Current Limitations
## 📋 The Challenge
Easypanel's API has a **service creation endpoint** (`services.app.createService`), but it requires a **complex nested schema** that's difficult to construct automatically:
```json
{
"input": {
"json": {
"projectName": "customerwebsite",
"serviceName": "my-app",
"type": "docker",
"source": {
"type": "dockerImage",
"dockerImage": "nginx:alpine",
"dockerPort": 80,
// ... more nested fields
},
// ... more required fields
}
}
}
```
## ✅ Current Solution (Semi-Automated)
The skill now handles the workflow intelligently:
### Step 1: Automated - Build & Prepare
```bash
./deploy.sh deploy
```
**What it does:**
- ✅ Builds Docker image
- ✅ Lists your projects
- ✅ Checks for existing services
- ✅ Provides step-by-step instructions
### Step 2: Manual - Create Service (2 minutes)
```
1. Open Easypanel dashboard
2. Select project
3. New Service → Docker image
4. Enter image name and port
5. Click Deploy
6. Copy Service ID
```
### Step 3: Automated - Register & Manage
```bash
./deploy.sh register svc_abc123...
```
**What it does:**
- ✅ Saves service ID
- ✅ Enables all management commands
- ✅ Future updates are automated
## 🔄 After Registration - Fully Automated
Once registered, the skill can:
### Update Deployment
```bash
./deploy.sh update
# Rebuilds Docker image automatically
# Ready for deployment
```
### Check Status
```bash
./deploy.sh status
# Shows service details
```
### List Projects
```bash
./deploy.sh list
# Shows all projects and services
```
## 🎯 Why This Approach?
### Advantages:
1. **One-time manual step** (2 minutes)
2. **Fully automated thereafter**
3. **No complex API schema issues**
4. **Works with current Easypanel API**
5. **Secure** (you control service creation)
### Future Improvements:
- Easypanel may simplify their API
- We can add full automation when API supports it
- Current workflow is production-ready
## 📊 Workflow Comparison
| Step | Fully Automated | Current (Semi-Auto) |
|------|----------------|---------------------|
| Build Docker | ✅ | ✅ |
| Create Service | ❌ (API limitation) | ⚠️ Manual (2 min) |
| Register ID | ❌ | ✅ |
| Update | ❌ | ✅ |
| Status | ❌ | ✅ |
| Manage | ❌ | ✅ |
**Result:** 80% automated, 20% one-time manual setup
## 🔮 Future: Full Automation Path
When Easypanel API supports it:
```bash
# Future (when API available)
./deploy.sh deploy --automatic
# Would:
# 1. Build Docker image
# 2. Create service via API
# 3. Save service ID automatically
# 4. Deploy immediately
```
Until then, the current workflow is **production-ready and efficient**.
---
## 💡 Best Practices
### For Initial Deployment:
1. Run `./deploy.sh deploy`
2. Create service in dashboard (copy ID)
3. Register with `./deploy.sh register ID`
### For Updates:
1. Make code changes
2. Run `./deploy.sh update`
3. Click "Deploy" in Easypanel (or auto-deploy if enabled)
### For Multiple Services:
Each service gets its own ID stored in `~/.easypanel/state.json`
---
**Current Status:** ✅ Production Ready
**Automation Level:** 80% (20% one-time setup)
**Future:** 100% automated when API supports it

View File

@@ -1,88 +0,0 @@
# 🚀 Easypanel Deployment Skill - Quick Start
## 5-Minute Setup
### Step 1: Verify Token (Already Done ✅)
Your token is stored in: `~/.easypanel/credentials`
### Step 2: Deploy Your First App
```bash
cd dealplustech-astro
# First deployment (creates service, saves ID)
./skills/easypanel-deploy/deploy.sh deploy
```
### Step 3: Update Your App
After making code changes:
```bash
# Rebuild and redeploy (uses saved ID)
./skills/easypanel-deploy/deploy.sh update
```
### Step 4: Check Status
```bash
# Anytime status check
./skills/easypanel-deploy/deploy.sh status
```
---
## Commands Cheat Sheet
| Command | What It Does |
|---------|-------------|
| `./deploy.sh deploy` | First-time deployment (saves ID) |
| `./deploy.sh update` | Rebuild and redeploy (uses saved ID) |
| `./deploy.sh restart` | Restart service |
| `./deploy.sh status` | Show status |
| `./deploy.sh logs` | View logs |
| `./deploy.sh list` | List all projects |
---
## How State Works
**First Deploy:**
```bash
./deploy.sh deploy
# Saves: service ID, project ID to ~/.easypanel/state.json
```
**Every Update After:**
```bash
./deploy.sh update
# Reads: service ID from state.json
# Does: Rebuild + Redeploy
```
**No need to remember IDs - skill handles it!**
---
## Files Created
After first deploy:
```
~/.easypanel/
├── credentials # Your API token (secure)
└── state.json # Service & project IDs (auto-generated)
```
---
## Next Steps
1. **Deploy now:** `./deploy.sh deploy`
2. **Check status:** `./deploy.sh status`
3. **Make changes, then update:** `./deploy.sh update`
---
**Full docs:** `SKILL_v2.md`

View File

@@ -1,623 +0,0 @@
# 🚀 Easypanel Deployment Skill
**Skill ID:** `easypanel-deploy`
**Version:** 2.0.0
**Author:** Deal Plus Tech DevOps
**Last Updated:** 2026-03-02
---
## Overview
Automated deployment skill for deploying Astro, Next.js, Vite, and other web applications to Easypanel via API.
---
## 🔐 Authentication Setup
### Store Your API Token
**Option 1: Environment Variable (Recommended)**
Add to your shell profile (`~/.zshrc`, `~/.bashrc`, or `~/.profile`):
```bash
export EASYPANEL_API_TOKEN="your-api-token-here"
export EASYPANEL_URL="http://110.164.146.46:3000"
```
Then reload:
```bash
source ~/.zshrc # or source ~/.bashrc
```
**Option 2: Credential File**
Create `~/.easypanel/credentials`:
```bash
mkdir -p ~/.easypanel
cat > ~/.easypanel/credentials << EOF
EASYPANEL_URL=http://110.164.146.46:3000
EASYPANEL_API_TOKEN=your-api-token-here
EASYPANEL_DEFAULT_PROJECT=default
EOF
chmod 600 ~/.easypanel/credentials
```
**Option 3: Pass Token Directly**
```bash
./deploy-easypanel.sh your-api-token
```
---
## 📋 Configuration File
Create `easypanel.config.json` in your project root:
```json
{
"easypanel": {
"url": "http://110.164.146.46:3000",
"project": "dealplustech",
"app": {
"name": "dealplustech-astro",
"port": 4321,
"image": "dealplustech-astro:latest"
},
"env": {
"NODE_ENV": "production",
"PORT": "4321",
"HOST": "0.0.0.0"
},
"docker": {
"context": ".",
"dockerfile": "Dockerfile",
"buildArgs": {}
},
"resources": {
"cpu": "0.5",
"memory": "512M",
"storage": "1G"
},
"domain": {
"enabled": false,
"name": "dealplustech.co.th",
"ssl": true
}
}
}
```
---
## 🎯 Usage
### Quick Deploy
```bash
# Navigate to project
cd your-project
# Run deployment (uses token from environment)
easypanel-deploy
# Or pass token directly
easypanel-deploy --token your-api-token
```
### Interactive Mode
```bash
easypanel-deploy --interactive
```
### Deploy Specific Environment
```bash
easypanel-deploy --environment production
easypanel-deploy --environment staging
```
---
## 🔧 Commands
### `deploy` - Deploy Application
```bash
easypanel-deploy deploy
# Options:
# --token, -t API token (or use EASYPANEL_API_TOKEN env)
# --project, -p Project name
# --name, -n Service name
# --image, -i Docker image
# --port, -P Container port
# --env, -e Environment variables (key=value)
# --build Force rebuild Docker image
# --no-build Skip Docker build
# --dry-run Show what would be deployed
```
### `status` - Check Deployment Status
```bash
easypanel-deploy status
# Shows:
# - Service status (running/stopped/error)
# - Resource usage
# - Recent deployments
# - Exposed URLs
```
### `logs` - View Service Logs
```bash
easypanel-deploy logs
# Options:
# --lines, -n Number of lines (default: 50)
# --follow, -f Follow logs in real-time
# --since Show logs since timestamp
```
### `restart` - Restart Service
```bash
easypanel-deploy restart
```
### `stop` - Stop Service
```bash
easypanel-deploy stop
```
### `delete` - Delete Service
```bash
easypanel-deploy delete --force # Force delete without confirmation
```
### `list` - List All Services
```bash
easypanel-deploy list
# Options:
# --project, -p Filter by project
# --format, -f Output format (table/json)
```
### `info` - Show Service Details
```bash
easypanel-deploy info
# Shows:
# - Configuration
# - Environment variables
# - Resource allocation
# - Deployment history
```
---
## 📁 Project Structure
```
your-project/
├── Dockerfile # Required
├── easypanel.config.json # Optional (uses defaults if missing)
├── .env # Optional (loaded as env vars)
├── .dockerignore # Optional
└── deploy-easypanel.sh # Deployment script (included in skill)
```
---
## 🔒 Security Best Practices
### Token Storage
**DO:**
- Use environment variables
- Store in credential manager (1Password, Keychain)
- Use `.env` files (gitignored)
- Rotate tokens regularly
**DON'T:**
- Commit tokens to Git
- Share tokens in chat/clear text
- Use tokens in CI/CD logs
- Store in plain text files
### Token Rotation
```bash
# Generate new token in Easypanel dashboard
# Update environment variable
export EASYPANEL_API_TOKEN="new-token"
# Test new token
easypanel-deploy status
# Revoke old token in Easypanel dashboard
```
---
## 🐳 Docker Configuration
### Standard Dockerfile (Astro)
```dockerfile
# Build Stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production Stage
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
EXPOSE 4321
CMD ["npx", "astro", "preview", "--host", "0.0.0.0", "--port", "4321"]
```
### Next.js Dockerfile
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
```
### Vite Dockerfile
```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
---
## ⚙️ Configuration Reference
### `easypanel.config.json`
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `easypanel.url` | string | `http://110.164.146.46:3000` | Easypanel instance URL |
| `easypanel.project` | string | `default` | Project name |
| `easypanel.app.name` | string | Project folder name | Service name |
| `easypanel.app.port` | number | `3000` | Container port |
| `easypanel.app.image` | string | `{name}:latest` | Docker image name |
| `easypanel.env` | object | `{}` | Environment variables |
| `easypanel.docker.context` | string | `.` | Docker build context |
| `easypanel.docker.dockerfile` | string | `Dockerfile` | Dockerfile path |
| `easypanel.resources.cpu` | string | `"0.5"` | CPU allocation |
| `easypanel.resources.memory` | string | `"512M"` | Memory allocation |
| `easypanel.resources.storage` | string | `"1G"` | Storage allocation |
| `easypanel.domain.enabled` | boolean | `false` | Enable custom domain |
| `easypanel.domain.name` | string | `""` | Custom domain name |
| `easypanel.domain.ssl` | boolean | `true` | Enable SSL |
---
## 🔍 Troubleshooting
### API Connection Failed
```bash
# Test connection
curl -I http://110.164.146.46:3000
# Check if token is set
echo $EASYPANEL_API_TOKEN
# Test API with token
curl -H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
http://110.164.146.46:3000/api/trpc/setup.getStatus \
--insecure
```
### Docker Build Fails
```bash
# Build with verbose output
docker build --no-cache --progress=plain -t your-image:latest .
# Check Dockerfile syntax
hadolint Dockerfile
# Test build locally
docker run -p 4321:4321 your-image:latest
```
### Service Won't Start
```bash
# Check logs
easypanel-deploy logs --lines 100
# Inspect service
easypanel-deploy info
# Restart service
easypanel-deploy restart
# Check resource allocation
easypanel-deploy info | grep -A 10 "Resources"
```
### Token Expired/Invalid
```bash
# Generate new token in Easypanel dashboard
# Update environment variable
export EASYPANEL_API_TOKEN="new-token"
# Add to shell profile for persistence
echo 'export EASYPANEL_API_TOKEN="new-token"' >> ~/.zshrc
source ~/.zshrc
# Verify
easypanel-deploy status
```
---
## 📊 Monitoring
### Check Service Health
```bash
easypanel-deploy status
# Output:
# ✅ Service: dealplustech-astro
# Status: Running
# Uptime: 2 days, 4 hours
# CPU: 12%
# Memory: 256MB / 512MB
# URL: http://dealplustech-astro.easypanel.app
```
### View Metrics
```bash
easypanel-deploy metrics
# Shows:
# - CPU usage over time
# - Memory usage
# - Network traffic
# - Request count
```
### Setup Alerts
```bash
easypanel-deploy alert --cpu 80 --memory 80 --email admin@example.com
```
---
## 🔄 CI/CD Integration
### GitHub Actions
```yaml
name: Deploy to Easypanel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Build Docker image
run: docker build -t dealplustech-astro:latest .
- name: Deploy to Easypanel
run: |
curl -X POST "$EASYPANEL_URL/api/trpc/services.app.deploy" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
-d '{"input":{"json":{"projectName":"dealplustech","serviceName":"dealplustech-astro"}}}' \
--insecure
env:
EASYPANEL_URL: ${{ secrets.EASYPANEL_URL }}
EASYPANEL_API_TOKEN: ${{ secrets.EASYPANEL_API_TOKEN }}
```
### GitLab CI
```yaml
deploy:
stage: deploy
image: docker:20
services:
- docker:20-dind
script:
- docker build -t dealplustech-astro:latest .
- docker push $CI_REGISTRY_IMAGE:latest
- |
curl -X POST "$EASYPANEL_URL/api/trpc/services.app.deploy" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
-d '{"input":{"json":{"projectName":"dealplustech","serviceName":"dealplustech-astro"}}}' \
--insecure
only:
- main
```
---
## 📞 Support & Resources
### Documentation
- **Easypanel Docs:** https://docs.easypanel.io
- **API Reference:** http://110.164.146.46:3000/api
- **Skill Repo:** [Link to your skill repository]
### Getting Help
1. Check troubleshooting section
2. Review service logs: `easypanel-deploy logs`
3. Check Easypanel dashboard
4. Contact DevOps team
---
## 🎓 Examples
### Deploy Astro Project
```bash
cd astro-project
easypanel-deploy deploy \
--project dealplustech \
--name my-astro-site \
--port 4321
```
### Deploy Next.js Project
```bash
cd nextjs-project
easypanel-deploy deploy \
--project dealplustech \
--name my-next-app \
--port 3000 \
--env NODE_ENV=production \
--env NEXT_PUBLIC_API_URL=https://api.example.com
```
### Deploy Vite Project
```bash
cd vite-project
easypanel-deploy deploy \
--project dealplustech \
--name my-vite-app \
--port 80 \
--image nginx:alpine
```
### Multi-Environment Setup
```bash
# Deploy to staging
easypanel-deploy deploy \
--project dealplustech \
--name my-app-staging \
--env NODE_ENV=staging \
--env DATABASE_URL=staging-db-url
# Deploy to production
easypanel-deploy deploy \
--project dealplustech \
--name my-app-production \
--env NODE_ENV=production \
--env DATABASE_URL=production-db-url
```
---
## ✅ Checklist for New Projects
- [ ] Create `Dockerfile` for project
- [ ] Add `easypanel.config.json` (optional)
- [ ] Set `EASYPANEL_API_TOKEN` environment variable
- [ ] Test local Docker build: `docker build -t test:latest .`
- [ ] Test locally: `docker run -p 4321:4321 test:latest`
- [ ] Deploy: `easypanel-deploy deploy`
- [ ] Verify: `easypanel-deploy status`
- [ ] Setup custom domain (optional)
- [ ] Enable SSL (optional)
- [ ] Configure monitoring/alerts
---
**Skill Version:** 2.0.0
**Last Updated:** 2026-03-02
**Status:** ✅ Production Ready
**API Version:** Easypanel 2.24.0

View File

@@ -1,563 +0,0 @@
# 🚀 Easypanel Deployment Skill v2.0
**Skill ID:** `easypanel-deploy`
**Version:** 2.0.0 - **With State Management**
**Author:** Deal Plus Tech DevOps
**Last Updated:** 2026-03-02
---
## ✨ What's New in v2.0
### Key Features:
-**Automatic ID Storage** - Saves project & service IDs after creation
-**Full Lifecycle Management** - Deploy, update, start, stop, restart
-**State Persistence** - Remembers your apps across sessions
-**One-Command Updates** - Rebuild and redeploy with single command
-**Status Monitoring** - Check app status anytime
-**Log Access** - View deployment and runtime logs
---
## 📁 File Structure
```
~/.easypanel/
├── credentials # API token (secure, 600 permissions)
└── state.json # Stored IDs and deployment history
your-project/
├── skills/easypanel-deploy/
│ ├── deploy.sh # Main deployment script
│ ├── SKILL.md # This documentation
│ └── README.md # Quick start
└── easypanel.config.json # Project configuration (optional)
```
---
## 🎯 Commands
### `deploy` - Deploy Application
First-time deployment creates service and saves ID:
```bash
./deploy.sh deploy
# Output:
# ✅ Docker image built
# ✅ Service created: my-app (svc_abc123...)
# ✅ State saved: service.my-app = svc_abc123...
# ✅ Deployment complete!
```
**Options:**
- `-b, --skip-build` - Skip Docker build
---
### `update` - Update Application
Rebuilds image and redeploys (uses stored service ID):
```bash
./deploy.sh update
# Does:
# 1. Rebuilds Docker image
# 2. Triggers redeployment
# 3. Shows deployment logs
```
---
### `restart` - Restart Service
Restarts running service:
```bash
./deploy.sh restart
```
---
### `start` - Start Service
Alias for restart:
```bash
./deploy.sh start
```
---
### `stop` - Stop Service
Stops running service:
```bash
./deploy.sh stop
# Note: May need manual action in dashboard depending on Easypanel API
```
---
### `status` - Show Service Status
Shows current status, resources, URLs:
```bash
./deploy.sh status
# Output:
# ============================================================
# Service: my-app
# ============================================================
# Status: running
# Type: docker
# Image: my-app:latest
# Port: 4321
#
# URLs:
# - https://my-app.easypanel.app
#
# Resources:
# CPU: 0.5
# Memory: 512M
# ============================================================
```
---
### `logs` - View Service Logs
Shows deployment and runtime logs:
```bash
./deploy.sh logs # Last 50 lines
./deploy.sh logs -n 100 # Last 100 lines
```
---
### `list` - List All Projects
Shows all projects and services:
```bash
./deploy.sh list
# Output:
# ID Name Services
# -----------------------------------------------------------------
# prj_abc123... dealplustech 3
# prj_def456... staging 1
```
---
## 📊 State Management
### What Gets Stored
After successful deployment, skill saves:
```json
{
"services": {
"dealplustech-astro": {
"id": "svc_abc123...",
"project_id": "prj_def456...",
"name": "dealplustech-astro",
"image": "dealplustech-astro:latest",
"port": 4321,
"updated": "2026-03-02T10:45:00Z"
}
},
"projects": {
"dealplustech": {
"id": "prj_def456...",
"name": "dealplustech",
"updated": "2026-03-02T10:45:00Z"
}
},
"deployments": [
{
"service": "dealplustech-astro",
"timestamp": "2026-03-02T10:45:00Z",
"status": "success"
}
]
}
```
### Why State Matters
With stored IDs, you can:
1. **Update without looking up IDs**
```bash
./deploy.sh update # Uses stored service ID
```
2. **Check status instantly**
```bash
./deploy.sh status # Uses stored service ID
```
3. **Manage multiple apps**
```bash
# Each app has its own stored ID
./deploy.sh status # Current project
```
4. **Resume after session ends**
- IDs persist across terminal sessions
- No need to re-lookup service IDs
---
## 🔄 Deployment Workflow
### First Deploy
```bash
# 1. Build and deploy
./deploy.sh deploy
# What happens:
# 1. Builds Docker image
# 2. Gets/creates project (saves project ID)
# 3. Creates service (saves service ID)
# 4. Triggers deployment
# 5. Saves all IDs to state.json
```
### Subsequent Updates
```bash
# 1. Update code
git pull
# 2. Rebuild and redeploy
./deploy.sh update
# What happens:
# 1. Reads service ID from state.json
# 2. Rebuilds Docker image
# 3. Triggers redeployment
# 4. Updates deployment history
```
### Check Status Anytime
```bash
# Quick status check
./deploy.sh status
# View recent logs
./deploy.sh logs -n 50
```
---
## 🔧 Configuration
### Project Config (`easypanel.config.json`)
```json
{
"easypanel": {
"url": "http://110.164.146.46:3000",
"project": "dealplustech",
"app": {
"name": "dealplustech-astro",
"port": 4321,
"image": "dealplustech-astro:latest"
},
"env": {
"NODE_ENV": "production",
"PORT": "4321",
"HOST": "0.0.0.0"
},
"resources": {
"cpu": "0.5",
"memory": "512M",
"storage": "1G"
}
}
}
```
### Credentials (`~/.easypanel/credentials`)
```bash
EASYPANEL_URL=http://110.164.146.46:3000
EASYPANEL_API_TOKEN=ep_live_abc123...
EASYPANEL_DEFAULT_PROJECT=dealplustech
```
### State (`~/.easypanel/state.json`)
Auto-generated on first deploy. Stores:
- Project IDs
- Service IDs
- Deployment history
- Configuration
---
## 📋 Usage Examples
### Example 1: Deploy New Project
```bash
cd my-astro-project
# Deploy
./skills/easypanel-deploy/deploy.sh deploy
# Check status
./skills/easypanel-deploy/deploy.sh status
```
### Example 2: Update Existing Project
```bash
cd my-astro-project
# Make code changes
git pull
# Update deployment
./skills/easypanel-deploy/deploy.sh update
# Watch logs
./skills/easypanel-deploy/deploy.sh logs -n 100 -f
```
### Example 3: Manage Multiple Apps
```bash
# Deploy app 1
cd app1
./deploy.sh deploy
# Deploy app 2
cd ../app2
./deploy.sh deploy
# Check status of current app
./deploy.sh status
# List all projects
./deploy.sh list
```
### Example 4: Quick Status Check
```bash
# Anytime, anywhere (in project directory)
./deploy.sh status
# Output shows:
# - Running status
# - Resource usage
# - Deployment URL
```
---
## 🐛 Troubleshooting
### Service Not Found in State
**Problem:** `Service not found in state`
**Solution:**
```bash
# Run deploy first to create service and save ID
./deploy.sh deploy
# Or manually add to state.json
nano ~/.easypanel/state.json
```
### Project Not Found
**Problem:** `Project not found`
**Solution:**
```bash
# Create project in Easypanel dashboard first
# Then run deploy again
./deploy.sh deploy
```
### API Call Failed
**Problem:** `API call failed (HTTP 401)`
**Solution:**
```bash
# Check token
cat ~/.easypanel/credentials
# Regenerate token if needed
# Update credentials file
nano ~/.easypanel/credentials
```
### State File Corrupted
**Problem:** JSON errors in state.json
**Solution:**
```bash
# Backup and recreate
cp ~/.easypanel/state.json ~/.easypanel/state.json.bak
rm ~/.easypanel/state.json
# Next deploy will recreate
./deploy.sh deploy
```
---
## 📊 Deployment History
View deployment history:
```bash
cat ~/.easypanel/state.json | python3 -m json.tool
```
Shows:
- All deployed services
- Project associations
- Last update timestamps
- Deployment success/failure
---
## 🔒 Security
### State File Security
```bash
# Set secure permissions
chmod 600 ~/.easypanel/state.json
# Never commit to Git
echo ".easypanel/" >> .gitignore
```
### What's Safe to Share
- ✅ Service names
- ✅ Project names
- ✅ Port numbers
- ✅ Image names
### What's NOT Safe to Share
- ❌ API token
- ❌ Service IDs (can be used to manipulate)
- ❌ Project IDs
- ❌ Deployment URLs with tokens
---
## 🎓 Advanced Usage
### Manual State Edit
```bash
# Edit state manually
nano ~/.easypanel/state.json
# Add service
{
"services": {
"my-app": {
"id": "svc_abc123...",
"project_id": "prj_def456...",
"name": "my-app",
"port": 3000
}
}
}
```
### Backup State
```bash
# Backup before major changes
cp ~/.easypanel/state.json ~/.easypanel/state.json.backup
# Restore if needed
cp ~/.easypanel/state.json.backup ~/.easypanel/state.json
```
### Migrate to New Machine
```bash
# Copy state and credentials
scp ~/.easypanel/* user@new-machine:~/.easypanel/
# Verify on new machine
./deploy.sh status
```
---
**Version:** 2.0.0
**Status:** ✅ Production Ready
**State Management:** ✅ Enabled
**Last Updated:** 2026-03-02
---
## 🔄 Automatic Service Creation
**Status:** Semi-automated (80% automated)
Easypanel's API requires initial service creation via dashboard. After that, everything is automated!
### Initial Setup (One-time, 2 minutes):
```bash
# 1. Build and prepare
./deploy.sh deploy
# 2. Create service in Easypanel dashboard
# - Open: http://110.164.146.46:3000
# - Project → New Service → Docker image
# - Copy service ID
# 3. Register service ID
./deploy.sh register svc_abc123...
```
### After Registration (100% Automated):
```bash
# Update deployment
./deploy.sh update
# Check status
./deploy.sh status
# List all services
./deploy.sh list
```
**Why this approach?**
- Easypanel API has complex service creation schema
- One-time manual step (2 minutes)
- Fully automated thereafter
- Production-ready now
See `AUTOMATIC_DEPLOYMENT.md` for detailed explanation.

View File

@@ -1,94 +0,0 @@
#!/bin/bash
set -e
CREDENTIALS_FILE="$HOME/.easypanel/credentials"
if [ -f "$CREDENTIALS_FILE" ]; then
export $(grep -v '^#' "$CREDENTIALS_FILE" | xargs) 2>/dev/null || true
fi
EASYPANEL_HOST="110.164.146.46"
EASYPANEL_URL="http://${EASYPANEL_HOST}:3000"
GITEA_REPO_URL="https://git.moreminimore.com/kunthawat/dealplustech.git"
PROJECT_NAME="customerwebsite"
GITEA_BRANCH="main"
TIMESTAMP=$(date +%s)
APP_NAME="dealplustech-${TIMESTAMP}"
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE} $1${NC}"; }
log_success() { echo -e "${GREEN}$1${NC}"; }
log_error() { echo -e "${RED}$1${NC}"; }
[ -z "$EASYPANEL_API_TOKEN" ] && { log_error "Token not set"; exit 1; }
echo "========================================"
echo "🚀 Deploying $APP_NAME"
echo "========================================"
echo ""
# Step 1
log_info "Step 1/5: Creating service..."
result=$(curl -s -X POST "${EASYPANEL_URL}/api/trpc/services.app.createService" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
-d "{\"json\":{\"projectName\":\"$PROJECT_NAME\",\"domains\":[{\"host\":\"\$(EASYPANEL_DOMAIN)\"}],\"serviceName\":\"$APP_NAME\"}}" \
--insecure)
if echo "$result" | grep -q '"error"'; then
log_error "Failed: $result"
exit 1
fi
log_success "✅ Service created: $APP_NAME"
# Step 2
log_info "Step 2/5: Configuring Git..."
log_info "Repository: $GITEA_REPO_URL"
result=$(curl -s -X POST "${EASYPANEL_URL}/api/trpc/services.app.updateSourceGit" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
-d "{\"json\":{\"projectName\":\"$PROJECT_NAME\",\"serviceName\":\"$APP_NAME\",\"repo\":\"$GITEA_REPO_URL\",\"ref\":\"$GITEA_BRANCH\",\"path\":\"/\"}}" \
--insecure)
if echo "$result" | grep -q '"error"'; then
log_error "Failed: $result"
exit 1
fi
log_success "✅ Git configured"
# Step 3
log_info "Step 3/5: Setting build type..."
result=$(curl -s -X POST "${EASYPANEL_URL}/api/trpc/services.app.updateBuild" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
-d "{\"json\":{\"projectName\":\"$PROJECT_NAME\",\"serviceName\":\"$APP_NAME\",\"build\":{\"type\":\"nixpacks\"}}}" \
--insecure)
log_success "✅ Build type set (nixpacks)"
# Step 4
log_info "Step 4/5: Getting domain..."
result=$(curl -s "${EASYPANEL_URL}/api/trpc/domains.getPrimaryDomain?input=%7B%22json%22%3A%7B%22projectName%22%3A%22$PROJECT_NAME%22%2C%22serviceName%22%3A%22$APP_NAME%22%7D%7D" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" --insecure)
domain=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',{}).get('data',{}).get('domain',{}).get('host','pending'))" 2>/dev/null || echo "pending")
log_success "✅ Domain: $domain"
# Step 5
log_info "Step 5/5: Waiting for deployment..."
for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 10
result=$(curl -s "${EASYPANEL_URL}/api/trpc/services.app.inspectService?input=%7B%22json%22%3A%7B%22projectName%22%3A%22$PROJECT_NAME%22%2C%22serviceName%22%3A%22$APP_NAME%22%7D%7D" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" --insecure)
status=$(echo "$result" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',{}).get('data',{}).get('status','building'))" 2>/dev/null || echo "building")
log_info " Status: $status (attempt $i/10)"
[ "$status" = "running" ] || [ "$status" = "ready" ] && break
done
log_success "✅ Deployment complete!"
log_info ""
log_info "Service: $APP_NAME"
log_info "URL: http://$domain"
log_info ""
log_info "To redeploy: ./deploy.sh redeploy $APP_NAME"

View File

@@ -1,280 +0,0 @@
#!/bin/bash
# Easypanel Deployment Skill v2.2
# Production-ready with error handling
set -e
CREDENTIALS_FILE="$HOME/.easypanel/credentials"
STATE_FILE="$HOME/.easypanel/state.json"
CONFIG_FILE="easypanel.config.json"
# Load credentials
if [ -f "$CREDENTIALS_FILE" ]; then
export $(grep -v '^#' "$CREDENTIALS_FILE" | xargs) 2>/dev/null || true
fi
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE} $1${NC}"; }
log_success() { echo -e "${GREEN}$1${NC}"; }
log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
log_error() { echo -e "${RED}$1${NC}"; }
# Check token
if [ -z "$EASYPANEL_API_TOKEN" ] || [ "$EASYPANEL_API_TOKEN" = "YOUR_API_TOKEN_HERE" ]; then
log_error "API token not set!"
echo ""
echo "Edit ~/.easypanel/credentials and add your token"
exit 1
fi
# Load config
load_config() {
if [ -f "$CONFIG_FILE" ]; then
APP_NAME=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['easypanel']['app']['name'])" 2>/dev/null || echo "")
PORT=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['easypanel']['app']['port'])" 2>/dev/null || echo "4321")
DOCKER_IMAGE=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['easypanel']['app']['image'])" 2>/dev/null || echo "")
PROJECT_NAME=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['easypanel']['project'])" 2>/dev/null || echo "default")
fi
APP_NAME="${APP_NAME:-$(basename "$(pwd)")}"
PORT="${PORT:-4321}"
DOCKER_IMAGE="${DOCKER_IMAGE:-$APP_NAME:latest}"
PROJECT_NAME="${PROJECT_NAME:-default}"
}
# Save state
save_state() {
python3 << PYEOF
import json, os
from datetime import datetime
state_file = "$STATE_FILE"
os.makedirs(os.path.dirname(state_file), exist_ok=True)
if os.path.exists(state_file):
with open(state_file, 'r') as f:
state = json.load(f)
else:
state = {"version":"2.0","projects":{},"services":{},"deployments":[]}
if "$1" == "service":
state['services']['$2'] = {'id':'$3','project_id':'$PROJECT_ID','name':'$APP_NAME','port':$PORT,'updated':datetime.utcnow().isoformat()+"Z"}
elif "$1" == "project":
state['projects']['$2'] = {'id':'$3','name':'$2','updated':datetime.utcnow().isoformat()+"Z"}
with open(state_file, 'w') as f:
json.dump(state, f, indent=2)
print(f"Saved: {$1}.$2 = $3")
PYEOF
}
# Get state
get_state() {
python3 << PYEOF 2>/dev/null || echo ""
import json
try:
with open('$STATE_FILE', 'r') as f:
state = json.load(f)
key, type = '$1', '$2'
if type == 'service' and key in state.get('services', {}):
print(state['services'][key].get('id', ''))
elif type == 'project' and key in state.get('projects', {}):
print(state['projects'][key].get('id', ''))
except:
pass
PYEOF
}
# List projects
list_projects() {
log_info "Fetching projects from Easypanel..."
local response=$(curl -s -w "\n%{http_code}" \
"http://110.164.146.46:3000/api/trpc/projects.listProjects" \
-H "Authorization: Bearer $EASYPANEL_API_TOKEN" \
--insecure --compressed)
local http_code=$(echo "$response" | tail -1)
local body=$(echo "$response" | sed '$d')
if [ "$http_code" != "200" ]; then
log_error "Failed to fetch projects (HTTP $http_code)"
return 1
fi
echo "$body" | python3 << 'PYEOF'
import json, sys
try:
data = json.load(sys.stdin)
result = data.get('result', {})
data_content = result.get('data', {}) if isinstance(result, dict) else {}
items = data_content.get('items', [])
if not items:
print("No projects found")
else:
print(f"\n{'ID':<25} {'Name':<30}")
print("-" * 60)
for proj in items:
pid = str(proj.get('id', ''))[:23]
name = str(proj.get('name', ''))[:28]
print(f"{pid:<25} {name:<30}")
print("-" * 60)
print(f"Total: {len(items)} project(s)\n")
except Exception as e:
print(f"Error parsing response: {e}")
print("Raw response:", sys.stdin.read()[:200])
PYEOF
}
# Deploy command
cmd_deploy() {
load_config
log_info "========================================"
log_info "Deploying $APP_NAME"
log_info "========================================"
log_info "Project: $PROJECT_NAME"
log_info "Image: $DOCKER_IMAGE"
log_info "Port: $PORT"
log_info ""
# Build Docker
log_info "Building Docker image..."
if [ ! "$SKIP_BUILD" = "true" ]; then
docker build -t "$DOCKER_IMAGE" . || {
log_error "Docker build failed"
exit 1
}
log_success "Built: $DOCKER_IMAGE"
fi
# List projects and find/create
log_info "Looking for project: $PROJECT_NAME"
list_projects
log_warning ""
log_warning "⚠️ Manual Step Required"
log_warning ""
log_info "Easypanel API requires service creation via dashboard"
log_info ""
log_info "Steps:"
log_info "1. Open: $EASYPANEL_URL"
log_info "2. Select project: $PROJECT_NAME (or create it)"
log_info "3. Click 'New Service' → 'Docker image'"
log_info "4. Enter:"
log_info " Name: $APP_NAME"
log_info " Image: $DOCKER_IMAGE"
log_info " Port: $PORT"
log_info "5. Deploy"
log_info ""
log_info "After deployment, save the service ID:"
log_info "./deploy.sh register SERVICE_ID"
log_info ""
# Save deployment attempt
save_state "last_attempt" "$APP_NAME" "deployment"
}
# Register service
cmd_register() {
local service_id="$1"
if [ -z "$service_id" ]; then
log_error "Service ID required"
log_info "Usage: ./deploy.sh register SERVICE_ID"
exit 1
fi
load_config
save_state "service" "$APP_NAME" "$service_id"
log_success "✅ Service registered!"
log_info "Service ID: $service_id"
log_info "Next update: ./deploy.sh update"
}
# Update service
cmd_update() {
load_config
SERVICE_ID=$(get_state "$APP_NAME" "service")
if [ -z "$SERVICE_ID" ]; then
log_error "Service not registered"
log_info "Run: ./deploy.sh deploy"
exit 1
fi
log_info "Updating: $APP_NAME ($SERVICE_ID)"
# Rebuild
if [ ! "$SKIP_BUILD" = "true" ]; then
docker build -t "$DOCKER_IMAGE" . || exit 1
log_success "Rebuilt: $DOCKER_IMAGE"
fi
log_info "✅ Image ready"
log_info ""
log_info "To deploy update:"
log_info "1. Go to $EASYPANEL_URL"
log_info "2. Select service: $APP_NAME"
log_info "3. Click 'Deploy' to pull new image"
}
# Status
cmd_status() {
load_config
SERVICE_ID=$(get_state "$APP_NAME" "service")
if [ -z "$SERVICE_ID" ]; then
log_error "Not deployed"
exit 1
fi
log_info "Service: $APP_NAME"
log_info "ID: $SERVICE_ID"
log_info "Image: $DOCKER_IMAGE"
log_info "Port: $PORT"
}
# Help
cmd_help() {
cat << 'EOF'
Easypanel Deployment Skill v2.2
Usage: ./deploy.sh [command]
Commands:
deploy Build and prepare deployment
register ID Register service with ID
update Update existing service
status Show status
list List projects
help This help
Workflow:
1. ./deploy.sh deploy
2. Create service in Easypanel dashboard
3. ./deploy.sh register SERVICE_ID
4. ./deploy.sh update (for future updates)
EOF
}
SKIP_BUILD="false"
[ "$1" = "-b" ] || [ "$1" = "--skip-build" ] && SKIP_BUILD="true"
case "${1:-deploy}" in
deploy) cmd_deploy ;;
register) cmd_register "$2" ;;
update) cmd_update ;;
status) cmd_status ;;
list) list_projects ;;
help|--help|-h) cmd_help ;;
*) cmd_help; exit 1 ;;
esac

View File

@@ -1,5 +0,0 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}