{
  "version": "1.0.0",
  "exported_at": "2026-06-02T12:00:00.000Z",
  "project": {
    "name": "Freelancermap Job Scraper Details",
    "description": "Equivalent UScraper template for the Octoparse Freelancermap Job Scraper (Details). This workflow is for supplied Freelancermap project/job detail page URLs. It uses navigate.urls[] plus loop-continue to visit all supplied detail URLs and append title, location, start, duration, provider, posted date, contact person, project ID, description, keywords, and source URL to one CSV. Invalid/non-detail URLs are skipped. The output filename uses a clean suffix so validation runs are not polluted by stale append-mode output.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://www.freelancermap.de/projektboerse/projekte/beratung-management/2485218-projekt-projektmitarbeiter-fuer-freigabe-und-zulassungsverfahren-m-w-d-remote-und-frankfurt-am-main-hessen.html"
        ],
        "color": "bg-[#4589ff]",
        "tags": [
          "freelancermap",
          "detail-url-loop",
          "replace-with-current-detail-urls"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 456,
      "position_y": 220,
      "config": {
        "timeout": 30,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 792,
      "position_y": 220,
      "config": {
        "duration": 2,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1128,
      "position_y": 220,
      "config": {
        "selector": "body",
        "timeout": 30,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Run custom JavaScript",
      "position_x": 1464,
      "position_y": 220,
      "config": {
        "jsCode": "(() => { const text = (document.body && document.body.innerText) || ''; const url = location.href; let hasJobPostingJsonLd = false; try { hasJobPostingJsonLd = Array.from(document.querySelectorAll('script[type=\"application/ld+json\"]')).some(script => /JobPosting|hiringOrganization|datePosted|jobLocation/i.test(script.textContent || '')); } catch (e) {} const hasFreelancermapDomain = /freelancermap\\./i.test(location.hostname); const hasProjectUrl = /\\/projektboerse\\/projekte\\//i.test(url) && /\\d{5,}/.test(url); const hasDetailLabels = /(^|\\n)\\s*(Projekt[_\\s-]*(ID|Nr\\.?|nummer)|Referenznummer|Starttermin|Einsatzort|Ansprechpartner|Projektanbieter|Eingestellt|Dauer)\\s*(:|\\n)/i.test(text); const hasDescriptionLength = text.length > 600; const isLikelyDetail = hasFreelancermapDomain && hasDescriptionLength && (hasJobPostingJsonLd || hasProjectUrl || hasDetailLabels); document.body.setAttribute('data-uscraper-fm-detail', isLikelyDetail ? 'true' : 'false'); })();",
        "waitForCompletion": true,
        "timeout": 10,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 1800,
      "position_y": 220,
      "config": {
        "selector": "body[data-uscraper-fm-detail=\"true\"]",
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2136,
      "position_y": 520,
      "config": {
        "rowSelector": "body[data-uscraper-fm-detail=\"true\"]",
        "fileName": "freelancermap-job-scraper-details-clean.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "tags": [
          "freelancermap",
          "job-details",
          "js-columns"
        ],
        "columns": [
          {
            "name": "titel",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const state = '(baden-wuerttemberg|bayern|berlin|brandenburg|bremen|hamburg|hessen|mecklenburg-vorpommern|niedersachsen|nordrhein-westfalen|rheinland-pfalz|saarland|sachsen|sachsen-anhalt|schleswig-holstein|thueringen)'; const last = decodeURIComponent((location.pathname.split('/').filter(Boolean).pop() || '')).replace(/\\.html.*$/i, ''); const slug = last.replace(/^\\d+-/i, '').replace(/^projekt-/i, '').replace(new RegExp('-remote-und-[a-z-]+-' + state + '$', 'i'), '').replace(new RegExp('-' + state + '$', 'i'), '').replace(/m-w-d/gi, 'm/w/d').replace(/fuer/gi, 'für').replace(/ae/g, 'ä').replace(/oe/g, 'ö').replace(/ue/g, 'ü'); const slugTitle = clean(slug.split('-').join(' ')); if (slugTitle.length > 12) return slugTitle; const bad = /^(it-excelsus GmbH|Hays AG|SOLCOM GmbH|Progressive IT)$/i; const h1s = Array.from(document.querySelectorAll('h1')).map(e => clean(e.textContent)).filter(Boolean); for (const t of h1s) { if (t.length > 12 && !bad.test(t) && !/^freelancermap/i.test(t)) return t; } const og = document.querySelector('meta[property=\"og:title\"], meta[name=\"title\"]'); if (og && clean(og.content).length > 12 && !bad.test(clean(og.content))) return clean(og.content.replace(/\\|.*$/g, '')); return clean(document.title.replace(/\\|.*$|Freelancermap.*$/gi, '')); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "standort",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const stateMap = { 'baden-wuerttemberg': 'Baden-Württemberg', 'bayern': 'Bayern', 'berlin': 'Berlin', 'brandenburg': 'Brandenburg', 'bremen': 'Bremen', 'hamburg': 'Hamburg', 'hessen': 'Hessen', 'mecklenburg-vorpommern': 'Mecklenburg-Vorpommern', 'niedersachsen': 'Niedersachsen', 'nordrhein-westfalen': 'Nordrhein-Westfalen', 'rheinland-pfalz': 'Rheinland-Pfalz', 'saarland': 'Saarland', 'sachsen': 'Sachsen', 'sachsen-anhalt': 'Sachsen-Anhalt', 'schleswig-holstein': 'Schleswig-Holstein', 'thueringen': 'Thüringen' }; const last = decodeURIComponent((location.pathname.split('/').filter(Boolean).pop() || '')).replace(/\\.html.*$/i, ''); const stateKeys = Object.keys(stateMap).join('|'); let m = last.match(new RegExp('remote-und-([a-z-]+)-(' + stateKeys + ')$', 'i')); if (m) { const city = m[1].split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); return stateMap[m[2].toLowerCase()] + ', ' + city; } m = last.match(new RegExp('-(' + stateKeys + ')$', 'i')); if (m) return stateMap[m[1].toLowerCase()]; const lines = (document.body.innerText || '').split('\\n').map(clean).filter(Boolean); const labels = /^(Standort|Einsatzort|Ort|Location)$/i; const bad = /^(Freiberuflich|Festanstellung|Vollzeit|Teilzeit|Remote|Hybrid)$/i; for (let i = 0; i < lines.length; i++) { const inline = lines[i].match(/^(Standort|Einsatzort|Ort|Location)\\s*:?\\s+(.+)$/i); if (inline && inline[2] && !bad.test(inline[2])) return clean(inline[2]); if (labels.test(lines[i].replace(/:$/, ''))) { for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) { if (!labels.test(lines[j]) && !bad.test(lines[j])) return lines[j]; } } } const el = document.querySelector('[itemprop=\"jobLocation\"], [data-testid*=\"location\"]'); return el ? clean(el.textContent) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "start",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const lines = (document.body.innerText || '').split('\\n').map(clean).filter(Boolean); const labels = /^(Start|Starttermin|Beginn|Beginn des Projekts)$/i; for (let i = 0; i < lines.length; i++) { const inline = lines[i].match(/^(Start|Starttermin|Beginn|Beginn des Projekts)\\s*:?\\s+(.+)$/i); if (inline && inline[2]) return clean(inline[2]); if (labels.test(lines[i].replace(/:$/, ''))) { for (let j = i + 1; j < Math.min(lines.length, i + 4); j++) { if (!labels.test(lines[j])) return lines[j]; } } } const text = document.body.innerText || ''; const lr = text.match(/Leistungszeitraum\\s*:\\s*([^–\\-\\n]+)/i); return lr ? clean(lr[1]) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "dauer",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const lines = (document.body.innerText || '').split('\\n').map(clean).filter(Boolean); const labels = /^(Dauer|Laufzeit|Projektlaufzeit|Duration)$/i; for (let i = 0; i < lines.length; i++) { const inline = lines[i].match(/^(Dauer|Laufzeit|Projektlaufzeit|Duration)\\s*:?\\s+(.+)$/i); if (inline && inline[2]) return clean(inline[2]); if (labels.test(lines[i].replace(/:$/, ''))) { for (let j = i + 1; j < Math.min(lines.length, i + 4); j++) { if (!labels.test(lines[j])) return lines[j]; } } } const text = document.body.innerText || ''; const m = text.match(/(\\d+\\s*(MM\\+*|Monate\\+*|Monat\\+*))/i); return m ? clean(m[1]) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "von",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const json = Array.from(document.querySelectorAll('script[type=\"application/ld+json\"]')).map(s => s.textContent || '').join('\\n'); const jm = json.match(/\"hiringOrganization\"[\\s\\S]*?\"name\"\\s*:\\s*\"([^\"]+)\"/i); if (jm) return clean(jm[1]); const lines = (document.body.innerText || '').split('\\n').map(clean).filter(Boolean); const labels = /^(Von|Anbieter|Projektanbieter|Firma|Unternehmen)$/i; for (let i = 0; i < lines.length; i++) { const inline = lines[i].match(/^(Von|Anbieter|Projektanbieter|Firma|Unternehmen)\\s*:?\\s+(.+)$/i); if (inline && inline[2] && inline[2].length < 120) return clean(inline[2]); if (labels.test(lines[i].replace(/:$/, ''))) { for (let j = i + 1; j < Math.min(lines.length, i + 4); j++) { if (!labels.test(lines[j]) && lines[j].length < 120) return lines[j]; } } } const el = document.querySelector('[itemprop=\"hiringOrganization\"], [data-testid*=\"company\"]'); return el ? clean(el.textContent) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "eingestellt",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const json = Array.from(document.querySelectorAll('script[type=\"application/ld+json\"]')).map(s => s.textContent || '').join('\\n'); const jm = json.match(/\"datePosted\"\\s*:\\s*\"([^\"]+)\"/i); if (jm) return clean(jm[1]); const lines = (document.body.innerText || '').split('\\n').map(clean).filter(Boolean); const labels = /^(Eingestellt|Veröffentlicht|Online seit|Posted)$/i; for (let i = 0; i < lines.length; i++) { const inline = lines[i].match(/^(Eingestellt|Veröffentlicht|Online seit|Posted)\\s*:?\\s+(.+)$/i); if (inline && inline[2]) return clean(inline[2]); if (labels.test(lines[i].replace(/:$/, ''))) { for (let j = i + 1; j < Math.min(lines.length, i + 4); j++) { if (!labels.test(lines[j])) return lines[j]; } } } const time = document.querySelector('time[datetime], [itemprop=\"datePosted\"]'); return time ? clean(time.getAttribute('datetime') || time.textContent) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "ansprechpartner",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const lines = (document.body.innerText || '').split('\\n').map(clean).filter(Boolean); const labels = /^(Ansprechpartner|Kontaktperson|Recruiter|Kontakt)$/i; for (let i = 0; i < lines.length; i++) { const inline = lines[i].match(/^(Ansprechpartner|Kontaktperson|Recruiter|Kontakt)\\s*:?\\s+(.+)$/i); if (inline && inline[2] && inline[2].length < 100) return clean(inline[2]); if (labels.test(lines[i].replace(/:$/, ''))) { for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) { if (!labels.test(lines[j]) && lines[j].length < 100 && !/@/.test(lines[j])) return lines[j]; } } } const m = (document.body.innerText || '').match(/\\n([A-ZÄÖÜ][a-zäöüß]+\\s+[A-ZÄÖÜ][a-zäöüß]+)\\s*\\n\\s*Recruiter/i); return m ? clean(m[1]) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "projekt_id",
            "selector": "(() => { const m = location.href.match(/\\/(\\d{5,})-/) || location.href.match(/(\\d{5,})/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "beschreibung",
            "selector": "(() => { const clean = s => (s || '').replace(/\\n{3,}/g, '\\n\\n').replace(/[ \\t]+/g, ' ').trim(); const json = Array.from(document.querySelectorAll('script[type=\"application/ld+json\"]')).map(s => s.textContent || '').join('\\n'); const jm = json.match(/\"description\"\\s*:\\s*\"([\\s\\S]*?)\"\\s*,\\s*\"/i); if (jm && clean(jm[1]).length > 80) return clean(jm[1].replace(/\\\\n/g, '\\n').replace(/\\\\\"/g, '\"')); const selectors = ['[itemprop=\"description\"]', '[class*=\"project-description\"]', '[class*=\"job-description\"]', '[data-testid*=\"description\"]', '[class*=\"description\"]', 'article', 'main']; for (const sel of selectors) { const el = document.querySelector(sel); if (el && clean(el.innerText).length > 120) return clean(el.innerText); } return clean(document.body.innerText); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "keyword",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const meta = document.querySelector('meta[name=\"keywords\"]'); if (meta && clean(meta.content)) return clean(meta.content); const chips = Array.from(document.querySelectorAll('[class*=\"tag\"], [class*=\"skill\"], [class*=\"keyword\"], a[href*=\"/it-projekte/\"], a[href*=\"/projektboerse/skills/\"]')).map(e => clean(e.textContent)).filter(t => t && t.length < 60); return Array.from(new Set(chips)).join(', '); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "source_url",
            "selector": "location.href",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2472,
      "position_y": 520,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "connections": [
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "element-exists-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "true",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "false",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 116,
      "width": 1328,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1392,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 1728,
      "position_y": 116,
      "width": 992,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "element-exists-1",
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2064,
      "position_y": 416,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Equivalent UScraper template for the Octoparse Freelancermap Job Scraper (Details). This workflow is for supplied Freelancermap project/job detail page URLs. It uses navigate.urls[] plus loop-continue to visit all supplied detail URLs and append title, location, start, duration, provider, posted date, contact person, project ID, description, keywords, and source URL to one CSV. Invalid/non-detail URLs are skipped. The output filename uses a clean suffix so validation runs are not polluted by stale append-mode output.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-navigate-1",
      "element_type": "note",
      "title": "Note: Navigate",
      "content": "Multi-URL loop over 1 pages. Pair with loop-continue at the end of each iteration.",
      "color": "#ee5396",
      "position_x": 320,
      "position_y": 200,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(() => { const text = (document.body && document.body.innerText) || ''; const url = location.href; l...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1664,
      "position_y": 200,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-element-exists-1",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `body[data-uscraper-fm-detail=\"true\"]`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 2000,
      "position_y": 200,
      "width": 340,
      "height": 142,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (titel, standort, start, dauer, von). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2336,
      "position_y": 500,
      "width": 340,
      "height": 125,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-loop-continue-1",
      "element_type": "note",
      "title": "Note: Loop Continue",
      "content": "Loop Continue advances a multi-URL or multi-text loop. Place at the end of the loop body with a clear back-edge to the loop start.",
      "color": "#ee5396",
      "position_x": 2672,
      "position_y": 500,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}