Skip to content

feat(blocks): credit — "Powered by / Created by / Managed by" attribution - #73

Merged
bytesbrains merged 1 commit into
devfrom
feat/credit-block
Jul 26, 2026
Merged

feat(blocks): credit — "Powered by / Created by / Managed by" attribution#73
bytesbrains merged 1 commit into
devfrom
feat/credit-block

Conversation

@nandal

@nandal nandal commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Catalog 53 → 54. Adds credit — the one-line attribution every generated site wants at the very bottom, linking out to whoever built or runs it.

copyright asserts ownership (© year holder). That is a different claim from "this was built by X", and the only way to express the latter today was to bend copyright's rights text into a line that cannot carry a link at all. The two compose: a copyright bar above a credit line.

The block

One brick rather than one per phrasing — label is free text, so "Created by", "Managed by", "Built by", "A project of" all fit the same contract with no new field and no new type.

field
label the relationship word (default "Powered by")
name * who is credited
href where it goes — blank for an unlinked credit
newTab default true
logo image URL or a single emoji
note trailing aside, after a ·
variant bar (default) · badge · inline
align start · center · end

Why the link is the guarded part

It is the whole point of the brick, so it gets the scrutiny:

  • href runs through sanitizeUrl, and a hostile scheme degrades to unlinked plain text rather than href="#" — a credit with no destination should read as text, not as a link that goes nowhere.
  • Outbound means newTab defaults to true, which brings rel="noopener noreferrer" and a visually hidden "(opens in a new tab)". The ↗ glyph is aria-hidden and announces nothing, so without that text a screen-reader user is moved to a new tab unwarned.
  • newTab: false drops the target and the warning together, so the promise can never drift from the behaviour.

Total render, as required by the brick DoD: blank href → unlinked; blank everything → the landmark still renders; name and note are escaped.

Samples

{ "type": "credit", "config": {
  "label": "Created by", "name": "BytesBrains", "href": "https://bytesbrains.com",
  "newTab": true, "variant": "bar", "align": "center" } }
{ "type": "credit", "config": {
  "label": "Created by", "name": "AiToolK.it", "href": "https://aitoolk.it",
  "newTab": true, "logo": "🛠️", "note": "Built in minutes, not weeks",
  "variant": "badge", "align": "center" } }

Both, plus the addBlock op form and when to reach for credit vs copyright, are documented in a new Crediting the maker section in AGENT.md.

Notes

  • Demoed on the block wall via SUPPLEMENT, not a starter placement — templates stay consumer-neutral, and a credit is only honest when it names someone real, so the wall credits this package's own maker.
  • 9 tests in src/credit.test.ts cover the new-tab contract, scheme neutralization, escaping, logo URL-vs-emoji, and the empty-config landmark. Full suite: 189 passing. catalog.json / CATALOG.md regenerated.
  • No package.json bump — CHANGELOG.md carries the 0.12.0 section for a later release PR.
  • Drive-by: the README block table still said 52 blocks and was missing progress from 0.11.0.

🤖 Generated with Claude Code

…tion

Catalog 53 → 54. Nothing in the catalog could credit the maker of a site.
`copyright` asserts ownership (© year holder) — a different claim from
"this was built by X", and the only way to express the latter was to bend
`copyright`'s rights text into a line that cannot carry a link at all.

One brick, not one per phrasing: `label` is free text, so "Created by",
"Managed by", "Built by" and "A project of" all fit the same contract
without a new field or a new type. `name`, an optional `logo` (image URL
or a single emoji) and a trailing `note` fill the rest, across bar / badge
/ inline variants.

The link is the whole point of the brick, so it is the part that is
guarded. `href` runs through sanitizeUrl and a hostile scheme degrades to
unlinked plain text rather than href="#" — a credit with no destination
should read as text, not as a link that goes nowhere. Outbound means
newTab defaults to true, which brings rel="noopener noreferrer" and a
visually hidden "(opens in a new tab)": the ↗ glyph is aria-hidden and
announces nothing, so without it a screen-reader user is moved to a new
tab unwarned. newTab:false drops the target and the warning together, so
the promise can never drift from the behaviour.

Total render: blank href → unlinked, blank everything → the landmark
still renders, and both name and note are escaped.

Demoed on the wall via SUPPLEMENT rather than a starter placement —
templates stay consumer-neutral, and a credit is only honest when it
names someone real, so the wall credits this package's own maker.

Drive-by: the README block table still said 52 and was missing
`progress` from 0.11.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wrokin

wrokin Bot commented Jul 26, 2026

Copy link
Copy Markdown

🤖 wrokin code review

Adds credit block for attribution with comprehensive tests; one defensive coding improvement recommended.

High-signal issues

  • src/blocks/credit.ts:62sanitizeUrl(config.href) may receive undefined if the href field is not provided, which could cause sanitizeUrl to throw if it does not handle non-string input. The render output already guards against an absent link, but the call should be hardened: use sanitizeUrl(config.href ?? '') to guarantee a string argument.

Model: deepseek/deepseek-v4-pro · your key, your model (BYOK)

@wrokin

wrokin Bot commented Jul 26, 2026

Copy link
Copy Markdown

🤖 wrokin security audit

No security-relevant findings in this diff. All user-supplied text is properly escaped (escapeHtml, escapeAttr), URLs are sanitized (sanitizeUrl), and outbound links apply rel="noopener noreferrer" with accessible new‑tab warnings. The new credit block introduces no injection, authorization, secret exposure, or unsafe deserialisation vulnerabilities.

Model: deepseek/deepseek-v4-pro · your key, your model (BYOK)

@nandal

nandal commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — checked the one finding; it's a false positive, so no change.

sanitizeUrl is typed (url: unknown) and opens with String(url ?? '') (src/schema.ts:157-164) — non-string input is coerced, not thrown on:

sanitizeUrl(undefined) → '#'
sanitizeUrl(null)      → '#'

It also can't be reached with an absent href in the first place: renderBlock runs parse(spec.schema, block.config) before calling a brick's render (src/render.ts:47), and the schema defaults href to ''. Total render is the engine's invariant, so every brick relies on that — socialLinks, footer and map all call sanitizeUrl(x.href) bare for the same reason. Adding ?? '' here would be dead code and would break that idiom.

The behaviour the finding is aimed at is covered by tests: a missing href renders an unlinked credit, and a javascript: scheme degrades to plain text rather than href="#" (src/credit.test.ts).

@bytesbrains
bytesbrains merged commit 76d8fe2 into dev Jul 26, 2026
5 checks passed
@bytesbrains
bytesbrains deleted the feat/credit-block branch July 26, 2026 08:14
nandal added a commit that referenced this pull request Jul 26, 2026
Catalog 53 → 54, additive and non-breaking: every 0.11.x manifest still
validates and renders identically.

- `credit` (#73) — "Powered by / Created by / Managed by <name>" with an
  outbound link. Free-text `label` so any phrasing fits one contract;
  optional logo (image URL or emoji) and note; bar / badge / inline.
  `sanitizeUrl` on the href with a hostile scheme degrading to plain text,
  and `newTab` (default) carrying rel="noopener noreferrer" plus a
  visually hidden "(opens in a new tab)" the ↗ glyph cannot convey.

Version stamped into the emitted catalog in the same commit — 0.11.1
needed a follow-up because it wasn't.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants