* ci(landing): split landing deploy into staging gate + manual production
A merge to `main` previously published the landing page straight to
production (open-design.ai) via `landing-page-deploy`. There was no
buffer to review the rendered site, so a bad merge was live instantly.
Split deploys across two Cloudflare Pages projects so production is only
ever reached by an explicit human action:
- `landing-page-staging` (push to main) -> staging project
`open-design-landing-staging` -> staging.open-design.ai.
- `landing-page-production` (manual workflow_dispatch only) -> production
project `open-design-landing` -> open-design.ai. Only this workflow
names the production project; gate it with required reviewers on the
`production` GitHub environment.
- `landing-page-ci` now also deploys a per-PR preview into the staging
project (`--branch=pr-<n>`) for same-repo branches and comments the URL.
Fork PRs (no secrets / read-only token) skip the deploy and keep just
the build validation. Path filters already scope this to landing edits.
Decouple search-engine indexing from staging:
- `blog-indexing-on-deploy` now triggers on `landing-page-production`
(not every main push), so the test environment is never submitted to
Google/IndexNow.
- It diffs from a new `blog-indexed-prod` tag (the last indexed prod
commit) instead of `HEAD^`, and force-advances the tag after a
successful run, so a manual promotion bundling several merged posts
indexes all of them rather than only the last commit.
Staging and PR-preview builds drop `PUBLIC_GA_MEASUREMENT_ID` so test
traffic does not pollute the production GA property.
* ci(landing): keep staging + PR previews out of the search index
staging.open-design.ai mirrors production and is exposed via cert
transparency logs, so search engines can discover it. Indexing the
mirror competes with open-design.ai for the same content.
Emit `<meta name="robots" content="noindex, nofollow">` whenever
OD_LANDING_NOINDEX=1, and set that flag on the staging and PR-preview
builds (production leaves it unset and stays indexable). noindex is
used rather than a robots.txt Disallow so crawlers can still fetch the
page and read both the tag and the canonical, which already points at
the production origin.
* fix(landing): make staging noindex actually take effect
The previous commit read `process.env.OD_LANDING_NOINDEX` directly in
`seo-head.astro`, but `.astro` frontmatter is transformed by Vite and
does not see process.env, so the meta never rendered. Two fixes:
- Inject the flag as the compile-time constant `__OD_LANDING_NOINDEX__`
via `vite.define` in astro.config.ts (config runs in Node and can read
process.env); SeoHead consumes that constant.
- The homepage (`index.astro`) and `og.astro` build their own <head> and
never use SeoHead, so a per-component meta can miss pages. Add an
`astro:build:done` integration that appends a catch-all
`/* X-Robots-Tag: noindex, nofollow` to the Cloudflare Pages `_headers`
on staging/preview builds, covering every response (homepage, assets,
any custom-head page) at the HTTP layer. Production builds leave
`_headers` untouched.
Verified: build with OD_LANDING_NOINDEX=1 emits the _headers block and
the SeoHead <meta>; build without the flag emits neither; astro check
clean.
* fix(landing): address review — pin prod checkout to main, defer index pointer
Two blockers from review:
- landing-page-production: workflow_dispatch can be launched from any ref
via the Actions "Use workflow from" dropdown, so an operator could ship
an arbitrary branch to open-design.ai. Pin the checkout to `ref: main`
so the deployed artifact always equals reviewed main.
- blog-indexing-on-deploy: the `blog-indexed-prod` pointer was advanced
right after sitemap submission, before Inspect / Search Analytics /
Render status / Open status PR. A failure in any of those still moved
the pointer, so the next production run skipped those posts. Move the
advance to the very end, gated on `success()`, so a failure leaves the
tag in place and the range is re-processed next run (submissions are
idempotent).
* fix(landing): gate production promotion to the main ref only
Follow-up to the production-path review note: pinning checkout to main
fixed the deployed content, but the workflow was still dispatchable from
any ref, which records a non-main production run and would dodge
blog-indexing's `workflow_run` `branches: [main]` filter. Gate the whole
job on `github.ref == 'refs/heads/main'` so a dispatch from any other
branch/tag is skipped outright.
* feat(landing-page): plugin detail page interactive preview + share dialog
The new `/plugins/<manifest-id>/` detail page that shipped in #2926
landed without the two affordances PR #2679 added to the legacy
`/skills/<slug>/` and `/templates/<slug>/` pages: a click-to-expand
iframe of the live artifact, and a share dialog with brand-keyword
copy plus four-channel jump buttons (X / LinkedIn / Reddit /
Facebook). This restores both, sourced from the bundled-plugin
manifest under `plugins/_official/<bucket>/<slug>/open-design.json`.
## Interactive preview
Three preview-type behaviours, gated on `od.preview.type`:
- `video` (Cloudflare Stream URLs already in the manifest) —
inline `<video controls poster=...>` with the playable MP4 as
`<source>`. Detail-page row is unchanged from #2926; controls
double as the open-full affordance.
- `html` (a local `example.html` referenced by `od.preview.entry`,
only the `examples/` bucket today) — `<details>` toggle wraps the
poster image as the summary; clicking opens a sandboxed
`<iframe>` that loads the entry HTML lazily, with an
"Open in new tab ↗" pill in the frame's top-right corner so the
artifact can be inspected at full screen.
- `image` or no entry — static `<img>` (existing behaviour).
`copy-example-html.ts` is extended to mirror the local entry and any
`./assets/...` siblings to `out/plugins/<manifest-id>/<entry>` so the
iframe URL resolves on Cloudflare Pages instead of SPA-falling-back to
the homepage. The four examples carrying sibling-asset references
(flowai-live-dashboard-template, trading-analysis-dashboard-template,
open-design-landing, open-design-landing-deck) all render in-place.
## Share dialog
Same `<dialog data-share-dialog>` markup the legacy detail pages use,
so the global click handlers in `header-enhancer.astro`
(`data-share-open` / `data-share-copy` / `data-copy-link`) wire up
the open / copy actions automatically — no extra client bundle. Four
platform jumps (X / LinkedIn / Reddit / Facebook) plus a Copy-text /
Copy-link pair, with a single English template for now (the new
`/plugins/...` routes only generate English pages; localisation can
land alongside the i18n catch-all follow-up).
## Bundled in
- The `copy-example-html.ts` sibling-assets fix from open PR #2880.
Without it the existing `/skills/<slug>/` iframe still 404s on
Cloudflare Pages for after-hours-editorial-template and the four
others; bundling it here means the same script handles both
sources in one pass and sidesteps two PRs touching identical
helper code.
* fix(plugins): remove dangling preview.entry from example-hyperframes
The hyperframes example folder ships a SKILL.md (it's an instruction
manual for using the HyperFrames HTML format) but no runnable
`example.html`. The manifest still claimed `preview.type: html` /
`preview.entry: ./example.html`, which made the marketing site try
to iframe a non-existent file and forced the preview pipeline into
its `Path 3` fallback card — leaving the catalog row visually
inconsistent with the eleven sibling `video-template-hyperframes-*`
plugins that have real Cloudflare-Stream poster URLs.
Drop the preview block entirely so the manifest stops promising a
demo it can't deliver. The landing-page detail row continues to
render the typographic fallback card (sourced from title /
description / mode), which is now the honest representation:
"this is an instruction skill, not a renderable template".
* fix(landing-page): address PR #2958 review feedback on plugin preview pipeline
Two blocking issues called out in code review:
1) `bundled-plugins.ts` exposed `previewEntryUrl` for every manifest
that declared `preview.type: "html"`, even when the entry file
wasn't shipped. Several first-party manifests fall in this state
(example-design-brief's `./brief-preview.html`, example-x-research,
example-pptx-html-fidelity-audit, example-hatch-pet,
example-last30days, example-guizang-ppt, example-replit-deck,
example-live-artifact, example-html-ppt, example-dcf-valuation).
The detail page then rendered a click-to-expand iframe and popout
link to a file that copy-example-html.ts had skipped, so the
iframe URL SPA-fell-back to the homepage on Cloudflare Pages.
`entryRelativeUrl()` now `existsSync()`-checks the resolved local
path before returning a URL. When the file's missing the detail
page falls through to the static thumbnail branch, exactly like
plugins that ship no preview entry at all.
2) `copy-example-html.ts` recognised only `(src|href|poster)="./assets/..."`
and then bulk-copied the entry's sibling `assets/` folder, so it
missed two real ref shapes: bare-relative (`href="assets/styles.css"`,
`src="assets/deck-stage.js"` under example-html-ppt-zhangzara-pin-and-paper)
and cross-folder (`src="../open-design-landing/assets/hero.png"`
under example-open-design-landing-deck).
Replaced the heuristic with a generic walker that:
- Parses every relative ref in the entry HTML
(`(src|href|poster|srcset|data-src)=` plus `url(...)`), splitting
srcset on whitespace/commas so multi-URL attrs are honoured.
- Resolves each ref against `dirname(entrypointSrc)` for the source
and against `dirname(iframeAbsPath)` for the destination —
identical to how a browser resolves the same ref against the
iframe URL. Files outside the source root or the iframe root
are dropped.
- Recurses into copied HTML / CSS / JS / SVG so multi-step chains
(entry → assets/template.html → assets/fonts/foo.woff) don't
strand intermediate files.
- Tracks visited *destinations* rather than sources, so a single
source that legitimately needs to land at two different out-paths
(same-folder copy at /plugins/example-X/assets/foo.png AND a
cross-folder copy at /plugins/open-design-landing/assets/foo.png
for sibling decks that use `../open-design-landing/assets/foo.png`)
gets both copies.
Verified manually:
- /plugins/example-html-ppt-zhangzara-pin-and-paper/assets/styles.css
and assets/deck-stage.js → 200 (bare-relative)
- /plugins/open-design-landing/assets/hero.png and assets/about.png
→ 200 (cross-folder destination, no manifest-id prefix because
iframe URL `..` collapses the prefix)
- /plugins/example-design-brief/ renders the static thumbnail only,
no click-to-expand iframe (broken entry guard)
- /plugins/example-flowai-live-dashboard-template/assets/template.html
→ 200 (existing same-folder behaviour preserved)
Build now reports `copied 266 entry files + 65 referenced files`,
where the 65 includes both the same-folder `./assets/...` payloads
the previous heuristic captured and the bare-relative + cross-folder
shapes it didn't.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* feat(landing): add /share-out redirect for X share button click tracking
Adds a Cloudflare Pages Function at /share-out/:eventId that records each
click of the "Share on X" button surfaced in the contributor card comments
on GitHub, then 302-redirects to the original twitter.com / x.com intent
URL (passed via ?to=, host-allowlisted).
Together with the existing /share/:eventId function this gives us both
sides of the X funnel without an X API key:
- /share-out/:eventId -> GitHub user clicked the X button (funnel step 1)
- /share/:eventId -> someone on X clicked the posted tweet (funnel step 2)
Per-event KV storage is optional (SHARE_OUT_CLICK_EVENTS). When no KV is
bound the function falls back to console.log; aggregate counts are visible
in Cloudflare Pages analytics with no extra setup.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: retrigger CI
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
The BYOK chat path streams model text directly through the provider API and
does not run the agent-runtime scaffolding that wires up Read/Write/Edit
tools — so a model that "decides" to edit a file just emits HTML or code
back into the chat. When users switch from Local CLI (Claude Code) to
BYOK with an Anthropic key and then ask the agent to keep adjusting a
design, nothing on disk changes and the failure mode is silent.
Until the BYOK tool loop is implemented (#313 / #699 / #719), surface a
clear notice in the BYOK panel of Settings that explains the limitation
and points users at Local CLI mode for file edits.
Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
Co-authored-by: libertecode <libertecode@proton.me>
Adds a new `--to zip` (and `--to all`) tools-pack Windows build target that
produces a portable `.zip` from the cached `win-unpacked` tree using the
bundled 7z. The zip lays files at the archive root so users can extract it
anywhere and launch `Open Design.exe` without going through the NSIS
installer, addressing the no-install download request.
Release plumbing is updated to publish the portable zip and its sha256
beside the existing installer on R2 for beta, preview, and stable channels
(default on, gated by `WINDOWS_INCLUDE_ZIP`/`WIN_INCLUDE_ZIP`). The
electron-updater `latest.yml` feed continues to point only at the
installer; the zip is a manual-download convenience and is intentionally
excluded from the in-app updater.
Closes#1121
Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
Co-authored-by: libertecode <libertecode@proton.me>
AgentIcon fell back to an initial-letter pill for Aider and Trae CLI
because neither id was registered in ICON_EXT and no asset shipped. Add
the bundled brand marks so both agents render their real logo:
- aider.png — Aider's published avatar, downscaled to 96px (~2.6KB).
- trae-cli.png — Trae's app icon, downscaled to 96px (~2.3KB). Keyed on
the `trae-cli` runtime id so the file and ICON_EXT entry match exactly.
Both vendors only publish rasterised marks, so they follow the existing
PNG-fallback path used for Devin.
Pi shipped a stale single-glyph silhouette in MONO_ICONS. Replace it
with the current dark-tile mark (white glyph on #09090b) and drop it
from MONO_ICONS — the tile has baked colors, so CSS-mask rendering with
currentColor would flatten it to a solid square. It now renders through
<img> like the other color-baked brands.
Adds AgentIcon test coverage for all three.