Repository: remoteintech/remote-jobs Branch: main Commit: bf76c82f4ba6 Files: 1039 Total size: 1.4 MB Directory structure: gitextract_lztpi08t/ ├── .eleventyignore ├── .env-sample ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── new_company.md │ ├── PULL_REQUEST_TEMPLATE.MD │ ├── dependabot.yml │ ├── scripts/ │ │ └── validate-companies.js │ └── workflows/ │ ├── ci.yml │ ├── codeql-analysis.yml │ └── validate-companies.yml ├── .gitignore ├── .nojekyll ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CLAUDE.md ├── LICENSE ├── README.md ├── eleventy.config.js ├── package.json ├── src/ │ ├── _config/ │ │ ├── collections.js │ │ ├── events/ │ │ │ ├── build-css.js │ │ │ ├── build-js.js │ │ │ └── svg-to-jpeg.js │ │ ├── events.js │ │ ├── filters/ │ │ │ ├── dates.js │ │ │ ├── fileExists.js │ │ │ ├── markdown-format.js │ │ │ ├── slugify.js │ │ │ ├── sort-alphabetic.js │ │ │ ├── sort-random.js │ │ │ ├── split.js │ │ │ ├── splitlines.js │ │ │ └── striptags.js │ │ ├── filters.js │ │ ├── plugins/ │ │ │ ├── drafts.js │ │ │ ├── html-config.js │ │ │ └── markdown.js │ │ ├── plugins.js │ │ ├── setup/ │ │ │ ├── create-colors.js │ │ │ └── generate-favicons.js │ │ ├── shortcodes/ │ │ │ ├── image.js │ │ │ └── svg.js │ │ ├── shortcodes.js │ │ └── utils/ │ │ ├── clamp-generator.js │ │ └── tokens-to-tailwind.js │ ├── _data/ │ │ ├── changelog.json │ │ ├── companyHelpers.js │ │ ├── designTokens/ │ │ │ ├── borderRadius.json │ │ │ ├── colors.json │ │ │ ├── colorsBase.json │ │ │ ├── fonts.json │ │ │ ├── spacing.json │ │ │ ├── textLeading.json │ │ │ ├── textSizes.json │ │ │ ├── textWeights.json │ │ │ └── viewports.json │ │ ├── github.js │ │ ├── helpers.js │ │ ├── labels.js │ │ ├── meta.js │ │ ├── navigation.js │ │ └── personal.js │ ├── _includes/ │ │ ├── head/ │ │ │ ├── css-inline.njk │ │ │ ├── js-defer.njk │ │ │ ├── js-inline.njk │ │ │ ├── meta-info.njk │ │ │ ├── preloads.njk │ │ │ └── schema.njk │ │ ├── partials/ │ │ │ ├── card-tag.njk │ │ │ ├── company-card.njk │ │ │ ├── date.njk │ │ │ ├── details.njk │ │ │ ├── edit-on.njk │ │ │ ├── footer.njk │ │ │ ├── header.njk │ │ │ ├── main-nav.njk │ │ │ ├── nav-search.njk │ │ │ ├── pagination.njk │ │ │ └── theme-switch.njk │ │ ├── schemas/ │ │ │ ├── BlogPosting.njk │ │ │ ├── BreadcrumbList.njk │ │ │ ├── Organization.njk │ │ │ └── WebSite.njk │ │ └── webc/ │ │ ├── custom-card.webc │ │ ├── custom-masonry.webc │ │ ├── custom-peertube-link.webc │ │ ├── custom-peertube.webc │ │ ├── custom-svg.webc │ │ ├── custom-youtube-link.webc │ │ └── custom-youtube.webc │ ├── _layouts/ │ │ ├── base.njk │ │ ├── company.njk │ │ ├── page.njk │ │ ├── post.njk │ │ └── tags.njk │ ├── assets/ │ │ ├── css/ │ │ │ ├── global/ │ │ │ │ ├── base/ │ │ │ │ │ ├── fonts.css │ │ │ │ │ ├── global-styles.css │ │ │ │ │ ├── reset.css │ │ │ │ │ └── variables.css │ │ │ │ ├── blocks/ │ │ │ │ │ ├── button.css │ │ │ │ │ ├── code.css │ │ │ │ │ ├── company-card.css │ │ │ │ │ ├── external-link.css │ │ │ │ │ ├── main-nav.css │ │ │ │ │ ├── prose.css │ │ │ │ │ ├── section.css │ │ │ │ │ ├── seperator.css │ │ │ │ │ ├── site-footer.css │ │ │ │ │ ├── site-logo.css │ │ │ │ │ ├── skip-link.css │ │ │ │ │ ├── textgradient.css │ │ │ │ │ └── theme-switch.css │ │ │ │ ├── compositions/ │ │ │ │ │ ├── cluster.css │ │ │ │ │ ├── flow.css │ │ │ │ │ ├── grid.css │ │ │ │ │ ├── repel.css │ │ │ │ │ ├── sidebar.css │ │ │ │ │ └── wrapper.css │ │ │ │ ├── global.css │ │ │ │ ├── tests/ │ │ │ │ │ ├── debug.css │ │ │ │ │ └── is-land.css │ │ │ │ └── utilities/ │ │ │ │ ├── grayscale.css │ │ │ │ ├── heading-line.css │ │ │ │ ├── ontop.css │ │ │ │ ├── region.css │ │ │ │ ├── spin.css │ │ │ │ └── visually-hidden.css │ │ │ └── local/ │ │ │ ├── custom-card.css │ │ │ ├── details.css │ │ │ ├── footnotes.css │ │ │ ├── forms.css │ │ │ ├── gallery.css │ │ │ ├── nav-main-drawer-cls.css │ │ │ ├── pagination.css │ │ │ ├── post.css │ │ │ ├── styleguide.css │ │ │ └── table.css │ │ └── scripts/ │ │ ├── bundle/ │ │ │ ├── details.js │ │ │ ├── dialog.js │ │ │ ├── is-land.js │ │ │ ├── nav-drawer.js │ │ │ ├── nav-sub.js │ │ │ └── theme-toggle.js │ │ ├── components/ │ │ │ └── custom-masonry.js │ │ └── outbound-tracking.js │ ├── blog/ │ │ ├── 2017-10-03-announcing-remote-in-tech-company.md │ │ ├── 2017-10-09-so-is-this-just-another-job-board.md │ │ ├── 2017-12-06-popular-company-profiles-and-the-github-traffic-charts.md │ │ ├── 2017-12-28-highest-view-count.md │ │ ├── 2018-01-01-new-year-new-job.md │ │ ├── 2018-01-06-advice-from-a-constant-job-seeker.md │ │ ├── 2018-09-19-website-updates.md │ │ ├── 2018-09-25-hacktoberfest-is-back.md │ │ ├── 2026-01-13-the-big-redesign.md │ │ ├── 2026-01-17-seo-improvements.md │ │ ├── 2026-02-15-keeping-things-tidy.md │ │ └── blog.json │ ├── common/ │ │ ├── 404.njk │ │ ├── _redirects.njk │ │ ├── carbon.njk │ │ ├── feed-atom.njk │ │ ├── feed-json.njk │ │ ├── humans.njk │ │ ├── og-images.njk │ │ ├── robots.njk │ │ ├── site-manifest.njk │ │ ├── sitemap.njk │ │ ├── tagList.njk │ │ └── tags.njk │ ├── companies/ │ │ ├── 10up.md │ │ ├── 15five.md │ │ ├── 1password.md │ │ ├── 37signals.md │ │ ├── 3blocks.md │ │ ├── 42-technologies.md │ │ ├── 90-seconds.md │ │ ├── a-1-auto-transport.md │ │ ├── abiturma.md │ │ ├── ably.md │ │ ├── abstract.md │ │ ├── acquia.md │ │ ├── activecampaign.md │ │ ├── ad-hoc.md │ │ ├── adaface.md │ │ ├── addstructure.md │ │ ├── adeva.md │ │ ├── adzuna.md │ │ ├── aerolab.md │ │ ├── aerostrat.md │ │ ├── aestudio.md │ │ ├── aha.md │ │ ├── aiir.md │ │ ├── aim-india.md │ │ ├── airbyte.md │ │ ├── airgarage.md │ │ ├── airtreks.md │ │ ├── aivitex.md │ │ ├── akamai.md │ │ ├── akka.md │ │ ├── alami.md │ │ ├── alan.md │ │ ├── algorand.md │ │ ├── algorithmia.md │ │ ├── alice.md │ │ ├── alight-solutions.md │ │ ├── alley.md │ │ ├── allydvm.md │ │ ├── alphasights.md │ │ ├── amazon.md │ │ ├── ambaum.md │ │ ├── andela.md │ │ ├── animalz.md │ │ ├── annertech.md │ │ ├── anomali.md │ │ ├── apartment-therapy.md │ │ ├── appcues.md │ │ ├── appen.md │ │ ├── appinio.md │ │ ├── applaudo.md │ │ ├── applied-ai-company-aaico.md │ │ ├── appstractor.md │ │ ├── appwrite.md │ │ ├── argyle.md │ │ ├── arkency.md │ │ ├── art-and-logic.md │ │ ├── artefactual.md │ │ ├── articulate.md │ │ ├── asana.md │ │ ├── astronomer.md │ │ ├── atento.md │ │ ├── atlassian.md │ │ ├── atozdebug.md │ │ ├── audiense.md │ │ ├── aula.md │ │ ├── auth0.md │ │ ├── automattic.md │ │ ├── axelerant.md │ │ ├── axios.md │ │ ├── bairesdev.md │ │ ├── balena.md │ │ ├── balsamiq.md │ │ ├── bandcamp.md │ │ ├── bandlab.md │ │ ├── bandzoogle.md │ │ ├── baremetrics.md │ │ ├── basecamp.md │ │ ├── bear-group.md │ │ ├── bebanjo.md │ │ ├── beenverified.md │ │ ├── best-practical-solutions.md │ │ ├── betable.md │ │ ├── betapeak.md │ │ ├── betterup.md │ │ ├── beyond-company.md │ │ ├── beyondpricing.md │ │ ├── bharatpe.md │ │ ├── big-cartel.md │ │ ├── bill.md │ │ ├── bilstein-group.md │ │ ├── bit-zesty.md │ │ ├── bitnami.md │ │ ├── bitovi.md │ │ ├── bizink.md │ │ ├── blameless.md │ │ ├── bloc.md │ │ ├── bluecat-networks.md │ │ ├── bluespark.md │ │ ├── boldare.md │ │ ├── bonsai.md │ │ ├── bounteous.md │ │ ├── brainstorm-force.md │ │ ├── bright-funds.md │ │ ├── brikl.md │ │ ├── britecore.md │ │ ├── broadwing.md │ │ ├── buffer.md │ │ ├── bugfender.md │ │ ├── buysellads.md │ │ ├── cabify.md │ │ ├── calamari.md │ │ ├── calendly.md │ │ ├── calibre.md │ │ ├── cancom.md │ │ ├── canonical.md │ │ ├── capchase.md │ │ ├── capgemini.md │ │ ├── capital-one.md │ │ ├── capital-placement.md │ │ ├── capmo.md │ │ ├── carbon-black.md │ │ ├── cards-against-humanity.md │ │ ├── carecru.md │ │ ├── caremessage.md │ │ ├── carmatec.md │ │ ├── cartodb.md │ │ ├── cartstack.md │ │ ├── casumo.md │ │ ├── chainlink.md │ │ ├── chargify.md │ │ ├── charity-water.md │ │ ├── chatgen.md │ │ ├── checkly.md │ │ ├── chef.md │ │ ├── chess.md │ │ ├── chroma.md │ │ ├── circleci.md │ │ ├── circonus.md │ │ ├── civicactions.md │ │ ├── civo.md │ │ ├── clevertech.md │ │ ├── clickup.md │ │ ├── clootrack.md │ │ ├── close.md │ │ ├── cloudapp.md │ │ ├── cloudera.md │ │ ├── cloudlinux.md │ │ ├── coalition-technologies.md │ │ ├── code-b-solutions.md │ │ ├── code-like-a-girl.md │ │ ├── codea-it.md │ │ ├── codepen.md │ │ ├── codesandbox.md │ │ ├── codeship.md │ │ ├── codingcops.md │ │ ├── cofense.md │ │ ├── coforma.md │ │ ├── cognizant.md │ │ ├── coinbase.md │ │ ├── coingape.md │ │ ├── collabora.md │ │ ├── comet.md │ │ ├── companies.11tydata.js │ │ ├── companies.json │ │ ├── compucorp.md │ │ ├── connexa.md │ │ ├── consensys.md │ │ ├── consumer-financial-protection-bureau.md │ │ ├── continu.md │ │ ├── conversio.md │ │ ├── convert.md │ │ ├── coodesh.md │ │ ├── core-apps.md │ │ ├── coreos.md │ │ ├── corgibytes.md │ │ ├── cosmic-chimps.md │ │ ├── coursera.md │ │ ├── cratedb.md │ │ ├── crazygames.md │ │ ├── creex-team.md │ │ ├── crossover.md │ │ ├── crowdstrike.md │ │ ├── cueup.md │ │ ├── customer-io.md │ │ ├── cuvette.md │ │ ├── cvs-health.md │ │ ├── cwt.md │ │ ├── cyber-whale.md │ │ ├── daktronics.md │ │ ├── dappradar.md │ │ ├── darecode.md │ │ ├── dashboardhub.md │ │ ├── dashlane.md │ │ ├── data-science-brigade.md │ │ ├── data-science-dojo.md │ │ ├── datacamp.md │ │ ├── datadog.md │ │ ├── datastax.md │ │ ├── dave.md │ │ ├── dealdash.md │ │ ├── deel.md │ │ ├── delighted.md │ │ ├── designcode.md │ │ ├── deskpass.md │ │ ├── dev-spotlight.md │ │ ├── devsquad.md │ │ ├── dgraph.md │ │ ├── digitalocean.md │ │ ├── discord.md │ │ ├── discourse.md │ │ ├── dnsimple.md │ │ ├── docker.md │ │ ├── doist.md │ │ ├── doit.md │ │ ├── dow-jones.md │ │ ├── dronedeploy.md │ │ ├── dropbox.md │ │ ├── drupal-jedi.md │ │ ├── duckduckgo.md │ │ ├── dynapictures.md │ │ ├── eatstreet.md │ │ ├── ebsco-information-services.md │ │ ├── ebury.md │ │ ├── eco-mind.md │ │ ├── ecosmic.md │ │ ├── edgar.md │ │ ├── edify.md │ │ ├── elastic.md │ │ ├── elsewhen.md │ │ ├── embraer.md │ │ ├── employment-hero.md │ │ ├── emsisoft.md │ │ ├── encora.md │ │ ├── engineyard.md │ │ ├── enjoei.md │ │ ├── enok.md │ │ ├── entrision.md │ │ ├── envato.md │ │ ├── envoy.md │ │ ├── epam.md │ │ ├── epic-games.md │ │ ├── epilocal.md │ │ ├── episource.md │ │ ├── epsy-health.md │ │ ├── equal-experts-portugal.md │ │ ├── ergeon.md │ │ ├── estately.md │ │ ├── etch.md │ │ ├── etsy.md │ │ ├── evelo.md │ │ ├── evil-martians.md │ │ ├── evrone.md │ │ ├── exoscale.md │ │ ├── exportdata.md │ │ ├── extreme-networks.md │ │ ├── eyeo.md │ │ ├── factorialhr.md │ │ ├── fairwinds.md │ │ ├── faithlife.md │ │ ├── fastly.md │ │ ├── fdte.md │ │ ├── fetlife.md │ │ ├── ffw-agency.md │ │ ├── figma.md │ │ ├── filament-group.md │ │ ├── findem.md │ │ ├── findify.md │ │ ├── fingerprint.md │ │ ├── fire-engine-red.md │ │ ├── firehire.md │ │ ├── fis-global.md │ │ ├── fiverr.md │ │ ├── fivexl.md │ │ ├── fleetio.md │ │ ├── flexera.md │ │ ├── flightaware.md │ │ ├── flip.md │ │ ├── flowing.md │ │ ├── flowpath.md │ │ ├── fly-io.md │ │ ├── fmx.md │ │ ├── focusnetworks.md │ │ ├── foh-and-boh.md │ │ ├── folotop.md │ │ ├── formidable.md │ │ ├── formstack.md │ │ ├── four-kitchens.md │ │ ├── fraudio.md │ │ ├── freeagent.md │ │ ├── freeletics.md │ │ ├── fuel-made.md │ │ ├── full-fabric.md │ │ ├── functionize.md │ │ ├── fyle.md │ │ ├── gaggle.md │ │ ├── geckoboard.md │ │ ├── general-assembly.md │ │ ├── geo-jobe.md │ │ ├── gerencianet.md │ │ ├── gft.md │ │ ├── ghost-inspector.md │ │ ├── giant-swarm.md │ │ ├── giant.md │ │ ├── gigsalad.md │ │ ├── gitbook.md │ │ ├── github.md │ │ ├── gitlab.md │ │ ├── gitprime.md │ │ ├── glenn-website-design.md │ │ ├── glitch.md │ │ ├── gluware.md │ │ ├── gmbapi.md │ │ ├── godaddy.md │ │ ├── gohiring.md │ │ ├── gojob.md │ │ ├── goldfire-agency.md │ │ ├── gorman-health-group.md │ │ ├── got-soccer.md │ │ ├── grafana.md │ │ ├── granicus.md │ │ ├── graylog.md │ │ ├── greenstitch-io.md │ │ ├── gremlin.md │ │ ├── gridium.md │ │ ├── groove.md │ │ ├── grou-ps.md │ │ ├── grubhub.md │ │ ├── gruntwork.md │ │ ├── guidesmiths.md │ │ ├── hack-reactor-remote.md │ │ ├── hanzo.md │ │ ├── happy-cog.md │ │ ├── harris-consult.md │ │ ├── harvest.md │ │ ├── hashex.md │ │ ├── hashicorp.md │ │ ├── he-labs.md │ │ ├── headforwards.md │ │ ├── headway.md │ │ ├── healthfinch.md │ │ ├── heap.md │ │ ├── heetch.md │ │ ├── help-scout.md │ │ ├── heroku.md │ │ ├── hireology.md │ │ ├── homeflic-wegrow.md │ │ ├── homevalet.md │ │ ├── honeybadger.md │ │ ├── honeycomb.md │ │ ├── hopper.md │ │ ├── hotjar.md │ │ ├── hubspot.md │ │ ├── hubstaff.md │ │ ├── hudl.md │ │ ├── hugo.md │ │ ├── human-made.md │ │ ├── husl-digital.md │ │ ├── hypergiant.md │ │ ├── hyperion.md │ │ ├── hypothesis.md │ │ ├── i-stem.md │ │ ├── ibm.md │ │ ├── iclinic.md │ │ ├── idonethis.md │ │ ├── ifit.md │ │ ├── igalia.md │ │ ├── imagine-learning.md │ │ ├── impala.md │ │ ├── impira.md │ │ ├── implisense.md │ │ ├── incsub.md │ │ ├── indrive.md │ │ ├── infinite-red.md │ │ ├── influxdata.md │ │ ├── inquicker.md │ │ ├── inshorts.md │ │ ├── instamobile.md │ │ ├── instructure.md │ │ ├── intellum.md │ │ ├── intent.md │ │ ├── inter-link.md │ │ ├── interactive-intelligence.md │ │ ├── intercom.md │ │ ├── interpersonal-frequency-i-f.md │ │ ├── intevity.md │ │ ├── intuit.md │ │ ├── intuition-machines-inc.md │ │ ├── invesco.md │ │ ├── iohk.md │ │ ├── iopipe.md │ │ ├── ios-app-templates.md │ │ ├── ipinfo.md │ │ ├── ips-group-inc.md │ │ ├── iqvia.md │ │ ├── ironin.md │ │ ├── iterative.md │ │ ├── iwantmyname.md │ │ ├── jackson-river.md │ │ ├── jaya-tech.md │ │ ├── jbs-custom-software-solutions.md │ │ ├── jitbit.md │ │ ├── jitera.md │ │ ├── jobsity.md │ │ ├── jolly-good-code.md │ │ ├── joor.md │ │ ├── journy-io.md │ │ ├── joyent.md │ │ ├── jupiterone.md │ │ ├── kake.md │ │ ├── karat.md │ │ ├── kaufland-e-commerce.md │ │ ├── kea.md │ │ ├── keen-io.md │ │ ├── kentik.md │ │ ├── kestra.md │ │ ├── khan-academy.md │ │ ├── kickback-rewards-systems.md │ │ ├── kindred.md │ │ ├── kinsta.md │ │ ├── kiprosh.md │ │ ├── kissmetrics.md │ │ ├── klanik.md │ │ ├── klaviyo.md │ │ ├── knack.md │ │ ├── kodify.md │ │ ├── koding.md │ │ ├── komoot.md │ │ ├── kona.md │ │ ├── konkurenta.md │ │ ├── kraken.md │ │ ├── kuali.md │ │ ├── labelbox.md │ │ ├── lambda-school.md │ │ ├── lambert-labs.md │ │ ├── laterpay.md │ │ ├── launch-potato.md │ │ ├── leadership-success.md │ │ ├── leadfeeder.md │ │ ├── leadiq.md │ │ ├── lets-encrypt.md │ │ ├── lifen.md │ │ ├── lifetime-value-company.md │ │ ├── lightspeed.md │ │ ├── linaro.md │ │ ├── lincoln-loop.md │ │ ├── line-plus-corporation.md │ │ ├── link11.md │ │ ├── linux-foundation.md │ │ ├── lionsher.md │ │ ├── litmus.md │ │ ├── liveperson.md │ │ ├── loadsys.md │ │ ├── localistico.md │ │ ├── locus-robotics.md │ │ ├── logdna.md │ │ ├── logdog.md │ │ ├── logrocket.md │ │ ├── lullabot.md │ │ ├── luxoft.md │ │ ├── luxor.md │ │ ├── lyseon-tech.md │ │ ├── lytx.md │ │ ├── madewithlove.md │ │ ├── madisoft.md │ │ ├── mailerlite.md │ │ ├── maintainnow.md │ │ ├── manifold.md │ │ ├── mapbox.md │ │ ├── marco-polo.md │ │ ├── mariadb.md │ │ ├── marketade.md │ │ ├── marsbased.md │ │ ├── massive-pixel-creation.md │ │ ├── mattermost.md │ │ ├── maxicus.md │ │ ├── mayven-studios.md │ │ ├── mayvue.md │ │ ├── meant4.md │ │ ├── mediacurrent.md │ │ ├── mediavine.md │ │ ├── medium.md │ │ ├── memberful.md │ │ ├── memory.md │ │ ├── mercari.md │ │ ├── merico.md │ │ ├── meridianlink.md │ │ ├── messagebird.md │ │ ├── metalab.md │ │ ├── metamask.md │ │ ├── meteorops.md │ │ ├── microsoft.md │ │ ├── mindful.md │ │ ├── mixcloud.md │ │ ├── mixmax.md │ │ ├── mixrank.md │ │ ├── mobile-jazz.md │ │ ├── modern-health.md │ │ ├── modern-tribe.md │ │ ├── modsquad.md │ │ ├── molteo.md │ │ ├── mongodb.md │ │ ├── monthly.md │ │ ├── mozilla.md │ │ ├── mtc.md │ │ ├── muck-rack.md │ │ ├── mux.md │ │ ├── mvpmule.md │ │ ├── mycelium.md │ │ ├── mysql.md │ │ ├── nagarro.md │ │ ├── namecheap.md │ │ ├── nationwide.md │ │ ├── nearform.md │ │ ├── nerdwallet.md │ │ ├── netapp.md │ │ ├── netguru.md │ │ ├── netlify.md │ │ ├── netris.md │ │ ├── netsparker.md │ │ ├── nettl-edinburgh.md │ │ ├── new-context.md │ │ ├── next.md │ │ ├── no-code-no-problem.md │ │ ├── nodesource.md │ │ ├── noredink.md │ │ ├── novoda.md │ │ ├── npm.md │ │ ├── nuage.md │ │ ├── nuharbor-security.md │ │ ├── nuna.md │ │ ├── nvidia.md │ │ ├── ocient.md │ │ ├── octopus-deploy.md │ │ ├── oddball.md │ │ ├── okta.md │ │ ├── olark.md │ │ ├── olist.md │ │ ├── ollie-order.md │ │ ├── ollie.md │ │ ├── olo.md │ │ ├── ombu-labs.md │ │ ├── omniti.md │ │ ├── on-the-go-systems.md │ │ ├── onna.md │ │ ├── opencity-labs.md │ │ ├── opencraft.md │ │ ├── openzeppelin.md │ │ ├── optoro.md │ │ ├── oracle.md │ │ ├── ordermentum.md │ │ ├── oreilly-media.md │ │ ├── oreilly-online-learning.md │ │ ├── our-hometown-inc.md │ │ ├── outsourcingdev.md │ │ ├── over.md │ │ ├── packlink.md │ │ ├── pagepro.md │ │ ├── pagerduty.md │ │ ├── paktor.md │ │ ├── palantir-net.md │ │ ├── panther-labs.md │ │ ├── parabol.md │ │ ├── parexel.md │ │ ├── park-assist.md │ │ ├── parsely.md │ │ ├── particular-software.md │ │ ├── pathable.md │ │ ├── payfully.md │ │ ├── paylocity.md │ │ ├── paypay.md │ │ ├── payscale.md │ │ ├── paytm-labs.md │ │ ├── payu.md │ │ ├── peachworks.md │ │ ├── peakforce.md │ │ ├── percona.md │ │ ├── pex.md │ │ ├── picpay.md │ │ ├── pindrop.md │ │ ├── plai.md │ │ ├── platform-builders.md │ │ ├── platform-sh.md │ │ ├── pleo.md │ │ ├── plex.md │ │ ├── pnc-financial-services.md │ │ ├── polygon.md │ │ ├── positiwise.md │ │ ├── powerschool.md │ │ ├── pragma.md │ │ ├── precision-nutrition.md │ │ ├── predict-mobile.md │ │ ├── prelude.md │ │ ├── previousnext.md │ │ ├── prezi.md │ │ ├── prezly.md │ │ ├── primer.md │ │ ├── primotly.md │ │ ├── prisma.md │ │ ├── privacycloud.md │ │ ├── procenge.md │ │ ├── procurify.md │ │ ├── progress-engine.md │ │ ├── prominent-edge.md │ │ ├── puppet.md │ │ ├── pwc.md │ │ ├── qatium.md │ │ ├── quaderno.md │ │ ├── quantify.md │ │ ├── questdb.md │ │ ├── quicktrials.md │ │ ├── quora.md │ │ ├── rackspace.md │ │ ├── raft.md │ │ ├── railscarma.md │ │ ├── rainforest-qa.md │ │ ├── rakuten-travel-xchange.md │ │ ├── ramp.md │ │ ├── reaction-commerce.md │ │ ├── reactiveops.md │ │ ├── realtimecrm.md │ │ ├── rebelmouse.md │ │ ├── reboot-studio.md │ │ ├── recharge.md │ │ ├── rechat.md │ │ ├── recurly.md │ │ ├── red-hat.md │ │ ├── reddit.md │ │ ├── redlio-designs.md │ │ ├── redmonk.md │ │ ├── redox.md │ │ ├── reducer.md │ │ ├── refundid.md │ │ ├── reinteractive.md │ │ ├── remote-garage.md │ │ ├── remotebase.md │ │ ├── remotemore.md │ │ ├── renaissance.md │ │ ├── rendr-software-group.md │ │ ├── renofi.md │ │ ├── replit.md │ │ ├── research-square.md │ │ ├── revolgy.md │ │ ├── revolut.md │ │ ├── roadpass-digital.md │ │ ├── roadtrippers.md │ │ ├── rocket-chat.md │ │ ├── roundproxies.md │ │ ├── rtcamp-solutions.md │ │ ├── sadapay.md │ │ ├── safeguard-global.md │ │ ├── salesforce.md │ │ ├── sandhills-development.md │ │ ├── sardine-ai.md │ │ ├── scalac.md │ │ ├── scaloy.md │ │ ├── scandit.md │ │ ├── schnell-solutions-limited.md │ │ ├── scopic-software.md │ │ ├── scrapingbee.md │ │ ├── scrapinghub.md │ │ ├── scylladb.md │ │ ├── seaplane.md │ │ ├── seatgeek.md │ │ ├── securityscorecard.md │ │ ├── seeq.md │ │ ├── semaphore.md │ │ ├── sendwave.md │ │ ├── serpapi.md │ │ ├── server-density.md │ │ ├── servmask.md │ │ ├── session.md │ │ ├── shareup.md │ │ ├── shattered-silicon.md │ │ ├── shippabo.md │ │ ├── shogun.md │ │ ├── shopify.md │ │ ├── sigma-defense.md │ │ ├── signeasy.md │ │ ├── silverfin.md │ │ ├── simplabs.md │ │ ├── simpletexting.md │ │ ├── six-to-start.md │ │ ├── sketch.md │ │ ├── skillcrush.md │ │ ├── skillshare.md │ │ ├── skyrocket-ventures.md │ │ ├── slack.md │ │ ├── smartcash.md │ │ ├── smile.md │ │ ├── smmile-digital.md │ │ ├── smugmug.md │ │ ├── socket-supply-co.md │ │ ├── softwaremill.md │ │ ├── sommo.md │ │ ├── sonatype.md │ │ ├── soostone.md │ │ ├── soshace.md │ │ ├── sourcegraph.md │ │ ├── spoqa.md │ │ ├── spotify.md │ │ ├── spreaker.md │ │ ├── spreedly.md │ │ ├── spruce.md │ │ ├── stack-exchange.md │ │ ├── stairlin.md │ │ ├── status.md │ │ ├── stencil.md │ │ ├── sticker-mule.md │ │ ├── stitch-fix.md │ │ ├── stoneco.md │ │ ├── stormx.md │ │ ├── strapi.md │ │ ├── streamnative.md │ │ ├── stripe.md │ │ ├── studysoup.md │ │ ├── superplayer-and-co.md │ │ ├── surevine.md │ │ ├── suse.md │ │ ├── sutherland-cloudsource.md │ │ ├── svix.md │ │ ├── sweetrush.md │ │ ├── swif-ai.md │ │ ├── swiggy.md │ │ ├── swimlane.md │ │ ├── syde.md │ │ ├── sysdig.md │ │ ├── tag1-consulting.md │ │ ├── taledo.md │ │ ├── taplytics.md │ │ ├── taskade.md │ │ ├── tatvasoft.md │ │ ├── taxjar.md │ │ ├── teamflow.md │ │ ├── teamsnap.md │ │ ├── teamultra.md │ │ ├── ted.md │ │ ├── teleport.md │ │ ├── telerik.md │ │ ├── telestax.md │ │ ├── tenable.md │ │ ├── teravision-technologies.md │ │ ├── test-double.md │ │ ├── testgorilla.md │ │ ├── the-crafters-lab.md │ │ ├── the-ghost-foundation.md │ │ ├── the-home-depot.md │ │ ├── the-planet-group.md │ │ ├── the-publisher-desk.md │ │ ├── the-scale-factory.md │ │ ├── the-wirecutter.md │ │ ├── theoremone.md │ │ ├── thinkful.md │ │ ├── third-iron.md │ │ ├── thorn.md │ │ ├── three-movers.md │ │ ├── thrive-market.md │ │ ├── tide.md │ │ ├── timespot.md │ │ ├── tipe.md │ │ ├── toast.md │ │ ├── toggl.md │ │ ├── toptal.md │ │ ├── tower.md │ │ ├── tractionboard.md │ │ ├── transfeera.md │ │ ├── transition-technologies-advanced-solutions.md │ │ ├── transloadit.md │ │ ├── travis-ci.md │ │ ├── travis.md │ │ ├── treehouse.md │ │ ├── treez.md │ │ ├── trello.md │ │ ├── tribe.md │ │ ├── truelogic.md │ │ ├── trussworks.md │ │ ├── tuft-and-needle.md │ │ ├── turing.md │ │ ├── turtlemint.md │ │ ├── twilio.md │ │ ├── two.md │ │ ├── udacity.md │ │ ├── uhuru.md │ │ ├── uncapped.md │ │ ├── upcell.md │ │ ├── upwork-pro.md │ │ ├── upworthy.md │ │ ├── usaa.md │ │ ├── ushahidi.md │ │ ├── uship.md │ │ ├── v0-report.md │ │ ├── valimail.md │ │ ├── varnish-software.md │ │ ├── vast-limits.md │ │ ├── veeva.md │ │ ├── vercel.md │ │ ├── verve-systems.md │ │ ├── veryfi.md │ │ ├── vgs.md │ │ ├── viperdev.md │ │ ├── virta-health.md │ │ ├── vivo.md │ │ ├── vmware.md │ │ ├── voiio.md │ │ ├── vox-media.md │ │ ├── voxy.md │ │ ├── vshn.md │ │ ├── wallethub.md │ │ ├── webdevstudios.md │ │ ├── webfx.md │ │ ├── webikon.md │ │ ├── webrunners.md │ │ ├── webscrapinghq.md │ │ ├── wells-fargo.md │ │ ├── wemake-services.md │ │ ├── wemakemvp.md │ │ ├── whitecap-seo.md │ │ ├── whitespectre.md │ │ ├── wikihow.md │ │ ├── wikimedia-foundation.md │ │ ├── wildbit.md │ │ ├── willings-inc.md │ │ ├── wingify.md │ │ ├── wipro.md │ │ ├── wirefuture.md │ │ ├── wizeline.md │ │ ├── wolfram.md │ │ ├── wolverine-trading.md │ │ ├── wombat-security.md │ │ ├── wp-media.md │ │ ├── wyeworks.md │ │ ├── x-team.md │ │ ├── xapo.md │ │ ├── xp-inc.md │ │ ├── xwp.md │ │ ├── yahoo.md │ │ ├── yandex.md │ │ ├── yazio.md │ │ ├── yodo1.md │ │ ├── yonder.md │ │ ├── you-are-launched.md │ │ ├── you-need-a-budget.md │ │ ├── youcanbook-me.md │ │ ├── zamp.md │ │ ├── zaneffi.md │ │ ├── zapier.md │ │ ├── zeit-io.md │ │ ├── zemoso.md │ │ ├── zenrows.md │ │ ├── zignaly.md │ │ ├── zip-co.md │ │ ├── zolar.md │ │ ├── zootools.md │ │ └── zup.md │ └── pages/ │ ├── about.md │ ├── blog.njk │ ├── changelog.njk │ ├── companies.njk │ ├── contact.md │ ├── contributing.md │ ├── index.njk │ ├── privacy.md │ ├── search.njk │ ├── tag.njk │ └── tags.njk └── tailwind.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eleventyignore ================================================ # Eleventy ignore file # Files and directories that Eleventy should not process # Node modules node_modules/ # README files (already being processed as markdown, might conflict) README.md # Migration plan (if you add it back to the repo) MIGRATION_PLAN.md # Test/temporary directories company-profiles-test/ # Git files .git/ .gitignore # Editor files .vscode/ .idea/ # OS files .DS_Store # Build artifacts dist/ # Documentation that shouldn't be in the build CONTRIBUTING.md LICENSE CHANGELOG.md ================================================ FILE: .env-sample ================================================ URL=http://localhost:8080 ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement. Please mention @dougaitken or email him directly `remote at dougaitken dot co dot uk` All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations _This supersedes any previous version of the code of conduct._ ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to Remote In Tech Thank you for your interest in contributing! This repository maintains a list of companies that offer remote work opportunities in tech. ## Quick Start 1. Fork the repository 2. Clone your fork locally 3. Create a new branch for your changes 4. Add or update a company profile in `src/companies/` 5. Test the site locally (optional but recommended) 6. Submit a pull request ## Adding a New Company ### Requirements - You are an employee of the company (or can verify the information) - Company directly hires employees (no bootcamps/freelance platforms) - Company offers genuine remote work opportunities - Company is in or around the tech industry ### Steps 1. **Create company file**: Add a new file at `src/companies/company-name.md` 2. **Add frontmatter**: Use the structured YAML format (see template below) 3. **Add content sections**: Fill in all required markdown sections 4. **Test locally**: Run `npm run build:11ty` to verify it builds ### Company Profile Template Create a new markdown file in `src/companies/` with this format: ```markdown --- title: "Company Name" slug: company-name website: https://example.com careers_url: https://example.com/careers region: worldwide remote_policy: fully-remote company_size: medium technologies: - javascript - python - cloud --- ## Company blurb Brief description of what the company does and what makes it unique. ## Company size Approximate size (e.g., "50-100 employees", "500+", etc.) ## Remote status Detailed description of remote work policy and culture. Be specific: - Fully remote or hybrid? - Remote-first culture? - Timezone requirements? - Office visit requirements? ## Region Geographic regions where the company hires from. ## Company technologies Main technologies and tools used. ## Office locations Physical office locations if any (or "None" if fully remote). ## How to apply Instructions for applying, including links to careers page. ``` ### Valid Frontmatter Values **region** (required): - `worldwide` - Hires globally - `americas` - North/South America - `europe` - Europe - `americas-europe` - Americas and Europe - `asia-pacific` - Asia Pacific region - `other` - Other regions **remote_policy** (required): - `fully-remote` - 100% remote, no office required - `remote-first` - Remote is the default, offices optional - `hybrid` - Mix of remote and office - `remote-friendly` - Office-based with remote options **company_size** (required): - `tiny` - 1-10 employees - `small` - 11-50 employees - `medium` - 51-200 employees - `large` - 201-1000 employees - `enterprise` - 1000+ employees **technologies** (optional array): - `javascript`, `python`, `ruby`, `go`, `java`, `php`, `rust`, `dotnet`, `elixir`, `scala` - `cloud`, `devops`, `mobile`, `data`, `ml`, `sql`, `nosql`, `search` **careers_url** (optional): - Direct link to the company's careers/jobs page - If provided, this is where the "Apply Now" button links - If omitted, falls back to the main `website` URL **addedAt / updatedAt** (managed by maintainers): - These date fields are added by project maintainers — do not include them in your PR - `addedAt` records when the company was first added to the project - `updatedAt` records when the profile content was last meaningfully changed ## Testing Locally ```bash # Install dependencies npm install # Run the development server (with hot reload) npm run start # Visit http://localhost:8080 # Or just build to check for errors npm run build:11ty ``` ## Content Guidelines ### Required Markdown Sections 1. **Company blurb** - What the company does 2. **Company size** - Approximate employee count 3. **Remote status** - Remote work policy and culture (be detailed!) 4. **Region** - Where the company hires from 5. **Company technologies** - Main tech stack 6. **Office locations** - Physical offices (if any) 7. **How to apply** - Application process and links ### Content Quality Standards - No placeholder text (TODO, FIXME, etc.) - Complete sentences and proper grammar - Working links and email addresses - Clear, helpful information for job seekers - Be honest about remote work reality (not just marketing copy) ### File Naming - Use lowercase with hyphens: `awesome-company.md` - Match company name: "Awesome Company, Inc." → `awesome-company.md` - The `slug` in frontmatter should match the filename (without `.md`) ## Example **File**: `src/companies/acme-corp.md` ```markdown --- title: "Acme Corp" slug: acme-corp website: https://acme-corp.com careers_url: https://acme-corp.com/careers region: americas-europe remote_policy: fully-remote company_size: medium technologies: - go - python - cloud - sql --- ## Company blurb Acme Corp builds cloud-native solutions for enterprise data management. We help companies migrate legacy systems to modern architectures with minimal downtime. ## Company size 150-200 employees ## Remote status Fully distributed company since 2018. All roles are remote-first with optional coworking stipends. We operate on async-first principles with 4-hour overlap in EST timezone. No mandatory office visits. ## Region North America and Europe (must be able to work within UTC-8 to UTC+2 timezones) ## Company technologies Go, Kubernetes, PostgreSQL, React, TypeScript, AWS, Terraform ## Office locations Small office in San Francisco for those who prefer in-person work (optional) ## How to apply Visit our careers page at https://acme-corp.com/careers or email jobs@acme-corp.com with your resume and GitHub profile. ``` ## Tips for Success 1. **Be specific about remote work** - "Remote-friendly" can mean many things. Explain timezone requirements, in-person expectations, and remote culture maturity. 2. **Keep it current** - Only add companies that are actively hiring remotely. 3. **Be honest** - Don't oversell remote culture. Job seekers appreciate honesty. 4. **Include details** - More detail about technologies and remote work = more helpful. 5. **Proofread** - Check spelling, grammar, and links before submitting. ## Common Mistakes to Avoid - Using invalid values for `region`, `remote_policy`, or `company_size` - Missing required frontmatter fields - Incomplete remote status section (most important!) - Broken or invalid URLs - `slug` not matching the filename - Copy-pasted marketing language without real info ## Need Help? - Check existing company profiles in `src/companies/` for examples - Look at recently merged PRs to see what good submissions look like - Ask questions in your PR if you need clarification - Review [CLAUDE.md](../CLAUDE.md) for detailed project documentation ## License Note By contributing, you agree that your contributions will be licensed under the CC0 1.0 Universal license (public domain dedication). See [LICENSE](../LICENSE) for details. ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: dougaitken ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[Bug]" labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Feature Requests & Ideas url: https://github.com/remoteintech/remote-jobs/discussions/new?category=ideas about: Suggest new features or improvements via GitHub Discussions - name: Questions & Help url: https://github.com/remoteintech/remote-jobs/discussions/new?category=q-a about: Ask questions or get help with contributions ================================================ FILE: .github/ISSUE_TEMPLATE/new_company.md ================================================ --- name: New Company Request about: Request a remote-friendly company to be added to the directory title: "[Company] " labels: new company assignees: '' --- ## Company Information **Company Name:** **Website:** **Careers Page URL (if different):** ## Remote Work Details **Remote Policy:** (select one) - [ ] Fully Remote - 100% remote, no office required - [ ] Remote First - Remote is the default, offices optional - [ ] Hybrid - Mix of remote and office - [ ] Remote Friendly - Office-based with remote options **Hiring Region:** (select one) - [ ] Worldwide - [ ] Americas - [ ] Europe - [ ] Americas & Europe - [ ] Asia Pacific - [ ] Other (please specify): **Company Size:** - [ ] Tiny (1-10 employees) - [ ] Small (11-50 employees) - [ ] Medium (51-200 employees) - [ ] Large (201-1000 employees) - [ ] Enterprise (1000+ employees) ## Company Description ## Remote Culture ## Technologies Used ## How to Apply ## Verification - [ ] I am an employee of this company or can verify this information - [ ] This company directly hires employees (not a bootcamp/freelance platform) - [ ] This company genuinely offers remote work opportunities ## Additional Notes ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.MD ================================================ ## Description ## Type of Change - [ ] New company addition - [ ] Company information update - [ ] Bug fix - [ ] Documentation update - [ ] Other (please describe) ## Checklist ### General Requirements - [ ] I have read the [Contributing Guidelines](https://github.com/remoteintech/remote-jobs/blob/main/.github/CONTRIBUTING.md) - [ ] This pull request adheres to the repository's Code of Conduct - [ ] I have tested the build locally with `npm run build:11ty` ### For Company Additions/Updates - [ ] Company file is in `src/companies/` with correct filename (lowercase, hyphenated) - [ ] The company directly hires employees (no bootcamps/freelance platforms) - [ ] The company offers genuine remote work opportunities #### Frontmatter Requirements - [ ] `title` - Company name in quotes - [ ] `slug` - Matches filename (without .md) - [ ] `website` - Valid company URL - [ ] `region` - One of: `worldwide`, `americas`, `europe`, `americas-europe`, `asia-pacific`, `other` - [ ] `remote_policy` - One of: `fully-remote`, `remote-first`, `hybrid`, `remote-friendly` - [ ] `company_size` - One of: `tiny`, `small`, `medium`, `large`, `enterprise` - [ ] `technologies` - Array of valid tech tags (optional) - [ ] `careers_url` - Direct link to careers page (optional) - **Note:** `addedAt` and `updatedAt` dates are managed by maintainers — do not include them #### Content Requirements - [ ] "Company blurb" section describes what the company does - [ ] "Remote status" section details the remote work policy and culture - [ ] "How to apply" section includes application instructions ## Additional Information ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: weekly allow: - dependency-type: direct - dependency-type: indirect ================================================ FILE: .github/scripts/validate-companies.js ================================================ #!/usr/bin/env node /** * Validates company profile files for correct frontmatter and content structure. * * Usage: node validate-companies.js [file2.md ...] * * Outputs JSON results to stdout. Exit code 1 if any validation errors found. */ import { readFileSync } from "node:fs"; import { basename } from "node:path"; // Canonical enum values (from src/_data/companyHelpers.js) const VALID_REGIONS = [ "worldwide", "americas", "europe", "americas-europe", "asia-pacific", "other", ]; const VALID_REMOTE_POLICIES = [ "fully-remote", "remote-first", "hybrid", "remote-friendly", ]; const VALID_COMPANY_SIZES = ["tiny", "small", "medium", "large", "enterprise"]; const VALID_TECHNOLOGIES = [ "javascript", "python", "ruby", "go", "java", "php", "rust", "dotnet", "elixir", "scala", "cloud", "devops", "mobile", "data", "ml", "sql", "nosql", "search", ]; const REQUIRED_FIELDS = [ "title", "slug", "website", "region", "remote_policy", "company_size", ]; const REQUIRED_SECTIONS = ["Company blurb", "Remote status", "How to apply"]; /** * Parse YAML frontmatter from a markdown file's content. * Returns null if no frontmatter block is found. */ function parseFrontmatter(content) { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match) return null; const yaml = match[1]; const data = {}; let currentArrayKey = null; for (const line of yaml.split("\n")) { // Array item (indented "- value") const arrayItem = line.match(/^\s+-\s+(.+)$/); if (arrayItem && currentArrayKey) { data[currentArrayKey].push(arrayItem[1].trim()); continue; } // Key-value pair const kv = line.match(/^(\w[\w_]*):\s*(.*)$/); if (kv) { const key = kv[1]; let value = kv[2].trim(); // Check if this starts an array (empty value followed by "- items") if (value === "" || value === "[]") { data[key] = []; currentArrayKey = key; continue; } // Strip surrounding quotes if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } data[key] = value; currentArrayKey = null; } } return data; } /** * Validate a single company file. Returns an array of error strings. */ function validateCompanyFile(filePath) { const errors = []; const warnings = []; let content; try { content = readFileSync(filePath, "utf-8"); } catch (err) { errors.push(`Could not read file: ${err.message}`); return { errors, warnings }; } // Parse frontmatter const data = parseFrontmatter(content); if (!data) { errors.push( "Missing YAML frontmatter. The file must start with `---` followed by YAML fields and a closing `---`." ); return { errors, warnings }; } // Check required fields for (const field of REQUIRED_FIELDS) { if (!data[field] || (typeof data[field] === "string" && !data[field].trim())) { errors.push(`Missing required field: \`${field}\``); } } // Validate enum values (only if field exists) if (data.region && !VALID_REGIONS.includes(data.region)) { errors.push( `Invalid \`region\`: "${data.region}". Must be one of: ${VALID_REGIONS.join(", ")}` ); } if (data.remote_policy && !VALID_REMOTE_POLICIES.includes(data.remote_policy)) { errors.push( `Invalid \`remote_policy\`: "${data.remote_policy}". Must be one of: ${VALID_REMOTE_POLICIES.join(", ")}` ); } if (data.company_size && !VALID_COMPANY_SIZES.includes(data.company_size)) { errors.push( `Invalid \`company_size\`: "${data.company_size}". Must be one of: ${VALID_COMPANY_SIZES.join(", ")}` ); } // Validate technologies array if (data.technologies && Array.isArray(data.technologies)) { for (const tech of data.technologies) { if (!VALID_TECHNOLOGIES.includes(tech)) { errors.push( `Invalid technology tag: "${tech}". Valid tags: ${VALID_TECHNOLOGIES.join(", ")}` ); } } } // Validate slug matches filename if (data.slug) { const expectedSlug = basename(filePath, ".md"); if (data.slug !== expectedSlug) { errors.push( `Slug "${data.slug}" does not match filename "${basename(filePath)}". The slug must be "${expectedSlug}".` ); } } // Validate URL format if (data.website && !isValidUrl(data.website)) { errors.push( `Invalid \`website\` URL: "${data.website}". Must be a full URL starting with https:// or http://` ); } if (data.careers_url && !isValidUrl(data.careers_url)) { errors.push( `Invalid \`careers_url\`: "${data.careers_url}". Must be a full URL starting with https:// or http://` ); } // Check required markdown sections const bodyContent = content.replace(/^---[\s\S]*?---/, ""); for (const section of REQUIRED_SECTIONS) { const sectionPattern = new RegExp( `^##\\s+${escapeRegExp(section)}\\s*$`, "m" ); if (!sectionPattern.test(bodyContent)) { errors.push(`Missing required section: "## ${section}"`); } } // Check that "Company blurb" has content (not just the heading) const blurbMatch = bodyContent.match( /^##\s+Company blurb\s*\n([\s\S]*?)(?=^##\s|\s*$)/m ); if (blurbMatch && !blurbMatch[1].trim()) { warnings.push( 'The "Company blurb" section is empty. Please add a description of the company.' ); } return { errors, warnings }; } function isValidUrl(str) { try { const url = new URL(str); return url.protocol === "https:" || url.protocol === "http:"; } catch { return false; } } function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // --- Main --- const args = process.argv.slice(2); if (args.length === 0) { console.error("Usage: node validate-companies.js [file2.md ...]"); process.exit(2); } // Separate company files from old-format files const companyFiles = []; const oldFormatFiles = []; for (const file of args) { // Strip pr-head/ prefix used in pull_request_target checkout const normalized = file.replace(/^pr-head\//, ""); if (normalized.startsWith("company-profiles/")) { oldFormatFiles.push(normalized); } else if (normalized.startsWith("src/companies/") && normalized.endsWith(".md")) { // Store both the actual path (for reading) and the display path (for output) companyFiles.push({ actual: file, display: normalized }); } } const results = { oldFormatFiles, files: {}, summary: { total: 0, passed: 0, failed: 0, warnings: 0 }, }; for (const { actual, display } of companyFiles) { const { errors, warnings } = validateCompanyFile(actual); results.files[display] = { errors, warnings }; results.summary.total++; if (errors.length > 0) { results.summary.failed++; } else { results.summary.passed++; } if (warnings.length > 0) { results.summary.warnings++; } } console.log(JSON.stringify(results, null, 2)); const hasErrors = results.summary.failed > 0 || results.oldFormatFiles.length > 0; process.exit(hasErrors ? 1 : 0); ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - name: Install dependencies run: npm ci - name: Build site run: npm run build env: NODE_OPTIONS: '--max-old-space-size=8192' - name: Verify build output run: | echo "Build completed successfully" echo "Pages generated: $(find dist -name '*.html' | wc -l)" ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. name: "CodeQL" on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above branches: [main] schedule: - cron: '0 6 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['javascript'] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/validate-companies.yml ================================================ name: Validate Company Profiles on: pull_request_target: branches: [main] paths: - 'src/companies/**' - 'company-profiles/**' permissions: contents: read pull-requests: write jobs: validate: # Skip validation for repo owner and bots if: >- github.event.pull_request.user.login != 'dougaitken' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.author_association != 'OWNER' && github.event.pull_request.author_association != 'MEMBER' runs-on: ubuntu-latest steps: - name: Checkout base (for workflow scripts) uses: actions/checkout@v4 - name: Checkout PR company files uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} path: pr-head sparse-checkout: | src/companies company-profiles - name: Get changed files id: changed run: | FILES=$(gh pr diff ${{ github.event.pull_request.number }} --name-only | grep -E '(^src/companies/.*\.md$|^company-profiles/)' || true) echo "files<> "$GITHUB_OUTPUT" echo "$FILES" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run validation id: validate if: steps.changed.outputs.files != '' run: | set +e # Remap file paths to the PR checkout directory MAPPED_FILES="" for f in ${{ steps.changed.outputs.files }}; do MAPPED_FILES="$MAPPED_FILES pr-head/$f" done RESULT=$(node .github/scripts/validate-companies.js $MAPPED_FILES) EXIT_CODE=$? set -e echo "json<> "$GITHUB_OUTPUT" echo "$RESULT" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" - name: Post PR comment if: steps.changed.outputs.files != '' uses: actions/github-script@v7 with: script: | const result = JSON.parse(process.env.VALIDATION_JSON); const exitCode = parseInt(process.env.EXIT_CODE, 10); let body = ''; // Old-format detection if (result.oldFormatFiles && result.oldFormatFiles.length > 0) { body += '### Thanks for your contribution!\n\n'; body += 'It looks like your PR uses an older file format. We\'ve updated how company profiles work. '; body += 'Please create a new file in `src/companies/` instead.\n\n'; body += '**Files using the old format:**\n'; for (const f of result.oldFormatFiles) { body += `- \`${f}\`\n`; } body += '\n
\nNew format template\n\n'; body += '```markdown\n'; body += '---\n'; body += 'title: "Your Company Name"\n'; body += 'slug: your-company-slug\n'; body += 'website: https://yourcompany.com\n'; body += 'careers_url: https://yourcompany.com/careers\n'; body += 'region: worldwide\n'; body += 'remote_policy: fully-remote\n'; body += 'company_size: small\n'; body += 'technologies:\n'; body += ' - javascript\n'; body += ' - python\n'; body += '---\n\n'; body += '> **Note:** `addedAt` and `updatedAt` dates are managed by maintainers — do not include them in your PR.\n\n'; body += '## Company blurb\n\n'; body += 'A short description of your company.\n\n'; body += '## Remote status\n\n'; body += 'Describe your remote work culture.\n\n'; body += '## How to apply\n\n'; body += 'Link to your careers page or application instructions.\n'; body += '```\n\n'; body += '
\n\n'; body += 'The filename should be `src/companies/{slug}.md` where `{slug}` matches the `slug` field in the frontmatter.\n\n'; } // Validation results for new-format files if (result.summary && result.summary.total > 0) { if (result.summary.failed === 0 && result.oldFormatFiles.length === 0) { body += '### Company profile validation passed!\n\n'; body += `All ${result.summary.total} company file(s) look good. Thanks for following the format!\n`; if (result.summary.warnings > 0) { body += '\n**Warnings:**\n'; for (const [file, res] of Object.entries(result.files)) { for (const w of res.warnings) { body += `- \`${file}\`: ${w}\n`; } } } } else if (result.summary.failed > 0) { if (!body) body += '### Thanks for your contribution!\n\n'; body += 'The following issues were found with your company profile(s). '; body += 'Please fix them and push an update to this PR:\n\n'; for (const [file, res] of Object.entries(result.files)) { if (res.errors.length === 0 && res.warnings.length === 0) continue; body += `**\`${file}\`**\n`; for (const e of res.errors) { body += `- :x: ${e}\n`; } for (const w of res.warnings) { body += `- :warning: ${w}\n`; } body += '\n'; } body += 'See the [company profile format docs](https://github.com/remoteintech/remote-jobs/blob/main/CLAUDE.md#company-profiles) for reference.\n'; } } if (!body) return; // Find and update existing bot comment, or create new one const marker = ''; body = marker + '\n' + body; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const existing = comments.find(c => c.body && c.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body, }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body, }); } env: VALIDATION_JSON: ${{ steps.validate.outputs.json }} EXIT_CODE: ${{ steps.validate.outputs.exit_code }} - name: Fail if validation errors if: steps.validate.outputs.exit_code == '1' run: | echo "Validation failed. See the PR comment for details." exit 1 ================================================ FILE: .gitignore ================================================ # Node modules node_modules npm-debug.log* yarn-debug.log* yarn-error.log* # generated files dist src/_includes/css src/_includes/scripts # cache .cache # secret data .env .env.* !.env.example # OS files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # Editor files .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .idea *.swp *.swo *~ # Test files coverage .nyc_output # Logs logs *.log # Temporary files tmp temp *.tmp # Package manager package-lock.json.bak yarn.lock.bak # Local tools and working files _tools backup-companies-old # Link check reports extracted-urls.txt link-check-results.csv link-check-results.csv.bak check-links.sh link-report.html ================================================ FILE: .nojekyll ================================================ ================================================ FILE: .nvmrc ================================================ 22 ================================================ FILE: .prettierignore ================================================ src/**/*.md src/_includes/components/**/custom-*.njk src/common/* src/_includes/scripts/* ================================================ FILE: .prettierrc ================================================ { "printWidth": 110, "tabWidth": 2, "singleQuote": true, "bracketSpacing": false, "quoteProps": "consistent", "trailingComma": "none", "arrowParens": "avoid", "plugins": ["prettier-plugin-jinja-template"], "overrides": [ { "files": "*.njk", "options": { "parser": "jinja-template" } } ] } ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md ## Shorthand - **"Punch it"** = commit, push, and open a PR to upstream, all in one go. ## Project Overview Remote In Tech is a community-maintained directory of remote-friendly tech companies. The site is built with [Eleventy](https://www.11ty.dev/) (v3) and deployed on Netlify. **Live site:** https://remoteintech.company ## Directory Structure ``` src/ ├── companies/ # Company profiles (Markdown with YAML frontmatter) ├── blog/ # Blog posts (Markdown) ├── pages/ # Static pages and dynamic templates (Nunjucks) ├── common/ # Shared templates (404, redirects, sitemap, feeds) ├── _layouts/ # Page layouts (base, page, post, company) ├── _includes/ # Partials, components, and WebC components ├── _config/ # Eleventy configuration (collections, filters, plugins) ├── _data/ # Global data files (navigation, meta, companyTags) └── assets/ # Static assets (CSS, JS, fonts, images) dist/ # Build output (gitignored) ``` ## Build Commands ```bash npm run start # Development server with hot reload npm run build # Production build (clean + eleventy + pagefind) npm run build:11ty # Eleventy build only (faster for testing) npm run clean # Remove dist and generated files ``` ## Company Profiles Companies are Markdown files in `src/companies/` with this frontmatter structure: ```yaml --- title: "Company Name" slug: company-slug # URL slug (required) website: https://example.com # Main company website URL careers_url: https://example.com/jobs # Optional: careers/jobs page URL region: worldwide # worldwide, americas, europe, asia-pacific, americas-europe, other remote_policy: fully-remote # fully-remote, remote-first, remote-friendly, hybrid company_size: startup # startup, small, medium, large, enterprise technologies: # Array of tech tags - javascript - python - devops addedAt: 2024-03-15 # Date first contributed (from git history) updatedAt: 2025-06-20 # Date of last real content change (from git history) --- ``` **URL fields:** - `website` - Main company URL (used to verify the company, identify brand) - `careers_url` - Optional careers/jobs page URL. When present, the "Apply Now" button links here; otherwise falls back to `website` Valid technology tags are defined in `src/_data/companyHelpers.js` under `techLabels`. ## Key Configuration Files - `eleventy.config.js` - Main Eleventy config (imports from src/_config/) - `src/_config/collections.js` - Company and blog collections - `src/_data/companyHelpers.js` - Tech labels, region labels, featured companies - `src/_data/companyTags.js` - Generates browse page tags with counts - `src/_data/meta.js` - Site metadata (title, description, analytics) - `src/common/_redirects.njk` - Netlify redirects (auto-generates company redirects) ## Coding Conventions - **Templates:** Nunjucks (.njk) for layouts and pages - **Content:** Markdown with YAML frontmatter for companies and blog posts - **Styles:** CUBE CSS methodology with Tailwind as a design token processor (not traditional utility classes) - **Components:** WebC components in `src/_includes/webc/` - **Local CSS:** Use `{%- css "local" -%}` blocks for page-specific styles - **Design Tokens:** Defined in `src/_data/designTokens/` (colors, spacing, typography) ## Collections Access these in templates: - `collections.companies` - All companies (alphabetically sorted) - `collections.featuredCompanies` - Randomly shuffled subset (8 of 12) from curated list - `collections.recentCompanies` - Recently added (by addedAt date) - `collections.companiesByRegion` - Grouped by region - `collections.companiesByTech` - Grouped by technology - `collections.allPosts` - Blog posts (reverse chronological) ## Search Site search is powered by [Pagefind](https://pagefind.app/): - **Nav search:** Dropdown search in the navigation bar (quick results) - **Search page:** `/search/` - Full search results page with pagination - Search index is built during `npm run build` via the pagefind post-process step ## Redirects Legacy URL redirects are auto-generated in `src/common/_redirects.njk`: - Old company URLs (`/company-slug`) redirect to new format (`/companies/company-slug/`) - Blog subdomain redirects from old WordPress site ## Analytics Fathom Analytics (privacy-focused) - only loads in production builds. - Site ID configured in `src/_data/meta.js` - 404 errors tracked via custom event with the attempted URL path ## SEO ### AI Bot Policy `src/common/robots.njk` generates robots.txt with a dual policy: - **Allowed:** AI search bots (ChatGPT-User, Claude-User, PerplexityBot, YouBot, Applebot-Extended) - **Blocked:** AI training crawlers (GPTBot, CCBot, ClaudeBot, Google-Extended, FacebookBot, anthropic-ai, cohere-ai) `AGENTS.md` is a symlink to `CLAUDE.md` for broader AI agent compatibility. ### Structured Data (JSON-LD) Schema.org markup in `src/_includes/schemas/`: - `WebSite.njk` - Site info with SearchAction for sitelinks search box - `BreadcrumbList.njk` - Auto-generated breadcrumb trail from URL path - `Organization.njk` - Company profile structured data - `BlogPosting.njk` - Blog post structured data Schemas are included via `src/_includes/head/schema.njk`. Page-specific schemas use the `schema` frontmatter field. ### Meta Descriptions Company pages auto-generate meta descriptions from the "Company blurb" section via computed data in `src/companies/companies.11tydata.js`. Descriptions are truncated to ~155 characters at sentence boundaries. ### Company Dates Company profiles have `addedAt` and `updatedAt` dates stored directly in frontmatter: - **`addedAt`** - Date when the company was first contributed to the project (traced through git history, including the pre-migration `company-profiles/` path) - **`updatedAt`** - Date of the last real content change (excludes bulk migrations and infrastructure commits). Some companies only have `addedAt` if no genuine content update occurred after the initial contribution. These dates were backfilled from a decade of git history using `git log --follow` to trace files through the October 2025 migration from `company-profiles/` to `src/companies/`. They are now static frontmatter values — no git commands run during build. The homepage "Recently Added" section uses `addedAt` to show the most recently added companies. Company profile pages display "Last updated: [date]" in the footer. ### Social Cards Twitter/X card meta tags are included in `src/_includes/head/meta-info.njk`: - `twitter:card` - summary_large_image - `twitter:title`, `twitter:description`, `twitter:image` Open Graph tags are also present for Facebook/LinkedIn sharing. ## Deployment - **Platform:** Netlify (auto-deploys from main branch) - **Build command:** `npm run build` - **Publish directory:** `dist` - **Node version:** 22 (specified in package.json engines) ## Versioning The site uses semantic versioning in `package.json`. The version displays in the footer and links to `/changelog/`. - **Major version bump**: Reserved for full site eras/redesigns (v1 = flat list, v2 = linked profiles, v3 = original live site, v4 = Eleventy rebuild) - **Minor version bump** (e.g. 4.3.0 → 4.4.0): Visible changes — new companies, blog posts, feature additions/removals, data changes visitors can see - **Patch version bump** (e.g. 4.4.0 → 4.4.1): Invisible changes — bug fixes, refactoring, documentation, CI/CD, performance improvements When bumping the version: 1. Update `version` in `package.json` 2. Add a new entry at the top of `src/_data/changelog.json` Changelog entries should be summaries. Call out each company addition by name, but summarise edits, deletions, and fixes. Change types: `added`, `changed`, `fixed`, `removed`. ## Contributing a Company 1. Create `src/companies/{slug}.md` with required frontmatter 2. Add company description in Markdown body 3. Run `npm run build` to verify it builds correctly 4. Submit PR to `remoteintech/remote-jobs` ## Processing Contributor PRs PRs that touch company files are automatically validated by the **Validate Company Profiles** Action (`.github/workflows/validate-companies.yml`). The bot posts a comment with specific feedback and blocks merging until issues are fixed. **Workflow:** 1. **Check the automated validation comment** on the PR for any issues 2. **If the contributor needs more guidance** beyond what the bot provided, leave a helpful comment explaining what to fix 3. **For old-format PRs** (files in `company-profiles/` or changes to `README.md`), the bot will explain the new format with a template — ask the contributor to update their PR 4. **Only re-create a PR yourself as a last resort** if the contributor is unresponsive after reasonable follow-up **Reject PRs that:** - Promote harmful services (hacking tools, spam, etc.) - Have minimal or no meaningful content - Are duplicates of existing companies ## GitHub Actions - **CI** (`.github/workflows/ci.yml`): Runs on push/PR to main. Builds the site with Node 22. - **Validate Company Profiles** (`.github/workflows/validate-companies.yml`): Runs on PRs that touch company files. Validates frontmatter, enum values, slug/filename match, URL format, and required sections. Posts a PR comment with results and blocks merge on errors. - **CodeQL** (`.github/workflows/codeql-analysis.yml`): Security scanning on push/PR and weekly schedule. ## Branch Protection The `main` branch has protection rules: - Requires pull request reviews (1 approval) - Requires `build` status check to pass - Only repository owner can push directly - Force pushes and deletions disabled - Admins can bypass when needed ## Link Checker A script to check all outbound URLs in company profiles for broken links and redirects. ### Files (gitignored) - `check-links.sh` - Link checker script - `extracted-urls.txt` - List of all URLs extracted from company files - `link-check-results.csv` - Results with status codes and explanations - `link-report.html` - Interactive HTML report for viewing results ### Commands ```bash # Full check - all URLs (~8 min for ~2,200 URLs) ./check-links.sh # Quick check - only re-check URLs that weren't OK last time ./check-links.sh --quick # Refresh - re-extract URLs from company files, then full check ./check-links.sh --refresh ``` ### Viewing Results Open `link-report.html` directly in a browser and load `link-check-results.csv` via the file picker (or drag and drop). ### CSV Columns | Column | Description | |--------|-------------| | `source_file` | Company profile containing the link | | `original_url` | URL as it appears in the file | | `resolved_url` | Final URL after redirects | | `status_code` | HTTP status (200, 404, ERROR, etc.) | | `explanation` | What happened (OK, Redirects, Not found, etc.) | | `no_change_needed` | Yes / No / Review | ### Workflow 1. Run `./check-links.sh` for initial full scan 2. Fix broken links in company profiles 3. Run `./check-links.sh --quick` to verify fixes 4. Repeat until satisfied ================================================ FILE: LICENSE ================================================ ## Licenses This project is licensed under the ISC License. Additionally, it includes components that are licensed under the MIT and SIL License. ### ISC License Copyright (c) 2024 Lene Saile Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ### MIT License The **Cube Boilerplate** is licensed under the MIT License: Copyright (c) 2024 Set Studio Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ### SIL OPEN FONT LICENSE The Fonts **Red Hat Display** and **Atkinson Hyperlegible** are licensed under the SIL License, Version 1.1. Copyright (c) Red Hat, Inc. and (c) Braille Institute. This license allows - Commercial use - Modifying - Redistribution - Full license: https://openfontlicense.org/ ================================================ FILE: README.md ================================================ # Remote In Tech **[remoteintech.company](https://remoteintech.company)** — A community-maintained directory of remote-friendly tech companies. > This repository is the source code for the site. To browse the directory, visit **[remoteintech.company](https://remoteintech.company)**. ## Contributing a Company We welcome contributions! To add a company to the directory: 1. Create a file at `src/companies/{company-slug}.md` 2. Use the frontmatter template below 3. Add a company description in the Markdown body 4. Run `npm run build` to verify it builds 5. Submit a PR to this repo ### Company Profile Template ```yaml --- title: "Company Name" slug: company-slug website: https://example.com careers_url: https://example.com/careers region: worldwide # worldwide, americas, europe, asia-pacific, americas-europe, other remote_policy: fully-remote # fully-remote, remote-first, remote-friendly, hybrid company_size: small # startup, small, medium, large, enterprise technologies: - javascript - python --- ## Company blurb A short description of the company and what they do. ``` PRs are automatically validated — the bot will comment with any issues to fix. ## Development ```bash npm install # Install dependencies npm run start # Dev server with hot reload npm run build # Production build ``` Requires Node.js 22+. ## Credits Built with [Eleventy Excellent](https://github.com/madrilene/eleventy-excellent) by [Lene Saile](https://github.com/madrilene). ## License ISC ================================================ FILE: eleventy.config.js ================================================ /** * Most adjustments must be made in `./src/_config/*` * * Hint VS Code for eleventyConfig autocompletion. * © Henry Desroches - https://gist.github.com/xdesro/69583b25d281d055cd12b144381123bf * @param {import("@11ty/eleventy/src/UserConfig")} eleventyConfig - * @returns {Object} - */ // register dotenv for process.env.* variables to pickup import dotenv from 'dotenv'; dotenv.config(); // add yaml support import yaml from 'js-yaml'; // config import import { getAllPosts, getAllCompanies, getFeaturedCompanies, getRecentCompanies, getCompaniesByRegion, getCompaniesByTech, getCompanyTags, showInSitemap, tagList } from './src/_config/collections.js'; import events from './src/_config/events.js'; import filters from './src/_config/filters.js'; import plugins from './src/_config/plugins.js'; import shortcodes from './src/_config/shortcodes.js'; export default async function (eleventyConfig) { // --------------------- Events: before build eleventyConfig.on('eleventy.before', async () => { await events.buildAllCss(); await events.buildAllJs(); }); // --------------------- custom wtach targets eleventyConfig.addWatchTarget('./src/assets/**/*.{css,js,svg,png,jpeg}'); eleventyConfig.addWatchTarget('./src/_includes/**/*.{webc}'); // --------------------- layout aliases eleventyConfig.addLayoutAlias('base', 'base.njk'); eleventyConfig.addLayoutAlias('page', 'page.njk'); eleventyConfig.addLayoutAlias('post', 'post.njk'); eleventyConfig.addLayoutAlias('company', 'company.njk'); eleventyConfig.addLayoutAlias('tags', 'tags.njk'); // --------------------- Collections eleventyConfig.addCollection('allPosts', getAllPosts); eleventyConfig.addCollection('companies', getAllCompanies); eleventyConfig.addCollection('featuredCompanies', getFeaturedCompanies); eleventyConfig.addCollection('recentCompanies', getRecentCompanies); eleventyConfig.addCollection('companiesByRegion', getCompaniesByRegion); eleventyConfig.addCollection('companiesByTech', getCompaniesByTech); eleventyConfig.addCollection('companyTags', getCompanyTags); eleventyConfig.addCollection('showInSitemap', showInSitemap); eleventyConfig.addCollection('tagList', tagList); // --------------------- Plugins eleventyConfig.addPlugin(plugins.htmlConfig); eleventyConfig.addPlugin(plugins.drafts); eleventyConfig.addPlugin(plugins.EleventyRenderPlugin); eleventyConfig.addPlugin(plugins.rss); eleventyConfig.addPlugin(plugins.syntaxHighlight); eleventyConfig.addPlugin(plugins.webc, { components: ['./src/_includes/webc/**/*.webc'], useTransform: true }); eleventyConfig.addPlugin(plugins.eleventyImageTransformPlugin, { formats: ['webp', 'jpeg'], widths: ['auto'], htmlOptions: { imgAttributes: { loading: 'lazy', decoding: 'async', sizes: 'auto' }, pictureAttributes: {} } }); // --------------------- bundle eleventyConfig.addBundle('css', {hoist: true}); // --------------------- Library and Data eleventyConfig.setLibrary('md', plugins.markdownLib); eleventyConfig.addDataExtension('yaml', contents => yaml.load(contents)); // --------------------- Filters eleventyConfig.addFilter('toIsoString', filters.toISOString); eleventyConfig.addFilter('formatDate', filters.formatDate); eleventyConfig.addFilter('markdownFormat', filters.markdownFormat); eleventyConfig.addFilter('splitlines', filters.splitlines); eleventyConfig.addFilter('striptags', filters.striptags); eleventyConfig.addFilter('alphabetic', filters.sortAlphabetically); eleventyConfig.addFilter('slugify', filters.slugifyString); eleventyConfig.addFilter('fileExists', filters.fileExists); eleventyConfig.addFilter('split', filters.split); // --------------------- Shortcodes eleventyConfig.addShortcode('svg', shortcodes.svgShortcode); eleventyConfig.addShortcode('image', shortcodes.imageShortcode); eleventyConfig.addShortcode('year', () => `${new Date().getFullYear()}`); // --------------------- Events: after build if (process.env.ELEVENTY_RUN_MODE === 'serve') { eleventyConfig.on('eleventy.after', events.svgToJpeg); } // --------------------- Passthrough File Copy // -- same path ['src/assets/fonts/', 'src/assets/images/template', 'src/assets/og-images', 'src/assets/scripts/'].forEach(path => eleventyConfig.addPassthroughCopy(path) ); eleventyConfig.addPassthroughCopy({ // -- to root 'src/assets/images/favicon/*': '/', // -- node_modules 'node_modules/lite-youtube-embed/src/lite-yt-embed.{css,js}': `assets/components/` }); // --------------------- general config return { markdownTemplateEngine: 'njk', dir: { output: 'dist', input: 'src', includes: '_includes', layouts: '_layouts' } }; } ================================================ FILE: package.json ================================================ { "name": "remote-in-tech", "version": "4.4.1", "description": "A list of semi to fully remote-friendly companies in or around tech", "author": "Doug Aitken", "license": "ISC", "type": "module", "engines": { "node": ">=22" }, "scripts": { "clean": "rimraf dist src/_includes/css src/_includes/scripts", "clean:og": "rimraf src/assets/og-images", "favicons": "node ./src/_config/setup/generate-favicons.js", "colors": "node ./src/_config/setup/create-colors.js", "dev:11ty": "cross-env ELEVENTY_ENV=development eleventy --serve", "build:11ty": "cross-env ELEVENTY_ENV=production eleventy", "pagefind": "npx pagefind --site dist --glob \"**/*.html\"", "start": "npm run dev:11ty", "build": "npm run clean && npm run build:11ty && npm run pagefind" }, "keywords": [], "repository": { "type": "git", "url": "https://github.com/remoteintech/remote-jobs.git" }, "dependencies": { "@11ty/eleventy": "^3.1.2", "@11ty/eleventy-fetch": "^5.1.0", "@11ty/eleventy-img": "^6.0.4", "@11ty/eleventy-plugin-rss": "^2.0.4", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "@11ty/eleventy-plugin-webc": "^0.11.2", "@11ty/is-land": "^4.0.1", "lite-youtube-embed": "^0.3.3", "tailwindcss": "^3.4.17" }, "overrides": { "nanoid": "^5.0.9" }, "devDependencies": { "@toycode/markdown-it-class": "^1.2.4", "autoprefixer": "^10.4.24", "colorjs.io": "^0.5.2", "cross-env": "^10.1.0", "cssnano": "^7.1.2", "dayjs": "^1.11.18", "dotenv": "^17.2.3", "esbuild": "^0.25.10", "fast-glob": "^3.3.3", "html-minifier-terser": "^7.2.0", "js-yaml": "^4.1.0", "markdown-it": "^14.1.1", "markdown-it-abbr": "^2.0.0", "markdown-it-anchor": "^9.2.0", "markdown-it-attrs": "^4.3.1", "markdown-it-emoji": "^3.0.0", "markdown-it-footnote": "^4.0.0", "markdown-it-link-attributes": "^4.0.1", "markdown-it-mark": "^4.0.0", "markdown-it-prism": "^3.0.0", "netlify-plugin-cache": "^1.0.3", "pagefind": "^1.2.0", "postcss": "^8.5.6", "postcss-cli": "^11.0.1", "postcss-import": "^16.1.1", "postcss-import-ext-glob": "^2.1.1", "postcss-js": "^5.0.3", "prettier-plugin-jinja-template": "^2.1.0", "rimraf": "^6.1.2", "sanitize-html": "^2.17.1", "sharp": "^0.34.5", "sharp-ico": "^0.1.5", "slugify": "^1.6.6", "svgo": "^4.0.0" } } ================================================ FILE: src/_config/collections.js ================================================ import { featuredCompanySlugs, regionLabels, remotePolicyLabels, techLabels } from '../_data/companyHelpers.js'; import { shuffleArray } from './filters/sort-random.js'; /** Memoized glob results — avoids filtering ~850 items 6 times */ let _companyCache = null; const getCompanies = collection => { if (!_companyCache) { _companyCache = collection.getFilteredByGlob('./src/companies/**/*.md'); } return _companyCache; }; /** All blog posts as a collection. */ export const getAllPosts = collection => { return collection.getFilteredByGlob('./src/blog/**/*.md').reverse(); }; /** All company profiles as a collection, sorted alphabetically */ export const getAllCompanies = collection => { return [...getCompanies(collection)].sort((a, b) => { const nameA = (a.data.title || '').toLowerCase(); const nameB = (b.data.title || '').toLowerCase(); return nameA.localeCompare(nameB); }); }; /** Featured companies - randomly selected from curated list */ export const getFeaturedCompanies = collection => { const companies = getCompanies(collection); const matched = featuredCompanySlugs .map(slug => companies.find(c => c.data.slug === slug || c.fileSlug === slug)) .filter(Boolean); return shuffleArray(matched).slice(0, 8); }; /** Recently added companies (by addedAt date from frontmatter) */ export const getRecentCompanies = collection => { return [...getCompanies(collection)] .filter(c => c.data.addedAt) .sort((a, b) => b.data.addedAt - a.data.addedAt) .slice(0, 12); }; /** Companies grouped by region */ export const getCompaniesByRegion = collection => { const companies = getCompanies(collection); const regionGroups = {}; // Initialize all regions Object.keys(regionLabels).forEach(region => { regionGroups[region] = []; }); companies.forEach(company => { const region = company.data.region || 'other'; if (!regionGroups[region]) { regionGroups[region] = []; } regionGroups[region].push(company); }); return regionGroups; }; /** Companies grouped by technology */ export const getCompaniesByTech = collection => { const companies = getCompanies(collection); const techGroups = {}; // Initialize all technologies Object.keys(techLabels).forEach(tech => { techGroups[tech] = []; }); companies.forEach(company => { const technologies = company.data.technologies || []; technologies.forEach(tech => { if (!techGroups[tech]) { techGroups[tech] = []; } techGroups[tech].push(company); }); }); return techGroups; }; /** All relevant pages as a collection for sitemap.xml */ export const showInSitemap = collection => { return collection.getFilteredByGlob('./src/**/*.{md,njk}'); }; /** All tags from blog posts as a collection - excluding custom collections */ export const tagList = collection => { const tagsSet = new Set(); // Only get tags from blog posts, not from companies or other content collection.getFilteredByGlob('./src/blog/**/*.md').forEach(item => { if (!item.data.tags || !Array.isArray(item.data.tags)) return; item.data.tags .filter(tag => typeof tag === 'string' && !['posts', 'docs', 'all'].includes(tag)) .forEach(tag => tagsSet.add(tag)); }); return Array.from(tagsSet).sort(); }; /** Tag type definitions with metadata */ const tagTypes = { technology: { labels: techLabels, plural: 'Technologies', description: 'Companies using this technology' }, region: { labels: regionLabels, plural: 'Regions', description: 'Companies hiring in this region' }, 'remote-policy': { labels: remotePolicyLabels, plural: 'Remote Policies', description: 'Companies with this remote work policy' } }; /** * Company tags for browse/tag pages — derived from Eleventy collections * instead of manual file I/O. Replaces the old companyTags.js data file. */ export const getCompanyTags = collection => { const companies = getCompanies(collection); const tagData = { technology: {}, region: {}, 'remote-policy': {} }; for (const company of companies) { const d = company.data; const companyInfo = { title: d.title || company.fileSlug, slug: d.slug || company.fileSlug, website: d.website, region: d.region, remote_policy: d.remote_policy }; // Technologies if (Array.isArray(d.technologies)) { for (const tech of d.technologies) { if (!tagData.technology[tech]) tagData.technology[tech] = []; tagData.technology[tech].push(companyInfo); } } // Region if (d.region) { if (!tagData.region[d.region]) tagData.region[d.region] = []; tagData.region[d.region].push(companyInfo); } // Remote policy if (d.remote_policy) { if (!tagData['remote-policy'][d.remote_policy]) tagData['remote-policy'][d.remote_policy] = []; tagData['remote-policy'][d.remote_policy].push(companyInfo); } } // Build final tag list with metadata const allTags = []; for (const [type, tags] of Object.entries(tagData)) { const typeInfo = tagTypes[type]; for (const [slug, tagCompanies] of Object.entries(tags)) { tagCompanies.sort((a, b) => a.title.localeCompare(b.title)); allTags.push({ slug, type, label: typeInfo.labels[slug] || slug, description: typeInfo.description, typePlural: typeInfo.plural, count: tagCompanies.length, companies: tagCompanies }); } } allTags.sort((a, b) => b.count - a.count); return allTags; }; ================================================ FILE: src/_config/events/build-css.js ================================================ import fs from 'node:fs/promises'; import path from 'node:path'; import postcss from 'postcss'; import postcssImport from 'postcss-import'; import postcssImportExtGlob from 'postcss-import-ext-glob'; import tailwindcss from 'tailwindcss'; import autoprefixer from 'autoprefixer'; import cssnano from 'cssnano'; import fg from 'fast-glob'; const buildCss = async (inputPath, outputPaths) => { const inputContent = await fs.readFile(inputPath, 'utf-8'); const result = await postcss([ postcssImportExtGlob, postcssImport, tailwindcss, autoprefixer, cssnano ]).process(inputContent, {from: inputPath}); for (const outputPath of outputPaths) { await fs.mkdir(path.dirname(outputPath), {recursive: true}); await fs.writeFile(outputPath, result.css); } return result.css; }; export const buildAllCss = async () => { const tasks = []; tasks.push(buildCss('src/assets/css/global/global.css', ['src/_includes/css/global.css'])); const localCssFiles = await fg(['src/assets/css/local/**/*.css']); for (const inputPath of localCssFiles) { const baseName = path.basename(inputPath); tasks.push(buildCss(inputPath, [`src/_includes/css/${baseName}`])); } const componentCssFiles = await fg(['src/assets/css/components/**/*.css']); for (const inputPath of componentCssFiles) { const baseName = path.basename(inputPath); tasks.push(buildCss(inputPath, [`dist/assets/css/components/${baseName}`])); } await Promise.all(tasks); }; ================================================ FILE: src/_config/events/build-js.js ================================================ import fs from 'node:fs/promises'; import path from 'node:path'; import fg from 'fast-glob'; import esbuild from 'esbuild'; export const buildJs = async (inputPath, outputPath) => { const result = await esbuild.build({ target: 'es2020', entryPoints: [inputPath], bundle: true, minify: true, write: false }); const output = result.outputFiles[0].text; await fs.mkdir(path.dirname(outputPath), {recursive: true}); await fs.writeFile(outputPath, output); return output; }; export const buildAllJs = async () => { const tasks = []; const inlineBundleFiles = await fg(['src/assets/scripts/bundle/**/*.js']); for (const inputPath of inlineBundleFiles) { const baseName = path.basename(inputPath); const outputPath = `src/_includes/scripts/${baseName}`; tasks.push(buildJs(inputPath, outputPath)); } const componentFiles = await fg(['src/assets/scripts/components/**/*.js']); for (const inputPath of componentFiles) { const baseName = path.basename(inputPath); const outputPath = `dist/assets/scripts/components/${baseName}`; tasks.push(buildJs(inputPath, outputPath)); } await Promise.all(tasks); }; ================================================ FILE: src/_config/events/svg-to-jpeg.js ================================================ import {promises as fsPromises, existsSync} from 'node:fs'; import path from 'node:path'; import Image from '@11ty/eleventy-img'; const ogImagesDir = './src/assets/og-images'; export const svgToJpeg = async () => { const socialPreviewImagesDir = 'dist/assets/og-images/'; if (!existsSync(socialPreviewImagesDir)) { console.log('⚠ No OG images dir found'); return; } const files = await fsPromises.readdir(socialPreviewImagesDir); if (files.length > 0) { files.forEach(async function (filename) { const outputFilename = filename.substring(0, filename.length - 4); if (filename.endsWith('.svg') & !existsSync(path.join(ogImagesDir, outputFilename))) { const imageUrl = socialPreviewImagesDir + filename; await Image(imageUrl, { formats: ['jpeg'], outputDir: ogImagesDir, filenameFormat: function (id, src, width, format, options) { return `${outputFilename}.${format}`; } }); } }); } else { console.log('⚠ No images found on OG images dir'); } }; ================================================ FILE: src/_config/events.js ================================================ import {svgToJpeg} from './events/svg-to-jpeg.js'; import {buildAllCss} from './events/build-css.js'; import {buildAllJs} from './events/build-js.js'; export default { svgToJpeg, buildAllCss, buildAllJs }; ================================================ FILE: src/_config/filters/dates.js ================================================ import dayjs from 'dayjs'; /** Converts the given date string to ISO8610 format. */ export const toISOString = dateString => dayjs(dateString).toISOString(); /** Formats a date using dayjs's conventions: https://day.js.org/docs/en/display/format */ export const formatDate = (date, format) => dayjs(date).format(format); ================================================ FILE: src/_config/filters/fileExists.js ================================================ import fs from 'fs'; import path from 'path'; export const fileExists = (filePath) => { try { return fs.existsSync(path.resolve(process.cwd(), filePath)); } catch (e) { return false; } }; ================================================ FILE: src/_config/filters/markdown-format.js ================================================ // by Chris Burnell: https://chrisburnell.com/article/some-eleventy-filters/#markdown-format import markdownParser from 'markdown-it'; const markdown = markdownParser(); export const markdownFormat = string => { return markdown.render(string); }; ================================================ FILE: src/_config/filters/slugify.js ================================================ import slugify from 'slugify'; /** Converts string to a slug form. */ export const slugifyString = str => { return slugify(str, { replacement: '-', remove: /[#,&,+()$~%.'":*¿?¡!<>{}]/g, lower: true }); }; ================================================ FILE: src/_config/filters/sort-alphabetic.js ================================================ export const sortAlphabetically = array => { return array.sort((a, b) => { if (a.data.title < b.data.title) return -1; if (a.data.title > b.data.title) return 1; return 0; }); }; ================================================ FILE: src/_config/filters/sort-random.js ================================================ export const shuffleArray = array => { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }; ================================================ FILE: src/_config/filters/split.js ================================================ /** * Split a string by a delimiter, filtering out empty strings * @param {string} str - String to split * @param {string} delimiter - Delimiter to split by * @returns {string[]} Array of non-empty parts */ export function split(str, delimiter = '/') { if (!str) return []; return str.split(delimiter).filter(part => part.length > 0); } ================================================ FILE: src/_config/filters/splitlines.js ================================================ export const splitlines = (input, maxCharLength) => { const parts = input.split(' '); const lines = parts.reduce(function (acc, cur) { if (!acc.length) { return [cur]; } let lastOne = acc[acc.length - 1]; if (lastOne.length + cur.length >= maxCharLength) { return [...acc, cur]; } acc[acc.length - 1] = lastOne + ' ' + cur; return acc; }, []); return lines; }; ================================================ FILE: src/_config/filters/striptags.js ================================================ // Using single-character replacement as recommended by CodeQL to avoid // incomplete multi-character sanitization vulnerabilities export const striptags = string => { if (!string) return ''; return String(string).replace(/<|>/g, ''); }; ================================================ FILE: src/_config/filters.js ================================================ import {toISOString, formatDate} from './filters/dates.js'; import {markdownFormat} from './filters/markdown-format.js'; import {sortAlphabetically} from './filters/sort-alphabetic.js'; import {splitlines} from './filters/splitlines.js'; import {striptags} from './filters/striptags.js'; import {slugifyString} from './filters/slugify.js'; import {fileExists} from './filters/fileExists.js'; import {split} from './filters/split.js'; export default { toISOString, formatDate, markdownFormat, splitlines, striptags, sortAlphabetically, fileExists, slugifyString, split }; ================================================ FILE: src/_config/plugins/drafts.js ================================================ export const drafts = eleventyConfig => { eleventyConfig.addGlobalData('eleventyComputed.permalink', function () { return data => { // Always skip during non-watch/serve builds if (data.draft && !process.env.BUILD_DRAFTS) { return false; // Ensure templates that use this handle it correctly } return data.permalink; }; }); // When `eleventyExcludeFromCollections` is true, the file is not included in any collections eleventyConfig.addGlobalData('eleventyComputed.eleventyExcludeFromCollections', function () { return data => { // Always exclude from non-watch/serve builds if (data.draft && !process.env.BUILD_DRAFTS) { return true; } return data.eleventyExcludeFromCollections ?? false; }; }); eleventyConfig.on('eleventy.before', ({runMode}) => { // Set the environment variable if (runMode === 'serve' || runMode === 'watch') { process.env.BUILD_DRAFTS = true; } }); }; ================================================ FILE: src/_config/plugins/html-config.js ================================================ import htmlmin from 'html-minifier-terser'; const isProduction = process.env.ELEVENTY_ENV === 'production'; export const htmlConfig = eleventyConfig => { eleventyConfig.addTransform('html-minify', (content, path) => { if (path && path.endsWith('.html') && isProduction) { return htmlmin.minify(content, { collapseBooleanAttributes: true, collapseWhitespace: true, decodeEntities: true, includeAutoGeneratedTags: false, removeComments: true }); } return content; }); }; ================================================ FILE: src/_config/plugins/markdown.js ================================================ import markdownIt from 'markdown-it'; import markdownItAttrs from 'markdown-it-attrs'; import markdownItPrism from 'markdown-it-prism'; import markdownItAnchor from 'markdown-it-anchor'; import markdownItClass from '@toycode/markdown-it-class'; import markdownItLinkAttributes from 'markdown-it-link-attributes'; import {full as markdownItEmoji} from 'markdown-it-emoji'; import markdownItFootnote from 'markdown-it-footnote'; import markdownitMark from 'markdown-it-mark'; import markdownitAbbr from 'markdown-it-abbr'; import {slugifyString} from '../filters/slugify.js'; export const markdownLib = markdownIt({ html: true, breaks: true, linkify: true, typographer: true }) .disable('code') .use(markdownItAttrs) .use(markdownItPrism, { defaultLanguage: 'plaintext' }) .use(markdownItAnchor, { slugify: slugifyString, tabIndex: false, permalink: markdownItAnchor.permalink.headerLink({ class: 'heading-anchor' }) }) .use(markdownItClass, {}) .use(markdownItLinkAttributes, [ { // match external links matcher(href) { return href.match(/^https?:\/\//); }, attrs: { rel: 'noopener' } } ]) .use(markdownItEmoji) .use(markdownItFootnote) .use(markdownitMark) .use(markdownitAbbr) .use(md => { md.renderer.rules.image = (tokens, idx) => { const token = tokens[idx]; const src = token.attrGet('src'); const alt = token.content || ''; const caption = token.attrGet('title'); // Collect attributes const attributes = token.attrs || []; const hasEleventyWidths = attributes.some(([key]) => key === 'eleventy:widths'); if (!hasEleventyWidths) { attributes.push(['eleventy:widths', '650,960,1400']); } const attributesString = attributes.map(([key, value]) => `${key}="${value}"`).join(' '); const imgTag = `${alt}`; return caption ? `
${imgTag}
${caption}
` : imgTag; }; }); ================================================ FILE: src/_config/plugins.js ================================================ // Eleventy import {EleventyRenderPlugin} from '@11ty/eleventy'; import rss from '@11ty/eleventy-plugin-rss'; import syntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight'; import webc from '@11ty/eleventy-plugin-webc'; import {eleventyImageTransformPlugin} from '@11ty/eleventy-img'; // custom import {markdownLib} from './plugins/markdown.js'; import {drafts} from './plugins/drafts.js'; // Custom transforms import {htmlConfig} from './plugins/html-config.js'; export default { EleventyRenderPlugin, rss, syntaxHighlight, webc, eleventyImageTransformPlugin, markdownLib, drafts, htmlConfig }; ================================================ FILE: src/_config/setup/create-colors.js ================================================ import fs from 'node:fs'; import Color from 'colorjs.io'; const colorsBase = JSON.parse(fs.readFileSync('./src/_data/designTokens/colorsBase.json', 'utf-8')); const generatePalette = (baseColorHex, steps) => { const baseColor = new Color(baseColorHex).to('oklch'); return steps.map(step => { const color = new Color('oklch', [step.lightness, baseColor.c * step.chromaFactor, baseColor.h]).to( 'srgb' ); const [r, g, b] = color.coords.map(value => Math.round(Math.min(Math.max(value * 255, 0), 255))); const hexValue = `#${r.toString(16).padStart(2, '0')}${g .toString(16) .padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; return { name: `${step.label}`, value: hexValue }; }); }; const vibrantSteps = [ {label: '100', lightness: 0.96, chromaFactor: 0.19}, {label: '200', lightness: 0.94, chromaFactor: 0.45}, {label: '300', lightness: 0.86, chromaFactor: 0.78}, {label: '400', lightness: 0.75, chromaFactor: 0.9}, {label: '500', lightness: 0.62, chromaFactor: 1}, {label: '600', lightness: 0.5, chromaFactor: 1}, {label: '700', lightness: 0.42, chromaFactor: 1}, {label: '800', lightness: 0.36, chromaFactor: 0.85}, {label: '900', lightness: 0.2, chromaFactor: 0.55} ]; const neutralSteps = [ {label: '100', lightness: 0.98, chromaFactor: 0.12}, {label: '200', lightness: 0.92, chromaFactor: 0.14}, {label: '300', lightness: 0.75, chromaFactor: 0.14}, {label: '400', lightness: 0.6, chromaFactor: 0.25}, {label: '500', lightness: 0.5, chromaFactor: 0.3}, {label: '600', lightness: 0.4, chromaFactor: 0.35}, {label: '700', lightness: 0.35, chromaFactor: 0.3}, {label: '800', lightness: 0.3, chromaFactor: 0.27}, {label: '900', lightness: 0.2, chromaFactor: 0.25} ]; const colorTokens = { title: colorsBase.title, description: colorsBase.description, items: [] }; colorsBase.shades_neutral.forEach(color => { const palette = generatePalette(color.value, neutralSteps); palette.forEach(shade => { colorTokens.items.push({ name: `${color.name} ${shade.name}`, value: shade.value }); }); }); colorsBase.shades_vibrant.forEach(color => { const palette = generatePalette(color.value, vibrantSteps); palette.forEach(shade => { colorTokens.items.push({ name: `${color.name} ${shade.name}`, value: shade.value }); }); }); colorsBase.light_dark.forEach(color => { colorTokens.items.push({ name: color.name, value: color.value }); const lightDark = new Color(color.value).to('oklch'); const subduedColor = new Color('oklch', [ lightDark.l, lightDark.c * 0.8, // reduce chroma by 20% lightDark.h ]).to('srgb'); const [r, g, b] = subduedColor.coords.map(value => Math.round(Math.min(Math.max(value * 255, 0), 255))); const subduedHex = `#${r.toString(16).padStart(2, '0')}${g .toString(16) .padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; colorTokens.items.push({ name: `${color.name} Subdued`, value: subduedHex }); }); colorsBase.standalone.forEach(color => { colorTokens.items.push({ name: color.name, value: color.value }); }); fs.writeFileSync('./src/_data/designTokens/colors.json', JSON.stringify(colorTokens, null, 2)); ================================================ FILE: src/_config/setup/generate-favicons.js ================================================ import fs from 'node:fs'; import sharp from 'sharp'; import {sharpsToIco} from 'sharp-ico'; import {pathToSvgLogo} from '../../_data/meta.js'; async function createFavicons() { const outputDir = 'src/assets/images/favicon'; fs.mkdirSync(outputDir, {recursive: true}); // Get the SVG logo const svgBuffer = fs.readFileSync(pathToSvgLogo); // SVG icon fs.writeFileSync(`${outputDir}/favicon.svg`, svgBuffer); // PNG icons await sharp(svgBuffer).resize(192, 192).toFile(`${outputDir}/icon-192x192.png`); await sharp(svgBuffer).resize(512, 512).toFile(`${outputDir}/icon-512x512.png`); await sharp(svgBuffer).resize(180, 180).toFile(`${outputDir}/apple-touch-icon.png`); // maskable icon await sharp(svgBuffer) .resize(512, 512) .extend({ top: 50, bottom: 50, left: 50, right: 50, background: {r: 0, g: 0, b: 0, alpha: 0} // Transparent padding }) .toFile(`${outputDir}/maskable-icon.png`); // ICO icon const iconSharp = sharp(svgBuffer); await sharpsToIco([iconSharp], `${outputDir}/favicon.ico`, {sizes: [32]}); console.log('All favicons generated.'); } createFavicons(); ================================================ FILE: src/_config/shortcodes/image.js ================================================ import Image from '@11ty/eleventy-img'; import path from 'node:path'; import fs from 'fs'; const stringifyAttributes = attributeMap => { return Object.entries(attributeMap) .map(([attribute, value]) => { if (typeof value === 'undefined') return ''; return `${attribute}="${value}"`; }) .join(' '); }; export const imageShortcode = async ( src, alt = '', caption = '', loading = 'lazy', containerClass, imageClass, widths = [650, 960, 1400], sizes = 'auto', formats = ['avif', 'webp', 'jpeg'] ) => { // Prepend "./src" if not present if (!src.startsWith('./src')) { src = `./src${src}`; } // Check if file exists if (!fs.existsSync(src)) { console.warn(`Image not found: ${src}`); return ''; } const metadata = await Image(src, { widths: [...widths], formats: [...formats], urlPath: '/assets/images/', outputDir: './dist/assets/images/', filenameFormat: (id, src, width, format, options) => { const extension = path.extname(src); const name = path.basename(src, extension); return `${name}-${width}w.${format}`; } }); const lowsrc = metadata.jpeg[metadata.jpeg.length - 1]; const imageSources = Object.values(metadata) .map(imageFormat => { return ` `; }) .join('\n'); const imageAttributes = stringifyAttributes({ 'src': lowsrc.url, 'width': lowsrc.width, 'height': lowsrc.height, alt, loading, 'decoding': loading === 'eager' ? 'sync' : 'async', ...(imageClass && {class: imageClass}), 'eleventy:ignore': '' }); const pictureElement = ` ${imageSources}`; return caption ? `
${pictureElement}
${caption}
` : `${imageSources}`; }; ================================================ FILE: src/_config/shortcodes/svg.js ================================================ /** * Generates an optimized SVG shortcode with optional attributes. * * @param {string} svgName - The name of the SVG file (without the .svg extension). * @param {string} [ariaName=''] - The ARIA label for the SVG. * @param {string} [className=''] - The CSS class name for the SVG. * @param {string} [styleName=''] - The inline style for the SVG. * @returns {Promise} The optimized SVG shortcode. */ import {optimize} from 'svgo'; import {readFileSync} from 'node:fs'; export const svgShortcode = async (svgName, ariaName = '', className = '', styleName = '') => { const svgData = readFileSync(`./src/assets/svg/${svgName}.svg`, 'utf8'); const {data} = await optimize(svgData); return data.replace( //, `` ); }; ================================================ FILE: src/_config/shortcodes.js ================================================ import {imageShortcode} from './shortcodes/image.js'; import {svgShortcode} from './shortcodes/svg.js'; export default {imageShortcode, svgShortcode}; ================================================ FILE: src/_config/utils/clamp-generator.js ================================================ /** * Credits: * - © Andy Bell - https://buildexcellentwebsit.es/ */ /** * Takes an array of tokens and sends back and array of name * and clamp pairs for CSS fluid values. * * @param {array} tokens array of {name: string, min: number, max: number} * @returns {array} {name: string, value: string} */ import viewports from '../../_data/designTokens/viewports.json'; export const clampGenerator = tokens => { const rootSize = 16; return tokens.map(({name, min, max}) => { if (min === max) { return {name, value:`${min / rootSize}rem`}; } // Convert the min and max sizes to rems const minSize = min / rootSize; const maxSize = max / rootSize; // Convert the pixel viewport sizes into rems const minViewport = viewports.min / rootSize; const maxViewport = viewports.max / rootSize; // Slope and intersection allow us to have a fluid value but also keep that sensible const slope = (maxSize - minSize) / (maxViewport - minViewport); const intersection = -1 * minViewport * slope + minSize; return { name, value: `clamp(${minSize}rem, ${intersection.toFixed(2)}rem + ${(slope * 100).toFixed( 2 )}vw, ${maxSize}rem)` }; }); }; ================================================ FILE: src/_config/utils/tokens-to-tailwind.js ================================================ /** * Credits: * - © Andy Bell - https://buildexcellentwebsit.es/ */ /** * Converts human readable tokens into tailwind config friendly ones * * @param {array} tokens {name: string, value: any} * @return {object} {key, value} */ import slugify from 'slugify'; export const tokensToTailwind = tokens => { const nameSlug = text => slugify(text, {lower: true}); let response = {}; tokens.forEach(({name, value}) => { response[nameSlug(name)] = value; }); return response; }; ================================================ FILE: src/_data/changelog.json ================================================ [ { "version": "4.4.1", "date": "2026-02-22", "summary": "Date field documentation", "changes": [ { "type": "changed", "description": "Updated CLAUDE.md, contributing guide, PR template, and validation workflow to document addedAt/updatedAt fields" } ] }, { "version": "4.4.0", "date": "2026-02-22", "summary": "Accurate company dates from git history", "changes": [ { "type": "changed", "description": "Backfilled accurate addedAt/updatedAt dates for all 850+ company profiles from a decade of git history" }, { "type": "fixed", "description": "Build time reduced from ~29s to ~18s by storing dates in frontmatter instead of computing from git on every build" } ] }, { "version": "4.3.2", "date": "2026-02-22", "summary": "Fairer homepage rotation", "changes": [ { "type": "fixed", "description": "Replaced biased shuffle with Fisher-Yates algorithm for featured and recently added companies" } ] }, { "version": "4.3.1", "date": "2026-02-22", "summary": "Easter egg removal", "changes": [ { "type": "removed", "description": "Removed easter egg confetti component" } ] }, { "version": "4.3.0", "date": "2026-02-22", "summary": "Memory leak fix", "changes": [ { "type": "fixed", "description": "Fixed event listener memory leak in masonry grid web component" } ] }, { "version": "4.2.0", "date": "2026-02-15", "summary": "Blog post and discoverability improvements", "changes": [ { "type": "added", "description": "Blog post: Keeping Things Tidy" }, { "type": "changed", "description": "Improved site discoverability over GitHub repo" }, { "type": "changed", "description": "Skip PR validation for repo owner, members, and bots" } ] }, { "version": "4.1.2", "date": "2026-02-06", "summary": "PR validation and housekeeping", "changes": [ { "type": "added", "description": "Automated PR validation for company profile contributions" }, { "type": "removed", "description": "Removed defunct companies for various reasons (closed, acquired, no longer relevant, etc.)" }, { "type": "fixed", "description": "Fixed broken URLs across several company profiles" } ] }, { "version": "4.1.1", "date": "2026-01-18", "summary": "Search, dates, and social sharing improvements", "changes": [ { "type": "added", "description": "Pagefind-powered site search with nav dropdown and dedicated search page" }, { "type": "added", "description": "Git-based dates for company profiles (added at / last updated)" }, { "type": "added", "description": "Open Graph images for social sharing" }, { "type": "changed", "description": "Updated company profile frontmatter and documentation" } ] }, { "version": "4.1.0", "date": "2026-01-17", "summary": "New companies, SEO improvements, and security hardening", "changes": [ { "type": "added", "description": "Company: MaintainNow" }, { "type": "added", "description": "Company: Swif.ai" }, { "type": "added", "description": "Company: Verve Systems" }, { "type": "added", "description": "Blog post: SEO Improvements" }, { "type": "changed", "description": "SEO improvements and bulk company URL fixes across all profiles" }, { "type": "fixed", "description": "Security hardening (CodeQL sanitisation, template escaping)" }, { "type": "removed", "description": "Cleaned up legacy files and GitHub Actions workflows" } ] }, { "version": "4.0.0", "date": "2026-01-13", "summary": "Complete site redesign on Eleventy v3", "changes": [ { "type": "added", "description": "Full site redesign with company cards, browse/filter pages, and curated homepage" }, { "type": "added", "description": "Structured company profiles with region, remote policy, size, and technology tags" }, { "type": "added", "description": "Blog post: The Big Redesign" }, { "type": "changed", "description": "Migrated from static README-based list to Eleventy v3 static site" }, { "type": "added", "description": "Legacy URL redirects and 404 tracking with Fathom Analytics" } ] } ] ================================================ FILE: src/_data/companyHelpers.js ================================================ /** * Helper functions and constants for company data. * Label maps are imported from labels.js (the single source of truth). */ import labels from './labels.js'; const l = labels(); // Re-export label maps for JS consumers (collections.js, companyTags.js, etc.) export const regionLabels = l.region; export const remotePolicyLabels = l.remotePolicy; export const companySizeLabels = l.companySize; export const techLabels = l.tech; export function getRegionLabel(region) { return regionLabels[region] || region || 'Other'; } export function getRemotePolicyLabel(policy) { return remotePolicyLabels[policy] || policy || 'Unknown'; } export function getCompanySizeLabel(size) { return companySizeLabels[size] || size || 'Unknown'; } export function getTechLabel(tech) { return techLabels[tech] || tech; } /** * Featured companies list - manually curated high-quality examples */ export const featuredCompanySlugs = [ 'github', 'gitlab', 'automattic', 'zapier', 'buffer', 'doist', 'elastic', 'hashicorp', 'stripe', 'shopify', 'netlify', 'vercel' ]; ================================================ FILE: src/_data/designTokens/borderRadius.json ================================================ { "title": "Border Radius", "description": "", "meta": {}, "items": [ { "name": "small", "value": "0.1875rem" }, { "name": "Medium", "value": "0.3rem" } ] } ================================================ FILE: src/_data/designTokens/colors.json ================================================ { "title": "Colors", "description": "Hex color codes that can be shared, cross-platform. They can be converted at point of usage, such as HSL for web or CMYK for print. neutral and vibrant colors are converted to color palettes, fixed colors are kept as they are", "items": [ { "name": "Gray 100", "value": "#f6f9fc" }, { "name": "Gray 200", "value": "#e2e5e8" }, { "name": "Gray 300", "value": "#acaeb2" }, { "name": "Gray 400", "value": "#7c8086" }, { "name": "Gray 500", "value": "#5f646a" }, { "name": "Gray 600", "value": "#434850" }, { "name": "Gray 700", "value": "#373b41" }, { "name": "Gray 800", "value": "#2a2e33" }, { "name": "Gray 900", "value": "#13161b" }, { "name": "Indigo", "value": "#6366F1" }, { "name": "Indigo Subdued", "value": "#666eda" }, { "name": "Teal", "value": "#14B8A6" }, { "name": "Teal Subdued", "value": "#4cb4a5" }, { "name": "Cyan", "value": "#22D3EE" }, { "name": "Cyan Subdued", "value": "#5ccfe4" }, { "name": "Periwinkle", "value": "#818CF8" }, { "name": "Periwinkle Subdued", "value": "#8590e5" }, { "name": "Dark", "value": "#090E1A" }, { "name": "Light", "value": "#F5F7FF" } ] } ================================================ FILE: src/_data/designTokens/colorsBase.json ================================================ { "title": "Colors", "description": "Hex color codes that can be shared, cross-platform. They can be converted at point of usage, such as HSL for web or CMYK for print. neutral and vibrant colors are converted to color palettes, fixed colors are kept as they are", "shades_neutral": [ { "name": "Gray", "value": "#64748B" } ], "shades_vibrant": [], "light_dark": [ { "name": "Indigo", "value": "#6366F1" }, { "name": "Teal", "value": "#14B8A6" }, { "name": "Cyan", "value": "#22D3EE" }, { "name": "Periwinkle", "value": "#818CF8" } ], "standalone": [ { "name": "Dark", "value": "#090E1A" }, { "name": "Light", "value": "#F5F7FF" } ] } ================================================ FILE: src/_data/designTokens/fonts.json ================================================ { "title": "Fonts", "description": "Each array of fonts creates a priority-based order. The first font in the array should be the ideal font, followed by sensible, web-safe fallbacks", "items": [ { "name": "Display", "description": "Display font stack for headings and large text. Redhat Display is made for headlines and big statements, are low contrast and spaced tightly, with a large x-height and open counters.", "value": ["Redhat", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"] }, { "name": "Base", "description": "Base font stack for body text. Atkinson Hyperlegible, named after the founder of the Braille Institute, has been developed specifically to increase legibility for readers with low vision, and to improve comprehension.", "value": ["Atkinson Hyperlegible", "system-ui", "sans-serif"] }, { "name": "Mono", "description": "Monospace font for code and preformatted text.", "value": [ "ui-monospace", "Cascadia Code", "Source Code Pro", "Menlo", "Consolas", "DejaVu Sans Mono", "monospace" ] } ] } ================================================ FILE: src/_data/designTokens/spacing.json ================================================ { "title": "Spacing", "description": "Consistent spacing sizes, based on a ratio, with min and max sizes. This allows you to set spacing based on the context size. For example, min for mobile and max for desktop browsers.", "meta": { "scaleGenerator": "https://utopia.fyi/space/calculator?c=320,19,1.2,1350,28,1.25,6,2,&s=0.75%7C0.5%7C0.25,2%7C3%7C5%7C8%7C13,s-l&g=s,l,xl,12", "note": "shifing the scale: XS is equal to 3XS" }, "items": [ { "name": "3XS", "min": 2, "max": 3 }, { "name": "2XS", "min": 3, "max": 5 }, { "name": "XS", "min": 5, "max": 7 }, { "name": "S", "min": 10, "max": 14 }, { "name": "M", "min": 14, "max": 21 }, { "name": "L", "min": 19, "max": 28 }, { "name": "XL", "min": 38, "max": 56 }, { "name": "2XL", "min": 57, "max": 84 }, { "name": "3XL", "min": 95, "max": 140 }, { "name": "XS - S", "min": 5, "max": 14 }, { "name": "S - M", "min": 10, "max": 21 }, { "name": "M - L", "min": 14, "max": 31 }, { "name": "L - XL", "min": 19, "max": 56 }, { "name": "L - 2xl", "min": 38, "max": 84 }, { "name": "XL - 2XL", "min": 57, "max": 140 }, { "name": "2XL - 3xl", "min": 95, "max": 224 } ] } ================================================ FILE: src/_data/designTokens/textLeading.json ================================================ { "title": "Leading", "description": "Ratio-based leading/line-height values", "items": [ { "name": "Flat", "value": 1 }, { "name": "Fine", "value": 1.2 }, { "name": "Standard", "value": 1.4 } ] } ================================================ FILE: src/_data/designTokens/textSizes.json ================================================ { "title": "Text Sizes", "description": "A minimum and maximum text size size allows you to pick the right size from a ratio, depending on the context size. The min and max sizes are in pixels and should be converted to appropiate sizes, per context", "meta": { "scaleGenerator": "https://utopia.fyi/type/calculator?c=320,19,1.2,1350,28,1.25,6,2,&s=0.75|0.5|0.25,2|3|5|8|13,s-l&g=s,l,xl,12" }, "items": [ { "name": "Step min 2", "min": 13, "max": 16 }, { "name": "Step min 1", "min": 16, "max": 22 }, { "name": "Step 0", "min": 19, "max": 28 }, { "name": "Step 1", "min": 23, "max": 35 }, { "name": "Step 2", "min": 27, "max": 44 }, { "name": "Step 3", "min": 33, "max": 55 }, { "name": "Step 4", "min": 40, "max": 68 }, { "name": "Step 5", "min": 47, "max": 86 }, { "name": "Step 6", "min": 56, "max": 107 } ] } ================================================ FILE: src/_data/designTokens/textWeights.json ================================================ { "title": "Text Weights", "description": "Helper classes and custom properties for common font weights", "meta": {}, "items": [ { "name": "Regular", "value": 400 }, { "name": "Bold", "value": 700 }, { "name": "Extra Bold", "value": 900 } ] } ================================================ FILE: src/_data/designTokens/viewports.json ================================================ { "title": "Viewports", "description": "The min and maximum viewports used to generate fluid type and space scales.", "min": 320, "sm": 640, "navigation": 662, "md": 1000, "max": 1360 } ================================================ FILE: src/_data/github.js ================================================ import EleventyFetch from '@11ty/eleventy-fetch'; export default async function () { let url = 'https://api.github.com/repos/remoteintech/remote-jobs'; // returning promise let data = await EleventyFetch(url, { duration: '1d', type: 'json' }); return data; } ================================================ FILE: src/_data/helpers.js ================================================ /** * Returns back some attributes based on whether the * link is active or a parent of an active item. * * @param {String} itemUrl - The link in question. * @param {String} pageUrl - The page context. * @returns {String} - The attributes or empty. */ export function getLinkActiveState(itemUrl, pageUrl) { let response = ''; // Ensure pageUrl is a string before proceeding if (typeof pageUrl === 'string') { if (itemUrl === pageUrl) { response = ' aria-current="page"'; } if (itemUrl.length > 1 && pageUrl.startsWith(itemUrl.replace('/page-0/', ''))) { response += ' aria-current="page" data-state="active"'; } } return response; } /** * Take an array of keys and return back items that match. * Note: items in the collection must have a key attribute in * Front Matter. * * @param {Array} collection - 11ty collection. * @param {Array} keys - Collection of keys. * @returns {Array} - Result collection or empty. */ export function filterCollectionByKeys(collection, keys) { return collection.filter(x => keys.includes(x.data.key)); } /** * Generates a random UUID (Universally Unique Identifier). * * @returns {string} A random UUID. */ export function random() { const segment = () => { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); }; return `${segment()}-${segment()}-${segment()}`; } ================================================ FILE: src/_data/labels.js ================================================ /** * Centralised label definitions for regions, remote policies, technologies, and company sizes. * This is the single source of truth — used by both JS (via import from companyHelpers.js) * and Nunjucks templates (via the global data cascade as `labels.*`). */ export default function () { return { region: { 'worldwide': 'Worldwide', 'americas': 'Americas', 'europe': 'Europe', 'americas-europe': 'Americas & Europe', 'asia-pacific': 'Asia Pacific', 'other': 'Other' }, remotePolicy: { 'fully-remote': 'Fully Remote', 'remote-first': 'Remote First', 'hybrid': 'Hybrid', 'remote-friendly': 'Remote Friendly' }, companySize: { 'tiny': '1-10 employees', 'small': '11-50 employees', 'medium': '51-200 employees', 'large': '201-1000 employees', 'enterprise': '1000+ employees' }, tech: { 'javascript': 'JavaScript', 'python': 'Python', 'ruby': 'Ruby', 'go': 'Go', 'java': 'Java', 'php': 'PHP', 'rust': 'Rust', 'dotnet': '.NET', 'elixir': 'Elixir', 'scala': 'Scala', 'cloud': 'Cloud', 'devops': 'DevOps', 'mobile': 'Mobile', 'data': 'Data', 'ml': 'ML/AI', 'sql': 'SQL', 'nosql': 'NoSQL', 'search': 'Search' } }; } ================================================ FILE: src/_data/meta.js ================================================ export const url = process.env.URL || 'http://localhost:8080'; // Extract domain from `url` export const domain = new URL(url).hostname; export const siteName = 'Remote In Tech'; export const siteDescription = 'A list of semi to fully remote-friendly companies in or around tech'; export const siteType = 'WebSite'; // schema export const locale = 'en_EN'; export const lang = 'en'; export const skipContent = 'Skip to content'; export const author = { name: 'Remote In Tech Community' }; export const creator = { name: 'Doug Aitken', email: 'doug@dougaitken.co.uk', website: 'https://dougaitken.link' }; export const pathToSvgLogo = 'src/assets/svg/misc/logo.svg'; // used for favicon generation export const themeColor = '#6366F1'; // used in manifest, for example primary color value export const themeLight = '#F5F7FF'; // used for meta tag theme-color, if light colors are prefered. best use value set for light bg export const themeDark = '#090E1A'; // used for meta tag theme-color, if dark colors are prefered. best use value set for dark bg export const opengraph_default = '/assets/images/template/opengraph-default.jpg'; // fallback/default meta image export const opengraph_default_alt = "Remote In Tech - A list of semi to fully remote-friendly companies in or around tech"; // alt text for default meta image" export const blog = { // RSS feed name: 'Remote In Tech', description: 'A list of semi to fully remote-friendly companies in or around tech', // feed links are looped over in the head. You may add more to the array. feedLinks: [ { title: 'Atom Feed', url: '/feed.xml', type: 'application/atom+xml' }, { title: 'JSON Feed', url: '/feed.json', type: 'application/json' } ], // Tags tagSingle: 'Tag', tagPlural: 'Tags', tagMore: 'More tags:', // pagination paginationLabel: 'Blog', paginationPage: 'Page', paginationPrevious: 'Previous', paginationNext: 'Next', paginationNumbers: true }; export const details = { aria: 'section controls', expand: 'expand all', collapse: 'collapse all' }; export const dialog = { close: 'Close', next: 'Next', previous: 'Previous' }; export const navigation = { navLabel: 'Menu', ariaTop: 'Main', ariaBottom: 'Complementary', ariaPlatforms: 'Platforms', drawerNav: false, subMenu: false }; export const themeSwitch = { title: 'Theme', light: 'light', dark: 'dark' }; export const greenweb = { // https://carbontxt.org/ disclosures: [ { docType: 'sustainability-page', url: `${url}/sustainability/`, domain: domain } ], services: [{domain: 'netlify.com', serviceType: 'cdn'}] }; export const viewRepo = { // this is for the view/edit on github link. The value in the package.json will be pulled in. allow: true, infoText: 'View this page on GitHub' }; export const analytics = { // Fathom Analytics - privacy-focused analytics // Only loads in production (ELEVENTY_ENV=production) fathom: { siteId: 'KVRCNWLT' } }; ================================================ FILE: src/_data/navigation.js ================================================ export default { top: [ { text: 'Companies', url: '/companies/' }, { text: 'Browse', url: '/browse/' }, { text: 'About', url: '/about/' }, { text: 'Blog', url: '/blog/' }, { text: 'Contributing', url: '/contributing/' }, { text: 'Contact', url: '/contact/' } ], bottom: [ { text: 'Privacy', url: '/privacy/' }, { text: 'Changelog', url: '/changelog/' } ] }; ================================================ FILE: src/_data/personal.js ================================================ export const platforms = { github: 'https://github.com/remoteintech/remote-jobs' }; ================================================ FILE: src/_includes/head/css-inline.njk ================================================ {%- if eleventy.env.runMode === "serve" %} {%- else %} {%- endif %} {% css "global" %}{% include "css/global.css" %}{% endcss %} ================================================ FILE: src/_includes/head/js-defer.njk ================================================ ================================================ FILE: src/_includes/head/js-inline.njk ================================================ {%- js "inline" %} {% include "scripts/is-land.js" %} {% include "scripts/theme-toggle.js" %} {% endjs %} ================================================ FILE: src/_includes/head/meta-info.njk ================================================ {% set metaDescription %} {%- if discover.description -%} {{- discover.description -}} {%- elif description -%} {{- description -}} {%- else -%} {{- meta.siteDescription -}} {%- endif -%} {% endset %} {% if personal.platforms.mastodon %} {% endif %} {% if meta.author.fediverse %} {% endif %} {% for feedLink in meta.blog.feedLinks %} {% endfor %} ================================================ FILE: src/_includes/head/preloads.njk ================================================ {%- if preloads -%} {% endif %} ================================================ FILE: src/_includes/head/schema.njk ================================================ {% include "schemas/WebSite.njk" %} {% include "schemas/BreadcrumbList.njk" %} {% if schema %} {%- include "schemas/" + schema + ".njk" -%} {% endif %} ================================================ FILE: src/_includes/partials/card-tag.njk ================================================ {% set headingLevel = headingLevel | default("h2") %} {% set definedDate = definedDate | default(item.date) %} <{{ headingLevel }} slot="headline" class="text-step-2"> {{ item.data.title }} {% include "partials/date.njk" %}

{{ item.data.description }}

{% css "local" %} {% include "css/custom-card.css" %} {% endcss %} ================================================ FILE: src/_includes/partials/company-card.njk ================================================ {# Company Card Macro - Reusable card component for company listings #} {% set regionLabels = { 'worldwide': 'Worldwide', 'americas': 'Americas', 'europe': 'Europe', 'americas-europe': 'Americas & Europe', 'asia-pacific': 'Asia Pacific', 'other': 'Other' } %} {% macro companyCard(company, showRegion = true, showWebsite = true) %} {% endmacro %} ================================================ FILE: src/_includes/partials/date.njk ================================================ ================================================ FILE: src/_includes/partials/details.njk ================================================
{%- for item in itemList | alphabetic -%}
{{ item.data.title }} {{- item.templateContent | safe -}}
{%- endfor -%}
{% css "local" %} {% include "css/details.css" %} {% endcss %} {% js "defer" %} {% include "scripts/details.js" %} {% endjs %} ================================================ FILE: src/_includes/partials/edit-on.njk ================================================ {% if meta.viewRepo.allow %}

{{ meta[page.lang].blog.githubEdit }} {{ meta.viewRepo.infoText }}.

{% endif %} ================================================ FILE: src/_includes/partials/footer.njk ================================================ ================================================ FILE: src/_includes/partials/header.njk ================================================
{% include "partials/main-nav.njk" %} {% include "partials/nav-search.njk" %} {% include 'partials/theme-switch.njk' %}
================================================ FILE: src/_includes/partials/main-nav.njk ================================================ {% set drawerNav = meta.navigation.drawerNav %} {% set subMenu = meta.navigation.subMenu %} {% if drawerNav %} {% css "local" %} {% include "css/nav-main-drawer-cls.css" %} {% endcss %} {% js "defer" %} {% include "scripts/nav-drawer.js" %} {% endjs %} {% endif %} {% if subMenu %} {% js "defer" %} {% include "scripts/nav-sub.js" %} {% endjs %} {% endif %} ================================================ FILE: src/_includes/partials/nav-search.njk ================================================ {%- css "global" -%} .nav-search { position: relative; } .nav-search__trigger { display: inline-flex; align-items: center; padding: var(--space-xs); color: var(--color-text); background: transparent; border: none; border-radius: var(--border-radius-small); cursor: pointer; transition: background-color 0.2s ease; } .nav-search__trigger:hover { background-color: var(--color-bg-accent); } .nav-search__trigger svg { width: 1.2em; height: 1.2em; } .nav-search__dropdown { position: absolute; top: calc(100% + var(--space-xs)); right: 0; width: min(400px, 90vw); background: var(--color-bg); border: 1px solid var(--color-bg-accent-2); border-radius: var(--border-radius-medium); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); padding: var(--space-s); z-index: 100; } .nav-search__dropdown[hidden] { display: none; } /* Pagefind UI overrides for nav search */ .nav-search__dropdown .pagefind-ui { background-color: var(--color-bg) !important; } .nav-search__dropdown .pagefind-ui__search-input { background-color: var(--color-bg) !important; border-color: var(--color-bg-accent-2) !important; color: var(--color-text) !important; font-size: var(--size-step--1) !important; } .nav-search__dropdown .pagefind-ui__search-input::placeholder { color: var(--color-text-accent) !important; } .nav-search__dropdown .pagefind-ui__drawer, .nav-search__dropdown .pagefind-ui__results, .nav-search__dropdown .pagefind-ui__results-area { background-color: var(--color-bg) !important; } .nav-search__dropdown .pagefind-ui__result { padding: var(--space-xs) !important; background-color: var(--color-bg) !important; } .nav-search__dropdown .pagefind-ui__result:hover { background-color: var(--color-bg-accent) !important; } .nav-search__dropdown .pagefind-ui__result-link { color: var(--color-text) !important; font-weight: var(--font-bold) !important; } .nav-search__dropdown .pagefind-ui__result-excerpt { color: var(--color-text-accent) !important; } .nav-search__dropdown .pagefind-ui__message { background-color: var(--color-bg) !important; color: var(--color-text-accent) !important; } .nav-search__view-all { display: block; padding: var(--space-xs) var(--space-s); margin-block-start: var(--space-xs); text-align: center; font-size: var(--size-step--1); color: var(--color-primary); text-decoration: none; border-top: 1px solid var(--color-bg-accent); } .nav-search__view-all:hover { background-color: var(--color-bg-accent); } .nav-search__view-all[hidden] { display: none; } {%- endcss -%} {%- js "defer" -%} (function() { const trigger = document.querySelector('.nav-search__trigger'); const dropdown = document.getElementById('nav-search-dropdown'); const viewAllLink = document.getElementById('nav-search-view-all'); if (!trigger || !dropdown) return; let pagefindLoaded = false; trigger.addEventListener('click', function(e) { e.preventDefault(); const expanded = trigger.getAttribute('aria-expanded') === 'true'; trigger.setAttribute('aria-expanded', !expanded); dropdown.hidden = expanded; if (!expanded && !pagefindLoaded && typeof PagefindUI !== 'undefined') { new PagefindUI({ element: "#nav-search-container", showSubResults: false, showImages: false, excerptLength: 10, pageSize: 5, placeholder: "Search companies..." }); pagefindLoaded = true; // Watch for input changes to update "View all" link setTimeout(() => { const input = dropdown.querySelector('input'); if (input) { input.focus(); input.addEventListener('input', function() { if (this.value.trim()) { viewAllLink.href = '/search/?q=' + encodeURIComponent(this.value); viewAllLink.hidden = false; } else { viewAllLink.hidden = true; } }); } }, 100); } }); // Close on click outside document.addEventListener('click', function(e) { if (!e.target.closest('.nav-search')) { trigger.setAttribute('aria-expanded', 'false'); dropdown.hidden = true; } }); // Close on escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && !dropdown.hidden) { trigger.setAttribute('aria-expanded', 'false'); dropdown.hidden = true; trigger.focus(); } }); })(); {%- endjs -%} ================================================ FILE: src/_includes/partials/pagination.njk ================================================
{% css "local" %} {% include "css/pagination.css" %} {% endcss %} ================================================ FILE: src/_includes/partials/theme-switch.njk ================================================

{{ meta.themeSwitch.title }}

================================================ FILE: src/_includes/schemas/BlogPosting.njk ================================================ ================================================ FILE: src/_includes/schemas/BreadcrumbList.njk ================================================ {# Generate breadcrumbs based on page URL - only for pages with paths #} {%- set cleanUrl = page.url | trim('/') -%} {%- if cleanUrl -%} {%- set urlParts = cleanUrl | split('/') -%} {%- endif -%} ================================================ FILE: src/_includes/schemas/Organization.njk ================================================ ================================================ FILE: src/_includes/schemas/WebSite.njk ================================================ ================================================ FILE: src/_includes/webc/custom-card.webc ================================================
================================================ FILE: src/_includes/webc/custom-masonry.webc ================================================ ================================================ FILE: src/_includes/webc/custom-peertube-link.webc ================================================ ================================================ FILE: src/_includes/webc/custom-peertube.webc ================================================ ================================================ FILE: src/_includes/webc/custom-svg.webc ================================================ ================================================ FILE: src/_includes/webc/custom-youtube-link.webc ================================================ ================================================ FILE: src/_includes/webc/custom-youtube.webc ================================================ ================================================ FILE: src/_layouts/base.njk ================================================ {% set assetHash = helpers.random() %} {%- if title -%} {{- title -}} {%- else -%} {{- meta.siteName -}} {%- endif -%} {% include "head/js-inline.njk" %} {% include "head/schema.njk" %} {% include "head/css-inline.njk" %} {% include "head/preloads.njk" %} {% include "head/js-defer.njk" %} {% include "head/meta-info.njk" %} {%- if meta.analytics.fathom.siteId and eleventy.env.runMode == "build" -%} {%- endif -%} {% set indicateActiveHome %} {% if page.url == "/" %} aria-current=page {% endif %} {% endset %} {% include "partials/header.njk" %}
{{ content | safe }}
{% include "partials/footer.njk" %} ================================================ FILE: src/_layouts/company.njk ================================================ --- layout: base schema: Organization ---
← All Companies

{{ title }}

{% set buttonUrl = careers_url or website %} {% if buttonUrl %} {% if careers_url %}Apply Now{% else %}Visit Website{% endif %} → {% endif %} {% if region %} {{ labels.region[region] or region }} {% endif %} {% if remote_policy %} {{ labels.remotePolicy[remote_policy] or remote_policy }} {% endif %}
{{ content | safe }} {% if technologies and technologies.length > 0 %}

Tech Stack

{% for tech in technologies %} {{ labels.tech[tech] or tech }} {% endfor %}
{% endif %}
{%- css "local" -%} .company-profile__header { margin-block-end: var(--space-l); } .company-profile__back { font-size: var(--size-step--1); color: var(--color-text-accent); text-decoration: none; } .company-profile__back:hover { color: var(--color-primary); } .company-profile__meta { margin-block-start: var(--space-m); flex-wrap: wrap; align-items: center; } .tag { display: inline-block; font-size: var(--size-step--1); padding: 0.2em 0.6em; border-radius: 4px; white-space: nowrap; text-decoration: none; transition: opacity var(--transition-duration) var(--transition-timing), transform var(--transition-duration) var(--transition-timing); } .tag:hover { opacity: 0.85; transform: translateY(-1px); } .tag--region { background-color: var(--color-highlight); color: var(--color-dark); } .tag--policy { background-color: var(--color-secondary); color: var(--color-light); } .tag--tech { background-color: var(--color-bg-accent); color: var(--color-text); border: 1px solid var(--color-bg-accent-2); } .tag--tech:hover { border-color: var(--color-primary); opacity: 1; } .company-profile__content { --flow-space: var(--space-m-l); } .company-profile__content h2 { margin-top: var(--space-l-xl); color: var(--color-primary); font-size: var(--size-step-2); padding-block-end: var(--space-2xs); border-block-end: 2px solid var(--color-bg-accent); } .company-profile__content ul { padding-inline-start: var(--space-m); } .company-profile__content li { margin-block-end: var(--space-2xs); } .company-profile__tech { margin-block-start: var(--space-l); } .company-profile__footer { margin-block-start: var(--space-xl); padding-block-start: var(--space-l); border-block-start: 1px solid var(--color-bg-accent); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: var(--space-s); } .company-profile__updated { font-size: var(--size-step--1); color: var(--color-text-accent); } {%- endcss -%} ================================================ FILE: src/_layouts/page.njk ================================================ --- layout: base ---

{{ title }}

{{ content | safe }}
================================================ FILE: src/_layouts/post.njk ================================================ --- layout: base schema: BlogPosting ---

{{ title }}

{% set imgPath = image %} {% if imgPath and imgPath | fileExists %} {% image imgPath, alt or "", credit, "eager", "feature" %} {% endif %}

{%- if draft -%} draft {%- endif %} {% set definedDate = date %} {% include "partials/date.njk" %} {% if tags.length > 1 %} {% for tag in tags %}{% if tag != "posts" %} {{ tag }} {% endif %}{% endfor %} {% endif %}

{{ content | safe }}
{%- css "local" -%} {%- include 'css/post.css' -%} {%- include 'css/footnotes.css' -%} {%- endcss -%} ================================================ FILE: src/_layouts/tags.njk ================================================ --- layout: base ---

{{ title }}

{{ content | safe }}
================================================ FILE: src/assets/css/global/base/fonts.css ================================================ @font-face { font-family: 'Atkinson Hyperlegible'; font-style: normal; font-display: swap; font-weight: 400; src: local(''), url('/assets/fonts/atkinson/atkinson-hyperlegible-regular.woff2') format('woff2'); } @font-face { font-family: 'Atkinson Hyperlegible'; font-style: italic; font-display: swap; font-weight: 400; src: local(''), url('/assets/fonts/atkinson/atkinson-hyperlegible-italic.woff2') format('woff2'); } @font-face { font-family: 'Atkinson Hyperlegible'; font-style: normal; font-display: swap; font-weight: 700; src: local(''), url('/assets/fonts/atkinson/atkinson-hyperlegible-bold.woff2') format('woff2'); } @font-face { font-family: 'Redhat'; font-style: normal; font-display: swap; font-weight: 900; src: local(''), url('/assets/fonts/redhat/red-hat-display-v7-latin-900.woff2') format('woff2'); } ================================================ FILE: src/assets/css/global/base/global-styles.css ================================================ /* Global styles Low-specificity, global styles that apply to the whole project: https://cube.fyi/css.html */ html { color-scheme: light dark; } body { display: flex; flex-direction: column; font-family: var(--font-base); font-size: var(--size-step-0); font-weight: var(--font-regular); font-size-adjust: from-font; line-height: var(--leading-standard); color: var(--color-text); background-color: var(--color-bg); accent-color: var(--color-primary); letter-spacing: var(--tracking); } main { flex: auto; } h1, h2, h3 { font-family: var(--font-display); font-weight: var(--font-extra-bold); line-height: var(--leading-fine); letter-spacing: var(--tracking-s); } h1 { font-size: var(--size-step-6); } h2 { font-size: var(--size-step-4); } h3 { font-size: var(--size-step-2); } blockquote { padding: var(--space-m-l); border-inline-start: 0.5rem solid var(--color-primary); font-size: var(--size-step-2); } blockquote > * + * { margin-block-start: var(--space-m-l); } blockquote :last-child { font-family: var(--font-base); font-style: normal; font-size: var(--size-step-1); } input, textarea { caret-color: var(--color-primary); } svg { block-size: 0.6lh; inline-size: auto; flex: none; } b, strong { font-weight: var(--font-bold); } hr { border: none; height: 1px; width: 10%; margin-block: var(--flow-space, var(--space-m-l)); margin-inline-start: 0; background-color: var(--color-bg-accent-2); } figcaption { margin-block-start: var(--space-s); font-size: var(--size-step-min-1); text-align: center; padding-block-end: var(--space-xs); } figcaption:after { border-block: var(--stroke); content: ''; display: block; margin-block: var(--space-xs); /* block-size: 1rem; */ inline-size: 50%; margin-inline: auto; opacity: 0.8; } a { text-decoration-thickness: 0.15ex; text-underline-offset: 0.2ch; } a:not([class]):hover { text-decoration: none; } :focus { outline: none; } :focus-visible { outline: 3px solid var(--focus-color, currentColor); outline-offset: var(--focus-offset, 0.3ch); } /* reduce focus offset for Firefox */ @supports (-moz-appearance: none) { :root { --focus-offset: 0.08em; } } ::selection { background-color: var(--color-text); color: var(--color-bg); } @media (scripting: none) { .require-js { display: none; } } ================================================ FILE: src/assets/css/global/base/reset.css ================================================ /* https://piccalil.li/blog/a-modern-css-reset/ */ /* https://keithjgrant.com/posts/2024/01/my-css-resets/ */ /* https://moderncss.dev/modern-css-for-dynamic-component-based-architecture/#css-reset-additions */ /* https://github.com/mayank99/reset.css/blob/main/package/index.css */ *, *::before, *::after { box-sizing: border-box; } * { text-wrap: pretty; } h1, h2, h3, h4 { text-wrap: balance; } html:focus-within { scroll-behavior: smooth; } body { min-height: 100vh; min-height: 100dvh; text-rendering: optimizeSpeed; line-height: 1.5; } body, h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; } ul[role='list'], ol[role='list'] { list-style: none; } [role='list'] { padding: 0; } a { text-decoration-skip-ink: auto; color: currentColor; } img, picture, svg, canvas { max-inline-size: 100%; block-size: auto; vertical-align: middle; font-style: italic; background-repeat: no-repeat; background-size: cover; shape-margin: 0.75rem; } picture { display: block; } button { all: unset; } button, input, select, textarea { font: inherit; color: inherit; } textarea { resize: vertical; resize: block; } textarea:not([rows]) { min-height: 10em; } button, label, select, summary, [role='button'], [role='option'] { cursor: pointer; } :target { scroll-margin-block-start: 2ex; } :focus { scroll-margin-block-end: 8vh; } dialog { border: none; background: none; inset: unset; max-width: unset; max-height: unset; } [popover] { border: none; background: none; inset: unset; color: inherit; } dialog:not([open], [popover]), [popover]:not(:popover-open) { display: none !important; } html:has(dialog[open]:modal) { overflow: hidden; } @media (prefers-reduced-motion: reduce) { :focus-within { scroll-behavior: auto; } *, ::after, ::before { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: 0.01ms !important; } } ================================================ FILE: src/assets/css/global/base/variables.css ================================================ /* Global variables. */ /* Basic variable definitions for color schemes */ :root { --gutter: var(--space-m-l); --transition-duration: 250ms; --transition-timing: ease; --wrapper-width: 85rem; --tracking: -0.04ch; --tracking-s: -0.075ch; --tracking-wide: 0.04ch; --stroke: 1px solid var(--color-bg-accent); --gradient-rainbow: linear-gradient(90deg, #6366F1 10%, #14B8A6 30%, #22D3EE 50%, #818CF8 75%, #6366F1 90%); --gradient-conic: conic-gradient( var(--color-primary) 0 28%, var(--color-secondary) 0 67%, var(--color-tertiary) 0 100% ); --gradient-stripes: linear-gradient( 45deg, var(--color-gray-900) 0 75%, var(--color-primary) 0 85%, var(--color-secondary) 0 92%, var(--color-tertiary) 0 100% ); --gradient-brand: linear-gradient(135deg, var(--color-indigo) 0%, var(--color-teal) 100%); --color-light: #F5F7FF; --color-dark: #090E1A; --color-mid: var(--color-gray-400); } /* Light theme */ :root, :root[data-theme='light'] { --color-text: var(--color-gray-800); --color-bg: var(--color-light); --color-primary: var(--color-indigo); --color-secondary: var(--color-teal); --color-tertiary: var(--color-cyan); --color-highlight: var(--color-periwinkle); --color-text-accent: var(--color-gray-600); --color-bg-accent: var(--color-gray-200); --color-bg-accent-2: var(--color-gray-300); } /* Dark theme */ @media (prefers-color-scheme: dark) { :root { --color-text: var(--color-gray-100); --color-bg: var(--color-dark); --color-primary: var(--color-periwinkle); --color-secondary: var(--color-teal-subdued); --color-tertiary: var(--color-cyan-subdued); --color-highlight: var(--color-indigo-subdued); --color-text-accent: var(--color-gray-300); --color-bg-accent: var(--color-gray-800); --color-bg-accent-2: var(--color-gray-700); } } :root[data-theme='dark'] { --color-text: var(--color-gray-100); --color-bg: var(--color-dark); --color-primary: var(--color-periwinkle); --color-secondary: var(--color-teal-subdued); --color-tertiary: var(--color-cyan-subdued); --color-highlight: var(--color-indigo-subdued); --color-text-accent: var(--color-gray-300); --color-bg-accent: var(--color-gray-800); --color-bg-accent-2: var(--color-gray-700); } ================================================ FILE: src/assets/css/global/blocks/button.css ================================================ /* based on Andy Bell's article: https://piccalil.li/blog/how-i-build-a-button-component/ */ .button { --button-bg: var(--color-text); --button-color: color-mix(in oklab, var(--button-bg) 10%, var(--color-bg)); --button-hover-bg: color-mix(in oklab, var(--button-bg) 90%, var(--color-bg)); --button-border-width: var(--border-thickness); --button-border-style: solid; --button-border-color: color-mix(in oklab, var(--button-bg) 80%, var(--color-text)); --button-radius: var(--border-radius-small); --button-gap: var(--space-2xs); --button-padding: var(--space-xs) var(--space-m); --button-font-family: var(--font-body); --button-font-weight: var(--font-regular); --button-font-size: var(--size-step-0); --button-text-transform: none; --button-tracking: normal; display: inline-flex; align-items: center; gap: var(--button-gap); padding: var(--button-padding); background: var(--button-bg); color: var(--button-color); border-width: var(--button-border-width); border-style: var(--button-border-style); border-color: var(--button-border-color); border-radius: var(--button-radius); text-decoration: none; font-family: var(--button-font-family); font-weight: var(--button-font-weight); font-size: var(--button-font-size); line-height: var(--leading-flat); text-transform: var(--button-text-transform); letter-spacing: var(--button-tracking); /* trim the space at the cap height - in Safari Technology Preview */ text-box-trim: trim-both; text-box-edge: cap alphabetic; } .button svg { block-size: var(--button-icon-size, 1.2cap); } /* Hover/focus/active */ .button:hover, .button[aria-current='page'], .button[aria-pressed='true'], .button[data-state='active'] { background: var(--button-hover-bg); color: var(--button-color); } .button:focus { outline-color: var(--button-outline-color, var(--button-border-color)); } .button:active { transform: scale(99%); } /* Variants */ .button[data-button-variant='primary'] { --button-bg: var(--color-primary); --button-color: var(--color-light); --button-color: color-mix(in oklab, var(--color-primary) 5%, var(--color-light)); } .button[data-button-variant='secondary'] { --button-bg: var(--color-secondary); --button-color: var(--color-light); --button-color: color-mix(in oklab, var(--color-secondary) 5%, var(--color-light)); } .button[data-button-variant='tertiary'] { --button-bg: var(--color-tertiary); --button-color: var(--color-dark); --button-color: color-mix(in oklab, var(--color-tertiary) 10%, var(--color-dark)); } .button[data-button-variant='outline'] { --button-bg: transparent; --button-border-color: var(--color-primary); --button-color: var(--color-primary); --button-hover-bg: var(--color-primary); } .button[data-button-variant='outline']:hover { --button-color: var(--color-light); } .button[data-ghost-button] { --button-bg: var(--color-bg); --button-border-color: var(--color-text); --button-color: var(--color-text); --button-hover-color: var(--color-bg); } .button[data-ghost-button]:hover { --_ghost-hover-bg: var(--color-bg); --_ghost-hover-bg: color-mix(in oklab, var(--button-bg) 95%, var(--color-dark)); background: var(--_ghost-hover-bg); color: var(--button-color); } .button[data-small-button] { --button-border-width: 2px; --button-radius: var(--border-radius-small); --button-font-size: var(--size-step-min-2); --button-padding: var(--space-2xs) var(--space-s) var(--space-3xs) var(--space-s); --button-text-transform: uppercase; --button-tracking: var(--tracking-wide); } /* Radius variants */ .button[data-button-radius='hard'] { --button-radius: 0; } ================================================ FILE: src/assets/css/global/blocks/code.css ================================================ :root { --color-code-orange: hsl(30, 70%, 60%); --color-code-blue: var(--color-secondary); --color-code-indigo: hsl(260, 48%, 56%); --color-code-violet: hsl(314, 70%, 60%); --color-code-pink: hsl(350, 70%, 60%); --color-code-gray: hsl(0, 0%, 58%); --color-code-bg: color-mix(in oklab, var(--color-bg) 92%, black); } code, pre { font-family: var(--font-mono); font-size: var(--size-step-min-1); background-color: var(--color-code-bg); border-radius: var(--border-radius-medium); } code[class*='language-'], pre[class*='language-'] { white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; hyphens: none; } /* Specific Styles for
 Elements */
pre {
  grid-column: popout !important;
  overflow-x: auto;
  padding: var(--space-s) var(--space-l);
}

/* Style Adjustments for  within different contexts */
:where(:not(pre)) > code {
  position: relative;
  top: -0.05em;
  padding: 0.1em 0.4em;
}

pre[class*='language-'] {
  overflow: auto;
  position: relative;
}

[class*='language-'] .namespace {
  opacity: 0.7;
}

.token.atrule {
  color: var(--color-code-pink);
}

.token.attr-name {
  color: var(--color-code-orange);
}

.token.attr-value {
  color: var(--color-text-accent);
}

.token.attribute {
  color: var(--color-code-blue);
}

.token.boolean {
  color: var(--color-code-pink);
}

.token.builtin {
  color: var(--color-code-orange);
}

.token.cdata {
  color: var(--color-code-orange);
}

.token.char {
  color: var(--color-code-orange);
}

.token.class {
  color: var(--color-code-orange);
}

.token.class-name {
  color: var(--color-code-orange);
}

.token.color {
  color: var(--color-code-orange);
}

.token.comment {
  color: var(--color-code-gray);
}

.token.constant {
  color: var(--color-code-pink);
}

.token.deleted {
  color: var(--color-code-pink);
}

.token.doctype {
  color: var(--color-code-orange);
}

.token.entity {
  color: var(--color-code-pink);
}

.token.function {
  color: var(--color-code-pink);
}

.token.hexcode {
  color: var(--color-code-orange);
}

.token.id {
  color: var(--color-code-pink);
  font-weight: var(--font-bold);
}

.token.important {
  color: var(--color-code-pink);
  font-weight: var(--font-bold);
}

.token.inserted {
  color: var(--color-code-orange);
}

.token.keyword {
  color: var(--color-code-pink);
  font-style: italic;
}

.token.number {
  color: var(--color-text-accent);
}

.token.operator {
  color: var(--color-code-gray);
}

.token.prolog {
  color: var(--color-code-orange);
}

.token.property {
  color: var(--color-code-orange);
}

.token.pseudo-class {
  color: var(--color-code-blue);
}

.token.pseudo-element {
  color: var(--color-code-blue);
}

.token.punctuation {
  color: var(--color-code-gray);
}

.token.regex {
  color: var(--color-code-orange);
}

.token.selector {
  color: var(--color-code-pink);
}

.token.string {
  color: var(--color-text-accent);
}

.token.symbol {
  color: var(--color-code-pink);
}

.token.tag {
  color: var(--color-code-pink);
}

.token.unit {
  color: var(--color-code-pink);
}

.token.url {
  color: var(--color-code-violet);
}

.token.variable {
  color: var(--color-code-pink);
}

/* CodePen iframe */
.codepen a {
  --icon-size: 1.2em;

  display: flex;
  align-items: center;
  gap: var(--space-2xs);
}

.prose .cp_embed_wrapper,
.prose .cp_embed_wrapper + script + *:not(h2) {
  --flow-space: var(--space-l);
}

.cp_embed_wrapper {
  grid-column: popout;
  position: relative;
  overflow: auto;
  display: grid;
  place-items: center;
  grid-template-areas: 'container';
  resize: horizontal;
}

.cp_embed_wrapper iframe {
  grid-area: container;
  width: 100%;
}


================================================
FILE: src/assets/css/global/blocks/company-card.css
================================================
/* Company Card Component */

.company-card {
  background-color: var(--color-bg);
  border: 2px solid var(--color-bg-accent);
  border-radius: var(--border-radius-medium);
  padding: var(--space-s-m);
  transition: border-color var(--transition-duration) var(--transition-timing),
              box-shadow var(--transition-duration) var(--transition-timing);
  position: relative;
}

.company-card:hover,
.company-card:focus-within {
  border-color: var(--color-primary);
  box-shadow: 0 4px 12px color-mix(in oklab, var(--color-primary) 15%, transparent);
}

.company-card__name {
  font-size: var(--size-step-1);
  font-weight: var(--font-bold);
  margin-block-end: var(--space-2xs);
  line-height: var(--leading-fine);
}

.company-card__name a {
  text-decoration: none;
  color: var(--color-text);
}

/* Make entire card clickable via name link */
.company-card__name a::after {
  content: '';
  position: absolute;
  inset: 0;
}

.company-card__region {
  font-size: var(--size-step--1);
  color: var(--color-text-accent);
  margin-block-end: var(--space-2xs);
  line-height: var(--leading-standard);
}

.company-card__website {
  font-size: var(--size-step--1);
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.company-card__website a {
  color: var(--color-secondary);
  text-decoration: none;
  position: relative;
  z-index: 1; /* Above the card link overlay */
}

.company-card__website a:hover {
  text-decoration: underline;
}

/* Prevent external link indicator on card website links */
.company-card__website a::after {
  display: none !important;
}

/* Grid layout for card collections */
.company-cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: var(--space-m);
}


================================================
FILE: src/assets/css/global/blocks/external-link.css
================================================
a[href^='http']:not([href*='localhost:8080']):not([href*='dougaitken.co.uk']):not(.button):not(
    .no-indicator
  ):not(.external-link),
.indicator {
  padding-inline-end: 0.8em;
}

a[href^='http']:not([href*='localhost:8080']):not([href*='dougaitken.co.uk']):not(.button):not(
    .no-indicator
  ):not(.external-link)::after,
.indicator::after {
  position: absolute;
  display: inline-block;
  inline-size: 1em;
  block-size: 1em;
  background-image: url('/assets/images/template/external.svg');
  background-repeat: no-repeat;
  background-position: center;
  background-size: 60% auto;
  /* alternative text rules */
  content: '(external link)';
  overflow: hidden;
  white-space: nowrap;
  text-indent: 1em; /* the width of the icon */
}

/* Ensure buttons never get external link indicators */
.button::after {
  display: none !important;
  content: none !important;
}


================================================
FILE: src/assets/css/global/blocks/main-nav.css
================================================
.mainnav {
  --nav-list-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
  position: var(--nav-position, absolute);
  inset-inline-end: 0;
}

.mainnav:has([data-drawer-toggle][aria-expanded='true']) {
  --nav-position: fixed;
  inset-inline-end: var(--gap);
}

.mainnav ul {
  /* configuration */
  --gutter: var(--space-xs);
  --cluster-vertical-alignment: normal;
  --cluster-wrap: wrap;
  --cluster-direction: column;
  --nav-list-background: var(--color-bg);
  --nav-list-shadow: -5px 0 11px 0 hsl(0 0% 0% / 0.2);
  --nav-list-height: 100dvh;
  --nav-list-padding-block: var(--space-2xl);
  --nav-list-padding-inline: var(--space-s);
  --nav-list-position: fixed;
  --nav-list-width: min(18rem, 100vw);
  --nav-list-visibility: hidden;
  --nav-list-opacity: 0;

  background: var(--nav-list-background);
  box-shadow: var(--nav-list-shadow);
  block-size: var(--nav-list-height);
  list-style: none;
  margin: 0;
  padding-block: var(--nav-list-padding-block);
  padding-inline: var(--nav-list-padding-inline);
  position: var(--nav-list-position);
  inset-block-start: 0;
  inset-inline-end: 0;
  inline-size: var(--nav-list-width);
  opacity: var(--nav-list-opacity);
  transition:
    opacity 0.3 var(--nav-list-timing-function),
    visibility 0.3s ease-in-out;
  visibility: var(--nav-list-visibility);
}

.mainnav ul[no-flash] {
  transition: none;
}

@media (prefers-reduced-motion: no-preference) {
  .mainnav > ul {
    --nav-list-transform: translateX(100%);
    --nav-list-opacity: 1;
    transform: var(--nav-list-transform);
    transition:
      transform 0.5s var(--nav-list-timing-function),
      visibility 0.3s linear;
  }

  .mainnav svg {
    transition: transform 0.4s var(--nav-list-timing-function);
  }
}

.mainnav [data-drawer-toggle] {
  --gutter: var(--space-2xs);
  --cluster-vertical-alignment: center;
  background: var(--color-bg);
  display: var(--nav-button-display, inline-flex);
  position: relative;
  z-index: 2;
  padding: var(--space-2xs) 0;
  line-height: var(--leading-flat);
}

.mainnav [data-drawer-toggle][aria-expanded='true'] + ul {
  --cluster-wrap: nowrap;
  --nav-list-visibility: visible;
  --nav-list-transform: translateX(0);
  --nav-list-opacity: 1;
  overflow-y: auto;
}

body:has([data-drawer-toggle][aria-expanded='true']) {
  overflow: hidden;
}

.mainnav span {
  font-weight: var(--font-bold);
  text-transform: uppercase;
  font-family: var(--font-display);
  font-size: var(--size-step-min-1);
}

.mainnav svg {
  block-size: 0.9em;
  color: var(--color-text);
  stroke-width: 3;
}

.mainnav [aria-expanded='true'] svg {
  transform: rotate(45deg);
}

.mainnav :is(a, [data-submenu-toggle]) {
  /* configuration */
  --nav-item-background: transparent;
  --nav-item-text-color: var(--color-text);
  --nav-item-padding: var(--space-xs) var(--space-2xs);
  --nav-item-decoration-color: transparent;
  --nav-item-font-size: var(--size-step-0);
  --nav-item-font-weight: var(--font-bold);

  background: var(--nav-item-background);
  color: var(--nav-item-text-color);
  font-size: var(--nav-item-font-size);
  padding: var(--nav-item-padding);
  display: block;
  border-radius: var(--border-radius-medium);
  text-decoration-line: underline;
  text-decoration-color: var(--nav-item-decoration-color);
  text-decoration-thickness: 3px;
  text-underline-offset: 0.2em;
}

.mainnav:has(.nav-sublist) :is(a, [data-submenu-toggle]) {
  font-weight: var(--nav-item-font-weight);
}

.mainnav [data-submenu-toggle] {
  gap: var(--space-2xs);
  display: flex;
  inline-size: 100%;
  align-items: center;
  justify-content: space-between;
}

.mainnav [data-submenu-toggle] svg {
  margin-inline-end: calc(var(--gap) - var(--nav-list-padding-inline));
}

.mainnav :is(a, [data-submenu-toggle]):hover {
  --nav-item-background: transparent;
  --nav-item-text-color: var(--color-text);
  --nav-item-decoration-color: var(--color-bg-accent-2);
}

.mainnav [aria-current='page'],
.mainnav [data-state='active'] {
  --nav-item-background: var(--color-bg);
  --nav-item-text-color: var(--color-primary);
  --nav-item-decoration-color: var(--color-primary);
}

/* current parent (if submenu) */
li:has(ul a[aria-current='page']) > [data-submenu-toggle] {
  --nav-item-background: var(--color-bg);
  --nav-item-text-color: var(--color-text);
  --nav-item-decoration-color: var(--color-text);
}

/* sub menu */

.mainnav [data-submenu-toggle][aria-expanded='false'] + ul {
  display: none;
}

.mainnav .nav-sublist {
  --gutter: 0;
  --cluster-direction: column;
  --nav-sublist-position: relative;
  --nav-sublist-background: var(--color-bg);
  --nav-sublist-width: 100%;
  --nav-list-visibility: visible;
  --nav-list-opacity: 1;
  --nav-list-padding-block: 0 var(--space-m);
  --nav-list-padding-inline: 0;
  box-shadow: none;
  position: var(--nav-sublist-position);
  inline-size: var(--nav-sublist-width);
  block-size: auto;
  background: var(--nav-sublist-background);
  z-index: 2;
}

.mainnav .nav-sublist a {
  --nav-item-font-weight: var(--font-regular);
}

@media screen(navigation) {
  .mainnav {
    --nav-position: static;
    --nav-button-display: none;
  }

  .mainnav :is(a, [data-submenu-toggle]) {
    --nav-item-font-weight: var(--font-regular);
  }

  .mainnav ul {
    --cluster-direction: row;
    --nav-list-position: static;
    --nav-list-padding-block: 0;
    --nav-list-padding-inline: 0;
    --nav-list-height: auto;
    --nav-list-width: 100%;
    --nav-list-shadow: none;
    --nav-list-visibility: visible;
    --nav-list-transform: translateX(0);
    --nav-list-opacity: 1;
  }

  .mainnav [aria-current='page'],
  .mainnav [data-state='active'],
  li:has(ul a[aria-current='page']) > [data-submenu-toggle] {
    --nav-item-background: transparent;
    --nav-item-text-color: var(--color-primary);
    --nav-item-decoration-color: var(--color-primary);
  }

  .mainnav [data-submenu-toggle] {
    inline-size: auto;
  }

  .mainnav [data-submenu-toggle] svg {
    margin-inline-end: 0;
  }

  .mainnav .nav-sublist {
    --nav-sublist-position: absolute;
    --nav-sublist-background: var(--color-bg);
    --nav-sublist-border: var(--color-text);
    --nav-sublist-width: max-content;
    --nav-list-padding-block: var(--space-xs);
    --nav-list-padding-inline: var(--space-xs);
    border: 3px solid var(--nav-sublist-border);
    inset-block-start: var(--space-xl);
    inset-inline-start: var(--space-2xs);
  }
}

/* Repeat the settings to provide a different styling when JavaScript is disabled or drawerNav is set to false. The selector
assumes that the button doesn’t exist without JS, making the list the first child within the navigation. */

.mainnav ul:first-child {
  --cluster-direction: row;
  --nav-list-position: static;
  --nav-list-padding-block: 0;
  --nav-list-padding-inline: 0;
  --nav-list-height: auto;
  --nav-list-width: 100%;
  --nav-list-shadow: none;
  --nav-list-visibility: visible;
  --nav-list-transform: translateX(0);
  --nav-list-opacity: 1;
}

.mainnav ul:first-child [aria-current='page'],
.mainnav ul:first-child [data-state='active'] {
  --nav-item-background: transparent;
  --nav-item-text-color: var(--color-primary);
  --nav-item-decoration-color: var(--color-primary);
}

/* make menu wrap without drawer */
.mainnav:has(ul:first-child) {
  --nav-position: relative;
}

/* no JS fallback for sub menus */
@media (scripting: none) {
  .mainnav ul:first-child ul.nav-sublist {
    --cluster-direction: row;
    --cluster-wrap: wrap;
  }
}

@media (scripting: none) {
  @media screen(navigation) {
    .mainnav ul:first-child ul.nav-sublist {
      --cluster-direction: column;
    }
  }
}


================================================
FILE: src/assets/css/global/blocks/prose.css
================================================
/* Based on Andy Bell, https://github.com/Andy-set-studio/personal-site-eleventy */

.prose {
  --flow-space: var(--space-m-l);
  --wrapper-width: 64rem;
}

.prose :is(pre, pre + *, figure, picture) {
  --flow-space: var(--space-m-l);
}

.prose :is(figure + *, picture + *) {
  --flow-space: var(--space-xl);
}

.prose :is(img, video) {
  border: var(--stroke);
}

.prose :is(h2, h3, h4) {
  --flow-space: 1.5em;
  overflow-wrap: anywhere;
  hyphens: auto;
}

@media screen(md) {
  .prose :is(h1, h2, h3) {
    overflow-wrap: unset;
    hyphens: unset;
  }
}

.prose :is(h1, h2, h3, h4) + *:not([class]):not(figure) {
  --flow-space: var(--space-l);
}

.prose :is(p, li, dl, blockquote) {
  max-inline-size: 60ch;
  text-wrap: pretty;
}

.prose .heading-anchor:where(:hover, :focus) {
  text-decoration: none;
}

.heading-anchor {
  text-decoration: none;
}

.prose mark {
  background-color: var(--color-tertiary);
  color: var(--color-dark);
}

/* block space only for "regular lists" and sublists */
.prose :is(ul, ol):not([class]) li + li,
.prose :is(ul, ol):not([class]) li > :is(ol, ul) {
  padding-block-start: var(--space-s);
}

/* marker only for "regular lists" */
.prose :is(ul:not([class]):not([role='list'])) li::marker {
  color: var(--color-primary);
  content: '– ';
}

.prose ul:not([class]) {
  padding-inline-start: 1.3ch;
}

.prose ol:not([class]) {
  padding-inline-start: 1.7ch;
}

.prose ol li::marker {
  color: var(--color-primary);
}

.prose img {
  border-radius: var(--border-radius-medium);
}

@media screen(ltsm) {
  .prose > *,
  .prose a {
    overflow-wrap: break-word;
    word-wrap: break-word;
    word-break: break-word;
    /* Adds a hyphen where the word breaks, if supported (No Blink) */
    hyphens: auto;
  }
}


================================================
FILE: src/assets/css/global/blocks/section.css
================================================
.section > .divider:first-child {
  transform: rotate(180deg) translateY(-1px);
}

.section__inner {
  background-color: var(--spot-color, var(--color-bg-accent));
  color: var(--color-text);
}

.section blockquote {
  font-weight: var(--font-bold);
  line-height: 1;
  font-size: var(--size-step-4);
  letter-spacing: var(--tracking-s);
}

.section :is(h1, h2, h3, blockquote) {
  opacity: 95%;
}


================================================
FILE: src/assets/css/global/blocks/seperator.css
================================================
.divider {
  display: block;
  block-size: 3.5em;
  inline-size: 100%;
  fill: var(--spot-color, var(--color-bg));
}

.divider > .divider {
  transform: translateY(-1px);
}


================================================
FILE: src/assets/css/global/blocks/site-footer.css
================================================
.site-footer {
  --cluster-horizontal-alignment: center;
}

/* increase tab size */
.site-footer a:not(.creator a) {
  padding: var(--space-xs);
}
.site-footer .creator a:hover {
  color: transparent;
  background-image: var(--gradient-rainbow);
  background-size: 100%;
  background-repeat: repeat;
  background-clip: text;
}

.site-footer .creator a:hover svg {
  color: var(--color-primary);
}


================================================
FILE: src/assets/css/global/blocks/site-logo.css
================================================
.logo {
  --gutter: var(--space-xs);
  padding: var(--space-s) 0;
  font-size: var(--size-step-0);
  text-decoration: none;
}


================================================
FILE: src/assets/css/global/blocks/skip-link.css
================================================
.skip-link {
  clip: rect(1px, 1px, 1px, 1px);
  display: block;
  block-size: 1px;
  overflow: hidden;
  position: absolute;
  inline-size: 1px;
  top: 1rem;
  left: 1rem;
  z-index: 999;
}

.skip-link:focus {
  clip: auto;
  block-size: auto;
  overflow: visible;
  inline-size: auto;
  background-color: var(--color-text);
  color: var(--color-bg);
  padding: var(--space-xs) var(--space-s-m);
  border-radius: var(--border-radius-medium);
  line-height: 1;
}

.skip-link:not(:focus) {
  border: 0;
  clip: rect(0 0 0 0);
  block-size: auto;
  margin: 0;
  overflow: hidden;
  padding: 0;
  position: absolute;
  inline-size: 1px;
  white-space: nowrap;
}


================================================
FILE: src/assets/css/global/blocks/textgradient.css
================================================
.gradient-text {
  color: transparent;
  background-image: var(--gradient-conic);
  padding: 0.6rem 0;
  background-size: 50%;
  background-clip: text;
}

.gradient-text-linear {
  color: transparent;
  background-image: var(--gradient-rainbow);
  background-size: 100%;
  background-repeat: repeat;
  background-clip: text;
}


================================================
FILE: src/assets/css/global/blocks/theme-switch.css
================================================
.theme-switch h2 {
  font-size: var(--size-step-min-1);
  font-family: var(--font-base);
}

.theme-switch .button[aria-pressed='true'] {
  --_color-contrast: var(--color-tertiary);
  --button-bg: var(--_color-contrast);
  --button-color: var(--color-dark);
  --button-border-color: var(--_color-contrast);
  outline-color: var(--_color-contrast);
}

/* Hide without JS */
is-land:not(:defined) .theme-switch {
  display: none;
}


================================================
FILE: src/assets/css/global/compositions/cluster.css
================================================
/*
CLUSTER
More info: https://every-layout.dev/layouts/cluster/
A layout that lets you distribute items with consistent
spacing, regardless of their size

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-s-m)): This defines the space
between each item.

--cluster-horizontal-alignment (flex-start) How items should align
horizontally. Can be any acceptable flexbox alignment value.

--cluster-vertical-alignment How items should align vertically.
Can be any acceptable flexbox alignment value.
*/

.cluster {
  display: flex;
  flex-direction: var(--cluster-direction, row);
  flex-wrap: var(--cluster-wrap, wrap);
  gap: var(--gutter, var(--space-s-l));
  justify-content: var(--cluster-horizontal-alignment, flex-start);
  align-items: var(--cluster-vertical-alignment, center);
}


================================================
FILE: src/assets/css/global/compositions/flow.css
================================================
/*
FLOW UTILITY
Like the Every Layout stack: https://every-layout.dev/layouts/stack/
Info about this implementation: https://piccalil.li/quick-tip/flow-utility/
*/
.flow > * + * {
  margin-block-start: var(--flow-space, 1em);
}


================================================
FILE: src/assets/css/global/compositions/grid.css
================================================
/* AUTO GRID
Related Every Layout: https://every-layout.dev/layouts/grid/
More info on the flexible nature: https://piccalil.li/tutorial/create-a-responsive-grid-layout-with-no-media-queries-using-css-grid/
A flexible layout that will create an auto-fill grid with
configurable grid item sizes

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-s-m)): This defines the space
between each item.

--grid-min-item-size (14rem): How large each item should be
ideally, as a minimum.

--grid-placement (auto-fill): Set either auto-fit or auto-fill
to change how empty grid tracks are handled */

.grid {
  display: grid;
  grid-template-columns: repeat(
    var(--grid-placement, auto-fill),
    minmax(var(--grid-min-item-size, 16rem), 1fr)
  );
  gap: var(--gutter, var(--space-s-m));
}

.grid[data-rows='masonry'] {
  grid-template-rows: masonry;
  align-items: start;
}

.grid[data-layout='50-50'] {
  --grid-placement: auto-fit;
  --grid-min-item-size: clamp(16rem, 50vw, 28rem);
}

.grid[data-layout='thirds'] {
  --grid-placement: auto-fit;
  --grid-min-item-size: clamp(16rem, 33%, 20rem);
}


================================================
FILE: src/assets/css/global/compositions/repel.css
================================================
/*
REPEL
A little layout that pushes items away from each other where
there is space in the viewport and stacks on small viewports

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-s-m)): This defines the space
between each item.

--repel-vertical-alignment How items should align vertically.
Can be any acceptable flexbox alignment value.
*/

.repel {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: var(--repel-vertical-alignment, center);
  gap: var(--gutter, var(--space-s-l));
}

.repel[data-nowrap] {
  flex-wrap: nowrap;
}


================================================
FILE: src/assets/css/global/compositions/sidebar.css
================================================
/*
SIDEBAR
More info: https://every-layout.dev/layouts/sidebar/
A layout that allows you to have a flexible main content area
and a "fixed" width sidebar that sits on the left or right.
If there is not enough viewport space to fit both the sidebar
width *and* the main content minimum width, they will stack
on top of each other

CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-size-1)): This defines the space
between the sidebar and main content.

--sidebar-target-width (20rem): How large the sidebar should be

--sidebar-content-min-width(50%): The minimum size of the main content area

EXCEPTIONS
.sidebar[data-direction='rtl']: flips the sidebar to be on the right
*/

.sidebar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gutter, var(--space-s-l));
}

.sidebar:not([data-direction]) > :first-child {
  flex-basis: var(--sidebar-target-width, 20rem);
  flex-grow: 1;
}

.sidebar:not([data-direction]) > :last-child {
  flex-basis: 0;
  flex-grow: 999;
  min-inline-size: var(--sidebar-content-min-width, 50%);
}

/*
A flipped version where the sidebar is on the right
*/
.sidebar[data-direction='rtl'] > :last-child {
  flex-basis: var(--sidebar-target-width, 20rem);
  flex-grow: 1;
}

.sidebar[data-direction='rtl'] > :first-child {
  flex-basis: 0;
  flex-grow: 999;
  min-inline-size: var(--sidebar-content-min-width, 50%);
}


================================================
FILE: src/assets/css/global/compositions/wrapper.css
================================================
/* © Ryan Mulligan - https://ryanmulligan.dev/blog/layout-breakouts/ */

.wrapper {
  --gap: clamp(1rem, 6vw, 3rem);
  --full: minmax(var(--gap), 1fr);
  --content: min(var(--wrapper-width, 85rem), 100% - var(--gap) * 2);
  --popout: minmax(0, 2rem);
  --feature: minmax(0, 5rem);

  display: grid;
  grid-template-columns:
    [full-start] var(--full)
    [feature-start] var(--feature)
    [popout-start] var(--popout)
    [content-start] var(--content) [content-end]
    var(--popout) [popout-end]
    var(--feature) [feature-end]
    var(--full) [full-end];
}

.wrapper > * {
  grid-column: content;
}

.prose-wrapper {
  --wrapper-width: 64rem;
}

.popout {
  grid-column: popout;
}

.feature {
  grid-column: feature;
}

.full {
  grid-column: full;
}


================================================
FILE: src/assets/css/global/global.css
================================================
@import 'tailwindcss/base' layer(tailwindBase);

@import 'base/reset.css' layer(reset);
@import 'base/fonts.css' layer(fonts);

@import 'tailwindcss/components' layer(tailwindComponents);

@import 'base/variables.css' layer(variables);
@import 'base/global-styles.css' layer(global);

@import-glob 'compositions/*.css' layer(compositions);
@import-glob 'blocks/*.css' layer(blocks);
@import-glob 'utilities/*.css' layer(utilities);

/* debugging */
/* @import-glob 'tests/*.css' layer(test); */

@import 'tailwindcss/utilities' layer(tailwindUtilities);


================================================
FILE: src/assets/css/global/tests/debug.css
================================================
/* https://heydonworks.com/article/testing-html-with-modern-css/ */

/* WIP */

:root {
  --highlight-outline: 0.25rem solid cornflowerblue;
  --warning-outline: 0.25rem solid orange;
  --error-outline: 0.25rem solid red;
}

/* outline all elements  */
* * * * * {
  outline: var(--highlight-outline);
}

/* highlight empty elements */
*:empty:not(svg *) {
  outline: var(--warning-outline);
  --warning-empty-element: 'The element is empty';
}

a:not([href]) {
  outline: var(--error-outline);
  --error-not-href: 'The link does not have an href. Did you mean to use a