The source for samizdata.co: a bilingual editorial site, studio portfolio, services catalogue, and English-language journalism training resource. It is a static Astro site with no client-side UI framework.
- Node.js 22.12 or newer
- npm 10 or newer (the committed lockfile is lockfile version 3)
Install the exact locked dependencies:
npm ciStart Astro's background development server:
npm run astro -- dev --background
npm run astro -- dev status
npm run astro -- dev logs
npm run astro -- dev stopThe default local URL is http://localhost:4321.
| Command | Purpose |
|---|---|
npm run check |
Run strict Astro and TypeScript checks; warnings fail the command. |
npm run build |
Check and build the static site into dist/. |
npm run test:dist |
Validate routes, links, metadata, feeds, assets, and budgets in an existing dist/. |
npm run verify |
Run the complete local quality gate: check, build once, then validate dist/. |
npm run preview |
Serve the production build locally. |
npm run smoke -- https://example.test |
Check a deployed site's key routes and GitHub Pages behavior. |
Run npm run verify after every change. The deployment smoke check is deliberately separate because slash normalization, the custom 404, and static redirect serving are hosting behavior rather than build behavior.
src/pages/defines thin route entry points.src/components/contains shared Astro components; localized service markup is shared undersrc/components/services/.src/layouts/Base.astroandsrc/components/BaseHead.astroown the typed page and SEO metadata contract.src/content/blog/{en,ro}/contains editorial Markdown/MDX.src/content/training/contains English-only training Markdown/MDX.src/lib/blog.ts,src/lib/training.ts, andsrc/lib/services.tsderive lists and routes from content and portfolio data.src/portfolio/cv.jsonis the source of truth for studio work, skills, services, publications, and projects.src/styles/global.cssowns design tokens and global prose styles; feature styles remain plain, scoped CSS.- The shared layout loads the existing privacy-friendly Simple Analytics endpoint at
analytics.samizdata.co.
The public route contract is:
| Area | English | Romanian |
|---|---|---|
| Studio | / |
/ro/ |
| Editorial index | /blog/ |
/ro/blog/ |
| Stories | /story/{slug}/ |
/ro/story/{slug}/ |
| Services | /services/ |
/ro/services/ |
| Contact | /contact/ |
/ro/contact/ |
| Feed | /rss.xml |
/ro/rss.xml |
| Training | /training/ |
English only |
HTML routes always use trailing slashes. The build uses directory output, and canonical URLs, internal links, feeds, and sitemaps must follow the same policy. The studio occupies the homepage; the editorial index is at /blog/ (and /ro/blog/ in Romanian).
- Add
{slug}.mdor{slug}.mdxundersrc/content/blog/en/orsrc/content/blog/ro/. - Supply the frontmatter defined in
src/content.config.ts:
---
title: A clear title
description: A concise search and social description
pubDate: '2026-08-07T10:00:00Z'
updatedDate: '2026-08-08T10:00:00Z' # optional
heroImage: '../../../assets/posts/example.png' # optional
geography: Eastern Europe # optional; currently not displayed
translationGroup: stable-shared-id # optional; translations only
---- Put post-only images in
src/assets/posts/. Put an image used by both a post and the portfolio insrc/assets/shared/instead of copying it. - Write useful alt text for informative Markdown images (
). Empty alt text is only for genuinely decorative images. - Keep editorial raster sources at or below 1440px wide.
Routes, indexes, RSS, and sitemap entries are generated from the collection. There is no draft field yet: every committed post is published. To connect translations, give both posts the same explicit translationGroup; never set it on an untranslated post.
- Add a Markdown or MDX file under
src/content/training/; nested paths become nested URLs. - Set
title,description,section(ResourcesorLessons), and numericorder. - The page appears at
/training/{content-id}/and in training navigation automatically.
Training is English-only. Romanian links to it must use hreflang="en" and visibly identify the destination language. redirects.mjs derives legacy Quarto-era redirect variants from this content tree, so do not add redirect pages manually.
Services are selected skills from src/portfolio/cv.json. Publications and projects whose skills include the exact skill name become examples on that service page.
To introduce a service, update the skill in cv.json, then add its name to serviceOrder, icon mapping, and Romanian translation in src/lib/services.ts. The slug is derived from the English skill name. Do not create separate locale pages; the existing thin route wrappers render both languages.
Add the publication or project to src/portfolio/cv.json. Use the existing JSON shape, valid dates and URLs, and exact skill names from skills. If it has local media, put the file in src/portfolio/img/ and set img to its filename. Shared blog/portfolio media belongs in src/assets/shared/. GIF and SVG portfolio media are passed through; raster media receives responsive variants.
| Concern | Authoritative file/location |
|---|---|
| Site name, production URL, contact details | src/lib/site.ts |
| Localized interface copy | src/i18n.ts |
| Content schemas | src/content.config.ts |
| Blog and training content | src/content/ |
| Studio, services, publications, projects | src/portfolio/cv.json |
| Service selection and Romanian service copy | src/lib/services.ts |
| Design tokens | src/styles/global.css |
| Legacy Training redirects | redirects.mjs plus src/content/training/ |
| URL/build policy | astro.config.mjs |
| Generated-site invariants and budgets | scripts/check-dist.mjs |
| Analytics integration | src/layouts/Base.astro and src/pages/404.astro |
| GitHub Pages deployment | .github/workflows/deploy.yml |
Use Astro-managed local images where possible. Responsive widths must not exceed the component's declared slot sizes. Current verification budgets are 36 MiB for all of dist/, 1 MiB for any optimized WebP, and one critical font preload. Exact duplicate source assets fail verification.
Preserve one logical h1, heading order, skip links, localized labels, and descriptive iframe titles. Use --color-accent-text for links and small raspberry text; --color-primary-container is for fills and controls. Keep the mobile menu as a native <details> disclosure and respect reduced-motion preferences.
The site is configured for GitHub Pages at the custom domain in public/CNAME. The workflow in .github/workflows/deploy.yml runs on pushes to main and manual dispatches; it uses npm ci, runs the unified verification command once, and deploys dist/ without dropping CNAME.
Before deployment:
npm ci
npm run verifyAfter deploying to a preview or production origin:
npm run smoke -- https://preview.example.com
# alternatively
SMOKE_BASE_URL=https://preview.example.com npm run smokeThe smoke check verifies representative English and Romanian pages and their production canonicals, a post, studio, contact, services, Training, RSS, sitemap compatibility, the custom 404, permanent no-slash-to-slash redirects, and representative legacy Training redirects. Run it against the final GitHub Pages artifact before launch; a local preview cannot prove GitHub Pages normalization behavior.
Static legacy redirects are noindex HTML pages with immediate meta refreshes, not HTTP 301 responses. Directory normalization may happen first. Update redirects.mjs rather than hand-writing redirect pages, and preserve public/CNAME and /sitemap.xml for continuity.