Neue services

This commit is contained in:
2026-03-25 20:07:27 -05:00
parent 42e0971a13
commit bcf9dc541c
85 changed files with 8589 additions and 4832 deletions

View File

@@ -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}`);

View File

@@ -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}`);

View 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
View 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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
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);
});

View 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.');
}