fix: extract URL and headline from Android share target, trigger article clipping and open editor

This commit is contained in:
chrisf committed 2026-10-22 20:43:13 -06:00
1 parent 94fb793f99
commit a99e05c53c
3 files changed
+191 -27

No files matched your search

+70 -16
View File
@@ -114,20 +114,55 @@ export async function POST(req: NextRequest) {
uploadedFiles.push(...files);
}
// Check if Android shared a URL inside the `text` or `content` (or `title`)
// Check if Android shared a URL inside the `text`, `content`, or `title`
if (!url && content) {
const match = /(https?:\/\/[^\s]+)/i.exec(content);
if (match) {
url = match[1];
if (content.trim() === url) {
const remaining = content.replace(url, '').trim();
if (!title && remaining) {
title = remaining;
}
if (
!remaining ||
remaining === title.trim() ||
title.trim().includes(remaining) ||
remaining.includes(title.trim()) ||
(!remaining.includes('\n') && remaining.length < 300)
) {
content = '';
}
}
}
if (!url && title) {
const titleMatch = /(https?:\/\/[^\s]+)/i.exec(title);
if (titleMatch) {
url = titleMatch[1];
title = title.replace(url, '').trim();
}
}
if (!url && title && /^https?:\/\/\S+$/i.test(title.trim())) {
url = title.trim();
title = '';
}
if (url) {
url = url.replace(/[.,;:)\]]+$/, '');
}
if (url && content) {
const remaining = content.replace(url, '').trim();
if (
!remaining ||
remaining === title.trim() ||
title.trim().includes(remaining) ||
remaining.includes(title.trim()) ||
(!remaining.includes('\n') && remaining.length < 300)
) {
if (!title && remaining) {
title = remaining;
}
content = '';
}
}
const notebookId = notebookName.trim()
? await getOrCreateNotebookByName(notebookName.trim(), userId)
@@ -197,21 +232,36 @@ export async function POST(req: NextRequest) {
// 2. If URL is provided without body content and no files, clip the full article on server!
if (url && !content && !markdown && !html && uploadedFiles.length === 0) {
const clippedId = await clipUrlToNote(url, {
title: title || undefined,
notebookId,
tags,
isShortcut,
});
try {
const clipTags = [...tags];
if (!clipTags.includes('web-clip')) {
clipTags.push('web-clip');
}
const clippedId = await clipUrlToNote(url, {
title: title || undefined,
notebookId,
tags: clipTags,
isShortcut,
});
if (isShareTarget) {
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
await applyAutoSortToNote(clippedId, userId).catch(() => {});
const wantsJson =
contentType.includes('application/json') ||
Boolean(req.headers.get('accept')?.includes('application/json'));
if (isShareTarget && !wantsJson) {
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
}
return NextResponse.json(
{ ok: true, noteId: clippedId, mode: 'web-clip' },
{ status: 201, headers: corsHeaders }
);
} catch (clipErr) {
console.warn('URL clipping failed in /api/hooks/note, falling back to basic link note:', clipErr);
content = `<p>Saved link: <a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a></p>`;
}
return NextResponse.json(
{ ok: true, noteId: clippedId, mode: 'web-clip' },
{ status: 201, headers: corsHeaders }
);
}
let finalHtml = html;
@@ -309,7 +359,11 @@ export async function POST(req: NextRequest) {
await applyAutoSortToNote(note.id, userId).catch(() => {});
if (isShareTarget) {
const wantsJson =
contentType.includes('application/json') ||
Boolean(req.headers.get('accept')?.includes('application/json'));
if (isShareTarget && !wantsJson) {
return NextResponse.redirect(getRedirectUrl(req, note.id), 303);
}
+64
View File
@@ -56,10 +56,35 @@ export async function GET(req: NextRequest) {
text = extracted.remainingText;
}
}
if (!url && title) {
const extracted = extractUrl(title);
if (extracted.extractedUrl) {
url = extracted.extractedUrl;
title = extracted.remainingText;
}
}
if (!url && title && /^https?:\/\/\S+$/i.test(title)) {
url = title;
title = '';
}
if (url) {
url = url.replace(/[.,;:)\]]+$/, '');
}
if (url && text) {
const remaining = text.replace(url, '').trim();
if (
!remaining ||
remaining === title ||
title.includes(remaining) ||
remaining.includes(title) ||
(!remaining.includes('\n') && remaining.length < 300)
) {
if (!title && remaining) {
title = remaining;
}
text = '';
}
}
let noteId: string;
if (url && !text) {
@@ -150,10 +175,39 @@ export async function POST(req: NextRequest) {
text = extracted.remainingText;
}
}
if (!url && title) {
const extracted = extractUrl(title);
if (extracted.extractedUrl) {
url = extracted.extractedUrl;
title = extracted.remainingText;
}
}
if (!url && title && /^https?:\/\/\S+$/i.test(title)) {
url = title;
title = '';
}
if (url) {
url = url.replace(/[.,;:)\]]+$/, '');
}
if (url && text) {
const remaining = text.replace(url, '').trim();
if (
!remaining ||
remaining === title ||
title.includes(remaining) ||
remaining.includes(title) ||
(!remaining.includes('\n') && remaining.length < 300)
) {
if (!title && remaining) {
title = remaining;
}
text = '';
}
}
const wantsJson =
contentType.includes('application/json') ||
Boolean(req.headers.get('accept')?.includes('application/json'));
// 1. If sharing a URL with no uploaded files and no extra body text, clip the webpage
if (url && !text && uploadedFiles.length === 0) {
@@ -164,6 +218,9 @@ export async function POST(req: NextRequest) {
tags: ['inbox', 'web-clip'],
});
await applyAutoSortToNote(clippedId, userId).catch(() => {});
if (wantsJson) {
return NextResponse.json({ ok: true, noteId: clippedId, mode: 'web-clip' }, { status: 201 });
}
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
} catch (e) {
console.warn('URL clipping failed in /share, falling back to basic link note:', e);
@@ -262,6 +319,13 @@ export async function POST(req: NextRequest) {
// 4. Run Smart Auto-Sorting & Auto-Tagging
await applyAutoSortToNote(note.id, userId).catch(() => {});
if (wantsJson) {
return NextResponse.json(
{ ok: true, noteId: note.id, title: note.title },
{ status: 201 }
);
}
// 5. Redirect browser / Android WebAPK straight into Everclone displaying the new note
return NextResponse.redirect(getRedirectUrl(req, note.id), 303);
} catch (err) {
+57 -11
View File
@@ -471,30 +471,76 @@ export default function EvercloneWorkspace() {
}
// Android PWA Web Share Target GET Handler (/?url=... or /?text=... or /?title=...)
const sharedUrl = sp.get('url') || '';
const sharedText = sp.get('text') || '';
const sharedTitle = sp.get('title') || '';
if (sharedUrl || sharedText || sharedTitle) {
const rawSharedUrl = sp.get('url') || '';
const rawSharedText = sp.get('text') || '';
const rawSharedTitle = sp.get('title') || '';
if (rawSharedUrl || rawSharedText || rawSharedTitle) {
window.history.replaceState({}, '', '/');
setShareToast('Saving shared note to Everclone...');
// Fetch notes immediately in parallel so the notes list is never left blank
fetchNotesList();
let extractedUrl = rawSharedUrl.trim();
let cleanText = rawSharedText.trim();
let cleanTitle = rawSharedTitle.trim();
if (!extractedUrl && cleanText) {
const urlMatch = /(https?:\/\/[^\s]+)/i.exec(cleanText);
if (urlMatch) {
extractedUrl = urlMatch[1];
cleanText = cleanText.replace(extractedUrl, '').trim();
}
}
if (!extractedUrl && cleanTitle) {
const urlMatch = /(https?:\/\/[^\s]+)/i.exec(cleanTitle);
if (urlMatch) {
extractedUrl = urlMatch[1];
cleanTitle = cleanTitle.replace(extractedUrl, '').trim();
}
}
if (extractedUrl) {
extractedUrl = extractedUrl.replace(/[.,;:)\]]+$/, '');
}
if (extractedUrl && cleanText) {
if (!cleanTitle && cleanText.length < 300 && !cleanText.includes('\n')) {
cleanTitle = cleanText;
cleanText = '';
} else if (
cleanText === cleanTitle ||
cleanTitle.includes(cleanText) ||
cleanText.includes(cleanTitle) ||
(!cleanText.includes('\n') && cleanText.length < 300)
) {
cleanText = '';
}
}
const isWebClip = Boolean(extractedUrl && !cleanText);
setShareToast(isWebClip ? 'Clipping web article to Everclone...' : 'Saving note to Everclone...');
(async () => {
try {
const res = await fetch('/api/hooks/note?source=share-target', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
url: sharedUrl,
text: sharedText,
title: sharedTitle,
tags: ['inbox', 'share'],
url: extractedUrl,
text: cleanText,
title: cleanTitle,
tags: isWebClip ? ['inbox', 'web-clip'] : ['inbox', 'share'],
}),
});
if (res.ok) {
const data = await res.json();
setShareToast('Saved note to Everclone from Share!');
setShareToast(isWebClip ? 'Web article clipped to Everclone!' : 'Saved note to Everclone!');
setTimeout(() => setShareToast(null), 4000);
if (data.noteId) {
if (typeof window !== 'undefined' && window.innerWidth < 768) {
window.history.pushState({ evcMobilePane: 'editor', noteId: data.noteId }, '');
}
setSelectedNoteId(data.noteId);
setMobilePane('editor');
fetchNotesList(data.noteId);