Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# zip. `bin/build-plugin-zip.sh` builds with `git archive`, which honours these,
# so this file is the single list of what is developer-only.
/.gitattributes export-ignore
/.wordpress-org export-ignore
/assets-src export-ignore
/.github export-ignore
/.gitignore export-ignore
/.phpcs.xml.dist export-ignore
Expand Down
14 changes: 14 additions & 0 deletions .phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,25 @@
Test files are not shipped: they legitimately reassign WordPress
globals, declare unprefixed test classes, and carry docblocks that hold
only PHPUnit annotations (@covers, @dataProvider).

The local rig (`bin/rig/*`, driven by `bin/local-rig.sh`) is the same
category and then some. It is not shipped — `bin/` is export-ignored and
the zip guard refuses an archive containing it — and it is not plugin
code: these are standalone scripts that install a throwaway WordPress,
seed it and assert against rendered pages. Two of them do not run inside
WordPress at all. Prefixing a local `$url` in a test harness with
`citecue_` would be ceremony that makes the harness harder to read
without making anything safer. Everything the sniffs flag there that IS
worth fixing — missing docblocks, short ternaries — is fixed rather than
excused; only these four categories are waived.
-->
<rule ref="WordPress.WP.GlobalVariablesOverride">
<exclude-pattern>/tests/*</exclude-pattern>
<exclude-pattern>/bin/rig/*</exclude-pattern>
</rule>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<exclude-pattern>/tests/*</exclude-pattern>
<exclude-pattern>/bin/rig/*</exclude-pattern>
</rule>
<rule ref="WordPress.PHP.DevelopmentFunctions">
<exclude-pattern>/tests/*</exclude-pattern>
Expand All @@ -83,6 +96,7 @@
</rule>
<rule ref="WordPress.Files.FileName">
<exclude-pattern>/tests/*</exclude-pattern>
<exclude-pattern>/bin/rig/*</exclude-pattern>
</rule>
<!-- PHPUnit calls assertion helpers in camelCase. -->
<rule ref="WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid">
Expand Down
Binary file added .wordpress-org/banner-1544x500.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/banner-772x250.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/icon-128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .wordpress-org/icon-256x256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions .wordpress-org/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
103 changes: 103 additions & 0 deletions assets-src/banner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<meta charset="utf-8">
<!--
Source for the WordPress.org banner. Rendered by build-assets.sh, which
inlines the brand fonts as data URIs before handing the file to Chrome.

The inlining is not an optimization: Chrome's --screenshot does not wait for
a font fetched over file://, so a relative @font-face silently renders the
whole layout in a fallback face with different metrics. Data URIs are parsed
with the stylesheet, so what is measured is what ships.

Sized in CSS pixels at 772x250 and rendered twice — once at 1x and once with
--force-device-scale-factor=2 — so the retina banner is the same design
rather than an upscale of the small one.
-->
<style>
@font-face{font-family:'Schibsted Grotesk';font-style:normal;font-weight:400 900;src:url(data:font/woff2;base64,__SG__) format('woff2')}
@font-face{font-family:'Schibsted Grotesk';font-style:italic;font-weight:400 900;src:url(data:font/woff2;base64,__SGI__) format('woff2')}
@font-face{font-family:'Instrument Sans';font-style:normal;font-weight:400 700;src:url(data:font/woff2;base64,__IS__) format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 700;src:url(data:font/woff2;base64,__JB__) format('woff2')}

*{margin:0;padding:0;box-sizing:border-box}
html,body{width:772px;height:250px;overflow:hidden}
body{
background:#F6F5EF;
position:relative;
font-kerning:normal;
-webkit-font-smoothing:antialiased;
text-rendering:geometricPrecision;
}
.band{position:absolute;top:0;left:0;width:772px;height:5px;background:#101b3d}

.lockup{position:absolute;left:44px;top:30px;display:flex;align-items:center;gap:9px}
.lockup svg{width:26px;height:26px;display:block}
.wordmark{
font-family:'Schibsted Grotesk';font-weight:700;font-size:21px;
letter-spacing:-0.02em;color:#101b3d;line-height:1;
}
.wordmark i{font-style:normal;color:#ee6c1e}

.eyebrow{
position:absolute;left:44px;top:79px;
font-family:'JetBrains Mono';font-weight:500;font-size:10px;
letter-spacing:0.2em;color:#ee6c1e;line-height:1;
}

.head{
position:absolute;left:44px;top:102px;
font-family:'Schibsted Grotesk';font-size:33px;line-height:37px;
letter-spacing:-0.035em;color:#23221C;font-weight:600;
}
.head em{display:block;font-style:italic;font-weight:500;color:#ee6c1e}

.pills{position:absolute;left:44px;top:192px;display:flex;gap:8px}
.pill{
font-family:'Instrument Sans';font-weight:500;font-size:11px;color:#23221C;
background:#fff;border:1px solid #E6E3D8;border-radius:14px;
height:27px;padding:0 13px;display:flex;align-items:center;line-height:1;
}

/* Sized to clear the top band and the lower edge, so the arc reads as the
mark rather than as a cropped shape; only the right side bleeds. Tinted
with explicit warm values rather than `opacity`, which mixes the navy
toward the paper through grey and turns the mark muddy. */
.mark{position:absolute;right:-34px;top:50%;transform:translateY(-50%);width:196px;height:196px}
.mark svg{width:100%;height:100%;display:block}
</style>

<div class="band"></div>

<div class="lockup">
<svg viewBox="4.95 2.75 34.5 34.5" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke-linecap="butt">
<path d="M 25.4 7.4 A 13 13 0 1 0 25.4 32.6" stroke="#101b3d" stroke-width="8.5"/>
<path d="M 24.7362 7.2498 A 13 13 0 0 1 31.8988 11.3435" stroke="#ee6c1e" stroke-width="8.5"/>
<path d="M 24.7362 32.7502 A 13 13 0 0 0 31.8988 28.6565" stroke="#ee6c1e" stroke-width="8.5"/>
</g>
</svg>
<span class="wordmark">CiteCue<i>.</i></span>
</div>

<div class="eyebrow">AI CRAWLERS &middot; LLMS.TXT &middot; METADATA</div>

<div class="head">Answer AI crawlers<em>with your best page.</em></div>

<!-- Every token here must exist in Citecue_Crawlers::bundled_tokens(). Google-Extended
is deliberately absent: it is robots.txt-only and never fetches a page, so the
plugin cannot serve it and the banner must not claim otherwise. -->
<div class="pills">
<span class="pill">GPTBot</span>
<span class="pill">ClaudeBot</span>
<span class="pill">PerplexityBot</span>
<span class="pill">GoogleOther</span>
</div>

<div class="mark">
<svg viewBox="4.95 2.75 34.5 34.5" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke-linecap="butt">
<path d="M 25.4 7.4 A 13 13 0 1 0 25.4 32.6" stroke="#E8E5D9" stroke-width="8.5"/>
<path d="M 24.7362 7.2498 A 13 13 0 0 1 31.8988 11.3435" stroke="#F9DDC7" stroke-width="8.5"/>
<path d="M 24.7362 32.7502 A 13 13 0 0 0 31.8988 28.6565" stroke="#F9DDC7" stroke-width="8.5"/>
</g>
</svg>
</div>
110 changes: 110 additions & 0 deletions assets-src/build-assets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
#
# Regenerates the WordPress.org directory assets in .wordpress-org/.
#
# These are the banner and icon the plugin directory shows above the readme.
# They are NOT part of the plugin: WordPress.org serves them from the `assets/`
# folder of the SVN repository, which sits beside `trunk/` and `tags/` and is
# never installed on a site. Both directories are export-ignored so they can
# never reach the distributed zip.
#
# Rendering is done by headless Chrome rather than a dedicated rasterizer
# because it is the one thing reliably present on a Mac that can lay out real
# webfont text, and --window-size with --force-device-scale-factor gives exact
# pixel dimensions. The 2x pass is a genuine re-render, not an upscale.
#
# Fonts come from the citecue2 site repository rather than being vendored
# here: they are the same faces the brand uses everywhere, and a second copy
# in a second repo is a second thing to update when they change.
#
# Usage: assets-src/build-assets.sh
# CITECUE_FONT_DIR override the font source directory
# CHROME override the Chrome binary

set -euo pipefail

ROOT=$(git rev-parse --show-toplevel)
SRC="$ROOT/assets-src"
OUT="$ROOT/.wordpress-org"

FONT_DIR=${CITECUE_FONT_DIR:-"$HOME/Sites/citecue2/public/fonts"}
CHROME=${CHROME:-"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"}

if [ ! -x "$CHROME" ]; then
echo "error: Chrome not found at $CHROME (set CHROME=)" >&2
exit 1
fi

for face in schibsted-grotesk schibsted-grotesk-italic instrument-sans jetbrains-mono; do
if [ ! -f "$FONT_DIR/$face.woff2" ]; then
echo "error: $FONT_DIR/$face.woff2 not found (set CITECUE_FONT_DIR=)" >&2
exit 1
fi
done

WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT

# Inline the faces. A relative @font-face would be a separate fetch that
# --screenshot does not wait for, and the banner would silently render in a
# fallback with different metrics.
python3 - "$SRC/banner.html" "$FONT_DIR" "$WORK/banner.html" <<'PY'
import base64, pathlib, sys

template, font_dir, out = (pathlib.Path(p) for p in sys.argv[1:4])
html = template.read_text()

for token, face in (
("__SG__", "schibsted-grotesk"),
("__SGI__", "schibsted-grotesk-italic"),
("__IS__", "instrument-sans"),
("__JB__", "jetbrains-mono"),
):
if token not in html:
sys.exit(f"error: {token} missing from {template}")
html = html.replace(token, base64.b64encode((font_dir / f"{face}.woff2").read_bytes()).decode())

out.write_text(html)
PY

shot() { # shot <output> <scale>
"$CHROME" --headless --disable-gpu --no-sandbox --hide-scrollbars \
--force-device-scale-factor="$2" --window-size=772,250 \
--screenshot="$1" "file://$WORK/banner.html" >/dev/null 2>&1
}

mkdir -p "$OUT"
shot "$OUT/banner-772x250.png" 1
shot "$OUT/banner-1544x500.png" 2

# The icon is already vector; Chrome just rasterizes it at the two sizes the
# directory asks for.
cat > "$WORK/icon.html" <<HTML
<meta charset="utf-8">
<style>*{margin:0;padding:0}html,body{width:256px;height:256px;overflow:hidden}
img{width:256px;height:256px;display:block}</style>
<img src="file://$OUT/icon.svg">
HTML
"$CHROME" --headless --disable-gpu --no-sandbox --hide-scrollbars \
--force-device-scale-factor=1 --window-size=256,256 \
--screenshot="$OUT/icon-256x256.png" "file://$WORK/icon.html" >/dev/null 2>&1
sips -Z 128 --out "$OUT/icon-128x128.png" "$OUT/icon-256x256.png" >/dev/null

# A banner at the wrong size is rejected by the directory, and the failure is
# a silently missing header rather than an error, so the sizes are asserted.
check() { # check <file> <w> <h>
local w h
w=$(sips -g pixelWidth "$1" | awk '/pixelWidth/ {print $2}')
h=$(sips -g pixelHeight "$1" | awk '/pixelHeight/ {print $2}')
if [ "$w" != "$2" ] || [ "$h" != "$3" ]; then
echo "error: $(basename "$1") is ${w}x${h}, expected $2x$3" >&2
exit 1
fi
echo " $(basename "$1") ${w}x${h}"
}

echo "Built $OUT"
check "$OUT/banner-772x250.png" 772 250
check "$OUT/banner-1544x500.png" 1544 500
check "$OUT/icon-256x256.png" 256 256
check "$OUT/icon-128x128.png" 128 128
2 changes: 1 addition & 1 deletion bin/build-plugin-zip.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ if [ -n "$stray_root" ]; then
exit 1
fi

stray_dev=$(printf '%s\n' "$entries" | grep -E "^$SLUG/(tests/|bin/|docs/|\.github/|composer\.|phpunit|\.phpcs|README\.md)" || true)
stray_dev=$(printf '%s\n' "$entries" | grep -E "^$SLUG/(tests/|bin/|docs/|assets-src/|\.wordpress-org/|\.github/|composer\.|phpunit|\.phpcs|README\.md)" || true)
if [ -n "$stray_dev" ]; then
echo "error: development files leaked into the archive:" >&2
printf '%s\n' "$stray_dev" >&2
Expand Down
Loading
Loading