Add UTM URL Generator tool with live preview and pre-generated PDFs
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
- Add utm-tool.html and utm-tool.js for UTM URL generation - Live URL generation as user types (no button required) - Support for all standard UTM parameters plus custom parameters - Add UTM Builder to nav dropdown and index.html tools section - Pre-generate all PDFs for faster Docker builds - Update git hooks for PDF generation
This commit is contained in:
parent
7a3665abae
commit
5b422a67b2
File diff suppressed because it is too large
Load Diff
|
|
@ -63,6 +63,7 @@
|
||||||
<a href="/one-pager-tools/csv-tool.html" id="nav-tools">Tools</a>
|
<a href="/one-pager-tools/csv-tool.html" id="nav-tools">Tools</a>
|
||||||
<div class="dropdown-content">
|
<div class="dropdown-content">
|
||||||
<a href="/one-pager-tools/csv-tool.html" id="nav-csv">CSV Tool</a>
|
<a href="/one-pager-tools/csv-tool.html" id="nav-csv">CSV Tool</a>
|
||||||
|
<a href="/one-pager-tools/utm-tool.html" id="nav-utm">UTM Builder</a>
|
||||||
<a href="https://md.colinknapp.com" id="nav-markdown" target="_blank" rel="noopener noreferrer">Markdown Tool</a>
|
<a href="https://md.colinknapp.com" id="nav-markdown" target="_blank" rel="noopener noreferrer">Markdown Tool</a>
|
||||||
<a href="https://nix.colinknapp.com" id="nav-nix" target="_blank" rel="noopener noreferrer">NixOS Validator</a>
|
<a href="https://nix.colinknapp.com" id="nav-nix" target="_blank" rel="noopener noreferrer">NixOS Validator</a>
|
||||||
<a href="https://qr.colinknapp.com" id="nav-qrcode" target="_blank" rel="noopener noreferrer">QR Code Tool</a>
|
<a href="https://qr.colinknapp.com" id="nav-qrcode" target="_blank" rel="noopener noreferrer">QR Code Tool</a>
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,11 @@
|
||||||
<span>Process and analyze CSV files directly in your browser</span>
|
<span>Process and analyze CSV files directly in your browser</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="/one-pager-tools/utm-tool.html" class="tool-link">
|
||||||
|
<strong>UTM Builder</strong>
|
||||||
|
<span>Generate campaign tracking URLs with UTM parameters</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="https://md.colinknapp.com" target="_blank" rel="noopener noreferrer" class="tool-link">
|
<a href="https://md.colinknapp.com" target="_blank" rel="noopener noreferrer" class="tool-link">
|
||||||
<strong>Markdown Tool</strong>
|
<strong>Markdown Tool</strong>
|
||||||
<span>Live markdown editor and previewer</span>
|
<span>Live markdown editor and previewer</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,353 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Free UTM URL builder tool. Generate tracking URLs with UTM parameters for marketing campaigns. Client-side processing for privacy.">
|
||||||
|
<title>UTM URL Generator - Free Campaign URL Builder</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css">
|
||||||
|
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-nsutlmVxvyIyABk8i0fzVyO+ram8rkuTntoETIGcou8=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-CJbyPP0VWE+XrarLtjHMffGrI2GlTXXAXOce+NE+aQg="></script>
|
||||||
|
<style>
|
||||||
|
.utm-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label .required {
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label .optional {
|
||||||
|
color: var(--date-color);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(69, 123, 157, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input.error {
|
||||||
|
border-color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .hint {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--date-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-params-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-params-section h3 {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-param-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-param-row input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-param-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-param {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: #e63946;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-param:hover {
|
||||||
|
background: #c1121f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-param {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-param:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url {
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url.empty {
|
||||||
|
color: var(--date-color);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url.valid {
|
||||||
|
border-color: #2a9d8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url.invalid {
|
||||||
|
border-color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy:disabled {
|
||||||
|
background: var(--border-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback {
|
||||||
|
color: #2a9d8f;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown th,
|
||||||
|
.url-breakdown td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown th {
|
||||||
|
font-weight: 600;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown td {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message.error {
|
||||||
|
background: rgba(230, 57, 70, 0.1);
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message.success {
|
||||||
|
background: rgba(42, 157, 143, 0.1);
|
||||||
|
color: #2a9d8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message.info {
|
||||||
|
background: rgba(69, 123, 157, 0.1);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<h1>UTM URL Generator</h1>
|
||||||
|
<p>Build tracking URLs with UTM parameters for your marketing campaigns.</p>
|
||||||
|
|
||||||
|
<div class="tool-container">
|
||||||
|
<div class="utm-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="baseUrl">Website URL <span class="required">*</span></label>
|
||||||
|
<input type="url" id="baseUrl" placeholder="https://example.com/page" aria-required="true">
|
||||||
|
<span class="hint">The full URL of the page you want to track</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmSource">Campaign Source <span class="required">*</span></label>
|
||||||
|
<input type="text" id="utmSource" placeholder="google, newsletter, facebook" aria-required="true">
|
||||||
|
<span class="hint">utm_source: Identifies which site sent the traffic</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmMedium">Campaign Medium <span class="required">*</span></label>
|
||||||
|
<input type="text" id="utmMedium" placeholder="cpc, email, social" aria-required="true">
|
||||||
|
<span class="hint">utm_medium: The marketing medium (e.g., cpc, email, social)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmCampaign">Campaign Name <span class="required">*</span></label>
|
||||||
|
<input type="text" id="utmCampaign" placeholder="spring_sale, product_launch" aria-required="true">
|
||||||
|
<span class="hint">utm_campaign: The specific campaign name</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmTerm">Campaign Term <span class="optional">(optional)</span></label>
|
||||||
|
<input type="text" id="utmTerm" placeholder="running+shoes, marketing+software">
|
||||||
|
<span class="hint">utm_term: Paid search keywords</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmContent">Campaign Content <span class="optional">(optional)</span></label>
|
||||||
|
<input type="text" id="utmContent" placeholder="logolink, textlink, banner_v1">
|
||||||
|
<span class="hint">utm_content: Differentiate ads or links pointing to the same URL</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="custom-params-section">
|
||||||
|
<h3>Custom Parameters <span class="optional">(optional)</span></h3>
|
||||||
|
<div id="customParams"></div>
|
||||||
|
<button type="button" class="btn-add-param" id="addParamBtn" aria-label="Add custom parameter">+ Add Parameter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="output-section">
|
||||||
|
<h2>Generated URL</h2>
|
||||||
|
<div id="validationMessage" class="validation-message" aria-live="polite"></div>
|
||||||
|
<div id="generatedUrl" class="generated-url empty" aria-live="polite">
|
||||||
|
Enter a URL and required parameters to generate your tracking URL
|
||||||
|
</div>
|
||||||
|
<div class="copy-section">
|
||||||
|
<button type="button" class="btn-copy" id="copyBtn" disabled aria-label="Copy URL to clipboard">Copy URL</button>
|
||||||
|
<span class="copy-feedback" id="copyFeedback">Copied!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="url-breakdown" id="urlBreakdown" style="display: none;">
|
||||||
|
<h3>URL Parameters</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Parameter</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="breakdownBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>About This Tool</h2>
|
||||||
|
<p>This UTM URL Generator helps you create campaign tracking URLs with:</p>
|
||||||
|
<ul>
|
||||||
|
<li>All standard UTM parameters (source, medium, campaign, term, content)</li>
|
||||||
|
<li>Custom parameters for additional tracking needs</li>
|
||||||
|
<li>Live URL preview as you type</li>
|
||||||
|
<li>Proper URL encoding for special characters</li>
|
||||||
|
<li>One-click copy to clipboard</li>
|
||||||
|
</ul>
|
||||||
|
<p>The tool processes everything in your browser - no data is sent to any server.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Include -->
|
||||||
|
<div id="footer-include"></div>
|
||||||
|
|
||||||
|
<script src="utm-tool.js" integrity="sha256-f5yZtBrRofKWD/H5Hth5syEYPun+WicrCGxKzyzwZ6A="></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
/**
|
||||||
|
* UTM URL Generator functionality
|
||||||
|
* Builds tracking URLs with UTM parameters
|
||||||
|
* Client-side only - no data sent to server
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// DOM Elements
|
||||||
|
const baseUrlInput = document.getElementById('baseUrl');
|
||||||
|
const utmSourceInput = document.getElementById('utmSource');
|
||||||
|
const utmMediumInput = document.getElementById('utmMedium');
|
||||||
|
const utmCampaignInput = document.getElementById('utmCampaign');
|
||||||
|
const utmTermInput = document.getElementById('utmTerm');
|
||||||
|
const utmContentInput = document.getElementById('utmContent');
|
||||||
|
const customParamsContainer = document.getElementById('customParams');
|
||||||
|
const addParamBtn = document.getElementById('addParamBtn');
|
||||||
|
const generatedUrlDiv = document.getElementById('generatedUrl');
|
||||||
|
const validationMessage = document.getElementById('validationMessage');
|
||||||
|
const copyBtn = document.getElementById('copyBtn');
|
||||||
|
const copyFeedback = document.getElementById('copyFeedback');
|
||||||
|
const urlBreakdown = document.getElementById('urlBreakdown');
|
||||||
|
const breakdownBody = document.getElementById('breakdownBody');
|
||||||
|
|
||||||
|
// Current generated URL
|
||||||
|
let currentUrl = '';
|
||||||
|
let customParamCount = 0;
|
||||||
|
|
||||||
|
// Debounce timer
|
||||||
|
let debounceTimer;
|
||||||
|
|
||||||
|
// Add input listeners to all UTM fields
|
||||||
|
const utmInputs = [
|
||||||
|
baseUrlInput,
|
||||||
|
utmSourceInput,
|
||||||
|
utmMediumInput,
|
||||||
|
utmCampaignInput,
|
||||||
|
utmTermInput,
|
||||||
|
utmContentInput
|
||||||
|
];
|
||||||
|
|
||||||
|
utmInputs.forEach(input => {
|
||||||
|
input.addEventListener('input', debounceGenerate);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add custom parameter button
|
||||||
|
addParamBtn.addEventListener('click', addCustomParam);
|
||||||
|
|
||||||
|
// Copy button
|
||||||
|
copyBtn.addEventListener('click', copyToClipboard);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced URL generation
|
||||||
|
*/
|
||||||
|
function debounceGenerate() {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(generateUrl, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom parameter row
|
||||||
|
*/
|
||||||
|
function addCustomParam() {
|
||||||
|
customParamCount++;
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'custom-param-row';
|
||||||
|
row.id = `customParam${customParamCount}`;
|
||||||
|
row.innerHTML = `
|
||||||
|
<input type="text" placeholder="Parameter name" class="custom-key" aria-label="Custom parameter name">
|
||||||
|
<input type="text" placeholder="Value" class="custom-value" aria-label="Custom parameter value">
|
||||||
|
<button type="button" class="btn-remove-param" aria-label="Remove parameter">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add input listeners
|
||||||
|
const keyInput = row.querySelector('.custom-key');
|
||||||
|
const valueInput = row.querySelector('.custom-value');
|
||||||
|
keyInput.addEventListener('input', debounceGenerate);
|
||||||
|
valueInput.addEventListener('input', debounceGenerate);
|
||||||
|
|
||||||
|
// Add remove listener
|
||||||
|
const removeBtn = row.querySelector('.btn-remove-param');
|
||||||
|
removeBtn.addEventListener('click', function() {
|
||||||
|
row.remove();
|
||||||
|
debounceGenerate();
|
||||||
|
});
|
||||||
|
|
||||||
|
customParamsContainer.appendChild(row);
|
||||||
|
keyInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate URL format
|
||||||
|
*/
|
||||||
|
function isValidUrl(string) {
|
||||||
|
try {
|
||||||
|
const url = new URL(string);
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all custom parameters
|
||||||
|
*/
|
||||||
|
function getCustomParams() {
|
||||||
|
const params = [];
|
||||||
|
const rows = customParamsContainer.querySelectorAll('.custom-param-row');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const key = row.querySelector('.custom-key').value.trim();
|
||||||
|
const value = row.querySelector('.custom-value').value.trim();
|
||||||
|
if (key && value) {
|
||||||
|
params.push({ key, value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the UTM URL
|
||||||
|
*/
|
||||||
|
function generateUrl() {
|
||||||
|
const baseUrl = baseUrlInput.value.trim();
|
||||||
|
const source = utmSourceInput.value.trim();
|
||||||
|
const medium = utmMediumInput.value.trim();
|
||||||
|
const campaign = utmCampaignInput.value.trim();
|
||||||
|
const term = utmTermInput.value.trim();
|
||||||
|
const content = utmContentInput.value.trim();
|
||||||
|
const customParams = getCustomParams();
|
||||||
|
|
||||||
|
// Clear previous state
|
||||||
|
validationMessage.textContent = '';
|
||||||
|
validationMessage.className = 'validation-message';
|
||||||
|
generatedUrlDiv.className = 'generated-url';
|
||||||
|
|
||||||
|
// Check if we have any input at all
|
||||||
|
if (!baseUrl) {
|
||||||
|
generatedUrlDiv.textContent = 'Enter a URL to start generating your tracking URL';
|
||||||
|
generatedUrlDiv.classList.add('empty');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
urlBreakdown.style.display = 'none';
|
||||||
|
currentUrl = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate base URL format
|
||||||
|
if (!isValidUrl(baseUrl)) {
|
||||||
|
generatedUrlDiv.textContent = baseUrl;
|
||||||
|
generatedUrlDiv.classList.add('invalid');
|
||||||
|
showValidation('Enter a valid URL (starting with http:// or https://)', 'error');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
urlBreakdown.style.display = 'none';
|
||||||
|
currentUrl = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the URL with whatever parameters are filled in
|
||||||
|
try {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
|
||||||
|
// Add UTM parameters only if they have values
|
||||||
|
if (source) {
|
||||||
|
url.searchParams.set('utm_source', source);
|
||||||
|
}
|
||||||
|
if (medium) {
|
||||||
|
url.searchParams.set('utm_medium', medium);
|
||||||
|
}
|
||||||
|
if (campaign) {
|
||||||
|
url.searchParams.set('utm_campaign', campaign);
|
||||||
|
}
|
||||||
|
if (term) {
|
||||||
|
url.searchParams.set('utm_term', term);
|
||||||
|
}
|
||||||
|
if (content) {
|
||||||
|
url.searchParams.set('utm_content', content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom parameters
|
||||||
|
customParams.forEach(param => {
|
||||||
|
url.searchParams.set(param.key, param.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
currentUrl = url.toString();
|
||||||
|
generatedUrlDiv.textContent = currentUrl;
|
||||||
|
generatedUrlDiv.classList.add('valid');
|
||||||
|
copyBtn.disabled = false;
|
||||||
|
|
||||||
|
// Show hint about missing recommended fields
|
||||||
|
const missing = [];
|
||||||
|
if (!source) missing.push('source');
|
||||||
|
if (!medium) missing.push('medium');
|
||||||
|
if (!campaign) missing.push('campaign');
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
showValidation(`Tip: Add ${missing.join(', ')} for complete tracking`, 'info');
|
||||||
|
} else {
|
||||||
|
showValidation('URL ready to copy', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update breakdown
|
||||||
|
updateBreakdown(url, customParams);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showValidation('Error generating URL: ' + error.message, 'error');
|
||||||
|
generatedUrlDiv.classList.add('invalid');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
urlBreakdown.style.display = 'none';
|
||||||
|
currentUrl = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show validation message
|
||||||
|
*/
|
||||||
|
function showValidation(message, type) {
|
||||||
|
validationMessage.textContent = message;
|
||||||
|
validationMessage.className = `validation-message ${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the URL breakdown table
|
||||||
|
*/
|
||||||
|
function updateBreakdown(url, customParams) {
|
||||||
|
urlBreakdown.style.display = 'block';
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
// Base URL
|
||||||
|
rows.push({ param: 'Base URL', value: url.origin + url.pathname });
|
||||||
|
|
||||||
|
// UTM parameters
|
||||||
|
const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
|
||||||
|
utmParams.forEach(param => {
|
||||||
|
const value = url.searchParams.get(param);
|
||||||
|
if (value) {
|
||||||
|
rows.push({ param, value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom parameters
|
||||||
|
customParams.forEach(param => {
|
||||||
|
rows.push({ param: param.key, value: param.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
breakdownBody.innerHTML = rows.map(row => `
|
||||||
|
<tr>
|
||||||
|
<th>${escapeHtml(row.param)}</th>
|
||||||
|
<td>${escapeHtml(row.value)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML to prevent XSS
|
||||||
|
*/
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy URL to clipboard
|
||||||
|
*/
|
||||||
|
async function copyToClipboard() {
|
||||||
|
if (!currentUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(currentUrl);
|
||||||
|
copyFeedback.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
copyFeedback.classList.remove('show');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = currentUrl;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-9999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
copyFeedback.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
copyFeedback.classList.remove('show');
|
||||||
|
}, 2000);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to copy:', e);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,847 @@
|
||||||
|
%PDF-1.4
|
||||||
|
%Óëéá
|
||||||
|
1 0 obj
|
||||||
|
<</Title (localhost:8765/includes/header.html)
|
||||||
|
/Creator (Mozilla/5.0 \(Macintosh; Intel Mac OS X 10_15_7\) AppleWebKit/537.36 \(KHTML, like Gecko\) HeadlessChrome/142.0.0.0 Safari/537.36)
|
||||||
|
/Producer (Skia/PDF m142)
|
||||||
|
/CreationDate (D:20251202161508+00'00')
|
||||||
|
/ModDate (D:20251202161508+00'00')>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<</ca 1
|
||||||
|
/BM /Normal>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<</CA 1
|
||||||
|
/ca 1
|
||||||
|
/LC 0
|
||||||
|
/LJ 0
|
||||||
|
/LW 1
|
||||||
|
/ML 4
|
||||||
|
/SA true
|
||||||
|
/BM /Normal>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 725.88 120.75 739.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/)>>>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 712.38 121.5 725.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/resumes/business-development.html)>>>>
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 698.88 188.25 712.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/resumes/business-development.html)>>>>
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [191.25 698.88 248.25 712.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/resumes/devsecops.html)>>>>
|
||||||
|
endobj
|
||||||
|
12 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [251.25 698.88 333.75 712.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/resumes/team-leadership.html)>>>>
|
||||||
|
endobj
|
||||||
|
13 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [336.75 698.88 403.5 712.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/resumes/tool-building.html)>>>>
|
||||||
|
endobj
|
||||||
|
14 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 685.38 111 698.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/)>>>>
|
||||||
|
endobj
|
||||||
|
15 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 671.88 138.75 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/airport-dns.html)>>>>
|
||||||
|
endobj
|
||||||
|
16 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [138.75 671.88 144.75 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/airport-dns.html)>>>>
|
||||||
|
endobj
|
||||||
|
17 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [147 671.88 238.5 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/app-development.html)>>>>
|
||||||
|
endobj
|
||||||
|
18 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [238.5 671.88 244.5 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/app-development.html)>>>>
|
||||||
|
endobj
|
||||||
|
19 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [246.75 671.88 342 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/athion-turnaround.html)>>>>
|
||||||
|
endobj
|
||||||
|
20 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [342 671.88 348 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/athion-turnaround.html)>>>>
|
||||||
|
endobj
|
||||||
|
21 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [351 671.88 439.5 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/fawe-plotsquared.html)>>>>
|
||||||
|
endobj
|
||||||
|
22 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [439.5 671.88 445.5 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/fawe-plotsquared.html)>>>>
|
||||||
|
endobj
|
||||||
|
23 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [448.5 671.88 546 685.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/healthcare-platform.html)>>>>
|
||||||
|
endobj
|
||||||
|
24 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 658.38 84 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/healthcare-platform.html)>>>>
|
||||||
|
endobj
|
||||||
|
25 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [87 658.38 187.5 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/home-infrastructure.html)>>>>
|
||||||
|
endobj
|
||||||
|
26 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [187.5 658.38 193.5 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/home-infrastructure.html)>>>>
|
||||||
|
endobj
|
||||||
|
27 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [196.5 658.38 297 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/motherboard-repair.html)>>>>
|
||||||
|
endobj
|
||||||
|
28 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [297 658.38 303 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/motherboard-repair.html)>>>>
|
||||||
|
endobj
|
||||||
|
29 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [306 658.38 393.75 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/nitric-leadership.html)>>>>
|
||||||
|
endobj
|
||||||
|
30 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [393.75 658.38 399.75 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/nitric-leadership.html)>>>>
|
||||||
|
endobj
|
||||||
|
31 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [402.75 658.38 470.25 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/nuclear-dns.html)>>>>
|
||||||
|
endobj
|
||||||
|
32 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [470.25 658.38 476.25 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/nuclear-dns.html)>>>>
|
||||||
|
endobj
|
||||||
|
33 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [479.25 658.38 542.25 671.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/open-source-success.html)>>>>
|
||||||
|
endobj
|
||||||
|
34 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 644.88 119.25 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/open-source-success.html)>>>>
|
||||||
|
endobj
|
||||||
|
35 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [119.25 644.88 125.25 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/open-source-success.html)>>>>
|
||||||
|
endobj
|
||||||
|
36 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [128.25 644.88 189 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/showerloop.html)>>>>
|
||||||
|
endobj
|
||||||
|
37 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [189 644.88 195 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/showerloop.html)>>>>
|
||||||
|
endobj
|
||||||
|
38 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [198 644.88 249 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/viperwire.html)>>>>
|
||||||
|
endobj
|
||||||
|
39 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [249 644.88 255 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/viperwire.html)>>>>
|
||||||
|
endobj
|
||||||
|
40 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [257.25 644.88 343.5 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/web-design-java.html)>>>>
|
||||||
|
endobj
|
||||||
|
41 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [343.5 644.88 349.5 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/web-design-java.html)>>>>
|
||||||
|
endobj
|
||||||
|
42 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [352.5 644.88 449.25 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/wordpress-security.html)>>>>
|
||||||
|
endobj
|
||||||
|
43 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [449.25 644.88 455.25 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/wordpress-security.html)>>>>
|
||||||
|
endobj
|
||||||
|
44 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [458.25 644.88 529.5 658.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/youtube-game-dev.html)>>>>
|
||||||
|
endobj
|
||||||
|
45 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 631.38 101.25 644.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/youtube-game-dev.html)>>>>
|
||||||
|
endobj
|
||||||
|
46 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [101.25 631.38 107.25 644.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/stories/youtube-game-dev.html)>>>>
|
||||||
|
endobj
|
||||||
|
47 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 617.88 104.25 631.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/one-pager-tools/csv-tool.html)>>>>
|
||||||
|
endobj
|
||||||
|
48 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 604.38 126 617.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/one-pager-tools/csv-tool.html)>>>>
|
||||||
|
endobj
|
||||||
|
49 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [129 604.38 194.25 617.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (http://localhost:8765/one-pager-tools/utm-tool.html)>>>>
|
||||||
|
endobj
|
||||||
|
50 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [197.25 604.38 274.5 617.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (https://md.colinknapp.com/)>>>>
|
||||||
|
endobj
|
||||||
|
51 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [277.5 604.38 357.75 617.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (https://nix.colinknapp.com/)>>>>
|
||||||
|
endobj
|
||||||
|
52 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [360.75 604.38 430.5 617.88]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (https://qr.colinknapp.com/)>>>>
|
||||||
|
endobj
|
||||||
|
53 0 obj
|
||||||
|
<</Type /Annot
|
||||||
|
/Subtype /Link
|
||||||
|
/F 4
|
||||||
|
/Border [0 0 0]
|
||||||
|
/Rect [78 590.88 102.75 604.38]
|
||||||
|
/A <</Type /Action
|
||||||
|
/S /URI
|
||||||
|
/URI (https://meet.colinknapp.com/)>>>>
|
||||||
|
endobj
|
||||||
|
54 0 obj
|
||||||
|
<</Filter /FlateDecode
|
||||||
|
/Length 3809>> stream
|
||||||
|
xœí\[<5B>·
~?¿BÏ¢ˆ7]€ €íÄyNj ?Àmë | ||||||