Skip to content
Closed
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
23 changes: 14 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,15 @@ jobs:
- run: npm run website:build
env:
SITE_URL: https://useanimations.com
- name: Deploy to Netlify
run: npx --yes netlify-cli@27 deploy --prod --no-build --dir website/dist --site "$NETLIFY_SITE_ID" --message "$GITHUB_SHA"
# The Worker and its assets, configured in wrangler.jsonc.
- name: Deploy to Cloudflare
run: npx --yes wrangler@4 deploy --message "$GITHUB_SHA"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ vars.NETLIFY_SITE_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

preview:
# Every PR from this repository gets its own preview at pr-<number>--<site>.netlify.app.
# Every PR from this repository gets its own preview at pr-<number>-useanimations.<subdomain>.workers.dev.
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
# The link shows up in the PR as "View deployment".
Expand All @@ -137,15 +138,19 @@ jobs:
# Keep previews out of search engines.
- run: |
printf '\n/*\n X-Robots-Tag: noindex\n' >> website/dist/_headers
- name: Deploy preview to Netlify
# A new version of the Worker that production doesn't serve, under a stable alias per PR.
- name: Deploy preview to Cloudflare
id: deploy
run: |
url=$(npx --yes netlify-cli@27 deploy --no-build --dir website/dist --site "$NETLIFY_SITE_ID" --alias "pr-$PR" --message "PR #$PR $GITHUB_SHA" --json | jq -r '.deploy_url')
out=$(npx --yes wrangler@4 versions upload --preview-alias "pr-$PR" --message "PR #$PR $GITHUB_SHA")
echo "$out"
url=$(grep -oE 'https://pr-[0-9]+-[a-z0-9.-]+\.workers\.dev' <<< "$out" | head -n 1)
test -n "$url"
echo "url=$url" >> "$GITHUB_OUTPUT"
env:
PR: ${{ github.event.pull_request.number }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ vars.NETLIFY_SITE_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# One comment per PR, updated on every push.
- name: Comment preview URL
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ dist
website/dist
website/.astro
website/.netlify
.wrangler
8 changes: 0 additions & 8 deletions netlify.toml

This file was deleted.

38 changes: 0 additions & 38 deletions website/netlify/edge-functions/canonical-url.ts

This file was deleted.

40 changes: 0 additions & 40 deletions website/netlify/edge-functions/markdown-negotiation.ts

This file was deleted.

8 changes: 4 additions & 4 deletions website/public/_headers
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# The same HSTS policy on every response (Netlify's default omits includeSubDomains and preload).
# The same HSTS policy on every response.
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

# The Markdown alternates of pages (Link and Vary: Accept) come from
# netlify/edge-functions/markdown-negotiation.ts.
# website/worker.ts.
/
Link: </robots.txt>; rel="robots"
Link: </licencing-and-terms>; rel="license"
Expand All @@ -23,7 +23,7 @@
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=3600

# The icon files are cached for an hour; the icon pages (/icons/<id>) keep Netlify's HTML caching.
# The icon files are cached for an hour; the icon pages (/icons/<id>) keep the default HTML caching.
/icons/*
Access-Control-Allow-Origin: *

Expand All @@ -33,7 +33,7 @@
/icons/*.zip
Cache-Control: public, max-age=3600

/icons/*/*.svg
/icons/:id/*
Cache-Control: public, max-age=3600

/r/*
Expand Down
27 changes: 12 additions & 15 deletions website/public/_redirects
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# Pages of the previous site (www and http are redirected by Netlify). Trailing slashes, .html
# pages and the aliases below are redirected first by netlify/edge-functions/canonical-url.ts;
# these rules are the fallback.
/index.html / 301!
/documentation.html /documentation 301!
/licencing-and-terms.html /licencing-and-terms 301!
/zh.html /zh 301!

# The all-icons index: icons/ is also the folder of the icon files, so serve icons.html explicitly.
/icons /icons.html 200!
# Pages of the previous site (www redirects with a Redirect Rule on the zone). Trailing slashes,
# .html pages and the aliases below are redirected first by website/worker.ts; these rules are
# the fallback.
/index.html / 301
/documentation.html /documentation 301
/licencing-and-terms.html /licencing-and-terms 301
/zh.html /zh 301

# Other spellings of pages.
/docs /documentation 301!
/license /licencing-and-terms 301!
/licence /licencing-and-terms 301!
/licensing /licencing-and-terms 301!
/licensing-and-terms /licencing-and-terms 301!
/docs /documentation 301
/license /licencing-and-terms 301
/licence /licencing-and-terms 301
/licensing /licencing-and-terms 301
/licensing-and-terms /licencing-and-terms 301

# Images of the previous site.
/images/OGuseAnimationsImage.jpg /og.png 301
Expand Down
73 changes: 73 additions & 0 deletions website/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// The Cloudflare Worker in front of useanimations.com's static files (website/dist). It runs only
// for the paths in run_worker_first (wrangler.jsonc); every other request is served straight from
// the assets, with public/_headers and public/_redirects applied.
type Env = { ASSETS: { fetch: (request: Request | URL) => Promise<Response> } };

// One 301 to the canonical URL for the variants static hosting would otherwise serve as copies:
// a trailing slash (/zh/, /icons/menu/), the .html file (/icons/menu.html) and old or misspelled
// paths (/docs, /licensing).
const ALIASES: Record<string, string> = {
'/docs': '/documentation',
'/license': '/licencing-and-terms',
'/licence': '/licencing-and-terms',
'/licensing': '/licencing-and-terms',
'/licensing-and-terms': '/licencing-and-terms',
};

/** The canonical path of a pathname, e.g. /icons/menu.html/ -> /icons/menu. */
const canonicalPath = (pathname: string) => {
let path = pathname.replace(/\/+$/, '') || '/';
if (path.endsWith('.html') && path !== '/404.html') path = path.slice(0, -'.html'.length);
path = path.replace(/\/index$/, '') || '/';
return ALIASES[path] ?? path;
};

// Pages with a Markdown version; files such as /icons/menu.json, .zip or .svg have none.
const MARKDOWN_PAGES =
/^\/(|documentation|licencing-and-terms|icons\/[a-z0-9-]+|categories\/[a-z0-9-]+)$/;

/** The Markdown file of a page: / -> /index.md, /icons/menu -> /icons/menu.md. */
const markdownPath = (pathname: string) => (pathname === '/' ? '/index.md' : `${pathname}.md`);

/**
* Serves the Markdown version of a page to clients that ask for it (Accept: text/markdown), such
* as AI agents. Everyone else gets the HTML page, which announces the Markdown version in a Link
* header. Both vary by Accept, so caches keep them apart.
*/
const negotiate = async (request: Request, env: Env, url: URL) => {
const markdown = markdownPath(url.pathname);
if (request.headers.get('accept')?.includes('text/markdown')) {
const response = await env.ASSETS.fetch(new URL(markdown, url));
if (response.ok) {
return new Response(response.body, {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
Vary: 'Accept',
},
});
}
}
const response = await env.ASSETS.fetch(request);
// Only pages that exist have a Markdown version; a 404 stays as it is.
if (!response.ok) return response;
const headers = new Headers(response.headers);
headers.append('Link', `<${markdown}>; rel="alternate"; type="text/markdown"`);
headers.append('Vary', 'Accept');
return new Response(response.body, { status: response.status, headers });
};

export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
const path = canonicalPath(url.pathname);
if (path !== url.pathname && url.pathname !== '/404.html') {
return new Response(null, {
status: 301,
headers: { Location: `${path}${url.search}`, 'Cache-Control': 'public, max-age=3600' },
});
}
if (MARKDOWN_PAGES.test(url.pathname)) return negotiate(request, env, url);
return env.ASSETS.fetch(request);
},
};
39 changes: 39 additions & 0 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// useanimations.com: the Astro site in website/, built from the icon set in icons/ with
// `npm run website:build`, then deployed by CI (.github/workflows/ci.yml).
{
"name": "useanimations",
"main": "website/worker.ts",
"compatibility_date": "2026-10-01",
"assets": {
"directory": "website/dist",
"binding": "ASSETS",
// Pages are written as documentation.html and served at /documentation.
"html_handling": "drop-trailing-slash",
"not_found_handling": "404-page",
// The Worker handles only the pages and the URL variants it redirects; icon files, scripts
// and the rest come straight from the assets.
"run_worker_first": [
"/",
"/documentation",
"/licencing-and-terms",
"/docs",
"/license",
"/licence",
"/licensing",
"/licensing-and-terms",
"/icons/*",
"/categories/*",
"/*/",
"/*.html",
"!/icons/*.json",
"!/icons/*.zip",
"!/icons/*.svg",
"!/icons/*.md",
],
},
// www and react.useanimations.com redirect here with a Redirect Rule on the zone.
"routes": [{ "pattern": "useanimations.com", "custom_domain": true }],
"workers_dev": true,
"preview_urls": true,
"observability": { "enabled": true },
}
Loading