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
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: monthly
groups:
astro:
patterns: ["astro", "@astrojs/*"]
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
branches: [dev]
pull_request:
branches: [dev, main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
quality:
name: Build and verify
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify production promotion source
if: github.base_ref == 'main' && github.head_ref != 'dev'
run: |
echo "::error::Only the dev branch may be promoted to main."
exit 1
- name: Check formatting
run: npm run format:check
- name: Type-check and test production build
run: npm run check
- name: Audit performance, accessibility and SEO
uses: treosh/lighthouse-ci-action@v12
with:
configPath: ./lighthouserc.json
uploadArtifacts: true
57 changes: 57 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Deploy to Cloudflare Pages

on:
push:
branches: [dev, main]
workflow_dispatch:

permissions:
contents: read
deployments: write

concurrency:
group: cloudflare-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
deploy:
name: Verify and deploy
runs-on: ubuntu-latest
timeout-minutes: 15
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify production build
env:
CLOUDFLARE_WEB_ANALYTICS_TOKEN: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_TOKEN }}
run: npm run check
- name: Record deployment identity
run: node scripts/write-deployment-metadata.mjs dist "$GITHUB_SHA" "$GITHUB_REF_NAME"
- name: Deploy branch to Cloudflare Pages
id: deploy
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "4"
command: pages deploy dist --project-name=${{ vars.CLOUDFLARE_PAGES_PROJECT || 'ldsa-website' }} --branch=${{ github.ref_name }} --commit-hash=${{ github.sha }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Verify deployed site
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
DEPLOYMENT_ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
EXPECTED_COMMIT: ${{ github.sha }}
EXPECTED_BRANCH: ${{ github.ref_name }}
EXPECT_NOINDEX: ${{ github.ref != 'refs/heads/main' }}
EXPECT_ANALYTICS: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_TOKEN != '' }}
CF_ACCESS_CLIENT_ID: ${{ secrets.CF_ACCESS_CLIENT_ID }}
CF_ACCESS_CLIENT_SECRET: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}
run: npm run check:deployment
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,9 @@ vite.config.ts.timestamp-*

# Turbo Repo
.turbo

# Cloudflare local state and platform files
.wrangler/

# OS metadata
.DS_Store
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist
node_modules
migration/raw
package-lock.json
public/images
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Contributing

Thank you for helping maintain the LDSA website.

1. Branch from `dev` using a name such as `feature/update-prep-course`.
2. Keep time-sensitive operational details in the LDSA wiki and link to them from the site.
3. Run `npm ci`, `npm run check` and `npm run format:check`.
4. Open a pull request into `dev` and explain the visible change and how it was checked.
5. Test the merged result on the protected Cloudflare `dev` deployment.
6. Promote tested changes with a pull request from `dev` to `main`.

Do not put subscriber exports, application data, API tokens or other secrets in the repository.

When changing a Flodesk form, preserve the form ID, field names and preference values unless the matching Flodesk configuration has also changed. Complete the account-level submission checks in `docs/operations/qa.md` before production promotion.
91 changes: 89 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,89 @@
# website
LDSA website
# LDSA Website

The official website of the [Lisbon Data Science Academy](https://www.lisbondatascience.org/). It is a static [Astro](https://astro.build/) site designed for Cloudflare Pages and maintained through the `feature/* → dev → main` workflow.

The hosting and delivery architecture uses free tooling and service tiers. Domain registration and renewal are the only expected website infrastructure cost.

- Production: `https://www.lisbondatascience.org/` (kept on Unbounce until the migration acceptance tests pass)
- Candidate production: `https://ldsa-website.pages.dev/`
- Integration preview: `https://dev.ldsa-website.pages.dev/`

## Local development

Use Node.js 24 or newer:

```bash
npm ci
npm run dev
```

The local server prints its URL, normally `http://localhost:4321/`.

Before opening a pull request, run:

```bash
npm run check
npm run format:check
```

`npm run check` type-checks the Astro source, builds the production site, validates internal links and local assets, rejects Unbounce dependencies, verifies the three preserved Flodesk endpoints, and confirms Cloudflare deployment files and the sitemap are present. After each Cloudflare deployment, GitHub Actions also checks the live routes, redirects, 404 page, security and indexing headers, sitemap, and deployed commit identity.

## Branches and deployment

- `feature/*`: one focused change; open a pull request into `dev`.
- `dev`: integration branch and protected Cloudflare preview.
- `main`: production branch. Merging here publishes the production build.

Pull requests into `dev` or `main` run formatting, type checks, a clean production build, site contract checks and Lighthouse audits. Feature branches never deploy to Cloudflare. A push to `dev` deploys the private development preview, while a push to `main` deploys production through the Cloudflare Wrangler GitHub Action.

The deployment workflow requires:

- GitHub secret `CLOUDFLARE_API_TOKEN`, scoped to edit Cloudflare Pages.
- GitHub secret `CLOUDFLARE_ACCOUNT_ID`.
- Optional GitHub variable `CLOUDFLARE_PAGES_PROJECT`; it defaults to `ldsa-website`.
- Production-environment variable `CLOUDFLARE_WEB_ANALYTICS_TOKEN` after the final public hostname is registered in Cloudflare Web Analytics. Preview builds deliberately omit it.

See [the Cloudflare runbook](docs/operations/cloudflare.md) for project creation, preview access, analytics and domain cutover.

## Repository structure

```text
src/
├── components/ shared navigation, footer, FAQ and Flodesk forms
├── data/ navigation, links and migrated content data
├── layouts/ metadata and global page shell
├── pages/ public routes
└── styles/ design system and responsive styles
public/ assets and Cloudflare Pages configuration
scripts/ build-time contract checks
migration/
├── raw/ public rendered-source snapshot from Unbounce
├── scripts/ migration audit helpers
└── *.md public-evidence audit, inventories and decisions
docs/operations/ deployment, DNS, QA and maintenance runbooks
.github/workflows/ CI and deployment automation
```

## Updating content

Most page copy lives in `src/pages`. Shared links, FAQs and the preserved volunteer roster live in `src/data/site.ts`. Keep time-sensitive dates in the LDSA wiki where possible and link to that source of truth instead of duplicating values that will become stale.

Images in `public/images` are locally hosted copies of LDSA assets recovered from the public Unbounce site. Add descriptive `alt` text for meaningful images and use an empty `alt` only for decorative images.

## Flodesk forms

`src/components/FlodeskForm.astro` preserves the three production Flodesk form IDs and submission endpoints:

- General interest: `63f9e4e2f0116a4cabfef7db`
- Starters Academy: `615c9641c71e8685f183bd18`
- Prep Course: `6172e62fe625581864e3f857`

The homepage also preserves the `option1` and `option2` interest values used by the current form. Do not replace these IDs or option values without updating the form in Flodesk and completing the end-to-end tests in [the QA runbook](docs/operations/qa.md).

## DNS and production safety

The selected cutover keeps authoritative DNS at Namecheap: `www` will become a CNAME to Pages and the apex will use Namecheap's permanent URL redirect to `www`. Email-related MX and TXT records remain untouched. The captured baseline, exact cutover and rollback sequence are in [the DNS runbook](docs/operations/dns.md).

## Contributing

Create a branch from `dev`, make a focused change, run the local checks, then open a pull request into `dev`. After the combined changes pass on the protected Cloudflare preview, open a pull request from `dev` into `main`.
12 changes: 12 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";

export default defineConfig({
site: "https://www.lisbondatascience.org",
output: "static",
trailingSlash: "always",
integrations: [sitemap()],
build: {
format: "directory",
},
});
49 changes: 49 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Website architecture

## System shape

```text
GitHub repository
├─ feature/* pull requests ── CI ──> dev
├─ dev ── verified GitHub Action ──> protected Cloudflare preview
└─ main ── verified GitHub Action ──> Cloudflare production
│
├─ static HTML, CSS and images
├─ Flodesk form submissions
├─ LDSA Portal and Wiki links
└─ privacy-enhanced YouTube embed
```

The site is generated at build time by Astro and deployed as static files. There is no application server, database, runtime JavaScript framework or server-side session. Cloudflare provides TLS, CDN caching, preview deployments, Access control, deployment history and production Web Analytics.

All migration infrastructure uses free services and open-source tooling: Astro, Node.js, npm, GitHub Actions within the repository's included allowance, Cloudflare Pages, Cloudflare Web Analytics, and Cloudflare Access Free for the small LDSA testing group. The domain registration/renewal remains the only unavoidable website infrastructure cost. Do not add a paid hosting, build, monitoring, analytics, testing, or CDN dependency without an explicit architecture decision.

## Why Astro

Astro provides reusable components and typed content without shipping a client framework to visitors. Header, footer, metadata, FAQs and Flodesk markup have one maintained implementation. Pages remain ordinary semantic HTML in the build output, which keeps performance high and reduces the maintenance surface for volunteers.

## Why there is no Docker image

The deployable artifact is the deterministic `dist` directory. GitHub-hosted runners and Cloudflare Pages only need the locked Node.js toolchain to create it. A Docker image would add another dependency and patching surface without improving runtime parity because there is no container at runtime. The `.nvmrc`, `engines` field and `package-lock.json` already pin the relevant build environment.

## Content ownership

- Stable marketing and explanatory content lives in version-controlled Astro pages.
- Shared navigation, official links, FAQs and roster data live in `src/data/site.ts`.
- Batch dates, admissions rules and other frequently changing details live in the LDSA wiki and are linked from the website.
- Subscriber records and email automations remain in Flodesk.
- Applications remain in the LDSA Portal.

This separation prevents the website from becoming a second, stale source of truth for operational course details.

## Integration boundaries

Flodesk form identifiers, endpoints, preference values, configuration and runtime bundles are treated as public integration contracts and covered by the production-build test. A successful browser submission still requires Flodesk account verification because only Flodesk can prove which subscriber group and automation received the address.

External destinations use normal links. The YouTube video uses the privacy-enhanced domain and lazy loading. Cloudflare's project-wide automatic analytics injection is disabled because it would mix private preview traffic into production data. The layout emits the free Web Analytics beacon only when the production GitHub environment supplies `CLOUDFLARE_WEB_ANALYTICS_TOKEN`; the build adds the matching CSP origins at the same time. Preview and ordinary local builds contain neither the beacon nor its CSP permissions.

## Security and privacy

Cloudflare `_headers` set a restrictive Content Security Policy, disable framing, narrow browser capabilities and prevent MIME sniffing. The policy allows only the Flodesk and YouTube origins needed by current features. Deployment credentials live in GitHub Secrets and are limited to Pages edit access. Preview access will be enforced by Cloudflare Access after the dashboard policy and CI service token in the Cloudflare runbook are configured.

The repository must never contain Unbounce lead exports, subscriber lists, Cloudflare tokens or other personal/secret data.
Loading
Loading