Перезавантаження сторінки дає неправильний GET-запит у режимі AngularJS HTML5


193

Я хочу включити режим HTML5 для свого додатка. Я поставив наступний код для конфігурації, як показано тут :

return app.config(['$routeProvider','$locationProvider', function($routeProvider,$locationProvider) {

    $locationProvider.html5Mode(true);
    $locationProvider.hashPrefix = '!';

    $routeProvider.when('/', {
        templateUrl: '/views/index.html',
        controller: 'indexCtrl'
    });
    $routeProvider.when('/about',{
        templateUrl: '/views/about.html',
        controller: 'AboutCtrl'
    });

Як ви бачите, я використав $locationProvider.html5modeі змінив усі свої посилання на, ng-hrefщоб виключити /#/.

Проблема

На даний момент я можу зайти на localhost:9000/сторінку з покажчиком та перейти на інші сторінки, як-от localhost:9000/about.

Однак проблема виникає, коли я оновлюю localhost:9000/aboutсторінку. Я отримую такий вихід:Cannot GET /about

Якщо я дивлюся на мережеві дзвінки:

Request URL:localhost:9000/about
Request Method:GET

Якщо я спершу заходжу, localhost:9000/а потім натискаю кнопку, яка переходить до /aboutмене, я отримую:

Request URL:http://localhost:9000/views/about.html

Що ідеально відображає сторінку.

Як я можу дозволити кутовому отримати правильну сторінку під час оновлення?



1
Я вирішую це за мене. Дивіться тут stackoverflow.com/questions/22394014/…
Саша Б.

Відповіді:


99

З кутових док

Сторона сервера
Використання цього режиму вимагає перезапис URL-адрес на стороні сервера, в основному вам потрібно переписати всі свої посилання на точку входу вашої програми (наприклад, index.html)

Причиною цього є те, що при першому відвідуванні сторінки ( /about), наприклад, після оновлення, браузер не може знати, що це не реальна URL-адреса, тому він продовжує і завантажує її. Однак якщо ви спочатку завантажили кореневу сторінку та весь код javascript, тоді, коли ви переходите до /aboutAngular, ви можете потрапити туди, перш ніж браузер намагатиметься вдарити сервер та обробляти його відповідно


21
Коли йдеться про те, що "ви повинні переписати всі свої посилання на точку входу вашої програми (наприклад, index.html)", що це означає? Чи означає це, що я повинен зайти в свій $routeProviderі змінити шляхи кожного, templateUrlнаприклад, від templateUrl : '/views/about.html',до templateUrl : 'example.com/views/about.html',?

7
Ні, ваші правила в $ routeProvider повинні залишатися такими, якими вони є. Відповідь стосується "серверної сторони". Це стосується зміни маршрутизації на сервері, який надає ваші кутові HTML-сторінки. Перш ніж кожен запит потрапить у ваш кутовий додаток, він повинен пройти спочатку через маршрутизацію на стороні сервера, яка повинна переписати всі запити, щоб вказувати на вашу вхідну точку введення додатка (наприклад, 'index.html' або '/', залежно від того, як ваш кутовий маршрути працюють).
Marni

У випадку, якщо я обслуговую свій мінімізований index.html через якийсь CDN, то як цей механізм маршрутизації виконає ??
CrazyGeek

1

5
Гарна стаття для apache, є приклад htaccess: ngmilk.rocks/2015/03/09/…
Alcalyn

63

Існує кілька речей, щоб налаштувати так, щоб ваше посилання у веб-переглядачі виглядало так http://yourdomain.com/path і це ваша кутова конфігурація + сторона сервера

1) КутовийJS

$routeProvider
  .when('/path', {
    templateUrl: 'path.html',
  });
$locationProvider
  .html5Mode(true);

2) сторону сервера, просто помістіть .htaccessвсередину вашої кореневої папки і вставте цю

RewriteEngine On 
Options FollowSymLinks

RewriteBase /

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /#/$1 [L]

Ще цікаві речі читати про режим html5 в angularjs та необхідну конфігурацію для різних середовищ https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-configure-your-server -to-work-with-html5mode Також це питання може допомогти вам $ location / переключенню між html5 та режимом hashbang / переписуванням посилань


Привіт. Я спробував вищевказане рішення, вніс зміни в свій конфігураційний файл. Але все одно це не працює. Оновлення URL-адреси все ще призводить до помилки "404 НЕ ЗНАЙДЕНО".
kTn

Як переадресувати / переписати додаток, розгорнутий у Websphere?
Гері

Я не бачу, чому ні!
lokers

36

У мене була подібна проблема, і я вирішив її:

  • Використання <base href="https://stackoverflow.com/index.html">на сторінці покажчика

  • Використовуючи програмне забезпечення вилучення всього проміжного програмного забезпечення маршруту на моєму вузлі / сервері Express наступним чином (поставте його після маршрутизатора):

app.use(function(req, res) {
    res.sendfile(__dirname + '/Public/index.html');
});

Я думаю, що це повинно підняти вас і працювати.

Якщо ви використовуєте сервер apache, можливо, ви захочете mod_rewrite свої посилання. Зробити це не важко. Всього кілька змін у конфігураційних файлах.

Все, що передбачає, що у вас увімкнено html5mode на angularjs. Тепер. зауважте, що в кутовій 1.2 оголошення оголошення базової URL-адреси фактично більше не рекомендується.


1
Це рішення працювало для мене із налаштуванням: <base href="https://stackoverflow.com/">та додаванням запропонованих вами маршрутів Express. Мухо дякую.
Гілад Пелег

Тахіре Чаде, вам все-таки потрібно було використовувати серверний перезапис URL-адрес після впровадження <base href="https://stackoverflow.com/index.html">?
Коді

Так, ви можете переглядати URL-адресу як спосіб встановлення базового URL-адреси вашої програми / веб-сайту в камені (включений домен). Html-оператор 'base url' - це лише спосіб переконатись, що всі відносні посилання правильно отримані з однієї бази. Базовий URL не є абсолютно необхідним, якщо ваші посилання абсолютні.
Taye

1
Коли я використовую це, я просто бачу сам html-код на сторінці, а не відображається як фактична сторінка.
Ной

2
На цю відповідь заслуговують потрійні зірки! Дякуємо за надання прямої відповіді. Всі інші просто повторюють один і той же рядок про "потрібно робити переадресацію сервера".
Ахмед Хаке

27

Рішення для BrowserSync та Gulp.

Від https://github.com/BrowserSync/browser-sync/isissue/204#issuecomment-102623643

Перша установка connect-history-api-fallback:

npm --save-dev install connect-history-api-fallback

Потім додайте його до свого gulpfile.js:

var historyApiFallback = require('connect-history-api-fallback');

gulp.task('serve', function() {
  browserSync.init({
    server: {
      baseDir: "app",
      middleware: [ historyApiFallback() ]
    }
  });
});

Ой оснастка! Нарешті полегшення від розвитку кутового виду! І як бонус, стандартні URL-адреси (без хеш-бангу) теж! Дякую!
трохи менше

1
Працює як шарм ... Дякую велике!
Нішант Наїр

1
Якщо ви використовуєте певний порт (наприклад, пустий шаблон Ionic працює на порт 8100), просто додайте додаткову властивість після сервера, тобто server: { ... }, port: 8100просто додайте serveзавдання до своєї задачі gulp за замовчуванням (тобто gulp.task('default', ['sass', 'serve']);), і тоді ви можете запустити додаток без Cannot GET ...помилок веб-переглядача, коли ви оновлюєте сторінку, запустивши gulp. Зауважте, що при запуску сервера ionic serveвсе ще зустрічаються помилки.
Люк Шоен

Це добре працює, коли я використовую браузер розробки синхронізації, в продажі я запускаю додаток як експрес-додаток і перенаправляю весь маршрут на кутовий додаток як app.get ('*', функція (req, res) {req.session.valid = true ; res.redirect ('/');}); Сторінка не завантажується, коли давати шлях безпосередньо?
Пан AJ

13

Вам потрібно налаштувати ваш сервер, щоб переписати все на index.html, щоб завантажити додаток:

https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#wiki-how-to-configure-your-server-to-work-with-html5mode


1
Ідеальною проблемою для мене було використання Laravel Forge для надання сервера, який мав відповідну лінію nginx try_files $uri $uri/ /index.php?...замість потрібної index.html. Дякуємо за посилання!
CJ Thompson

Повинен відповісти, надає будь-яку ситуацію незалежно від вашого сервера. Чудово співпрацював з NodeJS
Джо Ллойд,

10

Я написав просте проміжне програмне забезпечення для моделювання переписання URL-адрес на грунтовних проектах. https://gist.github.com/muratcorlu/5803655

Ви можете використовувати так:

module.exports = function(grunt) {
  var urlRewrite = require('grunt-connect-rewrite');

  // Project configuration.
  grunt.initConfig({
    connect: {
      server: {
        options: {
          port: 9001,
          base: 'build',
          middleware: function(connect, options) {
            // Return array of whatever middlewares you want
            return [
              // redirect all urls to index.html in build folder
              urlRewrite('build', 'index.html'),

              // Serve static files.
              connect.static(options.base),

              // Make empty directories browsable.
              connect.directory(options.base)
            ];
          }
        }
      }
    }
  })
};

Чи є вручну спосіб здійснити перезапис URL-адрес у $routeProvider?

1
Я отримую urlRewriteне визначене попередження при спробі його запуску, і мені важко знайти правильне з'єднання з переписати, що я можу використовувати.

Показує мені помилку "У об'єкті немає методу" initConfig "Використовувати - примушувати продовжувати".
edonbajrami

8

Якщо ви знаходитесь у стеці .NET з MVC з AngularJS, це те, що вам потрібно зробити, щоб видалити '#' з URL:

  1. Налаштуйте базовий href на своїй сторінці _Layout: <head> <base href="https://stackoverflow.com/"> </head>

  2. Потім додайте наступне у своїй кутовій конфігурації програми: $locationProvider.html5Mode(true)

  3. Вище буде видалено "#" з URL-адреси, але оновлення сторінки не працюватиме, наприклад, якщо ви перебуваєте на "yoursite.com/about", оновлення сторінки дасть вам 404. Це тому, що MVC не знає про кутову маршрутизацію та за MVC-схемою. шукатиме сторінку MVC для "about", яка не існує в маршрутизації MVC. Вирішення цього полягає в тому, щоб надіслати весь запит на сторінку MVC в один перегляд MVC, і ви можете це зробити, додавши маршрут, який захоплює всі URL-адреси


routes.MapRoute(
        name: "App",
        url: "{*url}",
        defaults: new { controller = "Home", action = "Index" }
    );

8

Правило перезапису URL-адреси IIS, щоб запобігти помилці 404 після оновлення сторінки в html5mode

Для кутового виконання під IIS в Windows

<rewrite>
  <rules>
    <rule name="AngularJS" stopProcessing="true">
      <match url=".*" />
      <conditions logicalGrouping="MatchAll">
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
      </conditions>
      <action type="Rewrite" url="/" />
    </rule>
  </rules>
</rewrite>

NodeJS / ExpressJS Маршрути для запобігання 404 помилок після оновлення сторінки в html5mode

Для кутового бігу під Node / Express

var express = require('express');
var path = require('path');
var router = express.Router();

// serve angular front end files from root path
router.use('/', express.static('app', { redirect: false }));

// rewrite virtual urls to angular app to enable refreshing of internal pages
router.get('*', function (req, res, next) {
    res.sendFile(path.resolve('app/index.html'));
});

module.exports = router;

Більше інформації за адресою: AngularJS - Увімкнути оновлення сторінки в режимі HTML5 без 404 помилок у NodeJS та IIS


Дякую, правила IIS допомогли мені з програмою angular2, розгорнутою в блакиті, яка не могла направити на дочірній маршрут.
бітпринт

Я не можу подякувати вам за рішення для IIS (localhost у VS 2013). Досить довго розчарувавшись головою, нарешті знайшов цю відповідь. Працював як шарм.
Флорида Г.

5

Як вже згадували інші, вам потрібно переписати маршрути на сервер і встановити <base href="https://stackoverflow.com/"/> .

Для gulp-connect:

npm install connect-pushstate

var gulp = require('gulp'),
  connect = require('gulp-connect'),
  pushState = require('connect-pushstate/lib/pushstate').pushState;
...
connect.server({
  ...
  middleware: function (connect, options) {
    return [
      pushState()
    ];
  }
  ...
})
....

4

Я використовую apache (xampp) в моєму середовищі розробників і apache на виробництві, додайте:

errorDocument 404 /index.html

до .htaccess вирішити для мене це питання.


4

Для Grunt і Browsersync використовуйте connect-modrewrite тут

var modRewrite = require('connect-modrewrite');    


browserSync: {
            dev: {
                bsFiles: {

                    src: [
                        'app/assets/css/*.css',
                        'app/*.js',
                        'app/controllers/*.js',
                        '**/*.php',
                        '*.html',
                        'app/jade/includes/*.jade',
                        'app/views/*.html',
               ],
            },
        options: {
            watchTask: true,
            debugInfo: true,
            logConnections: true,
            server: {
                baseDir :'./',
                middleware: [
                       modRewrite(['!\.html|\.js|\.jpg|\.mp4|\.mp3|\.gif|\.svg\|.css|\.png$ /index.html [L]'])
                ]
            },

            ghostMode: {
                scroll: true,
                links: true,
                forms: true
                    }
                }
            }
        },

вище 15 відповідей, які були за талію часу. 1. npm install connect-modrewrite --save2. require in gruntfile3. скопіюйте вищевказаний сервер obj
Омар

Дякую, це працювало для мене в gulp 4 із налаштуванням browserSync.
Abdeali Chandanwala

Радий, що це допомогло @Abdeali Chandanwala!
MrThunder

@Omar Я не згоден з вами, менш розвиненим користувачам, таким як я, вигідно бачити, як додати його до gulpfile. Найкраще пам’ятати, що люди володіють різним рівнем знань, і просто писати потрібно в gruntfile не було б корисно, оскільки я б припускав, що вони розуміють, як додати це до глота.
MrThunder

3

Я вирішив

test: {
        options: {
          port: 9000,
          base: [
            '.tmp',
            'test',
            '<%= yeoman.app %>'
          ],
         middleware: function (connect) {
                  return [
                      modRewrite(['^[^\\.]*$ /index.html [L]']),
                      connect.static('.tmp'),
                      connect().use(
                          '/bower_components',
                          connect.static('./bower_components')
                      ),
                      connect.static('app')
                  ];
              }
        }
      },

3

Я відповідаю на це питання з більшого запитання:

Коли я додаю $ locationProvider.html5Mode (вірно), мій сайт не дозволить вставляти URL-адреси. Як налаштувати сервер на роботу, коли html5Mode є правдою?

Якщо у вас увімкнено html5Mode, символ # більше не буде використовуватися у ваших URL-адресах. Символ # корисний, оскільки не потребує конфігурації на стороні сервера. Без # URL виглядає набагато приємніше, але він також потребує перезаписування на стороні сервера. Ось кілька прикладів:

Для Express Rewrites з AngularJS ви можете вирішити це за допомогою наступних оновлень:

app.get('/*', function(req, res) {
res.sendFile(path.join(__dirname + '/public/app/views/index.html'));
});

і

<!-- FOR ANGULAR ROUTING -->
<base href="/">

і

app.use('/',express.static(__dirname + '/public'));

Завдяки його вирішенню. Для Nodejs додайте в app.js до app.use ('/ *', індекс);
Тигін

2

Я вважаю, що ваше питання стосується сервера. У кутовій документації щодо режиму HTML5 (за посиланням у вашому запитанні) зазначено:

Сторона сервера Використання цього режиму вимагає перезапис URL-адрес на стороні сервера, в основному вам потрібно переписати всі свої посилання на точку входу вашої програми (наприклад, index.html)

Я вважаю, що вам знадобиться налаштувати перезапис URL-адреси з / близько до /.


Спасибі за вашу відповідь. Отже, коли я перебуваю на сторінці about і оновлююсь, сервер повинен переписати URL на /? Проблема полягає в тому, що я працюю з рельєфом рейки, який знаходиться в іншому домені. Все спілкування здійснюється за допомогою дзвінків API. Як мені зробити ці перезаписи URL-адрес?
Дітер Де Месмейкер

Річ у тому, що ваш додаток доступний на маршруті вашого сервера /, тож коли ви оновите URL-адресу, що стосується / про браузер, запитає від вашого сервера незалежно від сторінки (у цьому випадку у вас немає сторінок, тому вам потрібно перенаправити ті назад).
lucuma

2

У нас було переадресація сервера в Express:

app.get('*', function(req, res){
    res.render('index');
});

і ми все ще отримували проблеми з оновленням сторінки, навіть після того, як ми додали <base href="https://stackoverflow.com/" />.

Рішення: переконайтеся, що ви використовуєте реальні посилання на своїй сторінці для навігації; не вводьте маршрут у URL-адресі, або ви отримаєте оновлення сторінки. (дурна помилка, я знаю)

:-P


але завдяки цьому ВСЕ http запит матиме індексний файл, як наприклад: / getJsonFromServer - http запит, який повертає дані, але завдяки цій конфігурації він також поверне індексну сторінку
vijay

1

Нарешті, я отримав спосіб вирішити цю проблему на стороні сервера, оскільки це більше нагадує проблему з самим AngularJs, я використовую 1,5 Angularjs, і у мене виникла така ж проблема при перезавантаженні сторінки. Але після додавання нижче коду до мого server.jsфайлу це врятує мій день, але це не правильне рішення чи не гарний спосіб.

app.use(function(req, res, next){
  var d = res.status(404);
     if(d){
        res.sendfile('index.html');
     }
});

0

Я знайшов ще кращий плагін Grunt, який працює, якщо ви маєте свої index.html та Gruntfile.js в одному каталозі;

https://npmjs.org/package/grunt-connect-pushstate

Після цього у вашому Gruntfile:

 var pushState = require('grunt-connect-pushstate/lib/utils').pushState;


    connect: {
    server: {
      options: {
        port: 1337,
        base: '',
        logger: 'dev',
        hostname: '*',
        open: true,
        middleware: function (connect, options) {
          return [
            // Rewrite requests to root so they may be handled by router
            pushState(),
            // Serve static files
            connect.static(options.base)
          ];
        }
      },
    }
},

Модуль grunt-connect-
pushstate

0
I solved same problem using modRewrite.  
AngularJS is reload page when after # changes.  
But HTML5 mode remove # and invalid the reload.  
So we should reload manually.
# install connect-modrewrite
    $ sudo npm install connect-modrewrite --save

# gulp/build.js
    'use strict';
    var gulp = require('gulp');
    var paths = gulp.paths;
    var util = require('util');
    var browserSync = require('browser-sync');
    var modRewrite  = require('connect-modrewrite');
    function browserSyncInit(baseDir, files, browser) {
        browser = browser === undefined ? 'default' : browser;
        var routes = null;
        if(baseDir === paths.src || (util.isArray(baseDir) && baseDir.indexOf(paths.src) !== -1)) {
            routes = {
                '/bower_components': 'bower_components'
            };
        }

        browserSync.instance = browserSync.init(files, {
            startPath: '/',
            server: {
            baseDir: baseDir,
            middleware: [
                modRewrite([
                    '!\\.\\w+$ /index.html [L]'
                ])
            ],
            routes: routes
            },
            browser: browser
        });
    }

0

У мене була така ж проблема з Java + angular app, створеним за допомогою JHipster . Я вирішив це за допомогою фільтра та списку всіх кутових сторінок у властивостях:

application.yml:

angular-pages:
  - login
  - settings
...

AngularPageReloadFilter.java

public class AngularPageReloadFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        request.getRequestDispatcher("index.html").forward(request, response);
    }
}

WebConfigurer.java

private void initAngularNonRootRedirectFilter(ServletContext servletContext,
                                              EnumSet<DispatcherType> disps) {
    log.debug("Registering angular page reload Filter");
    FilterRegistration.Dynamic angularRedirectFilter =
            servletContext.addFilter("angularPageReloadFilter",
                    new AngularPageReloadFilter());
    int index = 0;
    while (env.getProperty("angular-pages[" + index + "]") != null) {
        angularRedirectFilter.addMappingForUrlPatterns(disps, true, "/" + env.getProperty("angular-pages[" + index + "]"));
        index++;
    }
    angularRedirectFilter.setAsyncSupported(true);
}

Сподіваюся, комусь це буде корисно.


0

Gulp + браузерSync:

Встановіть connect-history-api-backback через npm, пізніше налаштуйте сервісне завдання gulp

var historyApiFallback = require('connect-history-api-fallback');

gulp.task('serve', function() {
  browserSync.init({
    proxy: {
            target: 'localhost:' + port,
            middleware: [ historyApiFallback() ]
        }
  });
});

0

Ваш код на стороні сервера - JAVA, після чого виконайте наведені нижче дії

крок 1: Завантажте urlrewritefilter JAR Клацніть тут і збережіть, щоб створити шлях WEB-INF / lib

крок 2: Увімкніть режим HTML5 $ $ locationProvider.html5Мод (вірно);

крок 3: встановіть базову URL-адресу <base href="https://stackoverflow.com/example.com/"/>

крок 4: скопіюйте та вставте у свій WEB.XML

 <filter>
     <filter-name>UrlRewriteFilter</filter-name>
 <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>UrlRewriteFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

крок 5: створіть файл у WEN-INF / urlrewrite.xml

 <urlrewrite default-match-type="wildcard">


    <rule>
            <from>/</from>
            <to>/index.html</to>
        </rule>

    <!--Write every state dependent on your project url-->
    <rule>
            <from>/example</from>
            <to>/index.html</to>
        </rule>
    </urlrewrite>

Як додати правило для динамічної URL-адреси на кшталт - test.com/user/101 test.com/user/102
Крішна Кумар Чурасія

0

У мене є це просте рішення, яке я використовую, і його роботи.

У додатку / Винятки / Handler.php

Додайте це вгорі:

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

Потім всередині методу візуалізації

public function render($request, Exception $exception)
{
    .......

       if ($exception instanceof NotFoundHttpException){

        $segment = $request->segments();

        //eg. http://site.dev/member/profile
        //module => member
        // view => member.index
        //where member.index is the root of your angular app could be anything :)
        if(head($segment) != 'api' && $module = $segment[0]){
            return response(view("$module.index"), 404);
        }

        return response()->fail('not_found', $exception->getCode());

    }
    .......

     return parent::render($request, $exception);
}

0

Я вирішив проблему, додавши нижче фрагмент коду у файл node.js.

app.get("/*", function (request, response) {
    console.log('Unknown API called');
    response.redirect('/#' + request.url);
});

Примітка. Коли ми оновлюємо сторінку, вона шукатиме API, а не кутова сторінка (через тег немає в URL-адресі). Використовуючи наведений вище код, я переспрямовую URL-адресу за допомогою #


-2

Просто випишіть правило в web.config

<system.webServer>
  <rewrite>
    <rules>
    <rule name="AngularJS Routes" stopProcessing="true">
      <match url=".*" />
      <conditions logicalGrouping="MatchAll">
        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
        <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" />
      </conditions>
      <action type="Rewrite" url="/" />
    </rule>
       </rules>
    </rewrite>
</system.webServer>

В індекс додайте це

<base href="/">

це працює для мене, можливо, ви забули встановити Html5Mode app.config

app.config(function ($stateProvider, $urlRouterProvider, $locationProvider){
    $locationProvider.html5Mode({ enabled: true, requireBase: true });
    $locationProvider.hashPrefix('!');
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.