New Entry page improvements on link detection

This commit is contained in:
2026-08-28 09:04:24 +02:00
parent cf83c32b25
commit 50d61371e1
8 changed files with 159 additions and 37 deletions
+82 -15
View File
@@ -11,8 +11,9 @@
const existingTagsEl = document.getElementById('existing-tags');
const mastodonPublishing = document.getElementById('mastodon-publishing');
const mastodonEnabledCheckbox = document.getElementById('mastodon-enabled');
const scrapeButton = document.getElementById('scrape-button');
const scrapeStatus = document.getElementById('scrape-status');
const duplicateStatus = document.getElementById('duplicate-status');
const refetchTitleButton = document.getElementById('refetch-title-button');
const submitButton = document.getElementById('submit-button');
const submitStatus = document.getElementById('submit-status');
@@ -21,9 +22,14 @@
let selectedTags = new Set();
let currentUser = null;
// Utility function to add status messages
// Utility function to add status messages; splits on \n into real <br> line breaks without using innerHTML.
function setStatus(statusEl, message, isError = false) {
statusEl.textContent = message;
const lines = message.split('\n');
statusEl.replaceChildren(
...lines.flatMap((line, index) => (
index === 0 ? [document.createTextNode(line)] : [document.createElement('br'), document.createTextNode(line)]
)),
);
statusEl.className = `status ${isError ? 'error' : 'success'}`;
statusEl.classList.remove('hidden');
if (!isError) {
@@ -106,17 +112,29 @@
});
}
// Scrape URL for title
scrapeButton.addEventListener('click', async (e) => {
e.preventDefault();
const url = urlInput.value.trim();
if (!url) {
setStatus(scrapeStatus, 'Please enter a URL', true);
return;
// Strip known tracking parameters so duplicate detection and scraping ignore them, mirroring the browser extension.
function removeKnownTrackingParams(urlString) {
try {
const url = new URL(urlString);
const known = new Set([
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'utm_id', 'utm_name', 'gclid', 'fbclid', 'dclid', 'msclkid',
]);
for (const key of known) {
url.searchParams.delete(key);
}
return url.toString();
} catch (error) {
return urlString;
}
}
scrapeButton.disabled = true;
setStatus(scrapeStatus, 'Scraping...', false);
// Fetch the page title for the given URL and fill it in, unless the user already typed one.
let lastScrapedUrl = null;
async function fetchTitle(url, { force = false } = {}) {
if (!url || (!force && (titleInput.value.trim() || url === lastScrapedUrl))) return;
setStatus(scrapeStatus, 'Looking up title...', false);
try {
const response = await fetch(`/api/scrape?url=${encodeURIComponent(url)}`, {
@@ -132,6 +150,7 @@
throw new Error(`HTTP ${response.status}`);
}
lastScrapedUrl = url;
const data = await response.json();
if (data.title) {
titleInput.value = data.title;
@@ -142,16 +161,64 @@
} catch (error) {
console.error('Scrape error:', error);
setStatus(scrapeStatus, `Error: ${error.message}`, true);
} finally {
scrapeButton.disabled = false;
}
}
urlInput.addEventListener('blur', async () => {
await fetchTitle(removeKnownTrackingParams(urlInput.value.trim()));
checkDuplicate();
});
refetchTitleButton.addEventListener('click', (e) => {
e.preventDefault();
const url = removeKnownTrackingParams(urlInput.value.trim());
if (!url) {
setStatus(scrapeStatus, 'Please enter a URL', true);
return;
}
titleInput.value = '';
fetchTitle(url, { force: true });
});
// Warn when the URL/title combination already exists for this user, mirroring the browser extension.
titleInput.addEventListener('blur', checkDuplicate);
urlInput.addEventListener('input', () => duplicateStatus.classList.add('hidden'));
titleInput.addEventListener('input', () => duplicateStatus.classList.add('hidden'));
async function checkDuplicate() {
const url = removeKnownTrackingParams(urlInput.value.trim());
const title = titleInput.value.trim();
if (!url || !title) return;
try {
const response = await fetch(`/api/links/check?${new URLSearchParams({ title, url })}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) return;
const data = await response.json();
if (data.exists) {
let message = 'This link already exists. ';
if (!data.url_matches) {
message += 'But, the stored link has a different URL (missing or different parameters).';
message += '\nWhen you save the entry you risk a duplicate entry.';
} else {
message += '\nYou can still save the entry, which will update the existing entry\'s comment and or tags.';
}
setStatus(duplicateStatus, message, true);
} else {
duplicateStatus.classList.add('hidden');
}
} catch (error) {
// Duplicate checking is advisory; submission remains available.
}
}
// Handle form submission
entryForm.addEventListener('submit', async (e) => {
e.preventDefault();
const url = urlInput.value.trim();
const url = removeKnownTrackingParams(urlInput.value.trim());
const title = titleInput.value.trim();
const comment = commentInput.value.trim();
const newTags = newTagsInput.value.trim();
+1 -18
View File
@@ -1133,8 +1133,7 @@ button:disabled {
transition: all 0.2s ease;
}
.form-actions button[type='submit'],
#scrape-button:not(:disabled) {
.form-actions button[type='submit'] {
background: var(--mauve);
border: 1px solid var(--mauve);
color: var(--crust);
@@ -1150,22 +1149,6 @@ button:disabled {
cursor: not-allowed;
}
#scrape-button {
background: var(--surface-1);
border: 1px solid var(--border);
color: var(--text);
}
#scrape-button:not(:disabled):hover {
background: var(--surface-2);
border-color: var(--text);
}
#scrape-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.form-actions a {
background: var(--surface-1);
border: 1px solid var(--border);
+4 -2
View File
@@ -52,7 +52,6 @@
<span>URL</span>
<input id="url-input" name="url" type="url" required placeholder="https://example.com" />
</label>
<button id="scrape-button" class="secondary" type="button">Auto-fill Title</button>
<div id="scrape-status" class="status hidden" aria-live="polite"></div>
</div>
@@ -61,8 +60,11 @@
<span>Title</span>
<input id="title-input" name="title" type="text" required />
</label>
<button id="refetch-title-button" class="secondary" type="button">Re-fetch Title</button>
</div>
<div id="duplicate-status" class="status hidden" aria-live="polite"></div>
<div class="form-section">
<label>
<span>Comment</span>
@@ -101,6 +103,6 @@
</footer>
<script src="/static/auth-header.js"></script>
<script src="/static/new-entry.js?v=2"></script>
<script src="/static/new-entry.js?v=7"></script>
</body>
</html>