├── .gitignore ├── LICENSE ├── README.md ├── configure.js ├── package.json └── script.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Brandon Strittmatter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Params 2 | 3 | Control your website content using query parameters. 4 | 5 | ## Installation 6 | 7 | Add `Params` to your project using npm: 8 | 9 | ```bash 10 | npm install @burcs/params 11 | ``` 12 | 13 | Or, embed the script directly on your website. 14 | 15 | ## Quick Start 16 | 17 | `Params` is designed for ease of use. Modify content based on the order of site elements. For example, to alter the second paragraph on a page: 18 | 19 | ```html 20 | https://params.org/page?p_2=updated+content+here 21 | ``` 22 | 23 | ## Party Mode 🎉 24 | 25 | Activate "Party Mode" to effortlessly update your site. By enabling this mode, you can edit your page content in-line. Try it out: 26 | 27 | ```html 28 | https://params.org/page?party=true 29 | ``` 30 | 31 | Make your changes, enjoy the party! 32 | 33 | ## Supported Elements 34 | 35 | Currently, `Params` supports: 36 | 37 | - Headings: `h1`, `h2`, `h3`, `h4`, `h5`, `h6` 38 | - Text & Styling: `p`, `i`, `b`, `span` 39 | - Lists: `ul`, `ol` 40 | - Media: `img` 41 | 42 | More elements are in the pipeline. 43 | 44 | ## Styling on the Fly 45 | 46 | Change element styles by invoking different CSS classes: 47 | 48 | ```html 49 | https://params.org/page?p_2_class=class-name 50 | ``` 51 | 52 | This feature works well with libraries like TailwindCSS. 53 | 54 | ## Security 55 | 56 | Image sources are limited to whitelisted domains to prevent malicious code injections. 57 | 58 | ## Contribute 59 | 60 | `Params` is open-source. Submit a PR if you have suggestions or improvements. 61 | 62 | ## License 63 | 64 | MIT License. 65 | -------------------------------------------------------------------------------- /configure.js: -------------------------------------------------------------------------------- 1 | const readline = require("readline"); 2 | const fs = require("fs"); 3 | 4 | const rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout, 7 | }); 8 | 9 | let urls = []; 10 | 11 | rl.question("Do you need to whitelist addtional domains (besides root)? y/n: ", (whitelist) => { 12 | if (whitelist === "y") { 13 | askForURL(whitelist); 14 | } else { 15 | rl.close() 16 | } 17 | }); 18 | 19 | function askForURL(whitelist) { 20 | rl.question( 21 | 'Enter a domain to whitelist (or "done" to finish): ', 22 | (urlInput) => { 23 | if (urlInput.toLowerCase() !== "done") { 24 | urls.push(urlInput.trim()); 25 | askForURL(whitelist); // Recursively ask for the next URL 26 | } else { 27 | rl.close() 28 | } 29 | } 30 | ); 31 | } 32 | 33 | // function askForStyleEditing(allowDomain, urls) { 34 | // rl.question( 35 | // "Enable style editing via query params? y/n: ", 36 | // (styleEditing) => { 37 | // modifyScript(allowDomain, urls, styleEditing); 38 | // rl.close(); 39 | // } 40 | // ); 41 | // } 42 | 43 | function modifyScript(allowDomain, urls, styleEditing) { 44 | // Load the original script 45 | // let scriptContent = fs.readFileSync("script.js", "utf8"); 46 | 47 | // Modify script based on the configurations 48 | // if (allowDomain === "y") { 49 | // Add party mode enabling code if required 50 | // (Given the provided script, no changes seem necessary for this option) 51 | // } 52 | 53 | if (urls.length > 0) { 54 | // Replace the placeholder whitelist with the provided URLs, comma separated 55 | scriptContent = scriptContent.replace( 56 | '["ENTERYOURURLHERE.COM"]', 57 | JSON.stringify(urls) 58 | ); 59 | } 60 | 61 | // if (styleEditing === "y") { 62 | // Add style editing enabling code if required 63 | // (Again, based on the provided script, no specific changes seem necessary for this option) 64 | // } 65 | 66 | // Save the modified script 67 | fs.writeFileSync("script.js", scriptContent); 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@burcs/params", 3 | "version": "0.1.1", 4 | "scripts": { 5 | "postinstall": "node configure.js" 6 | }, 7 | "description": "Your site powered by your URL.", 8 | "main": "script.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/stritt/params.git" 12 | }, 13 | "keywords": [ 14 | "params", 15 | "cms", 16 | "bodyless", 17 | "realtime", 18 | "content" 19 | ], 20 | "author": "burcs", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/stritt/params/issues" 24 | }, 25 | "homepage": "https://github.com/stritt/params#readme" 26 | } 27 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const paramTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'i', 'b', 'ul', 'ol', 'span', 'img']; 2 | 3 | const QueryParamUtils = { 4 | get(name) { 5 | const queryParams = new URLSearchParams(window.location.search); // Correct variable name 6 | const value = queryParams.get(name); // Use correct variable name 7 | if (value && this.validate(value)) { 8 | return this.sanitize(value); 9 | } 10 | return null; 11 | }, 12 | validate(paramValue) { 13 | return !/|<|"/.test(paramValue); 14 | }, 15 | sanitize(value) { 16 | return value.replace(//g, ">"); 17 | }, 18 | updateURL(paramName, paramValue) { 19 | if (history.pushState) { 20 | let searchParams = new URLSearchParams(window.location.search); 21 | searchParams.set(paramName, paramValue); 22 | let newurl = 23 | window.location.protocol + 24 | "//" + 25 | window.location.host + 26 | window.location.pathname + 27 | "?" + 28 | searchParams.toString(); 29 | window.history.pushState({ path: newurl }, "", newurl); 30 | } 31 | }, 32 | isURLWhitelisted(url) { 33 | const whitelistedDomains = [ 34 | window.location.hostname.split(":")[0], // Always whitelist the current domain without port 35 | "localhost", // Whitelist localhost for local testing 36 | "DOMAIN.COM", 37 | ]; // Add other domains to this list as needed 38 | const urlObj = new URL(url); 39 | const urlDomain = urlObj.hostname.split(":")[0]; // Extract the domain without port 40 | return whitelistedDomains.includes(urlDomain); 41 | }, 42 | }; 43 | document.addEventListener("DOMContentLoaded", function () { 44 | 45 | // Handle content updates 46 | for (const selector of paramTags) { 47 | const elements = document.querySelectorAll(selector); 48 | elements.forEach((element, index) => { 49 | const param = `${selector}_${index + 1}`; 50 | const contentValue = QueryParamUtils.get(param); 51 | 52 | if (contentValue) { 53 | if (selector === "img") { 54 | if (QueryParamUtils.isURLWhitelisted(contentValue)) { 55 | element.setAttribute("src", contentValue); // Set src attribute for tags 56 | } else { 57 | console.warn("Domain not whitelisted:", contentValue); 58 | } 59 | } else { 60 | element.textContent = contentValue; 61 | } 62 | } 63 | }); 64 | } 65 | 66 | // Handle class configuration 67 | for (const [baseParam, selector] of Object.entries(paramTags)) { 68 | let index = 1; 69 | while (true) { 70 | const param = `${baseParam}_${index}_class`; 71 | const classValue = QueryParamUtils.get(param); 72 | const allElementsClassValue = QueryParamUtils.get(`${baseParam}_class`); 73 | 74 | if (!classValue && !allElementsClassValue) break; 75 | 76 | const elements = document.querySelectorAll(selector); 77 | if (allElementsClassValue) { 78 | elements.forEach((element) => { 79 | const classesToAdd = allElementsClassValue.split(","); 80 | classesToAdd.forEach((className) => { 81 | element.classList.add(className.trim()); 82 | }); 83 | }); 84 | } 85 | if (baseParam === "body" || allElementsClassValue) { 86 | elements.forEach((element) => { 87 | const classesToAdd = classValue.split(","); 88 | classesToAdd.forEach((className) => { 89 | element.classList.add(className.trim()); 90 | }); 91 | }); 92 | } else { 93 | const element = elements[index - 1]; 94 | if (element) { 95 | const classesToAdd = classValue.split(","); 96 | classesToAdd.forEach((className) => { 97 | element.classList.add(className.trim()); 98 | }); 99 | } 100 | } 101 | 102 | index++; 103 | } 104 | } 105 | 106 | // Handle style changes from query parameters based on class names 107 | const urlParams = new URLSearchParams(window.location.search); 108 | 109 | // Update content based on URL parameters 110 | for (const [param, value] of urlParams.entries()) { 111 | if (paramTags.includes(param.split('_')[0])) { // Check if the parameter corresponds to a known tag 112 | const elements = document.querySelectorAll(param); 113 | elements.forEach(element => { 114 | if (element.tagName.toLowerCase() === 'a') { 115 | // If the element is an tag, update its href and text content 116 | const [href, text] = value.split('|'); // Assuming a format "href|text" for tags in URL parameters 117 | element.setAttribute('href', href); 118 | element.textContent = text; 119 | } else { 120 | // For other elements, just update the text content 121 | element.textContent = value; 122 | } 123 | }); 124 | } 125 | } 126 | for (const [key, value] of urlParams.entries()) { 127 | if (key.endsWith("_class")) { 128 | const parts = key.split("_"); 129 | const tagName = parts[0]; 130 | const index = parts.length === 3 ? parseInt(parts[1]) - 1 : null; // Subtract 1 to get 0-based index 131 | const elements = 132 | index !== null 133 | ? [document.querySelectorAll(tagName)[index]] 134 | : document.querySelectorAll(tagName); 135 | const classesToAdd = value.split(","); 136 | elements.forEach((element) => { 137 | if (element) { 138 | classesToAdd.forEach((className) => { 139 | element.classList.add(className.trim()); 140 | }); 141 | } 142 | }); 143 | } 144 | } 145 | //Party Mode 146 | const partyMode = QueryParamUtils.get("party"); 147 | if (partyMode === "true") { 148 | for (const selector of Object.values(paramTags)) { 149 | document.querySelectorAll(selector).forEach(elem => { 150 | elem.setAttribute('contenteditable', 'true'); 151 | elem.addEventListener('input', function(event) { 152 | // Unwrap any span elements inside the contenteditable (specifically for emojis) 153 | const spans = elem.querySelectorAll('span'); 154 | spans.forEach(span => { 155 | const parent = span.parentNode; 156 | while (span.firstChild) { 157 | parent.insertBefore(span.firstChild, span); 158 | } 159 | parent.removeChild(span); 160 | }); 161 | 162 | const elementType = elem.tagName.toLowerCase(); 163 | const index = [...document.querySelectorAll(elementType)].indexOf(elem) + 1; 164 | const param = `${elementType}_${index}`; 165 | const newValue = elem.textContent; 166 | QueryParamUtils.updateURL(param, newValue); 167 | }); 168 | }); 169 | } 170 | } 171 | }); 172 | --------------------------------------------------------------------------------