Nine wrong names shipped. A check I wrote afterwards stopped eight more.

aigeodocumentation

I want to start with the part that does not flatter me.

Across my Webflow lessons I published nine identifiers that do not exist. Not typos — names. An attribute prefix that was wrong in every one of its sixteen appearances. A CSS variable someone would have spent an evening chasing. An ARIA value with the wrong capital letter, which silently kills the thing it is supposed to enable.

They were all wrong the same way: I did not copy them from the project. I reconstructed them from a transcript of me talking about the project.

Then I found eight more before they shipped, and I only found them because I had stopped trusting myself and written a check that fails the build. This is what that costs and how it works.

The short version

  • I have 73 lessons across 12 courses, in English and Italian, and most were written from transcripts of me speaking. That is a good way to keep a voice. It is a terrible way to get a name right.
  • One audit pass over 21 lessons found nine wrong identifiers. Earlier passes had already found others, so nine is a pass, not a total.
  • Not one of them was a mishearing I could have caught by rereading. Every one was plausible — a correct-sounding name derived from a real convention.
  • Building a knowledge base from those lessons, I nearly published eight fabricated attribute names at once, by reasoning correctly about a naming pattern the component happened to break.
  • So I wrote the rule down — identifiers are copied from the real project, never recalled — and then made the build enforce it: a page carrying project-specific names while citing only prose as its source fails. Not a warning. A failed build.
  • The check caught two real provenance errors on its first run, and one false positive that taught me what the rule actually is.
  • And it has a blind spot I can prove, because it passed a lesson that named no identifiers at all.

Nine names, and why none of them looks like a mistake

Here is the table from my own notes. Left column shipped. Right column is what is actually in the project.

What I publishedWhat it actually is
fc-fluid-gradient-gooeyfc-fluid-gradient-gui
FC-drop-*fc-dropdown-* — the whole prefix, in 16 places per language
fs-mirror-input-elementfs-mirrorinput-element
var(--glow)var(--glow-color)
aria-label="breadcrumb"aria-label="Breadcrumb"
class toc-sub-itemsclass toc-subitems
FC-ripples*fc-ripples* — 17 occurrences per language
FC-marquee-*fc-marquee-*
pixel tuning ranges for an SVG filterfractions — the filter uses primitiveUnits="objectBoundingBox"

Look at the first one for a second, because it is my favourite and the least defensible. The component has a small control panel, and in the video I said “GUI”, out loud, like a normal person. The transcription heard gooey. And fc-fluid-gradient-gooey reads fine — it is a fluid gradient, gooey is a word you would expect near it, and there is a well-known Webflow effect called a gooey blob. Nothing about that string asks to be checked.

The fourth one is the one I would be most annoyed about as a reader. --glow is a better variable name than --glow-color. It is shorter, it is what I would choose today, and it is not what is in the file. Someone copies that line, gets no glow, no error, no console warning, and reasonably concludes they did something wrong.

The fifth is the one that is actually dangerous. aria-label="breadcrumb" versus "Breadcrumb" — attribute selectors in CSS are case-sensitive, and the separators between the breadcrumb links are drawn by nav[aria-label="Breadcrumb"]::after. Publish the lowercase version and every separator disappears, on an accessibility component, in a lesson about accessibility.

And two of them are only about capital letters — FC- where the project uses fc-. Which sounds like the most trivial entry in the table until you remember that a person following the lesson types what they read.

The reason they all sounded right

There is one mechanism under all nine, and it is not carelessness.

A transcript preserves what I meant and discards how it is spelled. Speech has no casing, no hyphens, no underscores. “eff cee dash dropdown” and “FC dot drop” are the same sound with different consequences. So when I turned a transcript into a lesson, the meaning survived intact and every identifier arrived as an impression of a name — and to finish the sentence I had to guess the spelling.

Guessing is not what it feels like. It feels like remembering. And the guesses are good, because they are produced by real knowledge of my own conventions: I know I prefix component attributes with fc-, so fc-dropdown-behavior comes out with total confidence. It happens to be right. FC-drop-behavior came out with the same confidence and was wrong sixteen times.

If that sounds familiar, it should. It is exactly what an AI assistant does when it answers a question about a library it has read about rather than read. Which is how I ended up on the other side of the same problem.

The eight that did not ship

While building a knowledge base out of those 23 GSAP lessons, one page blocked me: the stagger component. Its lesson explains every option’s behaviour correctly and never once says what to type. Nine options described, zero attribute names on the page.

So I did what any competent system would do — I inferred them. Every other component in my corpus follows the same shape: fc-<component>-<option>. Nine options, nine names:

fc-gsap-staggered-threshold
fc-gsap-staggered-each
fc-gsap-staggered-amount

Confident, consistent, derived from a convention that genuinely exists across a dozen components.

All wrong. Here is the real thing, from the cloneable:

fc-gsap-staggered="list"
threshold="0.4"   each="0.2"   amount="0.5"   duration="0.5"
offset-y="3rem"   from="start" ease="power3.out" staggered-ease="power1.out"

Only the marker is prefixed. The options are bare. fc-gsap-staggered-each does not exist and never did. One component, out of all of them, breaks the pattern — and it is the one whose lesson forgot to write the names down.

That is eight fabricated identifiers, produced by correct reasoning about a real convention. Not a bad transcript, not a hallucination in the sloppy sense: a valid inference from a pattern with one exception in it. This is the failure mode I find genuinely hard to defend against, because everything about the process was sound except the output.

If you would rather watch this happen than read about it, it is the opening of a video I made about the same problem — an assistant confidently naming an attribute of my own component that has never existed.

And the same page had two more errors of a different kind, both of which fail without any error message:

  • The lesson said offsetY. The real attribute is offset-y. HTML lower-cases attribute names, so offsetY arrives at the script as offsety, matches nothing, and quietly leaves you on the default 3rem. Your animation works. It just ignores you.
  • The stagger’s second easing was described as “the stagger ease” and never named. It is staggered-ease. You cannot type a name nobody wrote.

The rule, in one line

Identifiers are copied from the real project. Never recalled, never reconstructed, never inferred from a convention — even a convention I invented.

Everything else in this article is machinery for making that rule survive contact with a tired author on the fortieth page. Because a rule you have to remember at the moment you are least likely to remember it is not a rule, it is a hope.

The check that fails the build

The knowledge base has 42 pages and every one declares where its content came from, in a field called verified_against, with four allowed values in descending strength:

  • cloneable — copied from the real Webflow project. The strongest, and the only one that can carry a code block honestly.
  • designer — for facts that exist only as Webflow’s own interface, where there is no file to copy from.
  • author — for facts that are in no lesson and no document, only in my head. I had to invent this tier partway through, which was itself informative: the lessons are dense but not exhaustive about their own reasoning.
  • lesson-prose — derived from the text of a lesson. Fine for explanation. Not acceptable for a name.

Then a script runs after every build, next to the accessibility and compliance gates, and one of its rules is the whole point:

A page whose only source is lesson-prose must not carry a code block containing project-specific identifiers, or an attribute table.

If it does, the build fails. Not a warning in a log nobody reads — an exit code. My reasoning was that a fabricated identifier in a corpus written for AI assistants is worse than a broken link on a page a human reads, because the reader has no way to tell it is wrong. A broken link announces itself. A plausible attribute name does not.

Two things made this harder to write than it sounds.

The schema has no default values anywhere. If a page needs an empty list, the author has to type the empty list. It sounds pedantic and it is the load-bearing part: a field with a sensible default is a field you can forget, and a forgotten provenance field silently means “trust me”.

“Project-specific identifier” needed a definition, and I got it wrong first. My first version flagged height: 100dvh — standard CSS, in every browser’s spec, not mine to verify. A false positive on run one, and a useful one: it forced the distinction. The check now looks for names that only exist inside my real projectsfc-* attribute families, data-* targeting attributes, custom properties being declared, class selectors being defined, the interactions runtime handle — not for code blocks as such. A code block is not the risk. A name someone could plausibly invent is the risk.

What it caught immediately

On its first real run the check failed the build twice, on pages I had written and reviewed:

  • The stagger page carried that nine-row attribute table — the correct one, the one I got from the cloneable — while its source field still said lesson-prose. The names were right and the provenance was a lie. Which matters, because the next person to edit that page has no way to know the table was ever verified.
  • The timeline control page carried a snippet using the interactions runtime handle, copied out of the cloneable, with the same wrong claim about where it came from.

Both were fixed by telling the truth: the source is a list now, [cloneable, lesson-prose], because a real page usually has two.

Neither error would have hurt a reader today. Both would have made the page unverifiable later, which for a document written to be quoted by machines is the same thing as being wrong, only slower.

What the check cannot see, and I can prove it

Here is the limit, and I would rather state it than let you discover it.

The rule only verifies identifiers that are present. A lesson that names no identifiers at all sails straight through — there is nothing to check. Which is exactly what happened: an audit pass marked the stagger lesson complete, and it was not. It reads as complete. It explains every option’s behaviour correctly and never says what to type. The gap survived precisely because nothing was wrong on the page; something was missing from it, and missing is invisible to a check that looks at what is there.

So the rule needs a companion question, asked by a human, and it is embarrassingly simple: does this page name its identifiers at all? I have no automated version of that yet. I am not sure a useful one exists, because “should this page have contained a name” is a judgement about intent.

Two smaller limits, in the same spirit:

  • verified_against: author cannot be checked by anything. It means “Francesco says so”. It is the strongest tier and the least verifiable, which is an uncomfortable pair. It is honest about who to blame, and that is all it is.
  • A check on my own repository proves nothing about my videos. The 74 spoken tutorials that started all this have no build gate and never will.

If you publish anything an assistant is going to read

Three things, in the order I would do them.

1. Find out whether your identifiers were copied or remembered. Not whether they are right — whether anyone ever checked. Pick five names from your own docs and try to say where each one came from. If the answer is “from the article”, you have the same problem I had, and you do not yet know its size.

2. Write the provenance down in the file. One field, per page, no default value. This is the cheap step and it is most of the benefit, because a name whose origin is recorded can be re-verified by someone who is not you. A name without one has to be re-derived, and re-deriving is how you get a second generation of plausible fiction.

3. Then make it fail something. A rule enforced by attention decays at exactly the rate your attention does. Mine runs after every build and takes under a second. It has failed my build four times and each time I was grateful, which is not a sentence I expected to write about a linter.

And the part that has nothing to do with tooling: when a name matters, go and look at it. Open the project. Copy the string. Nine of mine would have survived that one habit, and the eight I nearly published would have too — because the moment I opened that cloneable, the pattern I had reasoned my way into evaporated in about four seconds.

The mechanism I keep coming back to is that being confident and being correct are produced by different processes, and only one of them leaves a trace you can check later. That is true of an assistant answering a question about your library. It turned out to be just as true of me, describing my own.

The things this article is about

The corpus all of this protects, 42 pages with their sources declared on every one:

Browse the knowledge base →

The same pages as plain Markdown, if you would rather install them in your own assistant than read them:

github.com/francesco-castronuovo/gsap-webflow-skill

And the reason any of this exists: I measured whether AI assistants send anyone to my site, before announcing anything. Ten questions, four assistants, 31 points out of 80 — and the knowledge base scored zero. That story, including the eight invented names told the long way, is in Credited Elsewhere and in the video. The numbers themselves live on one page that grows every round: Credited →.

There is also a companion to this one, from the other direction: writing pages an assistant can trust is half the problem, and which crawler ever reads them is the other half. Anthropic’s read all 42 of these pages in an hour and Claude still credited my old domain — Three kinds of AI crawler →.

Have an awesome journey,

Francesco