Neue services
This commit is contained in:
@@ -1,28 +1,26 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
||||
|
||||
const generateRobots = () => {
|
||||
const content = `User-agent: *
|
||||
Allow: /
|
||||
Disallow: /admin
|
||||
Disallow: /api
|
||||
|
||||
Sitemap: ${BASE_URL}/sitemap.xml
|
||||
`;
|
||||
return content;
|
||||
};
|
||||
|
||||
const robots = generateRobots();
|
||||
const outputPath = path.resolve(process.cwd(), 'public/robots.txt');
|
||||
|
||||
// Ensure public directory exists
|
||||
const publicDir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, robots);
|
||||
console.log(`✅ Robots.txt generated at ${outputPath}`);
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
||||
|
||||
const generateRobots = () => {
|
||||
const content = `User-agent: *
|
||||
Allow: /
|
||||
Disallow: /admin
|
||||
Disallow: /api
|
||||
|
||||
Sitemap: ${BASE_URL}/sitemap.xml
|
||||
`;
|
||||
return content;
|
||||
};
|
||||
|
||||
const robots = generateRobots();
|
||||
const outputPath = path.resolve(process.cwd(), 'public/robots.txt');
|
||||
const publicDir = path.dirname(outputPath);
|
||||
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, robots);
|
||||
console.log(`Robots.txt generated at ${outputPath}`);
|
||||
|
||||
@@ -1,85 +1,78 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { locationData, serviceData, blogPostData } from '../src/data/seoData';
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
||||
|
||||
/**
|
||||
* Generates the sitemap.xml content
|
||||
*/
|
||||
const generateSitemap = () => {
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
`;
|
||||
|
||||
// Static Pages
|
||||
const staticPages = [
|
||||
'',
|
||||
'/services',
|
||||
'/blog',
|
||||
'/contact',
|
||||
'/about'
|
||||
];
|
||||
|
||||
staticPages.forEach(page => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}${page}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>${page === '' ? '1.0' : '0.8'}</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
// Location Pages
|
||||
locationData.forEach(page => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}/${page.slug}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
// Service Pages
|
||||
serviceData.forEach(page => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}/${page.slug}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
// Blog Posts
|
||||
blogPostData.forEach(post => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}/blog/${post.slug}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
xml += `</urlset>`;
|
||||
return xml;
|
||||
};
|
||||
|
||||
// Write to public/sitemap.xml
|
||||
const sitemap = generateSitemap();
|
||||
const outputPath = path.resolve(process.cwd(), 'public/sitemap.xml');
|
||||
|
||||
// Ensure public directory exists
|
||||
const publicDir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, sitemap);
|
||||
console.log(`✅ Sitemap generated at ${outputPath}`);
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { locationData, serviceData, blogPostData } from '../src/data/seoData';
|
||||
|
||||
const BASE_URL = process.env.BASE_URL || 'https://bayareait.services';
|
||||
|
||||
const generateSitemap = () => {
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
`;
|
||||
|
||||
const staticPages = [
|
||||
'',
|
||||
'/locations',
|
||||
'/services',
|
||||
'/blog',
|
||||
'/contact',
|
||||
'/about',
|
||||
'/privacy-policy',
|
||||
'/terms-of-service'
|
||||
];
|
||||
|
||||
staticPages.forEach(page => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}${page}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>${page === '' ? '1.0' : '0.8'}</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
locationData.forEach(page => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}/${page.slug}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
serviceData.forEach(page => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}/${page.slug}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
blogPostData.filter(post => !('redirect' in post) || !post.redirect).forEach(post => {
|
||||
xml += ` <url>
|
||||
<loc>${BASE_URL}/${post.slug}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
`;
|
||||
});
|
||||
|
||||
xml += `</urlset>`;
|
||||
return xml;
|
||||
};
|
||||
|
||||
const sitemap = generateSitemap();
|
||||
const outputPath = path.resolve(process.cwd(), 'public/sitemap.xml');
|
||||
const publicDir = path.dirname(outputPath);
|
||||
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, sitemap);
|
||||
console.log(`Sitemap generated at ${outputPath}`);
|
||||
|
||||
61
scripts/optimize-images.mjs
Normal file
61
scripts/optimize-images.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const roots = ['public', path.join('src', 'assets')];
|
||||
|
||||
async function walk(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return walk(fullPath);
|
||||
}
|
||||
return fullPath;
|
||||
}),
|
||||
);
|
||||
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
function pickWidth(filePath) {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
|
||||
if (normalized.includes('/images/blog/')) return 1280;
|
||||
if (normalized.endsWith('/hero-bg.png')) return 1600;
|
||||
if (normalized.endsWith('/process-illustration.png')) return 1600;
|
||||
if (normalized.includes('/assets/services/')) return 1200;
|
||||
|
||||
return 1280;
|
||||
}
|
||||
|
||||
async function optimize(filePath) {
|
||||
const targetPath = filePath.replace(/\.png$/i, '.webp');
|
||||
const width = pickWidth(filePath);
|
||||
const image = sharp(filePath, { animated: false }).rotate();
|
||||
const metadata = await image.metadata();
|
||||
|
||||
const pipeline =
|
||||
metadata.width && metadata.width > width ? image.resize({ width, withoutEnlargement: true }) : image;
|
||||
|
||||
await pipeline.webp({ quality: 76, effort: 6 }).toFile(targetPath);
|
||||
}
|
||||
|
||||
const pngFiles = (
|
||||
await Promise.all(
|
||||
roots.map(async (root) => {
|
||||
try {
|
||||
return await walk(root);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter((filePath) => filePath.toLowerCase().endsWith('.png'));
|
||||
|
||||
await Promise.all(pngFiles.map((filePath) => optimize(filePath)));
|
||||
|
||||
console.log(`Optimized ${pngFiles.length} PNG files to WebP.`);
|
||||
206
scripts/prerender-routes.ts
Normal file
206
scripts/prerender-routes.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { blogPostData, locationData, serviceData } from '../src/data/seoData';
|
||||
|
||||
type RouteMeta = {
|
||||
route: string;
|
||||
title: string;
|
||||
description: string;
|
||||
canonicalUrl: string;
|
||||
keywords?: string[];
|
||||
schema?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const DIST_DIR = path.resolve(process.cwd(), 'dist');
|
||||
const BASE_URL = 'https://bayareait.services';
|
||||
const DEFAULT_OG_IMAGE = `${BASE_URL}/logo.svg`;
|
||||
|
||||
const staticRoutes: RouteMeta[] = [
|
||||
{
|
||||
route: '/',
|
||||
title: 'IT Service & IT Support for Businesses in Corpus Christi, TX',
|
||||
description:
|
||||
'Reliable IT support and IT services for businesses in Corpus Christi, TX. Fast response, outsourced IT support and help desk solutions.',
|
||||
canonicalUrl: `${BASE_URL}/`,
|
||||
keywords: ['IT Service', 'IT Support', 'Corpus Christi', 'IT Help Desk'],
|
||||
schema: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ITService',
|
||||
name: 'Bay Area IT',
|
||||
url: BASE_URL,
|
||||
telephone: '+1-361-765-8400',
|
||||
areaServed: ['Corpus Christi', 'Portland', 'Rockport', 'Aransas Pass', 'Kingsville'],
|
||||
},
|
||||
},
|
||||
{
|
||||
route: '/about',
|
||||
title: 'About Bay Area IT | Local IT Support in Corpus Christi',
|
||||
description:
|
||||
'Learn about Bay Area IT, a local IT partner serving Corpus Christi and the Coastal Bend with practical support, reliable service, and over 25 years of experience.',
|
||||
canonicalUrl: `${BASE_URL}/about`,
|
||||
schema: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'Bay Area IT',
|
||||
url: BASE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
route: '/services',
|
||||
title: 'IT Services | Bay Area IT Support, Email, Networking and Web',
|
||||
description:
|
||||
'Explore Bay Area IT services for Corpus Christi businesses, including help desk support, business email, networking, hardware, web design, and day-to-day IT support.',
|
||||
canonicalUrl: `${BASE_URL}/services`,
|
||||
},
|
||||
{
|
||||
route: '/blog',
|
||||
title: 'Blog | Bay Area IT Insights for Corpus Christi Businesses',
|
||||
description:
|
||||
'Read practical IT guidance for Corpus Christi and Coastal Bend businesses, from managed IT support and costs to business email and local service coverage.',
|
||||
canonicalUrl: `${BASE_URL}/blog`,
|
||||
},
|
||||
{
|
||||
route: '/contact',
|
||||
title: 'Contact Bay Area IT | Free IT Assessment in Corpus Christi',
|
||||
description:
|
||||
'Talk to Bay Area IT about managed IT support, help desk coverage, business email, networking, and technology support across Corpus Christi and the Coastal Bend.',
|
||||
canonicalUrl: `${BASE_URL}/contact`,
|
||||
},
|
||||
{
|
||||
route: '/locations',
|
||||
title: 'IT Support Service Areas - Corpus Christi & Coastal Bend, TX',
|
||||
description:
|
||||
'Bay Area IT provides IT support and IT services throughout the Coastal Bend. View all cities we serve in the Corpus Christi area.',
|
||||
canonicalUrl: `${BASE_URL}/locations`,
|
||||
},
|
||||
{
|
||||
route: '/privacy-policy',
|
||||
title: 'Privacy Policy | Bay Area IT',
|
||||
description:
|
||||
'Read how Bay Area IT collects, uses, and protects information submitted through this website.',
|
||||
canonicalUrl: `${BASE_URL}/privacy-policy`,
|
||||
},
|
||||
{
|
||||
route: '/terms-of-service',
|
||||
title: 'Terms of Service | Bay Area IT',
|
||||
description:
|
||||
'Review the Bay Area IT terms covering use of this website and our IT support services.',
|
||||
canonicalUrl: `${BASE_URL}/terms-of-service`,
|
||||
},
|
||||
];
|
||||
|
||||
const dynamicRoutes: RouteMeta[] = [
|
||||
...locationData.map((item) => ({
|
||||
route: `/${item.slug}`,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
canonicalUrl: `${BASE_URL}/${item.slug}`,
|
||||
keywords: item.keywords,
|
||||
schema: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'LocalBusiness',
|
||||
name: 'Bay Area IT',
|
||||
url: `${BASE_URL}/${item.slug}`,
|
||||
areaServed: item.city,
|
||||
},
|
||||
})),
|
||||
...serviceData.map((item) => ({
|
||||
route: `/${item.slug}`,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
canonicalUrl: `${BASE_URL}/${item.slug}`,
|
||||
keywords: item.keywords,
|
||||
schema: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Service',
|
||||
name: item.h1,
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: 'Bay Area IT',
|
||||
},
|
||||
url: `${BASE_URL}/${item.slug}`,
|
||||
},
|
||||
})),
|
||||
...blogPostData
|
||||
.filter((item) => !item.redirect)
|
||||
.map((item) => ({
|
||||
route: `/${item.slug}`,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
canonicalUrl: `${BASE_URL}/${item.slug}`,
|
||||
keywords: item.keywords,
|
||||
schema: {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: item.h1,
|
||||
description: item.description,
|
||||
url: `${BASE_URL}/${item.slug}`,
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Bay Area IT',
|
||||
},
|
||||
},
|
||||
})),
|
||||
];
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function buildHead(meta: RouteMeta) {
|
||||
const keywords = meta.keywords?.length
|
||||
? `\n <meta name="keywords" content="${escapeHtml(meta.keywords.join(', '))}" />`
|
||||
: '';
|
||||
const schema = meta.schema
|
||||
? `\n <script type="application/ld+json">${JSON.stringify(meta.schema)}</script>`
|
||||
: '';
|
||||
|
||||
return ` <title>${escapeHtml(meta.title)}</title>
|
||||
<meta name="description" content="${escapeHtml(meta.description)}" />${keywords}
|
||||
<link rel="canonical" href="${meta.canonicalUrl}" />
|
||||
<meta property="og:title" content="${escapeHtml(meta.title)}" />
|
||||
<meta property="og:description" content="${escapeHtml(meta.description)}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Bay Area IT" />
|
||||
<meta property="og:url" content="${meta.canonicalUrl}" />
|
||||
<meta property="og:image" content="${DEFAULT_OG_IMAGE}" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="${escapeHtml(meta.title)}" />
|
||||
<meta name="twitter:description" content="${escapeHtml(meta.description)}" />${schema}`;
|
||||
}
|
||||
|
||||
function injectHead(template: string, meta: RouteMeta) {
|
||||
const withoutTitle = template.replace(/<title>[\s\S]*?<\/title>/i, '');
|
||||
return withoutTitle.replace('</head>', `${buildHead(meta)}\n </head>`);
|
||||
}
|
||||
|
||||
async function writeRouteHtml(template: string, meta: RouteMeta) {
|
||||
const html = injectHead(template, meta);
|
||||
const cleanRoute = meta.route === '/' ? '' : meta.route.replace(/^\/+/, '');
|
||||
const filePath =
|
||||
meta.route === '/'
|
||||
? path.join(DIST_DIR, 'index.html')
|
||||
: path.join(DIST_DIR, cleanRoute, 'index.html');
|
||||
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, html, 'utf8');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const templatePath = path.join(DIST_DIR, 'index.html');
|
||||
const template = await fs.readFile(templatePath, 'utf8');
|
||||
const allRoutes = [...staticRoutes, ...dynamicRoutes];
|
||||
|
||||
await Promise.all(allRoutes.map((meta) => writeRouteHtml(template, meta)));
|
||||
console.log(`Prerendered ${allRoutes.length} route HTML files.`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to prerender route HTML files.');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
42
scripts/prune-dist-assets.mjs
Normal file
42
scripts/prune-dist-assets.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const distDir = 'dist';
|
||||
|
||||
async function walk(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return walk(fullPath);
|
||||
}
|
||||
return fullPath;
|
||||
}),
|
||||
);
|
||||
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
try {
|
||||
const files = await walk(distDir);
|
||||
const pngFiles = files.filter((filePath) => filePath.toLowerCase().endsWith('.png'));
|
||||
|
||||
let removed = 0;
|
||||
|
||||
for (const pngFile of pngFiles) {
|
||||
const webpFile = pngFile.replace(/\.png$/i, '.webp');
|
||||
|
||||
try {
|
||||
await fs.access(webpFile);
|
||||
await fs.unlink(pngFile);
|
||||
removed += 1;
|
||||
} catch {
|
||||
// Keep PNGs that have no optimized WebP counterpart.
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Pruned ${removed} PNG files from dist.`);
|
||||
} catch {
|
||||
console.log('No dist directory to prune.');
|
||||
}
|
||||
Reference in New Issue
Block a user