SEO Tweaks
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import sharp from 'sharp';
|
||||
import { writeFile, unlink, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
// Opaque-photo PNGs to convert to JPG (verified via metadata: no alpha)
|
||||
const targets = [
|
||||
'archie-auckland-dog-walking-review',
|
||||
'monty-auckland-dog-walking-review',
|
||||
'otis-auckland-dog-walking-review',
|
||||
'wallace-auckland-dog-walking-review',
|
||||
'one-on-one-dog-portrait-1',
|
||||
'one-on-one-dog-portrait-2',
|
||||
'one-on-one-dog-portrait-3',
|
||||
'small-medium-dogs-pack-walk',
|
||||
'auckland-pack-walk-small-dogs-group',
|
||||
'founder-image-aless-goodwalk'
|
||||
];
|
||||
|
||||
const dirs = ['src/lib/images', 'static/images'];
|
||||
const MAX_WIDTH = 1600;
|
||||
|
||||
let totalOrig = 0;
|
||||
let totalNew = 0;
|
||||
for (const dir of dirs) {
|
||||
for (const name of targets) {
|
||||
const png = join(dir, name + '.png');
|
||||
const jpg = join(dir, name + '.jpg');
|
||||
try {
|
||||
const orig = (await stat(png)).size;
|
||||
const buf = await sharp(png)
|
||||
.rotate()
|
||||
.resize({ width: MAX_WIDTH, withoutEnlargement: true })
|
||||
.jpeg({ quality: 82, mozjpeg: true, progressive: true })
|
||||
.toBuffer();
|
||||
await writeFile(jpg, buf);
|
||||
await unlink(png);
|
||||
totalOrig += orig;
|
||||
totalNew += buf.length;
|
||||
console.log(`${name.padEnd(45)} ${(orig/1024).toFixed(0).padStart(5)}KB png → ${(buf.length/1024).toFixed(0).padStart(4)}KB jpg`);
|
||||
} catch (err) {
|
||||
console.error('FAILED', png, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\nTotal: ${(totalOrig/1024/1024).toFixed(2)} MB → ${(totalNew/1024/1024).toFixed(2)} MB`);
|
||||
@@ -429,6 +429,22 @@ if (( nginx_args_present )); then
|
||||
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
|
||||
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
|
||||
|
||||
# Pre-flight: SSL certificate for onboarding.goodwalk.co.nz must exist before
|
||||
# nginx can reload — the config references this cert path directly.
|
||||
ONBOARDING_CERT="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/fullchain.pem"
|
||||
ONBOARDING_KEY="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/privkey.pem"
|
||||
if [[ ! -f "$ONBOARDING_CERT" || ! -f "$ONBOARDING_KEY" ]]; then
|
||||
fail "SSL certificate for onboarding.goodwalk.co.nz is not present on this server.
|
||||
Expected: $ONBOARDING_CERT
|
||||
One-time setup on the droplet:
|
||||
1. Ensure the DNS A record for onboarding.goodwalk.co.nz points to this server's IP
|
||||
2. Bring nginx up so the ACME webroot is reachable, then obtain the certificate:
|
||||
certbot certonly --webroot -w /var/www/certbot \\
|
||||
-d onboarding.goodwalk.co.nz \\
|
||||
--non-interactive --agree-tos -m info@goodwalk.co.nz
|
||||
3. Re-run this deploy script"
|
||||
fi
|
||||
|
||||
MAINTENANCE_HTML_SRC="$DEPLOY_PATH/nginx/maintenance.html"
|
||||
MAINTENANCE_LOGO_SRC="$DEPLOY_PATH/nginx/logo.png"
|
||||
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
|
||||
|
||||
@@ -287,6 +287,22 @@ if (( nginx_args_present )); then
|
||||
[[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE"
|
||||
[[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE"
|
||||
|
||||
# Pre-flight: SSL certificate for onboarding.goodwalk.co.nz must exist before
|
||||
# nginx can reload — the config references this cert path directly.
|
||||
ONBOARDING_CERT="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/fullchain.pem"
|
||||
ONBOARDING_KEY="/etc/letsencrypt/live/onboarding.goodwalk.co.nz/privkey.pem"
|
||||
if [[ ! -f "$ONBOARDING_CERT" || ! -f "$ONBOARDING_KEY" ]]; then
|
||||
fail "SSL certificate for onboarding.goodwalk.co.nz is not present on this server.
|
||||
Expected: $ONBOARDING_CERT
|
||||
One-time setup on the droplet:
|
||||
1. Ensure the DNS A record for onboarding.goodwalk.co.nz points to this server's IP
|
||||
2. Bring nginx up so the ACME webroot is reachable, then obtain the certificate:
|
||||
certbot certonly --webroot -w /var/www/certbot \\
|
||||
-d onboarding.goodwalk.co.nz \\
|
||||
--non-interactive --agree-tos -m info@goodwalk.co.nz
|
||||
3. Re-run this deploy script"
|
||||
fi
|
||||
|
||||
MAINTENANCE_HTML_SRC="$DEPLOY_PATH/nginx/maintenance.html"
|
||||
MAINTENANCE_LOGO_SRC="$DEPLOY_PATH/nginx/logo.png"
|
||||
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import sharp from 'sharp';
|
||||
import { readdir, stat, rename } from 'node:fs/promises';
|
||||
import { join, extname, basename } from 'node:path';
|
||||
|
||||
const MAX_WIDTH = 1600;
|
||||
const MIN_BYTES_TO_OPTIMISE = 250 * 1024;
|
||||
|
||||
const dirs = ['src/lib/images', 'static/images'];
|
||||
|
||||
async function optimiseFile(file) {
|
||||
const ext = extname(file).toLowerCase();
|
||||
const input = await sharp(file, { failOn: 'none' }).rotate();
|
||||
const meta = await input.metadata();
|
||||
const width = meta.width ?? 0;
|
||||
const targetWidth = width > MAX_WIDTH ? MAX_WIDTH : width;
|
||||
const pipeline = sharp(file, { failOn: 'none' })
|
||||
.rotate()
|
||||
.resize({ width: targetWidth, withoutEnlargement: true });
|
||||
|
||||
let buf;
|
||||
if (ext === '.png') {
|
||||
if (meta.hasAlpha) {
|
||||
buf = await pipeline
|
||||
.png({ palette: true, quality: 88, compressionLevel: 9, effort: 10 })
|
||||
.toBuffer();
|
||||
} else {
|
||||
buf = await pipeline
|
||||
.png({ palette: true, quality: 82, compressionLevel: 9, effort: 10 })
|
||||
.toBuffer();
|
||||
}
|
||||
} else if (ext === '.jpg' || ext === '.jpeg') {
|
||||
buf = await pipeline.jpeg({ quality: 82, mozjpeg: true }).toBuffer();
|
||||
} else if (ext === '.webp') {
|
||||
buf = await pipeline.webp({ quality: 80, effort: 6 }).toBuffer();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const original = (await stat(file)).size;
|
||||
if (buf.length >= original) return { file, original, optimised: original, skipped: true };
|
||||
|
||||
const tmp = file + '.opt.tmp';
|
||||
await sharp(buf).toFile(tmp);
|
||||
await rename(tmp, file);
|
||||
return { file, original, optimised: buf.length, skipped: false };
|
||||
}
|
||||
|
||||
let totalOrig = 0;
|
||||
let totalNew = 0;
|
||||
for (const dir of dirs) {
|
||||
const entries = await readdir(dir);
|
||||
for (const name of entries) {
|
||||
if (!/\.(png|jpe?g|webp)$/i.test(name)) continue;
|
||||
const file = join(dir, name);
|
||||
const s = await stat(file);
|
||||
if (s.size < MIN_BYTES_TO_OPTIMISE) continue;
|
||||
try {
|
||||
const res = await optimiseFile(file);
|
||||
if (!res) continue;
|
||||
totalOrig += res.original;
|
||||
totalNew += res.optimised;
|
||||
const pct = ((1 - res.optimised / res.original) * 100).toFixed(0);
|
||||
const flag = res.skipped ? ' (skipped: no gain)' : '';
|
||||
console.log(
|
||||
`${basename(file).padEnd(58)} ${(res.original / 1024).toFixed(0).padStart(5)}KB → ${(res.optimised / 1024).toFixed(0).padStart(5)}KB (-${pct}%)${flag}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('FAILED', file, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\nTotal: ${(totalOrig / 1024 / 1024).toFixed(2)} MB → ${(totalNew / 1024 / 1024).toFixed(2)} MB`);
|
||||
@@ -0,0 +1,24 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const libRoot = path.join(projectRoot, 'src', 'lib');
|
||||
|
||||
export function resolve(specifier, context, nextResolve) {
|
||||
if (specifier.startsWith('$lib/')) {
|
||||
const relative = specifier.slice('$lib/'.length);
|
||||
const candidates = [
|
||||
path.join(libRoot, relative + '.ts'),
|
||||
path.join(libRoot, relative + '.js'),
|
||||
path.join(libRoot, relative, 'index.ts'),
|
||||
path.join(libRoot, relative, 'index.js'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
return nextResolve(pathToFileURL(candidate).href, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { register } from 'node:module';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
register(
|
||||
pathToFileURL(path.join(scriptDir, 'sveltekit-hooks.mjs')).href,
|
||||
pathToFileURL(scriptDir + '/')
|
||||
);
|
||||
Reference in New Issue
Block a user