Initial static site implementation

- Node.js build script with gray-matter and marked
- Self-hosted fonts (DM Serif Display, Karla)
- Swedish badge system for origin transparency
- Filtering by category, region, tags, and search
- URL-based filter state for shareable links
- Individual entry pages
- About and badge info pages
- Privacy-focused: no cookies, no tracking, no external requests
- Hosted in Lerum, Sweden
This commit is contained in:
2026-01-25 22:45:50 +01:00
parent 3470a9c8e2
commit 47fc81bc72
1816 changed files with 2010 additions and 2 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
.DS_Store
*.log

101
README.md
View File

@@ -1,2 +1,99 @@
# ursprungsverige # Ursprung Sverige
Ursprung Sverige gathers all Swedish manufacturers and highlights products that are created and produced within the country
Ursprung Sverige is a curated collection of Swedish products, services, experiences and manufacturers. The goal is to make it easier for people to support the Swedish economy and the Swedish culture.
## Privacy Principles
This site practices what it preaches:
- **No cookies** - We don't use any cookies
- **No tracking** - No Google Analytics, no external tracking scripts
- **No third-party services** - Only Google Fonts for typography (consider self-hosting)
- **Swedish hosting** - The site is hosted on servers in Lerum, Sweden
**Do not add:**
- Analytics scripts (Google Analytics, Plausible, etc.)
- Cookie consent banners
- External tracking pixels
- Third-party comment systems
- Social media embeds with tracking
If analytics are needed in the future, consider privacy-respecting, self-hosted alternatives like Umami or Matomo hosted in Sweden.
## Quick Start
```bash
npm install
npm run build
```
The generated site will be in the `dist/` folder.
## Adding Content
Create a Markdown file in the appropriate `content/` subfolder:
- `content/products/` - Swedish products
- `content/services/` - Swedish services
- `content/experiences/` - Swedish experiences
- `content/manufacturers/` - Swedish manufacturers
### Content Format
Each entry is a Markdown file with YAML frontmatter:
```markdown
---
title: Fjällräven
category: manufacturers
region: Norrbotten
tags: [friluftsliv, kläder, hållbarhet]
website: https://fjallraven.com
---
Description of the entry in Markdown format...
```
### Frontmatter Fields
| Field | Required | Description |
|-------|----------|-------------|
| `title` | Yes | Name of the product/service/etc |
| `category` | No | Auto-detected from folder if not specified |
| `region` | No | Swedish region (län) |
| `tags` | No | Array of tags for filtering |
| `website` | No | Link to website |
| `image` | No | Featured image URL (displayed at top of card) |
| `logo` | No | Logo URL (displayed next to title) |
| `badge` | No | Swedish origin badge (see below) |
### Badges
Tiered badges indicate how Swedish a product/company is:
| Tier | Badge Value | Display | For | Description |
|------|-------------|---------|-----|-------------|
| 1 | `akta-svenskt` | Äkta Svenskt (gold) | All | 100% Swedish: company, production/hosting, suppliers |
| 2 | `tillverkat-i-sverige` | Tillverkat i Sverige (silver) | Physical | Swedish company, manufactured in Sweden |
| 2 | `svenskt-moln` | Svenskt Moln (silver) | Digital | Swedish company, Swedish servers/hosting |
| 3 | `designat-i-sverige` | Designat i Sverige (bronze) | Physical | Swedish design, production may be abroad |
| 3 | `utvecklat-i-sverige` | Utvecklat i Sverige (bronze) | Digital | Swedish development, hosting may be abroad |
| 4 | `svenskt-foretag` | Svenskt Företag (blue) | All | Swedish-founded/registered company |
### Images
Place images in the `static/` folder. They're symlinked to `dist/static/` (no copying), so reference them as `/static/images/example.png` in your content.
## Development
Watch mode rebuilds on file changes:
```bash
npm run dev
```
## Deployment
Deploy the `dist/` folder to a Swedish hosting provider. The production site is hosted in Lerum, Sweden.
**Important:** Do not deploy to non-Swedish hosting services (Netlify, Vercel, Cloudflare, etc.) as this would contradict the site's principles.

365
build.js Normal file
View File

@@ -0,0 +1,365 @@
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';
const CONTENT_DIR = './content';
const TEMPLATE_DIR = './templates';
const PUBLIC_DIR = './public';
const STATIC_DIR = './static';
const DIST_DIR = './dist';
// Swedish translations for categories
const CATEGORY_LABELS = {
manufacturers: 'Tillverkare',
products: 'Produkter',
services: 'Tjänster',
experiences: 'Upplevelser'
};
// Badge tiers (highest to lowest)
const BADGE_TIERS = {
// Tier 1 - Gold (universal)
'akta-svenskt': { label: 'Äkta Svenskt', tier: 1, description: 'Helt svenskt: företag, produktion/drift och leverantörer' },
// Tier 2 - Silver (physical or digital)
'tillverkat-i-sverige': { label: 'Tillverkat i Sverige', tier: 2, description: 'Svenskt företag, tillverkat i Sverige' },
'svenskt-moln': { label: 'Svenskt Moln', tier: 2, description: 'Svenskt företag med svenska servrar och hosting' },
// Tier 3 - Bronze (physical or digital)
'designat-i-sverige': { label: 'Designat i Sverige', tier: 3, description: 'Svensk design, produktion kan ske utomlands' },
'utvecklat-i-sverige': { label: 'Utvecklat i Sverige', tier: 3, description: 'Svensk utveckling, hosting kan ske utomlands' },
// Tier 4 - Blue (universal)
'svenskt-foretag': { label: 'Svenskt Företag', tier: 4, description: 'Svenskregistrerat eller svenskgrundat företag' }
};
function translateCategory(category) {
return CATEGORY_LABELS[category] || category;
}
function getBadgeHTML(badgeKey) {
if (!badgeKey || !BADGE_TIERS[badgeKey]) return '';
const badge = BADGE_TIERS[badgeKey];
return `<span class="badge badge-tier-${badge.tier}" title="${badge.description}">${badge.label}</span>`;
}
// Ensure directory exists
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
// Copy directory recursively
function copyDirRecursive(src, dest) {
const items = fs.readdirSync(src, { withFileTypes: true });
for (const item of items) {
const srcPath = path.join(src, item.name);
const destPath = path.join(dest, item.name);
if (item.isDirectory()) {
ensureDir(destPath);
copyDirRecursive(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
// Read all markdown files from a directory recursively
function readContentFiles(dir) {
const entries = [];
if (!fs.existsSync(dir)) {
return entries;
}
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
entries.push(...readContentFiles(fullPath));
} else if (item.name.endsWith('.md')) {
const content = fs.readFileSync(fullPath, 'utf-8');
const { data, content: body } = matter(content);
const category = path.basename(path.dirname(fullPath));
const slug = path.basename(item.name, '.md');
entries.push({
...data,
category: data.category || category,
body: marked(body),
bodyRaw: body,
slug,
url: `${category}/${slug}.html`,
sourcePath: fullPath
});
}
}
return entries;
}
// Simple template replacement
function renderTemplate(template, data) {
let result = template;
for (const [key, value] of Object.entries(data)) {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
result = result.replace(regex, value ?? '');
}
return result;
}
// Generate entry cards HTML
function generateEntryCards(entries) {
return entries.map(entry => {
const imageHtml = entry.image
? `<div class="entry-image"><img src="${entry.image}" alt="${entry.title}" loading="lazy"></div>`
: '';
const logoHtml = entry.logo
? `<img src="${entry.logo}" alt="${entry.title} logo" class="entry-logo">`
: '';
const badgeHtml = getBadgeHTML(entry.badge);
return `
<article class="entry-card"
data-category="${entry.category || ''}"
data-region="${entry.region || ''}"
data-tags="${(entry.tags || []).join(',')}"
data-badge="${entry.badge || ''}">
${imageHtml}
<div class="entry-card-content">
<div class="entry-header">
${logoHtml}
<h2><a href="${entry.url}">${entry.title}</a></h2>
${badgeHtml}
</div>
<div class="entry-meta">
<button type="button" class="filter-btn category" data-filter-type="category" data-filter-value="${entry.category || ''}">${translateCategory(entry.category) || ''}</button>
${entry.region ? `<button type="button" class="filter-btn region" data-filter-type="region" data-filter-value="${entry.region}">${entry.region}</button>` : ''}
</div>
<div class="entry-body">${entry.body}</div>
${entry.tags?.length ? `
<div class="tags">
${entry.tags.map(tag => `<button type="button" class="tag filter-btn" data-filter-type="tag" data-filter-value="${tag}">${tag}</button>`).join('')}
</div>
` : ''}
</div>
</article>
`}).join('\n');
}
// Generate single entry page HTML
function generateEntryPage(entry, template) {
const imageHtml = entry.image
? `<div class="single-image"><img src="${entry.image}" alt="${entry.title}"></div>`
: '';
const logoHtml = entry.logo
? `<img src="${entry.logo}" alt="${entry.title} logo" class="single-logo">`
: '';
const badgeHtml = getBadgeHTML(entry.badge);
const entryHtml = `
<article class="single-entry">
${imageHtml}
<div class="single-header">
${logoHtml}
<div class="single-title-wrapper">
<h1>${entry.title}</h1>
${badgeHtml}
</div>
</div>
<div class="single-meta">
<a href="../index.html?category=${encodeURIComponent(entry.category || '')}" class="filter-link category">${translateCategory(entry.category) || ''}</a>
${entry.region ? `<a href="../index.html?region=${encodeURIComponent(entry.region)}" class="filter-link region">${entry.region}</a>` : ''}
</div>
<div class="single-body">${entry.body}</div>
${entry.website ? `<a href="${entry.website}" class="website-link" target="_blank" rel="noopener">Besök webbplats →</a>` : ''}
${entry.tags?.length ? `
<div class="tags">
${entry.tags.map(tag => `<a href="../index.html?tag=${encodeURIComponent(tag)}" class="tag">${tag}</a>`).join('')}
</div>
` : ''}
<a href="../index.html" class="back-link">← Tillbaka till alla poster</a>
</article>
`;
return renderTemplate(template, {
title: `${entry.title} | Ursprung Sverige`,
subtitle: entry.title,
content: entryHtml,
year: new Date().getFullYear()
});
}
// Extract unique values for filters
function extractFilters(entries) {
const categories = [...new Set(entries.map(e => e.category).filter(Boolean))].sort();
const regions = [...new Set(entries.map(e => e.region).filter(Boolean))].sort();
const tags = [...new Set(entries.flatMap(e => e.tags || []))].sort();
return { categories, regions, tags };
}
// Generate filter HTML
function generateFilterHTML(filters) {
const categoryOptions = filters.categories.map(c =>
`<option value="${c}">${translateCategory(c)}</option>`
).join('');
const regionOptions = filters.regions.map(r =>
`<option value="${r}">${r}</option>`
).join('');
const tagOptions = filters.tags.map(t =>
`<option value="${t}">${t}</option>`
).join('');
return `
<div class="filters">
<div class="filter-group">
<label for="category-filter">Kategori</label>
<select id="category-filter">
<option value="">Alla kategorier</option>
${categoryOptions}
</select>
</div>
<div class="filter-group">
<label for="region-filter">Region</label>
<select id="region-filter">
<option value="">Alla regioner</option>
${regionOptions}
</select>
</div>
<div class="filter-group">
<label for="tag-filter">Tagg</label>
<select id="tag-filter">
<option value="">Alla taggar</option>
${tagOptions}
</select>
</div>
<div class="filter-group">
<label for="search-input">Sök</label>
<input type="text" id="search-input" placeholder="Sök...">
</div>
<button type="button" id="clear-filters" class="clear-filters-btn">Rensa filter</button>
</div>
`;
}
// Main build function
function build() {
console.log('Building site...');
// Clean and create dist directory
if (fs.existsSync(DIST_DIR)) {
fs.rmSync(DIST_DIR, { recursive: true });
}
ensureDir(DIST_DIR);
// Read content
const entries = readContentFiles(CONTENT_DIR);
console.log(`Found ${entries.length} entries`);
// Extract filters
const filters = extractFilters(entries);
// Read templates
const indexTemplatePath = path.join(TEMPLATE_DIR, 'template.html');
const singleTemplatePath = path.join(TEMPLATE_DIR, 'single.html');
if (!fs.existsSync(indexTemplatePath)) {
console.error('Template not found:', indexTemplatePath);
process.exit(1);
}
const indexTemplate = fs.readFileSync(indexTemplatePath, 'utf-8');
const singleTemplate = fs.existsSync(singleTemplatePath)
? fs.readFileSync(singleTemplatePath, 'utf-8')
: indexTemplate; // Fallback to index template
// Generate index HTML
const indexHtml = renderTemplate(indexTemplate, {
title: 'Ursprung Sverige',
subtitle: 'En kurerad samling av svenska produkter, tjänster, upplevelser och tillverkare',
filters: generateFilterHTML(filters),
entries: generateEntryCards(entries),
entryCount: entries.length,
year: new Date().getFullYear()
});
// Write index.html
fs.writeFileSync(path.join(DIST_DIR, 'index.html'), indexHtml);
// Generate badges info page
const badgesTemplatePath = path.join(TEMPLATE_DIR, 'badges.html');
if (fs.existsSync(badgesTemplatePath)) {
const badgesTemplate = fs.readFileSync(badgesTemplatePath, 'utf-8');
const badgesHtml = renderTemplate(badgesTemplate, {
year: new Date().getFullYear()
});
fs.writeFileSync(path.join(DIST_DIR, 'om-markning.html'), badgesHtml);
}
// Generate about page
const aboutTemplatePath = path.join(TEMPLATE_DIR, 'about.html');
if (fs.existsSync(aboutTemplatePath)) {
const aboutTemplate = fs.readFileSync(aboutTemplatePath, 'utf-8');
const aboutHtml = renderTemplate(aboutTemplate, {
year: new Date().getFullYear()
});
fs.writeFileSync(path.join(DIST_DIR, 'om-oss.html'), aboutHtml);
}
// Generate individual entry pages
for (const entry of entries) {
const categoryDir = path.join(DIST_DIR, entry.category);
ensureDir(categoryDir);
const entryHtml = generateEntryPage(entry, singleTemplate);
fs.writeFileSync(path.join(categoryDir, `${entry.slug}.html`), entryHtml);
}
console.log(`Generated ${entries.length} entry pages`);
// Copy public assets (CSS, JS - small files)
if (fs.existsSync(PUBLIC_DIR)) {
copyDirRecursive(PUBLIC_DIR, DIST_DIR);
}
// Symlink static folder (images, large assets - avoids copying)
const staticDest = path.join(DIST_DIR, 'static');
if (fs.existsSync(STATIC_DIR) && !fs.existsSync(staticDest)) {
const absoluteStatic = path.resolve(STATIC_DIR);
fs.symlinkSync(absoluteStatic, staticDest);
}
console.log('Build complete! Output in', DIST_DIR);
}
// Watch mode
if (process.argv.includes('--watch')) {
console.log('Watching for changes...');
build();
const watchDirs = [CONTENT_DIR, TEMPLATE_DIR, PUBLIC_DIR];
for (const dir of watchDirs) {
if (fs.existsSync(dir)) {
fs.watch(dir, { recursive: true }, (event, filename) => {
console.log(`\nChange detected: ${filename}`);
build();
});
}
}
} else {
build();
}

View File

@@ -0,0 +1,10 @@
---
title: Icehotel
category: experiences
region: Norrbotten
tags: [vinter, is, konst, boende, norrsken]
website: https://icehotel.com
image: https://images.unsplash.com/photo-1520769945061-0a448c463865?w=800&q=80
---
Icehotel i Jukkasjärvi är världens första ishotell, byggt varje vinter sedan 1989. Hotellet återskapas varje år av konstnärer från hela världen och erbjuder unika upplevelser med isskulpturer, norrskenssafari och samisk kultur. En genuint svensk vinterupplevelse.

View File

@@ -0,0 +1,8 @@
---
title: Midsommarfirande
category: experiences
region: Dalarna
tags: [tradition, sommar, dans, kultur, mat]
---
Midsommar är en av Sveriges viktigaste högtider och firas traditionellt kring sommarsolståndet. Med midsommarstång, blommor i håret, sill och jordgubbar samlas svenskar för dans och festligheter. Dalarna är särskilt känt för sitt autentiska midsommarfirande i pittoreska byar.

View File

@@ -0,0 +1,11 @@
---
title: Fjällräven
region: Norrbotten
tags: [friluftsliv, kläder, hållbarhet, ryggsäckar]
website: https://fjallraven.com
image: https://images.unsplash.com/photo-1551632811-561732d1e306?w=800&q=80
logo: /static/images/fjallraven_logo.png
badge: designat-i-sverige
---
Fjällräven är ett svenskt friluftsföretag grundat 1960 i Örnsköldsvik av Åke Nordin. Företaget är mest känt för sin ikoniska Kånken-ryggsäck och sina hållbara friluftskläder. Med fokus på kvalitet och miljömedvetenhet har Fjällräven blivit en symbol för svensk friluftskultur.

View File

@@ -0,0 +1,9 @@
---
title: Hasselblad
category: manufacturers
region: Västra Götaland
tags: [kameror, fotografi, premium, teknik]
website: https://hasselblad.com
---
Hasselblad är en svensk tillverkare av mellanformatskameror, grundat 1841 i Göteborg. Företaget är legendariskt inom professionell fotografi och var kameran som dokumenterade månlandningen 1969. Hasselblad står för kompromisslös bildkvalitet och svensk ingenjörskonst.

View File

@@ -0,0 +1,9 @@
---
title: Dalahäst
region: Dalarna
tags: [hantverk, tradition, trä, present]
website: https://www.grannas.com
badge: akta-svenskt
---
Dalahästen är en handsnidad trähäst som har tillverkats i Dalarna sedan 1600-talet. Varje häst snittas för hand och målas i traditionella mönster, ofta i rött med vit och grön dekor. Dalahästen har blivit en av Sveriges mest kända symboler och ett uppskattat hantverk världen över.

View File

@@ -0,0 +1,9 @@
---
title: Orrefors Glas
region: Småland
tags: [glas, design, konst, kristall]
website: https://orrefors.se
badge: tillverkat-i-sverige
---
Orrefors är ett svenskt glasbruk grundat 1898 i Småland, hjärtat av det svenska glasriket. Bruket är känt för sitt konstnärliga glas och sin kristall av högsta kvalitet. Orrefors har samarbetat med några av Sveriges främsta designers och konstnärer genom åren.

View File

@@ -0,0 +1,10 @@
---
title: Mediaflow
region: Uppsala
tags: [digital, media, video, varumärke, dam, saas]
website: https://www.mediaflow.com
badge: svenskt-moln
image: /static/images/mediaflow-cover.webp
---
Mediaflow är en svensk plattform för digital asset management, videohantering och varumärkeshantering. Företaget hjälper organisationer att samla, organisera och dela digitala filer på ett säkert sätt. Med svenska servrar och starkt fokus på GDPR används Mediaflow av tusentals marknadsförare, kommunikatörer och kreatörer inom både privat och offentlig sektor.

View File

@@ -0,0 +1,9 @@
---
title: PostNord
category: services
region: Stockholm
tags: [post, leverans, logistik, paket]
website: https://postnord.se
---
PostNord är Nordens ledande leverantör av kommunikations- och logistiklösningar. Med rötter i det svenska postväsendet sedan 1636 har företaget utvecklats till en modern logistikpartner för både privatpersoner och företag i hela Norden.

9
content/services/sj.md Normal file
View File

@@ -0,0 +1,9 @@
---
title: SJ
category: services
region: Stockholm
tags: [tåg, resor, hållbart, transport]
website: https://sj.se
---
SJ är Sveriges största tågoperatör och har fraktat passagerare sedan 1856. Med snabbtåg som kopplar samman Sveriges städer erbjuder SJ ett hållbart alternativ till flyg och bil. Att resa med tåg är en del av den svenska livsstilen och ett miljövänligt val.

136
package-lock.json generated Normal file
View File

@@ -0,0 +1,136 @@
{
"name": "ursprungsverige",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ursprungsverige",
"version": "1.0.0",
"dependencies": {
"gray-matter": "^4.0.3",
"marked": "^12.0.0"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
}
}
}

15
package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "ursprungsverige",
"version": "1.0.0",
"description": "A curated collection of Swedish products, services, experiences and manufacturers",
"type": "module",
"scripts": {
"build": "node build.js",
"dev": "node build.js --watch",
"serve": "npx --yes serve dist -p 3000"
},
"dependencies": {
"gray-matter": "^4.0.3",
"marked": "^12.0.0"
}
}

194
public/filter.js Normal file
View File

@@ -0,0 +1,194 @@
// Ursprung Sverige - Filtering functionality
(function() {
'use strict';
const categoryFilter = document.getElementById('category-filter');
const regionFilter = document.getElementById('region-filter');
const tagFilter = document.getElementById('tag-filter');
const searchInput = document.getElementById('search-input');
const clearFiltersBtn = document.getElementById('clear-filters');
const entriesGrid = document.getElementById('entries-grid');
const visibleCount = document.getElementById('visible-count');
const noResults = document.getElementById('no-results');
if (!entriesGrid) return;
const entries = Array.from(entriesGrid.querySelectorAll('.entry-card'));
// Update URL with current filter state
function updateURL() {
const params = new URLSearchParams();
if (categoryFilter?.value) {
params.set('category', categoryFilter.value);
}
if (regionFilter?.value) {
params.set('region', regionFilter.value);
}
if (tagFilter?.value) {
params.set('tag', tagFilter.value);
}
if (searchInput?.value.trim()) {
params.set('search', searchInput.value.trim());
}
const queryString = params.toString();
const newURL = queryString
? `${window.location.pathname}?${queryString}`
: window.location.pathname;
window.history.replaceState({}, '', newURL);
}
// Apply filters from URL on page load
function applyFiltersFromURL() {
const params = new URLSearchParams(window.location.search);
if (params.has('category') && categoryFilter) {
categoryFilter.value = params.get('category');
}
if (params.has('region') && regionFilter) {
regionFilter.value = params.get('region');
}
if (params.has('tag') && tagFilter) {
tagFilter.value = params.get('tag');
}
if (params.has('search') && searchInput) {
searchInput.value = params.get('search');
}
filterEntries(false); // Don't update URL on initial load
}
function filterEntries(shouldUpdateURL = true) {
const categoryValue = categoryFilter?.value.toLowerCase() || '';
const regionValue = regionFilter?.value.toLowerCase() || '';
const tagValue = tagFilter?.value.toLowerCase() || '';
const searchValue = searchInput?.value.toLowerCase().trim() || '';
let visibleEntries = 0;
entries.forEach(entry => {
const entryCategory = (entry.dataset.category || '').toLowerCase();
const entryRegion = (entry.dataset.region || '').toLowerCase();
const entryTags = (entry.dataset.tags || '').toLowerCase().split(',');
const entryText = entry.textContent.toLowerCase();
const matchesCategory = !categoryValue || entryCategory === categoryValue;
const matchesRegion = !regionValue || entryRegion === regionValue;
const matchesTag = !tagValue || entryTags.includes(tagValue);
const matchesSearch = !searchValue || entryText.includes(searchValue);
const isVisible = matchesCategory && matchesRegion && matchesTag && matchesSearch;
entry.hidden = !isVisible;
if (isVisible) visibleEntries++;
});
if (visibleCount) {
visibleCount.textContent = visibleEntries;
}
if (noResults) {
noResults.hidden = visibleEntries > 0;
}
if (entriesGrid) {
entriesGrid.hidden = visibleEntries === 0;
}
updateClearButtonVisibility();
if (shouldUpdateURL) {
updateURL();
}
}
function updateClearButtonVisibility() {
if (!clearFiltersBtn) return;
const hasFilters =
(categoryFilter?.value || '') !== '' ||
(regionFilter?.value || '') !== '' ||
(tagFilter?.value || '') !== '' ||
(searchInput?.value || '') !== '';
clearFiltersBtn.hidden = !hasFilters;
}
function clearFilters() {
if (categoryFilter) categoryFilter.value = '';
if (regionFilter) regionFilter.value = '';
if (tagFilter) tagFilter.value = '';
if (searchInput) searchInput.value = '';
filterEntries(); // This will also clear the URL
}
// Handle clicking on filter buttons in cards
function handleFilterClick(e) {
const btn = e.target.closest('.filter-btn');
if (!btn) return;
const filterType = btn.dataset.filterType;
const filterValue = btn.dataset.filterValue;
if (filterType === 'category' && categoryFilter) {
categoryFilter.value = filterValue;
} else if (filterType === 'region' && regionFilter) {
regionFilter.value = filterValue;
} else if (filterType === 'tag' && tagFilter) {
tagFilter.value = filterValue;
}
filterEntries();
// Scroll to top to see filtered results
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// Handle browser back/forward
window.addEventListener('popstate', () => {
applyFiltersFromURL();
});
// Debounce for search input
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Event listeners
if (categoryFilter) {
categoryFilter.addEventListener('change', () => filterEntries());
}
if (regionFilter) {
regionFilter.addEventListener('change', () => filterEntries());
}
if (tagFilter) {
tagFilter.addEventListener('change', () => filterEntries());
}
if (searchInput) {
searchInput.addEventListener('input', debounce(() => filterEntries(), 300));
}
if (clearFiltersBtn) {
clearFiltersBtn.addEventListener('click', clearFilters);
}
// Delegate click events for filter buttons in cards
if (entriesGrid) {
entriesGrid.addEventListener('click', handleFilterClick);
}
// Apply URL filters on load
applyFiltersFromURL();
updateClearButtonVisibility();
})();

Binary file not shown.

Binary file not shown.

840
public/styles.css Normal file
View File

@@ -0,0 +1,840 @@
/* Ursprung Sverige - Scandinavian Minimal Design */
/* Self-hosted fonts - DM Serif Display */
@font-face {
font-family: 'DM Serif Display';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('fonts/DMSerifDisplay-Regular.ttf') format('truetype');
}
/* Self-hosted fonts - Karla (variable font) */
@font-face {
font-family: 'Karla';
font-style: normal;
font-weight: 200 800;
font-display: swap;
src: url('fonts/Karla-Variable.ttf') format('truetype');
}
:root {
/* Swedish-inspired palette */
--color-bg: #faf9f7;
--color-bg-alt: #f0eeea;
--color-text: #1a1a1a;
--color-text-muted: #5a5a5a;
--color-accent: #005baa;
--color-accent-gold: #fecc00;
--color-border: #d8d4cc;
--color-card-bg: #ffffff;
/* Badge tier colors */
--badge-tier-1-bg: linear-gradient(135deg, #c9a227 0%, #f4d03f 50%, #c9a227 100%);
--badge-tier-1-text: #1a1a1a;
--badge-tier-2-bg: linear-gradient(135deg, #7b8a8b 0%, #bdc3c7 50%, #7b8a8b 100%);
--badge-tier-2-text: #1a1a1a;
--badge-tier-3-bg: linear-gradient(135deg, #a0522d 0%, #cd853f 50%, #a0522d 100%);
--badge-tier-3-text: #ffffff;
--badge-tier-4-bg: var(--color-accent);
--badge-tier-4-text: #ffffff;
/* Typography */
--font-display: 'DM Serif Display', Georgia, serif;
--font-body: 'Karla', -apple-system, BlinkMacSystemFont, sans-serif;
/* Spacing */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 2rem;
--space-xl: 4rem;
/* Sizes */
--max-width: 1200px;
--border-radius: 4px;
}
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Base */
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
font-family: var(--font-body);
background-color: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
padding: 0 var(--space-lg);
}
.container--narrow {
max-width: 800px;
}
/* Header */
.site-header {
background: linear-gradient(135deg, var(--color-bg) 0%, var(--color-bg-alt) 100%);
padding: var(--space-xl) 0;
border-bottom: 1px solid var(--color-border);
position: relative;
overflow: hidden;
}
.site-header--single {
padding: var(--space-lg) 0;
}
.site-header .container {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-lg);
}
.header-content {
flex: 1;
}
.site-title {
font-family: var(--font-display);
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 400;
color: var(--color-text);
letter-spacing: -0.02em;
margin-bottom: var(--space-sm);
}
.site-title--small {
font-size: 1.5rem;
margin-bottom: 0;
}
.site-title-link {
text-decoration: none;
color: inherit;
}
.site-title-link:hover .site-title {
color: var(--color-accent);
}
.site-subtitle {
font-size: 1.125rem;
color: var(--color-text-muted);
max-width: 500px;
}
.header-decoration {
flex-shrink: 0;
}
.sweden-motif {
width: 80px;
height: 48px;
opacity: 0.9;
}
/* Filters */
.filter-section {
padding: var(--space-lg) 0;
border-bottom: 1px solid var(--color-border);
margin-bottom: var(--space-lg);
}
.filters {
display: flex;
flex-wrap: wrap;
gap: var(--space-md);
margin-bottom: var(--space-md);
align-items: flex-end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.filter-group label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.filter-group select,
.filter-group input {
font-family: var(--font-body);
font-size: 0.9375rem;
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
background-color: var(--color-card-bg);
color: var(--color-text);
min-width: 160px;
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.filter-group select:hover,
.filter-group input:hover {
border-color: var(--color-accent);
}
.filter-group select:focus,
.filter-group input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px rgba(0, 91, 170, 0.1);
}
.clear-filters-btn {
font-family: var(--font-body);
font-size: 0.875rem;
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
background-color: var(--color-bg-alt);
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.2s ease;
}
.clear-filters-btn:hover {
background-color: var(--color-text);
color: var(--color-bg);
border-color: var(--color-text);
}
.filter-footer {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--space-sm);
}
.entry-count {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.badges-info-link {
font-size: 0.875rem;
color: var(--color-accent);
text-decoration: none;
}
.badges-info-link:hover {
text-decoration: underline;
}
/* Main content */
.site-main {
flex: 1;
padding-bottom: var(--space-xl);
}
.site-main--single {
padding-top: var(--space-lg);
}
/* Entry cards grid */
.entries-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--space-lg);
}
.entry-card {
background: var(--color-card-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex;
flex-direction: column;
}
.entry-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
.entry-image {
aspect-ratio: 16/9;
overflow: hidden;
background: var(--color-bg-alt);
}
.entry-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.entry-card-content {
padding: var(--space-lg);
display: flex;
flex-direction: column;
flex: 1;
}
.entry-header {
display: flex;
align-items: flex-start;
gap: var(--space-md);
margin-bottom: var(--space-sm);
flex-wrap: wrap;
}
.entry-logo {
width: 48px;
height: 48px;
object-fit: contain;
border-radius: var(--border-radius);
flex-shrink: 0;
}
.entry-card h2 {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 400;
color: var(--color-text);
flex: 1;
}
.entry-card h2 a {
color: inherit;
text-decoration: none;
transition: color 0.2s ease;
}
.entry-card h2 a:hover {
color: var(--color-accent);
}
/* Badges */
.badge {
display: inline-block;
font-family: var(--font-body);
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.25rem 0.5rem;
border-radius: 3px;
white-space: nowrap;
cursor: help;
}
.badge-tier-1 {
background: var(--badge-tier-1-bg);
color: var(--badge-tier-1-text);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.badge-tier-2 {
background: var(--badge-tier-2-bg);
color: var(--badge-tier-2-text);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.badge-tier-3 {
background: var(--badge-tier-3-bg);
color: var(--badge-tier-3-text);
}
.badge-tier-4 {
background: var(--badge-tier-4-bg);
color: var(--badge-tier-4-text);
}
.entry-meta {
display: flex;
gap: var(--space-sm);
margin-bottom: var(--space-md);
flex-wrap: wrap;
}
.entry-meta .filter-btn,
.filter-link {
font-family: var(--font-body);
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--border-radius);
border: none;
cursor: pointer;
transition: opacity 0.2s ease, transform 0.1s ease;
text-decoration: none;
}
.entry-meta .filter-btn:hover,
.filter-link:hover {
opacity: 0.8;
transform: scale(1.02);
}
.entry-meta .category,
.filter-link.category {
background: var(--color-accent);
color: white;
}
.entry-meta .region,
.filter-link.region {
background: var(--color-accent-gold);
color: var(--color-text);
}
.entry-body {
color: var(--color-text-muted);
margin-bottom: var(--space-md);
font-size: 0.9375rem;
flex: 1;
}
.entry-body p {
margin-bottom: var(--space-sm);
}
.entry-body p:last-child {
margin-bottom: 0;
}
.website-link {
display: inline-block;
color: var(--color-accent);
text-decoration: none;
font-weight: 500;
font-size: 0.875rem;
margin-bottom: var(--space-md);
transition: opacity 0.2s ease;
}
.website-link:hover {
opacity: 0.7;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
margin-top: auto;
}
.tag {
font-family: var(--font-body);
font-size: 0.75rem;
color: var(--color-text-muted);
background: var(--color-bg-alt);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--border-radius);
border: none;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
text-decoration: none;
}
.tag:hover {
background: var(--color-accent);
color: white;
}
/* No results */
.no-results {
text-align: center;
padding: var(--space-xl);
color: var(--color-text-muted);
font-size: 1.125rem;
}
/* Hidden utility */
[hidden] {
display: none !important;
}
/* Single entry page */
.single-entry {
background: var(--color-card-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
overflow: hidden;
}
.single-image {
aspect-ratio: 21/9;
overflow: hidden;
background: var(--color-bg-alt);
}
.single-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.single-header {
display: flex;
align-items: center;
gap: var(--space-lg);
padding: var(--space-lg);
padding-bottom: var(--space-sm);
}
.single-logo {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: var(--border-radius);
flex-shrink: 0;
}
.single-title-wrapper {
flex: 1;
}
.single-header h1 {
font-family: var(--font-display);
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 400;
color: var(--color-text);
margin-bottom: var(--space-xs);
}
.single-header .badge {
font-size: 0.75rem;
padding: 0.375rem 0.75rem;
}
.single-meta {
display: flex;
gap: var(--space-sm);
padding: 0 var(--space-lg);
margin-bottom: var(--space-lg);
flex-wrap: wrap;
}
.single-body {
padding: 0 var(--space-lg);
margin-bottom: var(--space-lg);
font-size: 1.0625rem;
line-height: 1.7;
color: var(--color-text);
}
.single-body p {
margin-bottom: var(--space-md);
}
.single-entry .website-link {
display: block;
padding: 0 var(--space-lg);
margin-bottom: var(--space-lg);
}
.single-entry .tags {
padding: 0 var(--space-lg);
margin-bottom: var(--space-lg);
}
.back-link {
display: block;
padding: var(--space-md) var(--space-lg);
background: var(--color-bg-alt);
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.875rem;
transition: color 0.2s ease;
}
.back-link:hover {
color: var(--color-accent);
}
/* Footer */
.site-footer {
background: var(--color-bg-alt);
border-top: 1px solid var(--color-border);
padding: var(--space-lg) 0;
text-align: center;
}
.site-footer p {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.footer-tagline {
margin-top: var(--space-xs);
font-style: italic;
}
.footer-nav {
margin-top: var(--space-md);
display: flex;
justify-content: center;
gap: var(--space-lg);
}
.footer-nav a {
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.875rem;
}
.footer-nav a:hover {
color: var(--color-accent);
}
/* Responsive */
@media (max-width: 768px) {
.site-header .container {
flex-direction: column;
text-align: center;
}
.header-decoration {
order: -1;
}
.site-subtitle {
margin: 0 auto;
}
.filters {
flex-direction: column;
align-items: stretch;
}
.filter-group select,
.filter-group input {
width: 100%;
}
.clear-filters-btn {
width: 100%;
}
.entries-grid {
grid-template-columns: 1fr;
}
.single-header {
flex-direction: column;
text-align: center;
}
}
/* About page */
.about-page {
background: var(--color-card-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--space-lg);
}
.about-page h1 {
font-family: var(--font-display);
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 400;
margin-bottom: var(--space-lg);
}
.about-section {
margin-bottom: var(--space-lg);
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--color-border);
}
.about-section:last-of-type {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.about-section h2 {
font-family: var(--font-display);
font-size: 1.25rem;
font-weight: 400;
margin-bottom: var(--space-md);
}
.about-section p {
margin-bottom: var(--space-sm);
line-height: 1.7;
}
.about-section ul {
list-style: none;
margin: var(--space-md) 0;
}
.about-section li {
padding: var(--space-sm) 0;
padding-left: var(--space-lg);
position: relative;
line-height: 1.6;
}
.about-section li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-accent);
}
.about-page .back-link {
display: inline-block;
margin-top: var(--space-lg);
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.875rem;
}
.about-page .back-link:hover {
color: var(--color-accent);
}
/* Badges info page */
.badges-page {
background: var(--color-card-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--space-lg);
}
.badges-page h1 {
font-family: var(--font-display);
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 400;
margin-bottom: var(--space-md);
}
.badges-page .intro {
font-size: 1.125rem;
color: var(--color-text-muted);
margin-bottom: var(--space-xl);
line-height: 1.7;
}
.badge-tier-section {
margin-bottom: var(--space-lg);
padding-bottom: var(--space-lg);
border-bottom: 1px solid var(--color-border);
}
.badge-tier-section:last-of-type {
border-bottom: none;
}
.badge-tier-header {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-md);
flex-wrap: wrap;
}
.tier-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
background: var(--color-bg-alt);
padding: var(--space-xs) var(--space-sm);
border-radius: var(--border-radius);
}
.badge-tier-section p {
margin-bottom: var(--space-sm);
line-height: 1.7;
}
.badge-tier-section .criteria {
font-size: 0.875rem;
color: var(--color-text-muted);
background: var(--color-bg-alt);
padding: var(--space-sm) var(--space-md);
border-radius: var(--border-radius);
margin-top: var(--space-md);
}
.why-section {
margin-top: var(--space-xl);
padding-top: var(--space-lg);
border-top: 1px solid var(--color-border);
}
.why-section h2 {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 400;
margin-bottom: var(--space-md);
}
.why-section ul {
list-style: none;
margin: var(--space-md) 0;
}
.why-section li {
padding: var(--space-sm) 0;
padding-left: var(--space-lg);
position: relative;
}
.why-section li::before {
content: '→';
position: absolute;
left: 0;
color: var(--color-accent);
}
.badges-page .back-link {
display: inline-block;
margin-top: var(--space-lg);
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.875rem;
}
.badges-page .back-link:hover {
color: var(--color-accent);
}
/* Print styles */
@media print {
.filter-section,
.header-decoration,
.back-link {
display: none;
}
.entry-card {
break-inside: avoid;
border: 1px solid #ccc;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

78
templates/about.html Normal file
View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Om Oss | Ursprung Sverige</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="site-header site-header--single">
<div class="container">
<a href="index.html" class="site-title-link">
<h1 class="site-title site-title--small">Ursprung Sverige</h1>
</a>
</div>
</header>
<main class="site-main site-main--single">
<div class="container container--narrow">
<article class="about-page">
<h1>Om Ursprung Sverige</h1>
<section class="about-section">
<h2>Vårt Syfte</h2>
<p>Ursprung Sverige är en kurerad samling av svenska produkter, tjänster, upplevelser och tillverkare. Målet är att göra det enklare för dig att stödja svensk ekonomi och kultur genom medvetna val.</p>
<p>Jag tror på att synliggöra företag och produkter som bidrar till det svenska samhället från traditionellt hantverk till moderna digitala tjänster.</p>
</section>
<section class="about-section">
<h2>Vem Står Bakom?</h2>
<p>Ursprung Sverige skapas och underhålls av <strong>Jonas Raneryd Imaizumi</strong>. Det här är ett personligt projekt drivet av en önskan att göra det enklare att hitta och stödja svenska alternativ.</p>
</section>
<section class="about-section">
<h2>Hosting i Sverige</h2>
<p>Vi lever som vi lär. Denna webbplats hostas på servrar i <strong>Lerum, Sverige</strong>. Dina besök stannar inom Sveriges gränser.</p>
</section>
<section class="about-section">
<h2>Din Integritet</h2>
<p>Vi respekterar din integritet fullt ut:</p>
<ul>
<li><strong>Inga cookies</strong> Vi använder inga cookies överhuvudtaget</li>
<li><strong>Ingen spårning</strong> Inget Google Analytics eller liknande tjänster</li>
<li><strong>Ingen dataförsäljning</strong> Vi samlar inte in eller säljer personuppgifter</li>
<li><strong>Inga tredjepartstjänster</strong> Allt laddas från våra egna servrar</li>
</ul>
<p>Du kan surfa här utan att lämna digitala spår.</p>
</section>
<section class="about-section">
<h2>Öppen Källkod</h2>
<p>Webbplatsen är byggd med enkel teknik ren HTML, CSS och minimal JavaScript. Ingen spårningskod, inga tunga ramverk, inga dolda skript.</p>
</section>
<section class="about-section">
<h2>Kontakt</h2>
<p>Har du förslag på svenska företag, produkter eller tjänster som borde finnas med? Eller har du upptäckt att en märkning eller annan information är felaktig? Hör av dig!</p>
<p><a href="mailto:jonas@ursprungsverige.se">jonas@ursprungsverige.se</a></p>
</section>
<a href="index.html" class="back-link">← Tillbaka till alla poster</a>
</article>
</div>
</main>
<footer class="site-footer">
<div class="container">
<p>Ursprung Sverige &copy; {{ year }}</p>
<p class="footer-tagline">Stöd svensk ekonomi och kultur</p>
<nav class="footer-nav">
<a href="om-oss.html">Om oss</a>
<a href="om-markning.html">Om märkning</a>
</nav>
</div>
</footer>
</body>
</html>

89
templates/badges.html Normal file
View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Om Märkning | Ursprung Sverige</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="site-header site-header--single">
<div class="container">
<a href="index.html" class="site-title-link">
<h1 class="site-title site-title--small">Ursprung Sverige</h1>
</a>
</div>
</header>
<main class="site-main site-main--single">
<div class="container container--narrow">
<article class="badges-page">
<h1>Om Märkning</h1>
<p class="intro">Vi använder ett märkningssystem för att visa hur svenskt ett företag, en produkt eller tjänst verkligen är. Märkningarna hjälper dig att göra medvetna val när du vill stödja svensk ekonomi och kultur.</p>
<section class="badge-tier-section">
<div class="badge-tier-header">
<span class="badge badge-tier-1">Äkta Svenskt</span>
<span class="tier-label">Högsta nivån</span>
</div>
<p>Den finaste märkningen. Företaget är svenskt, produktionen eller driften sker i Sverige, och de använder svenska underleverantörer och tjänster. För digitala tjänster innebär detta svenska servrar och svensk hosting genomgående.</p>
<p class="criteria"><strong>Kriterier:</strong> Svenskt företag + svensk produktion/drift + svenska leverantörer</p>
</section>
<section class="badge-tier-section">
<div class="badge-tier-header">
<span class="badge badge-tier-2">Tillverkat i Sverige</span>
<span class="badge badge-tier-2">Svenskt Moln</span>
</div>
<p><strong>Tillverkat i Sverige</strong> gäller fysiska produkter som tillverkas i Sverige av ett svenskt företag.</p>
<p><strong>Svenskt Moln</strong> gäller digitala tjänster där ett svenskt företag har sina servrar och hosting i Sverige.</p>
<p class="criteria"><strong>Kriterier:</strong> Svenskt företag + svensk produktion (fysisk) eller svensk hosting (digital)</p>
</section>
<section class="badge-tier-section">
<div class="badge-tier-header">
<span class="badge badge-tier-3">Designat i Sverige</span>
<span class="badge badge-tier-3">Utvecklat i Sverige</span>
</div>
<p><strong>Designat i Sverige</strong> gäller produkter som designas i Sverige men kan tillverkas utomlands.</p>
<p><strong>Utvecklat i Sverige</strong> gäller digitala tjänster som utvecklas i Sverige men kan hostas utomlands.</p>
<p class="criteria"><strong>Kriterier:</strong> Svenskt företag + svensk design/utveckling</p>
</section>
<section class="badge-tier-section">
<div class="badge-tier-header">
<span class="badge badge-tier-4">Svenskt Företag</span>
</div>
<p>Grundnivån. Företaget är registrerat eller grundat i Sverige. Produktion, utveckling eller drift kan ske var som helst.</p>
<p class="criteria"><strong>Kriterier:</strong> Svenskregistrerat eller svenskgrundat företag</p>
</section>
<section class="why-section">
<h2>Varför spelar det roll?</h2>
<p>Genom att välja svenska alternativ bidrar du till:</p>
<ul>
<li><strong>Svenska jobb</strong> Produktion och utveckling i Sverige skapar arbetstillfällen</li>
<li><strong>Kortare transporter</strong> Mindre miljöpåverkan när varor inte fraktas långt</li>
<li><strong>Dataskydd</strong> Svenska servrar innebär att dina data skyddas av svensk och europeisk lag</li>
<li><strong>Kvalitet och tradition</strong> Svenskt hantverk och ingenjörskonst har lång tradition</li>
<li><strong>Lokal ekonomi</strong> Pengarna stannar i Sverige och stärker samhället</li>
</ul>
</section>
<a href="index.html" class="back-link">← Tillbaka till alla poster</a>
</article>
</div>
</main>
<footer class="site-footer">
<div class="container">
<p>Ursprung Sverige &copy; {{ year }}</p>
<p class="footer-tagline">Stöd svensk ekonomi och kultur</p>
<nav class="footer-nav">
<a href="om-oss.html">Om oss</a>
<a href="om-markning.html">Om märkning</a>
</nav>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<footer class="site-footer">
<div class="container">
<p>Ursprung Sverige &copy; {{ year }}</p>
<p class="footer-tagline">Stöd svensk ekonomi och kultur</p>
<nav class="footer-nav">
<a href="{{ rootPath }}om-oss.html">Om oss</a>
<a href="{{ rootPath }}om-markning.html">Om märkning</a>
</nav>
</div>
</footer>

35
templates/single.html Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<header class="site-header site-header--single">
<div class="container">
<a href="../index.html" class="site-title-link">
<h1 class="site-title site-title--small">Ursprung Sverige</h1>
</a>
</div>
</header>
<main class="site-main site-main--single">
<div class="container container--narrow">
{{ content }}
</div>
</main>
<footer class="site-footer">
<div class="container">
<p>Ursprung Sverige &copy; {{ year }}</p>
<p class="footer-tagline">Stöd svensk ekonomi och kultur</p>
<nav class="footer-nav">
<a href="../om-oss.html">Om oss</a>
<a href="../om-markning.html">Om märkning</a>
</nav>
</div>
</footer>
</body>
</html>

59
templates/template.html Normal file
View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<meta name="description" content="{{ subtitle }}">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="site-header">
<div class="container">
<div class="header-content">
<h1 class="site-title">{{ title }}</h1>
<p class="site-subtitle">{{ subtitle }}</p>
</div>
<div class="header-decoration">
<svg viewBox="0 0 100 60" class="sweden-motif" aria-hidden="true">
<rect x="0" y="0" width="100" height="60" fill="var(--color-accent)"/>
<rect x="0" y="24" width="100" height="12" fill="var(--color-accent-gold)"/>
<rect x="30" y="0" width="12" height="60" fill="var(--color-accent-gold)"/>
</svg>
</div>
</div>
</header>
<main class="site-main">
<div class="container">
<section class="filter-section">
{{ filters }}
<div class="filter-footer">
<p class="entry-count"><span id="visible-count">{{ entryCount }}</span> av {{ entryCount }} poster</p>
<a href="om-markning.html" class="badges-info-link">Vad betyder märkningarna?</a>
</div>
</section>
<section class="entries-section">
<div class="entries-grid" id="entries-grid">
{{ entries }}
</div>
<p class="no-results" id="no-results" hidden>Inga resultat hittades.</p>
</section>
</div>
</main>
<footer class="site-footer">
<div class="container">
<p>Ursprung Sverige &copy; {{ year }}</p>
<p class="footer-tagline">Stöd svensk ekonomi och kultur</p>
<nav class="footer-nav">
<a href="om-oss.html">Om oss</a>
<a href="om-markning.html">Om märkning</a>
</nav>
</div>
</footer>
<script src="filter.js"></script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More