Astro + Starlight site for shinylib.net, deployed to GitHub Pages.
.
├── .github/workflows/deploy.yml CI: builds the site and deploys to Pages
├── astro.config.mjs Astro + Starlight config, redirects
├── package.json
├── public/ Static assets copied verbatim into dist/
│ └── playground/index.html Landing page linking out to the six
│ per-library demo deployments
└── src/
├── assets/
├── components/ Custom Starlight component overrides
├── content/docs/ Documentation pages (.md / .mdx)
├── sidebar-topics.mjs Shared sidebar topic config
└── styles/
| Command | Action |
|---|---|
npm install |
Install dependencies |
npm run dev |
Start the Astro dev server |
npm run build |
Build the production site to dist/ |
npm run preview |
Preview the built site locally |
npm run astro … |
Run Astro CLI commands directly |
Each library demo lives in its own sibling repo and deploys to its own GitHub Pages site. This docs repo links out to them — it does not build or host them.
| Library | URL | Source repo |
|---|---|---|
| AI Conversation | https://shinyorg.github.io/speech/ | shinyorg/speech |
| Controls | https://shinyorg.github.io/controls/ | shinyorg/controls |
| DocumentDb | https://shinyorg.github.io/DocumentDb/ | shinyorg/DocumentDb |
| Mediator | https://shinyorg.github.io/mediator/ | shinyorg/mediator |
| Shiny Core | https://shinyorg.github.io/shiny/ | shinyorg/shiny |
| Speech | https://shinyorg.github.io/speech/ | shinyorg/speech |
public/playground/index.html is a static landing page on this site that cards out to those URLs — kept so old /playground/ bookmarks still resolve. Each library's sidebar in Starlight also has a direct "Blazor Playground" link to the corresponding external URL.
Each demo is deployed by its own .github/workflows/deploy-blazor-sample.yml in the sibling repo: triggers on push to that repo's active branch (paths-scoped to the sample + relevant src), publishes the WASM app, rewrites <base href> to /<repo-name>/, and uploads to GitHub Pages.
Blog posts get a giscus comment widget rendered after the article body. Any docs page can opt in by adding comments: true to its frontmatter.
The widget is wired through three pieces:
giscusConfiginastro.config.mjs— repo, repo-id, category, category-id, etc., exposed to components viaimport.meta.env.GISCUS.src/components/Giscus.astro— loadsgiscus.app/client.jsand syncs the iframe theme with Starlight's light/dark toggle.src/components/MarkdownContent.astro— Starlight override that injects<Giscus />after page content when the entry lives underblog/or hascomments: true.
- Install the giscus GitHub App on the
shinyorg/documentationrepo: https://github.com/apps/giscus. Grant it access to that repo only. - Enable Discussions on the repo: Settings → General → Features → check Discussions.
- Create (or pick) a Discussion category for comments. The default
Announcementsworks, but a dedicatedCommentscategory with the Announcement format is recommended so only maintainers can start threads (giscus creates them on demand). - Visit https://giscus.app, fill in:
- Repository:
shinyorg/documentation - Page ↔ Discussions Mapping:
pathname(matches the default inastro.config.mjs) - Discussion Category: the one created above
- Repository:
- Scroll to the Enable giscus snippet at the bottom of giscus.app and copy the four
data-*values:data-repo-id→giscusConfig.repoIddata-category→giscusConfig.categorydata-category-id→giscusConfig.categoryId- (
data-reposhould already match)
- Paste them into
giscusConfiginastro.config.mjs, replacing theREPLACE_WITH_*placeholders. npm run build && npm run preview, open any blog post, and confirm the widget loads.
Add comments: true to the page's frontmatter:
---
title: My page
comments: true
---The comments field is declared on the docs schema in src/content.config.ts, so unknown-field warnings won't fire.
.github/workflows/deploy.yml:
actions/checkout@v6withastro/action@v6— runsnpm ci && npm run buildand uploadsdist/as the Pages artifactactions/deploy-pages@v4— pushes the artifact to GitHub Pages
Builds run on every push to main and dev; only main deploys.
The site is served directly by GitHub Pages. Putting Cloudflare's free plan in front of it adds a CDN edge, per-request analytics, bot controls, rate limiting, and a WAF — none of which GitHub Pages provides on its own.
| Record | Name | Value |
|---|---|---|
A |
shinylib.net |
185.199.108.153, 185.199.109.153, 185.199.110.153, 185.199.111.153 |
CNAME |
www |
shinyorg.github.io |
Nameservers are currently Google Cloud DNS (ns-cloud-e{1..4}.googledomains.com).
-
Add the site. Cloudflare dashboard → Add a site →
shinylib.net→ Free plan. -
Check the imported records. Cloudflare scans existing DNS on import. Confirm all four apex
Arecords and thewwwCNAMEcame across exactly as above, and that anyTXT(domain verification) andMX(mail) records survived. Import misses records more often than you'd expect — compare againstdigoutput before continuing:dig +short shinylib.net A dig +short www.shinylib.net dig +short shinylib.net TXT
-
Leave the proxy OFF for now. Set the apex and
wwwrecords to DNS only (grey cloud) for the initial cutover. See step 5 for why. -
Move the nameservers. In Google Cloud DNS, replace the four
googledomains.comnameservers with the two Cloudflare assigns. Propagation is usually well under an hour; Cloudflare emails you when the zone goes active. -
Let GitHub issue the TLS certificate, then enable the proxy. GitHub Pages provisions its Let's Encrypt cert by reaching the domain directly, which fails while Cloudflare is proxying. So: with the records still grey-clouded, go to the repo's Settings → Pages, confirm the custom domain is
shinylib.netand wait for Enforce HTTPS to become available and checked. Once it is, flip the apex andwwwrecords to the orange cloud (Proxied).Skipping this ordering is the most common way to end up with a broken cert.
-
SSL/TLS → Overview → Full.
- Flexible causes an infinite redirect loop — GitHub Pages already redirects HTTP to HTTPS.
- Full (strict) fails validation — the GitHub Pages origin cert doesn't match the custom domain.
- Full is the correct setting.
-
SSL/TLS → Edge Certificates. Enable Always Use HTTPS and Automatic HTTPS Rewrites.
Once the zone is active and proxying:
Security → Bots
- Bot Fight Mode → on.
- Block AI Scrapers and Crawlers → on, if you want to exclude AI training crawlers at
the edge. Note this is broader than
public/robots.txt, which deliberately allows several AI assistants so/llms.txtand/llms-full.txtremain reachable. Turning this on overrides that intent.
Security → WAF → Rate limiting rules
Free plan allows one rule. A reasonable starting point:
| Field | Value |
|---|---|
| Requests | 200 |
| Period | 1 minute |
| Counting by | IP |
| Action | Managed Challenge |
| Duration | 10 seconds |
Start with Managed Challenge rather than Block — a hard block catches real users sharing an IP behind corporate or carrier NAT. Tune the threshold against Cloudflare's own traffic data rather than guessing.
Caching → Configuration
- Browser Cache TTL → Respect Existing Headers.
- The site is fully static, so the default Cloudflare cache behaviour (static assets cached, HTML passed through) works without further configuration.
Speed → Optimization — leave Auto Minify off. Astro already minifies at build time, and double-minification occasionally breaks inline scripts.
Cloudflare does not cache HTML by default, so deploys are visible immediately and no cache
purge is needed. If you later add a Cache Everything page rule, add a purge step to
.github/workflows/deploy.yml or content will go stale.
Once proxying, Analytics → Traffic gives per-ASN, per-user-agent, and per-country request breakdowns — data Google Analytics does not expose, since it only counts requests that execute JavaScript.
This covers shinylib.net only. The Blazor playground demos are served from separate
GitHub Pages sites on the shinyorg/* repos (shinyorg.github.io/{speech,controls,DocumentDb,mediator,shiny})
and are unaffected by anything configured here. Each would need its own Cloudflare zone.
GitHub Pages' soft bandwidth limit is 100 GB/month per site.