Rebuild on the RealBrain design system and retarget SEO to content credentials
Search Console (Jun 4 – Sep 3) shows the page ranking on the first results page for "content credentials" queries while converting badly: "ai credentials remover" at 113 impressions and 0.88% CTR from position 6.7, "remove content credentials from image online" at 2.15% from position 5.0, "remove ai content credentials from image" at 0% from position 7.8. The title led with "EXIF metadata", a phrase that appears in none of the top queries, so the snippet never matched the intent behind the impressions. SEO - Title, description and H1 rebuilt around "remove content credentials from an image"; EXIF kept as a secondary term. - Added a how-to section and a nine-question FAQ answering the question queries that had no content behind them, including the LinkedIn AI label and the Instagram AI tag. - Structured data as an @graph: WebSite (alternateName "Untrace AI" for the unserved brand query), Organization, WebApplication, HowTo, FAQPage. - Fixed sitemap.xml, which pointed at /untrace/ — a URL that does not exist. - Regenerated the Open Graph image to match the new design. Design - Vendored the design system at src/tokens.css and composed the app from it. The old styling broke most of its rules: gradients, font weights 800 and 900, raw #2563eb, Inter, and emoji as the only status signal. - Reworked the tool into a Scan / Review / Download flow that lists what it found before changing anything, with the primary action pinned within thumb reach on small screens. - Dark by default with a light theme, applied before first paint so the page never flashes the wrong surface. - Replaced the floating donation widget, which painted an off-token circle over the page and covered the mobile action bar, with a footer link. Structure - Moved detection into src/lib/metadata.js and removal into src/lib/clean.js; the canvas callbacks are now a promise chain. - Dropped Tailwind and PostCSS. Both were configured twice, and no Tailwind utility was used. Stylesheet is 6.2 kB gzipped with the whole system in it. Fixes - The stepper never advanced: stepState(index) read `current` from the closure, which Svelte 4 cannot track as a template dependency. - The cleaner's fallback returned the original, uncleaned file while the UI reported "All EXIF metadata removed". It now reports the failure instead. Verified against a JPEG carrying EXIF, GPS, XMP and an APP11 JUMBF C2PA manifest: the output retains only APP0 and the sRGB ICC profile, with every provenance marker gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018pRSdR57Ze9M2meDfYZgYE
This commit is contained in:
1 parent
83a4d3f2c4
commit
00991c1608
40 files changed
+3577
-3697
No files matched your search
@@ -0,0 +1,130 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { ACCEPTED_EXTENSIONS, isAccepted } from "./metadata.js";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let busy = false;
|
||||
|
||||
let dragging = false;
|
||||
let fileInput;
|
||||
let depth = 0;
|
||||
|
||||
function offer(file) {
|
||||
if (!file) return;
|
||||
if (!isAccepted(file)) {
|
||||
dispatch("reject", {
|
||||
message: `Untrace cannot read ${file.type || "that file type"}. Choose a JPEG, PNG, TIFF, HEIC or AVIF image.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
dispatch("select", { file });
|
||||
}
|
||||
|
||||
function onDrop(event) {
|
||||
event.preventDefault();
|
||||
depth = 0;
|
||||
dragging = false;
|
||||
if (busy) return;
|
||||
offer(event.dataTransfer?.files?.[0]);
|
||||
}
|
||||
|
||||
function onDragEnter(event) {
|
||||
event.preventDefault();
|
||||
if (busy) return;
|
||||
depth += 1;
|
||||
dragging = true;
|
||||
}
|
||||
|
||||
function onDragLeave(event) {
|
||||
event.preventDefault();
|
||||
depth = Math.max(0, depth - 1);
|
||||
if (depth === 0) dragging = false;
|
||||
}
|
||||
|
||||
function onChange(event) {
|
||||
offer(event.target.files?.[0]);
|
||||
// Reset so choosing the same file twice still fires a change event.
|
||||
event.target.value = "";
|
||||
}
|
||||
|
||||
function onPaste(event) {
|
||||
if (busy) return;
|
||||
const item = [...(event.clipboardData?.items || [])].find((entry) =>
|
||||
entry.type.startsWith("image/")
|
||||
);
|
||||
if (item) offer(item.getAsFile());
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:paste={onPaste}
|
||||
on:dragover|preventDefault
|
||||
on:drop|preventDefault
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="dropzone"
|
||||
data-drag={dragging}
|
||||
disabled={busy}
|
||||
on:click={() => fileInput.click()}
|
||||
on:dragenter={onDragEnter}
|
||||
on:dragover|preventDefault
|
||||
on:dragleave={onDragLeave}
|
||||
on:drop={onDrop}
|
||||
>
|
||||
<span class="glyph"><Icon name="upload" size="lg" /></span>
|
||||
<span class="title">
|
||||
{dragging ? "Drop to scan it" : "Drop an image, or choose a file"}
|
||||
</span>
|
||||
<span class="hint">
|
||||
Nothing is uploaded. The image is read and rewritten inside this tab.
|
||||
</span>
|
||||
<span class="chip-row formats">
|
||||
<span class="chip">JPEG</span>
|
||||
<span class="chip">PNG</span>
|
||||
<span class="chip">TIFF</span>
|
||||
<span class="chip">HEIC</span>
|
||||
<span class="chip">AVIF</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p class="paste-hint">
|
||||
You can also paste an image with <kbd>Ctrl</kbd>+<kbd>V</kbd>.
|
||||
</p>
|
||||
|
||||
<input
|
||||
class="visually-hidden"
|
||||
type="file"
|
||||
accept={ACCEPTED_EXTENSIONS}
|
||||
bind:this={fileInput}
|
||||
on:change={onChange}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.formats {
|
||||
justify-content: center;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.paste-hint {
|
||||
margin-top: var(--space-3);
|
||||
text-align: center;
|
||||
color: var(--text-3);
|
||||
font-size: var(--fs-1);
|
||||
line-height: var(--lh-1);
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--fs-1);
|
||||
color: var(--text-2);
|
||||
padding: 0 var(--space-1);
|
||||
border: var(--border-w) solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user