-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.js
287 lines (268 loc) · 10.3 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
const express = require('express');
const axios = require('axios');
const cheerio = require('cheerio');
const app = express();
// Helper: escape HTML for safe output.
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
app.get('/', async (req, res) => {
const url = req.query.url;
let content = `
<div class="container my-5">
<div class="row mb-4">
<div class="col">
<h1 class="mb-4">SEO Audit & Social Media Preview</h1>
<form method="GET" action="/">
<div class="input-group">
<input type="text" name="url" class="form-control" placeholder="Enter URL" value="${url ? escapeHtml(url) : ''}" required>
<button class="btn btn-primary" type="submit">Analyze</button>
</div>
</form>
</div>
</div>
`;
if (url) {
try {
// Fetch the target URL
const response = await axios.get(url);
const html = response.data;
const $ = cheerio.load(html);
// === Basic SEO Elements ===
const title = $('title').text().trim();
const metaDescription = $('meta[name="description"]').attr('content')
? $('meta[name="description"]').attr('content').trim()
: '';
const canonical = $('link[rel="canonical"]').attr('href')
? $('link[rel="canonical"]').attr('href').trim()
: '';
const firstH1 = $('h1').first().text().trim();
const lang = $('html').attr('lang')
? $('html').attr('lang').trim()
: '';
const robots = $('meta[name="robots"]').attr('content')
? $('meta[name="robots"]').attr('content').trim()
: '';
const viewport = $('meta[name="viewport"]').attr('content')
? $('meta[name="viewport"]').attr('content').trim()
: '';
// === Open Graph Tags ===
const ogTitle = $('meta[property="og:title"]').attr('content') || '';
const ogDescription = $('meta[property="og:description"]').attr('content') || '';
const ogImage = $('meta[property="og:image"]').attr('content') || "https://via.placeholder.com/600x315.png?text=No+Image";
const ogUrl = $('meta[property="og:url"]').attr('content') || url;
// === Twitter Card Tags ===
const twitterCard = $('meta[name="twitter:card"]').attr('content') || '';
const twitterTitle = $('meta[name="twitter:title"]').attr('content') || '';
const twitterDescription = $('meta[name="twitter:description"]').attr('content') || '';
const twitterImage = $('meta[name="twitter:image"]').attr('content') || ogImage;
// === SEO Audit Checks ===
const seoChecks = [
{
name: 'Title Tag',
status: title ? 'Found' : 'Missing',
suggestion: title
? ((title.length < 30 || title.length > 60) ? 'Title length should be between 30 and 60 characters.' : 'Looks good!')
: 'Add a <title> tag to the page.'
},
{
name: 'Meta Description',
status: metaDescription ? 'Found' : 'Missing',
suggestion: metaDescription
? ((metaDescription.length < 50 || metaDescription.length > 160) ? 'Meta description should be between 50 and 160 characters.' : 'Looks good!')
: 'Add a meta description for better SEO.'
},
{
name: 'Canonical Tag',
status: canonical ? 'Found' : 'Missing',
suggestion: canonical ? 'Looks good!' : 'Add a canonical tag to avoid duplicate content issues.'
},
{
name: 'H1 Tag',
status: firstH1 ? 'Found' : 'Missing',
suggestion: firstH1 ? 'Looks good!' : 'Add at least one <h1> tag for the main heading.'
},
{
name: 'Language Attribute',
status: lang ? `Found (${lang})` : 'Missing',
suggestion: lang ? 'Looks good!' : 'Specify the language attribute in the <html> tag, e.g., <html lang="en">.'
},
{
name: 'Robots Meta Tag',
status: robots ? 'Found' : 'Missing',
suggestion: robots ? 'Looks good!' : 'Add a robots meta tag to control search engine crawling, e.g., <meta name="robots" content="index,follow">.'
},
{
name: 'Viewport Meta Tag',
status: viewport ? 'Found' : 'Missing',
suggestion: viewport ? 'Looks good!' : 'Add a viewport meta tag to ensure mobile responsiveness.'
},
{
name: 'Open Graph Tags',
status: (ogTitle && ogDescription && ogImage && ogUrl) ? 'Complete' : 'Incomplete',
suggestion: (ogTitle && ogDescription && ogImage && ogUrl)
? 'Looks good!'
: 'Ensure all required OG tags are present: og:title, og:description, og:image, and og:url.'
},
{
name: 'Twitter Card Tags',
status: (twitterCard && twitterTitle && twitterDescription && twitterImage) ? 'Complete' : 'Incomplete',
suggestion: (twitterCard && twitterTitle && twitterDescription && twitterImage)
? 'Looks good!'
: 'Consider adding Twitter Card tags for better social sharing.'
}
];
// Build the SEO Audit Table rows
let seoAuditRows = '';
seoChecks.forEach(check => {
seoAuditRows += `
<tr>
<td>${escapeHtml(check.name)}</td>
<td>${escapeHtml(check.status)}</td>
<td>${escapeHtml(check.suggestion)}</td>
</tr>
`;
});
// === Collect All OG/Twitter Meta Tags (for reference) ===
let metaTags = {};
$('meta').each((i, el) => {
const property = $(el).attr('property');
const nameAttr = $(el).attr('name');
const contentAttr = $(el).attr('content');
if (contentAttr) {
if (property && (property.startsWith('og:') || property.startsWith('twitter:'))) {
metaTags[property] = contentAttr;
}
if (nameAttr && (nameAttr.startsWith('og:') || nameAttr.startsWith('twitter:'))) {
metaTags[nameAttr] = contentAttr;
}
}
});
let metaRows = '';
for (const [key, value] of Object.entries(metaTags)) {
metaRows += `
<tr>
<td>${escapeHtml(key)}</td>
<td>${escapeHtml(value)}</td>
</tr>`;
}
// === Build the Output HTML ===
// SEO Audit Section
content += `
<h2 class="mb-3">SEO Audit</h2>
<div class="table-responsive mb-5">
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>SEO Element</th>
<th>Status</th>
<th>Suggestion</th>
</tr>
</thead>
<tbody>
${seoAuditRows}
</tbody>
</table>
</div>
`;
// Website Preview Section (using OG data)
content += `
<h2 class="mb-3">Website Preview</h2>
<div class="card mb-4">
<img src="${ogImage}" class="card-img-top" alt="OG Image">
<div class="card-body">
<h5 class="card-title">${ogTitle || title || 'No Title Found'}</h5>
<p class="card-text">${ogDescription || metaDescription || 'No Description Found'}</p>
<a href="${ogUrl}" class="btn btn-primary" target="_blank">${ogUrl}</a>
</div>
</div>
`;
// Social Media Preview Section
content += `
<h2 class="mb-3">Social Media Previews</h2>
<div class="row mb-4">
<div class="col-md-4">
<div class="card">
<img src="${ogImage}" class="card-img-top" alt="Facebook Preview">
<div class="card-body">
<h5 class="card-title">${ogTitle || 'No Title'}</h5>
<p class="card-text">${ogDescription || 'No Description'}</p>
<span class="badge bg-primary">Facebook</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<img src="${twitterImage}" class="card-img-top" alt="Twitter Preview">
<div class="card-body">
<h5 class="card-title">${twitterTitle || ogTitle || 'No Title'}</h5>
<p class="card-text">${twitterDescription || ogDescription || 'No Description'}</p>
<span class="badge bg-info text-dark">Twitter</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<img src="${ogImage}" class="card-img-top" alt="TikTok Preview">
<div class="card-body">
<h5 class="card-title">${ogTitle || 'No Title'}</h5>
<p class="card-text">${ogDescription || 'No Description'}</p>
<span class="badge bg-dark">TikTok</span>
</div>
</div>
</div>
</div>
`;
// All Meta Tags Section
content += `
<h2 class="mb-3">All OG/Twitter Meta Tags</h2>
<div class="table-responsive mb-5">
<table class="table table-striped">
<thead class="table-light">
<tr>
<th>Tag</th>
<th>Content</th>
</tr>
</thead>
<tbody>
${metaRows}
</tbody>
</table>
</div>
`;
} catch (error) {
content += `
<div class="alert alert-danger" role="alert">
Error fetching URL: ${escapeHtml(error.message)}
</div>
`;
}
}
content += `</div>`; // Close container
// Wrap content in full HTML with Bootstrap
const htmlResponse = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SEO Audit & Preview</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
${content}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
`;
res.send(htmlResponse);
});
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});